stcp: ed25519 pubkey exchange in handshake, fix instance destroy segfault, fix chat_tcp_test
- u_async.c: process immediate_queue BEFORE epoll_wait in uasync_poll - stcp_link.c: defensive NULL etcp_conn/etcp_link in stcp_link_close - utun_instance.c: move stcp_server_list_destroy_all to Phase J (before ETCP), add deferred drain in both Phase J and L - stcp.h/c: STCP_HS_ENC_CLIENT 6→38, STCP_HS_ENC_SERVER 7→39, exchange ed25519_pubkey in both handshake directions - etcp_connections.c: copy peer_ed25519 from STCP to ETCP_CONN in etcp_link_enter_ready_tcp; guard link->etcp in tcp_link_close_cb - chat_sync.c: fix lk->conn NULL dereference for TCP links - test_stcp.c: update stcp_server_create/stcp_client_connect callsites - tools/chat_tcp_test: change transport udp→tcp
This commit is contained in:
1 parent
a3783b5f55
commit
38c9f229bc
1272 files changed
+968388
-1
No files matched your search
@@ -0,0 +1,294 @@
|
||||
#include "accountlist.h"
|
||||
#include "accountdelegate.h"
|
||||
#include "memberlistmodel.h"
|
||||
#include "memberpropsdialog.h"
|
||||
#include "renamedialog.h"
|
||||
#include "../db/db_manager.h"
|
||||
#include <QVBoxLayout>
|
||||
#include <QSplitter>
|
||||
#include <QLabel>
|
||||
#include <QDateTime>
|
||||
#include <QFont>
|
||||
#include "../../../lib/platform_compat.h"
|
||||
|
||||
extern "C" {
|
||||
#include "utun_instance.h"
|
||||
#include "etcp.h"
|
||||
#include "etcp_connections.h"
|
||||
#include "../lib/socket_compat.h"
|
||||
}
|
||||
|
||||
QString AccountList::addrStr(const void* addr, int family) {
|
||||
if (family == 4) {
|
||||
const uint8_t* a = (const uint8_t*)addr;
|
||||
return QString("%1.%2.%3.%4").arg(a[0]).arg(a[1]).arg(a[2]).arg(a[3]);
|
||||
}
|
||||
if (family == 6) {
|
||||
const uint16_t* a = (const uint16_t*)addr;
|
||||
return QString("%1:%2:%3:%4:%5:%6:%7:%8")
|
||||
.arg(ntohs(a[0]), 0, 16).arg(ntohs(a[1]), 0, 16)
|
||||
.arg(ntohs(a[2]), 0, 16).arg(ntohs(a[3]), 0, 16)
|
||||
.arg(ntohs(a[4]), 0, 16).arg(ntohs(a[5]), 0, 16)
|
||||
.arg(ntohs(a[6]), 0, 16).arg(ntohs(a[7]), 0, 16);
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
QString AccountList::relativeTimeStr(qint64 timestamp) {
|
||||
if (timestamp <= 0) return "";
|
||||
qint64 diff = QDateTime::currentSecsSinceEpoch() - timestamp;
|
||||
if (diff < 0) diff = 0;
|
||||
if (diff < 60) return QString("%1s").arg(diff);
|
||||
if (diff < 3600) return QString("%1m").arg(diff / 60);
|
||||
if (diff < 86400) return QString("%1h").arg(diff / 3600);
|
||||
return QString("%1d").arg(diff / 86400);
|
||||
}
|
||||
|
||||
AccountList::AccountList(DbManager* db, QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, m_listView(new QListView(this))
|
||||
, m_model(new MemberListModel(db, this))
|
||||
, m_db(db)
|
||||
, m_detailText(nullptr)
|
||||
{
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(0);
|
||||
|
||||
auto *header = new QLabel("Members", this);
|
||||
header->setAlignment(Qt::AlignCenter);
|
||||
header->setStyleSheet("font-weight: bold; padding: 8px; background: palette(window);");
|
||||
layout->addWidget(header);
|
||||
|
||||
m_listView->setModel(m_model);
|
||||
m_listView->setItemDelegate(new AccountDelegate(m_listView));
|
||||
m_listView->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
m_listView->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
m_listView->setIconSize(QSize(24, 24));
|
||||
m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
m_listView->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
|
||||
m_listView->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
m_detailText = new QTextEdit(this);
|
||||
m_detailText->setReadOnly(true);
|
||||
m_detailText->setFont(QFont("monospace", 10));
|
||||
m_detailText->setStyleSheet(
|
||||
"QTextEdit { border-top: 1px solid palette(mid); border-bottom: none;"
|
||||
" border-left: none; border-right: none; padding: 6px; background: palette(base); }");
|
||||
m_detailText->setPlaceholderText("Select a member");
|
||||
|
||||
auto *memberSplitter = new QSplitter(Qt::Vertical, this);
|
||||
memberSplitter->addWidget(m_listView);
|
||||
memberSplitter->addWidget(m_detailText);
|
||||
memberSplitter->setStretchFactor(0, 3);
|
||||
memberSplitter->setStretchFactor(1, 2);
|
||||
memberSplitter->setSizes({360, 240});
|
||||
layout->addWidget(memberSplitter, 1);
|
||||
|
||||
connect(m_listView->selectionModel(), &QItemSelectionModel::selectionChanged,
|
||||
m_listView->viewport(), qOverload<>(&QWidget::update));
|
||||
connect(m_listView, &QListView::clicked, this, &AccountList::onMemberClicked);
|
||||
connect(m_listView, &QListView::doubleClicked, this, &AccountList::onMemberDoubleClicked);
|
||||
|
||||
m_refreshTimer = new QTimer(this);
|
||||
m_refreshTimer->start(2000);
|
||||
connect(m_refreshTimer, &QTimer::timeout, m_model, &MemberListModel::refreshRtt);
|
||||
|
||||
connect(m_listView, &QWidget::customContextMenuRequested,
|
||||
this, &AccountList::onMemberContextMenu);
|
||||
|
||||
m_detailTimer = new QTimer(this);
|
||||
m_detailTimer->setInterval(5000);
|
||||
connect(m_detailTimer, &QTimer::timeout, this, &AccountList::onDetailTimerTick);
|
||||
|
||||
m_linkTimer = new QTimer(this);
|
||||
m_linkTimer->setInterval(1000);
|
||||
connect(m_linkTimer, &QTimer::timeout, this, [this]{
|
||||
if (m_detailNodeId != 0) updateDetailDisplay();
|
||||
});
|
||||
}
|
||||
|
||||
void AccountList::setChannel(const QString& channelId) {
|
||||
m_channelId = channelId;
|
||||
m_model->setChannel(channelId);
|
||||
m_detailTimer->stop();
|
||||
m_linkTimer->stop();
|
||||
m_detailNodeId = 0;
|
||||
m_detailText->clear();
|
||||
}
|
||||
|
||||
void AccountList::refresh() { m_model->refresh(); }
|
||||
|
||||
void AccountList::onMemberClicked(const QModelIndex& index) {
|
||||
if (!index.isValid()) return;
|
||||
quint64 nodeId = m_model->nodeIdAt(index.row());
|
||||
if (nodeId) showMemberDetail(nodeId);
|
||||
}
|
||||
|
||||
void AccountList::showMemberDetail(quint64 nodeId) {
|
||||
if (!m_db || !m_db->m_db) return;
|
||||
|
||||
m_detailTimer->stop();
|
||||
m_linkTimer->stop();
|
||||
m_detailNodeId = nodeId;
|
||||
m_detailName.clear();
|
||||
m_detailOnline = false;
|
||||
m_detailLastSeen = 0;
|
||||
m_detailCreated = 0;
|
||||
m_detailAddrs.clear();
|
||||
|
||||
sqlite3* d = m_db->m_db;
|
||||
sqlite3_stmt* st = nullptr;
|
||||
if (sqlite3_prepare_v2(d,
|
||||
"SELECT name, online, last_seen_at, created_at FROM nodes WHERE node_id=?",
|
||||
-1, &st, nullptr) == SQLITE_OK) {
|
||||
sqlite3_bind_int64(st, 1, (sqlite3_int64)nodeId);
|
||||
if (sqlite3_step(st) == SQLITE_ROW) {
|
||||
m_detailName = DbManager::colText(st, 0);
|
||||
m_detailOnline = sqlite3_column_int(st, 1) != 0;
|
||||
m_detailLastSeen = sqlite3_column_int64(st, 2);
|
||||
m_detailCreated = sqlite3_column_int64(st, 3);
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
}
|
||||
|
||||
if (m_detailName.isEmpty())
|
||||
m_detailName = QString("0x%1").arg(nodeId, 8, 16, QChar('0')).right(4).toUpper();
|
||||
|
||||
/* prefer local nickname */
|
||||
QString tbl = m_db->peersTableName(m_channelId);
|
||||
sqlite3_stmt* nickSt = nullptr;
|
||||
QByteArray nickSql = QString(
|
||||
"SELECT local_nick FROM \"%1\" WHERE node_id=?"
|
||||
).arg(tbl).toUtf8();
|
||||
if (sqlite3_prepare_v2(d, nickSql.constData(), -1, &nickSt, nullptr) == SQLITE_OK) {
|
||||
sqlite3_bind_int64(nickSt, 1, (sqlite3_int64)nodeId);
|
||||
if (sqlite3_step(nickSt) == SQLITE_ROW) {
|
||||
QString n = DbManager::colText(nickSt, 0);
|
||||
if (!n.isEmpty()) m_detailName = n;
|
||||
}
|
||||
sqlite3_finalize(nickSt);
|
||||
}
|
||||
|
||||
sqlite3_stmt* na = nullptr;
|
||||
sqlite3_prepare_v2(d,
|
||||
"SELECT family, protocol, address, port FROM node_addresses WHERE node_id=? ORDER BY family, port",
|
||||
-1, &na, nullptr);
|
||||
if (na) {
|
||||
sqlite3_bind_int64(na, 1, (sqlite3_int64)nodeId);
|
||||
while (sqlite3_step(na) == SQLITE_ROW) {
|
||||
DetailAddress a;
|
||||
a.family = sqlite3_column_int(na, 0);
|
||||
a.protocol = sqlite3_column_int(na, 1);
|
||||
const void* adr = sqlite3_column_blob(na, 2);
|
||||
int sz = sqlite3_column_bytes(na, 2);
|
||||
if (adr && sz > 0) a.addr = QByteArray((const char*)adr, sz);
|
||||
a.port = sqlite3_column_int(na, 3);
|
||||
if (!a.addr.isEmpty()) m_detailAddrs.append(a);
|
||||
}
|
||||
sqlite3_finalize(na);
|
||||
}
|
||||
|
||||
m_detailTimer->start();
|
||||
m_linkTimer->start();
|
||||
updateDetailDisplay();
|
||||
}
|
||||
|
||||
void AccountList::updateDetailDisplay() {
|
||||
QStringList lines;
|
||||
|
||||
lines.append(QString("ID:0x%1").arg(m_detailNodeId, 16, 16, QChar('0')));
|
||||
|
||||
QString dotColor = m_detailOnline ? "#4CAF50" : "#9E9E9E";
|
||||
lines.append(QString("<span style='color:%1;'>●</span> %2")
|
||||
.arg(dotColor, m_detailName.toHtmlEscaped()));
|
||||
|
||||
if (m_detailLastSeen > 0)
|
||||
lines.append(QString("⌛ %1").arg(relativeTimeStr(m_detailLastSeen)));
|
||||
|
||||
if (m_detailCreated > 0)
|
||||
lines.append(QString("📅 %1").arg(relativeTimeStr(m_detailCreated)));
|
||||
|
||||
struct ETCP_CONN* conn = nullptr;
|
||||
if (m_inst) conn = instance_find_conn(m_inst, m_detailNodeId);
|
||||
|
||||
for (const auto& a : m_detailAddrs) {
|
||||
QString proto;
|
||||
switch (a.protocol) { case 1: proto = "UDP"; break; case 2: proto = "TCP"; break; default: proto = "UDP/TCP"; break; }
|
||||
QString linkColor = "#9E9E9E";
|
||||
if (conn) {
|
||||
for (struct ETCP_LINK* lk = conn->links; lk; lk = lk->next) {
|
||||
const struct sockaddr_storage* sa = &lk->remote_addr;
|
||||
int lfm = 0; QString lip; uint16_t lpo = 0;
|
||||
char ipbuf[INET6_ADDRSTRLEN];
|
||||
if (sa->ss_family == AF_INET) {
|
||||
struct sockaddr_in* sin = (struct sockaddr_in*)sa;
|
||||
inet_ntop(AF_INET, &sin->sin_addr, ipbuf, sizeof(ipbuf));
|
||||
lfm = 4; lpo = ntohs(sin->sin_port); lip = QString::fromLocal8Bit(ipbuf);
|
||||
} else if (sa->ss_family == AF_INET6) {
|
||||
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)sa;
|
||||
inet_ntop(AF_INET6, &sin6->sin6_addr, ipbuf, sizeof(ipbuf));
|
||||
lfm = 6; lpo = ntohs(sin6->sin6_port); lip = QString::fromLocal8Bit(ipbuf);
|
||||
}
|
||||
if (lfm == a.family && lpo == (uint16_t)a.port && lip == addrStr(a.addr.constData(), a.family)) {
|
||||
linkColor = (lk->link_state == 3 && lk->link_status == 1) ? "#4CAF50" : "#E53935";
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (linkColor == "#9E9E9E" && a.protocol == 2 && conn->transport_link) {
|
||||
linkColor = stcp_link_is_ready((struct stcp_link*)conn->transport_link) ? "#4CAF50" : "#E53935";
|
||||
}
|
||||
}
|
||||
lines.append(QString("<span style='color:%1;'>●</span> %2:%3:%4")
|
||||
.arg(linkColor, proto, addrStr(a.addr.constData(), a.family)).arg(a.port));
|
||||
}
|
||||
|
||||
m_detailText->setHtml(lines.join("<br>"));
|
||||
}
|
||||
|
||||
void AccountList::onDetailTimerTick() {
|
||||
if (m_detailNodeId != 0) updateDetailDisplay();
|
||||
}
|
||||
|
||||
void AccountList::onMemberDoubleClicked(const QModelIndex& index) {
|
||||
if (!index.isValid()) return;
|
||||
quint64 nodeId = m_model->nodeIdAt(index.row());
|
||||
if (!nodeId) return;
|
||||
QString name = m_model->data(index, Qt::DisplayRole).toString();
|
||||
MemberPropsDialog dlg(m_db, m_channelId, nodeId, name, this);
|
||||
if (dlg.exec() == QDialog::Accepted) {
|
||||
m_model->refresh();
|
||||
emit memberRenamed();
|
||||
}
|
||||
}
|
||||
|
||||
void AccountList::onMemberContextMenu(const QPoint& pos) {
|
||||
QModelIndex index = m_listView->indexAt(pos);
|
||||
if (!index.isValid()) return;
|
||||
|
||||
quint64 nodeId = m_model->nodeIdAt(index.row());
|
||||
if (!nodeId) return;
|
||||
|
||||
QString name = m_model->data(index, Qt::DisplayRole).toString();
|
||||
|
||||
QMenu menu(m_listView);
|
||||
QAction* renameAction = menu.addAction("Rename");
|
||||
if (nodeId == m_db->myNodeId()) renameAction->setEnabled(false);
|
||||
QAction* editAction = menu.addAction("Edit");
|
||||
QAction* chosen = menu.exec(m_listView->viewport()->mapToGlobal(pos));
|
||||
|
||||
if (chosen == renameAction) {
|
||||
RenameDialog dlg(m_db, m_channelId, nodeId, name, this);
|
||||
if (dlg.exec() == QDialog::Accepted) {
|
||||
m_model->refresh();
|
||||
emit memberRenamed();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (chosen != editAction) return;
|
||||
|
||||
MemberPropsDialog dlg(m_db, m_channelId, nodeId, name, this);
|
||||
if (dlg.exec() == QDialog::Accepted) {
|
||||
m_model->refresh();
|
||||
emit memberRenamed();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user