You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
376 lines
14 KiB
376 lines
14 KiB
#include "chat_propagator.h" |
|
#include "chat_proto.h" |
|
#include "utun_node.h" |
|
#include "crypto.h" |
|
#include "db_manager.h" |
|
|
|
#include <QDateTime> |
|
#include <algorithm> |
|
#include <random> |
|
|
|
static uint64_t read64(const uint8_t* p) { |
|
uint64_t v; memcpy(&v, p, 8); return v; |
|
} |
|
static void write64(uint8_t* p, uint64_t v) { memcpy(p, &v, 8); } |
|
static uint32_t read32(const uint8_t* p) { |
|
uint32_t v; memcpy(&v, p, 4); return v; |
|
} |
|
static void write32(uint8_t* p, uint32_t v) { memcpy(p, &v, 4); } |
|
static uint16_t read16(const uint8_t* p) { |
|
uint16_t v; memcpy(&v, p, 2); return v; |
|
} |
|
static void write16(uint8_t* p, uint16_t v) { memcpy(p, &v, 2); } |
|
|
|
ChatPropagator::ChatPropagator(UtunNode* node, DbManager* db, QObject* parent) |
|
: QObject(parent) |
|
, m_node(node) |
|
, m_db(db) |
|
, m_ackTimer(new QTimer(this)) |
|
{ |
|
connect(m_node, &UtunNode::messageReceived, |
|
this, &ChatPropagator::onReceived); |
|
connect(m_ackTimer, &QTimer::timeout, |
|
this, &ChatPropagator::onAckTimeout); |
|
} |
|
|
|
void ChatPropagator::setKeys(const QByteArray& edPriv, const QByteArray& edPub, quint64 nodeId) { |
|
m_edPriv = edPriv; |
|
m_edPub = edPub; |
|
m_myNodeId = nodeId; |
|
} |
|
|
|
void ChatPropagator::start() { |
|
if (!m_edPriv.isEmpty() && !m_ackTimer->isActive()) |
|
m_ackTimer->start(250); |
|
startCatchUp(); |
|
} |
|
|
|
/* ======================================================================== */ |
|
|
|
void ChatPropagator::propagateNewMessage(const QString& channelId, |
|
const QByteArray& content, |
|
qint64 timestamp, |
|
const QByteArray& signature) { |
|
if (m_edPriv.isEmpty() || !m_myNodeId) return; |
|
|
|
QByteArray packet = buildNewMsgPacket(channelId, content, timestamp, signature); |
|
QList<quint64> exclude; |
|
exclude.append(m_myNodeId); |
|
fanoutGossip(channelId, packet, signature, ChatProp::kDefaultFanout, exclude); |
|
} |
|
|
|
QByteArray ChatPropagator::buildNewMsgPacket(const QString& channelId, |
|
const QByteArray& content, |
|
qint64 timestamp, |
|
const QByteArray& signature) { |
|
QByteArray cid = channelId.toUtf8(); |
|
uint8_t cidLen = static_cast<uint8_t>(cid.size()); |
|
size_t total = 1 + 1 + cidLen + 8 + 2 + content.size() + ChatProp::kSignSize; |
|
|
|
QByteArray pkt(static_cast<int>(total), Qt::Uninitialized); |
|
uint8_t* p = reinterpret_cast<uint8_t*>(pkt.data()); |
|
*p++ = ChatProp::kNewMsg; |
|
*p++ = cidLen; |
|
memcpy(p, cid.constData(), cidLen); p += cidLen; |
|
write64(p, static_cast<uint64_t>(timestamp)); p += 8; |
|
write16(p, static_cast<uint16_t>(content.size())); p += 2; |
|
memcpy(p, content.constData(), content.size()); p += content.size(); |
|
memcpy(p, signature.constData(), ChatProp::kSignSize); |
|
return pkt; |
|
} |
|
|
|
QByteArray ChatPropagator::buildAckPacket(const QByteArray& sig) { |
|
QByteArray pkt(static_cast<int>(1 + ChatProp::kSignSize), Qt::Uninitialized); |
|
pkt[0] = static_cast<char>(ChatProp::kNewAck); |
|
memcpy(pkt.data() + 1, sig.constData(), ChatProp::kSignSize); |
|
return pkt; |
|
} |
|
|
|
/* ======================================================================== */ |
|
|
|
void ChatPropagator::fanoutGossip(const QString& channelId, const QByteArray& packet, |
|
const QByteArray& sig, int fanout, |
|
const QList<quint64>& exclude) { |
|
auto peers = pickRandomPeers(fanout, exclude); |
|
if (peers.isEmpty()) return; |
|
|
|
GossipEntry e; |
|
e.channelId = channelId; |
|
e.packet = packet; |
|
e.sig = sig; |
|
e.peers = peers; |
|
e.sentAt = QDateTime::currentMSecsSinceEpoch(); |
|
m_pending[sig] = e; |
|
|
|
for (auto peer : peers) |
|
sendGossip(peer, packet); |
|
} |
|
|
|
void ChatPropagator::sendGossip(quint64 peer, const QByteArray& packet) { |
|
m_node->send(peer, packet); |
|
} |
|
|
|
/* ======================================================================== */ |
|
|
|
void ChatPropagator::onReceived(quint64 peerId, const QByteArray& data) { |
|
if (data.isEmpty()) return; |
|
uint8_t type = static_cast<uint8_t>(data[0]); |
|
switch (type) { |
|
case ChatProp::kNewMsg: handleNewMsg(peerId, data); break; |
|
case ChatProp::kNewAck: handleNewAck(data); break; |
|
case ChatProp::kBucketsRsp: handleBucketsRsp(data); break; |
|
case ChatProp::kHashesRsp: handleHashesRsp(data); break; |
|
case ChatProp::kMsgsRsp: handleMsgsRsp(data); break; |
|
} |
|
} |
|
|
|
void ChatPropagator::handleNewMsg(quint64 fromPeer, const QByteArray& payload) { |
|
QString channelId; |
|
QByteArray content; |
|
qint64 timestamp; |
|
QByteArray sig; |
|
if (!parseNewMsg(payload, channelId, content, timestamp, sig)) |
|
return; |
|
|
|
/* verify sender's signature */ |
|
QByteArray peerEdPub = m_db->getNodeEdPub(fromPeer); |
|
if (!peerEdPub.isEmpty() && !Crypto::verify(peerEdPub, payload.left(payload.size() - 64), sig)) |
|
return; |
|
|
|
/* insert into DB */ |
|
m_db->insertMessage(channelId, fromPeer, content, timestamp, 0); |
|
|
|
emit messageReceived(channelId, fromPeer, content, timestamp); |
|
|
|
/* re-gossip */ |
|
QList<quint64> exclude; |
|
exclude.append(m_myNodeId); |
|
exclude.append(fromPeer); |
|
fanoutGossip(channelId, payload, sig, ChatProp::kDefaultFanout, exclude); |
|
|
|
/* ack to sender */ |
|
m_node->send(fromPeer, buildAckPacket(sig)); |
|
} |
|
|
|
void ChatPropagator::handleNewAck(const QByteArray& payload) { |
|
if (payload.size() < static_cast<int>(1 + ChatProp::kSignSize)) return; |
|
QByteArray sig = payload.mid(1, static_cast<int>(ChatProp::kSignSize)); |
|
|
|
auto it = m_pending.find(sig); |
|
if (it == m_pending.end()) return; |
|
|
|
/* mark all peers as acked (simple version: ACK means message delivered) */ |
|
m_pending.erase(it); |
|
} |
|
|
|
void ChatPropagator::onAckTimeout() { |
|
qint64 now = QDateTime::currentMSecsSinceEpoch(); |
|
QList<QByteArray> toRemove; |
|
|
|
for (auto it = m_pending.begin(); it != m_pending.end(); ++it) { |
|
auto& e = it.value(); |
|
if (now - e.sentAt < ChatProp::kDefaultAckTimeoutMs) continue; |
|
|
|
if (e.retries >= 3) { |
|
toRemove.append(it.key()); |
|
continue; |
|
} |
|
|
|
/* retry: pick new peers for each missing ACK */ |
|
QList<quint64> unacked; |
|
for (auto p : e.peers) |
|
if (!e.acked.contains(p)) unacked.append(p); |
|
|
|
if (!unacked.isEmpty()) { |
|
auto newPeers = pickRandomPeers(1, e.peers + e.acked + |
|
QList<quint64>{m_myNodeId}); |
|
if (!newPeers.isEmpty()) { |
|
sendGossip(newPeers.first(), e.packet); |
|
e.peers.append(newPeers.first()); |
|
} |
|
} |
|
|
|
e.sentAt = now; |
|
e.retries++; |
|
} |
|
|
|
for (auto& k : toRemove) m_pending.remove(k); |
|
} |
|
|
|
/* ======================================================================== |
|
* Catch-up |
|
* ======================================================================== */ |
|
|
|
void ChatPropagator::startCatchUp() { |
|
if (!m_db) return; |
|
auto channels = m_db->getChannels(); |
|
if (channels.isEmpty()) return; |
|
|
|
auto peers = pickRandomPeers(ChatProp::kDefaultFanout, {m_myNodeId}); |
|
if (peers.isEmpty()) return; |
|
|
|
for (auto& ch : channels) { |
|
QByteArray cid = ch.channelId.toUtf8(); |
|
QByteArray pkt(static_cast<int>(2 + cid.size()), Qt::Uninitialized); |
|
pkt[0] = static_cast<char>(ChatProp::kBucketsReq); |
|
pkt[1] = static_cast<char>(cid.size()); |
|
memcpy(pkt.data() + 2, cid.constData(), cid.size()); |
|
|
|
for (auto peer : peers) |
|
sendGossip(peer, pkt); |
|
} |
|
} |
|
|
|
void ChatPropagator::handleBucketsRsp(const QByteArray& payload) { |
|
if (payload.size() < 3) return; |
|
const uint8_t* p = reinterpret_cast<const uint8_t*>(payload.constData()); |
|
if (*p != ChatProp::kBucketsRsp) return; p++; |
|
uint8_t cidLen = *p++; |
|
if (p + cidLen + 2 > reinterpret_cast<const uint8_t*>(payload.constData()) + payload.size()) return; |
|
QString channelId = QString::fromUtf8(reinterpret_cast<const char*>(p), cidLen); p += cidLen; |
|
uint16_t bCount = read16(p); p += 2; |
|
|
|
for (int i = 0; i < bCount; i++) { |
|
if (p + 4 + 1 + 4 + 32 > reinterpret_cast<const uint8_t*>(payload.constData()) + payload.size()) break; |
|
qint64 day = static_cast<qint64>(read32(p)); p += 4; |
|
int hour = *p++; |
|
int rCount = static_cast<int>(read32(p)); p += 4; |
|
QByteArray rHash(reinterpret_cast<const char*>(p), 32); p += 32; |
|
|
|
auto local = m_db->getBucketMeta(channelId); |
|
bool found = false; |
|
for (auto& b : local) { |
|
if (b.day == day && b.hour == hour && b.count == rCount && b.hash == rHash) { |
|
found = true; break; |
|
} |
|
} |
|
if (found) continue; |
|
|
|
/* mismatch — request signatures */ |
|
QByteArray cid = channelId.toUtf8(); |
|
QByteArray req(static_cast<int>(7 + cid.size()), Qt::Uninitialized); |
|
uint8_t* rp = reinterpret_cast<uint8_t*>(req.data()); |
|
*rp++ = ChatProp::kHashesReq; |
|
*rp++ = static_cast<uint8_t>(cid.size()); |
|
memcpy(rp, cid.constData(), cid.size()); rp += cid.size(); |
|
write32(rp, static_cast<uint32_t>(day)); rp += 4; |
|
*rp = static_cast<uint8_t>(hour); |
|
|
|
auto peers = pickRandomPeers(1, {m_myNodeId}); |
|
if (!peers.isEmpty()) |
|
sendGossip(peers.first(), req); |
|
} |
|
} |
|
|
|
void ChatPropagator::handleHashesRsp(const QByteArray& payload) { |
|
if (payload.size() < 3) return; |
|
const uint8_t* p = reinterpret_cast<const uint8_t*>(payload.constData()); |
|
if (*p != ChatProp::kHashesRsp) return; p++; |
|
uint16_t sigCount = read16(p); p += 2; |
|
|
|
QList<QByteArray> remoteSigs; |
|
for (int i = 0; i < sigCount; i++) { |
|
if (p + 64 > reinterpret_cast<const uint8_t*>(payload.constData()) + payload.size()) break; |
|
remoteSigs.append(QByteArray(reinterpret_cast<const char*>(p), 64)); |
|
p += 64; |
|
} |
|
if (remoteSigs.isEmpty()) return; |
|
|
|
/* find missing */ |
|
QList<QByteArray> missing; |
|
QList<QByteArray> remote = remoteSigs; |
|
for (auto& s : remote) { |
|
/* TODO: efficient lookup — query DB for this sig */ |
|
auto msgs = m_db->getMessagesBySigs({s}); |
|
if (msgs.isEmpty()) missing.append(s); |
|
} |
|
if (missing.isEmpty()) return; |
|
|
|
/* request missing messages */ |
|
int mCount = missing.size(); |
|
QByteArray req(static_cast<int>(3 + mCount * 64), Qt::Uninitialized); |
|
uint8_t* rp = reinterpret_cast<uint8_t*>(req.data()); |
|
*rp++ = ChatProp::kMsgsReq; |
|
write16(rp, static_cast<uint16_t>(mCount)); rp += 2; |
|
for (auto& s : missing) { memcpy(rp, s.constData(), 64); rp += 64; } |
|
|
|
auto peers = pickRandomPeers(1, {m_myNodeId}); |
|
if (!peers.isEmpty()) |
|
sendGossip(peers.first(), req); |
|
} |
|
|
|
void ChatPropagator::handleMsgsRsp(const QByteArray& payload) { |
|
if (payload.size() < 4) return; |
|
const uint8_t* p = reinterpret_cast<const uint8_t*>(payload.constData()); |
|
const uint8_t* end = p + payload.size(); |
|
if (*p != ChatProp::kMsgsRsp) return; p++; |
|
uint8_t cidLen = *p++; |
|
if (p + cidLen + 2 > end) return; |
|
QString channelId = QString::fromUtf8(reinterpret_cast<const char*>(p), cidLen); p += cidLen; |
|
uint16_t msgCount = read16(p); p += 2; |
|
|
|
for (int i = 0; i < msgCount; i++) { |
|
if (p + 64 + 8 + 8 + 2 > end) break; |
|
QByteArray sig(reinterpret_cast<const char*>(p), 64); p += 64; |
|
quint64 author = read64(p); p += 8; |
|
qint64 ts = static_cast<qint64>(read64(p)); p += 8; |
|
uint16_t cLen = read16(p); p += 2; |
|
if (p + cLen > end) break; |
|
QByteArray content(reinterpret_cast<const char*>(p), cLen); p += cLen; |
|
|
|
auto existing = m_db->getMessagesBySigs({sig}); |
|
if (!existing.isEmpty()) continue; |
|
|
|
m_db->insertMessage(channelId, author, content, ts, 0); |
|
emit messageReceived(channelId, author, content, ts); |
|
} |
|
} |
|
|
|
/* ======================================================================== */ |
|
|
|
bool ChatPropagator::parseNewMsg(const QByteArray& payload, QString& channelId, |
|
QByteArray& content, qint64& timestamp, |
|
QByteArray& sig) { |
|
if (payload.size() < 14) return false; /* minimum: 1+1+1+8+2+1 */ |
|
|
|
const uint8_t* p = reinterpret_cast<const uint8_t*>(payload.constData()); |
|
if (*p != ChatProp::kNewMsg) return false; |
|
p++; |
|
|
|
uint8_t cidLen = *p++; |
|
if (p + cidLen + 8 + 2 > reinterpret_cast<const uint8_t*>(payload.constData()) + payload.size()) |
|
return false; |
|
channelId = QString::fromUtf8(reinterpret_cast<const char*>(p), cidLen); p += cidLen; |
|
|
|
timestamp = static_cast<qint64>(read64(p)); p += 8; |
|
|
|
uint16_t contentLen = read16(p); p += 2; |
|
if (p + contentLen + ChatProp::kSignSize > |
|
reinterpret_cast<const uint8_t*>(payload.constData()) + payload.size()) |
|
return false; |
|
|
|
content = QByteArray(reinterpret_cast<const char*>(p), contentLen); p += contentLen; |
|
sig = QByteArray(reinterpret_cast<const char*>(p), static_cast<int>(ChatProp::kSignSize)); |
|
return true; |
|
} |
|
|
|
QList<quint64> ChatPropagator::pickRandomPeers(int count, const QList<quint64>& exclude) { |
|
QList<quint64> result; |
|
if (!m_db) return result; |
|
|
|
auto accs = m_db->getAccounts(true); |
|
QList<quint64> pool; |
|
for (auto& a : accs) { |
|
if (!exclude.contains(a.nodeId)) |
|
pool.append(a.nodeId); |
|
} |
|
if (pool.isEmpty()) return result; |
|
|
|
std::random_device rd; |
|
std::mt19937 g(rd()); |
|
std::shuffle(pool.begin(), pool.end(), g); |
|
|
|
for (int i = 0; i < count && i < pool.size(); i++) |
|
result.append(pool[i]); |
|
return result; |
|
}
|
|
|