Browse Source

feat(core): use user editable bootstrap node list

Fix #5767
reviewable/pr6126/r4
Anthony Bilinski 5 years ago
parent
commit
365a452fb8
No known key found for this signature in database
GPG Key ID: 2AA8E0DA1B31FB3C
  1. 10
      doc/user_manual_en.md
  2. 25
      src/core/core.cpp
  3. 10
      src/core/dhtserver.cpp
  4. 7
      src/core/dhtserver.h
  5. 243
      src/net/bootstrapnodeupdater.cpp
  6. 2
      src/net/bootstrapnodeupdater.h
  7. 7
      src/persistence/paths.cpp
  8. 1
      src/persistence/paths.h

10
doc/user_manual_en.md

@ -12,6 +12,7 @@
* [Keyboard Shortcuts](#keyboard-shortcuts) * [Keyboard Shortcuts](#keyboard-shortcuts)
* [Commandline Options](#commandline-options) * [Commandline Options](#commandline-options)
* [Emoji Packs](#emoji-packs) * [Emoji Packs](#emoji-packs)
* [Bootstrap nodes](#bootstrap-nodes)
## Profile corner ## Profile corner
@ -427,7 +428,7 @@ The following shortcuts are currently supported:
## Push to talk ## Push to talk
In audio group chat microphone mute state will be changed while `Ctrl` + In audio group chat microphone mute state will be changed while `Ctrl` +
`p` pressed and reverted on release. `p` pressed and reverted on release.
## Commandline Options ## Commandline Options
@ -458,6 +459,13 @@ files have to be in a subfolder also containing `emoticon.xml`, see the
structure of https://github.com/qTox/qTox/tree/v1.5.2/smileys for further structure of https://github.com/qTox/qTox/tree/v1.5.2/smileys for further
information. information.
## Bootstrap Nodes
qTox uses bootstrap nodes to find its way in to the DHT. The list of nodes is
stored in `bootstrapNodes.json` and can be found and modified if wanted at
`~/.config/tox/` on Linux, `%APPDATA%\Roaming\tox` on Windows, and
`~/Library/Application Support/Tox` on macOS.
[ToxMe service]: #register-on-toxme [ToxMe service]: #register-on-toxme
[user profile]: #user-profile [user profile]: #user-profile

25
src/core/core.cpp

@ -803,22 +803,29 @@ void Core::bootstrapDht()
// i think the more we bootstrap, the more we jitter because the more we overwrite nodes // i think the more we bootstrap, the more we jitter because the more we overwrite nodes
while (i < 2) { while (i < 2) {
const DhtServer& dhtServer = bootstrapNodesList[j % listSize]; const DhtServer& dhtServer = bootstrapNodesList[j % listSize];
QString dhtServerAddress = dhtServer.address.toLatin1();
QString port = QString::number(dhtServer.port); QString port = QString::number(dhtServer.port);
QString name = dhtServer.name;
qDebug("Connecting to bootstrap node %d", j % listSize); qDebug("Connecting to bootstrap node %d", j % listSize);
QByteArray address = dhtServer.address.toLatin1();
QByteArray address;
if (dhtServer.ipv4.isEmpty() && !dhtServer.ipv6.isEmpty()) {
address = dhtServer.ipv6.toLatin1();
} else {
address = dhtServer.ipv4.toLatin1();
}
// TODO: constucting the pk via ToxId is a workaround // TODO: constucting the pk via ToxId is a workaround
ToxPk pk = ToxId{dhtServer.userId}.getPublicKey(); ToxPk pk = ToxId{dhtServer.userId}.getPublicKey();
const uint8_t* pkPtr = pk.getData(); const uint8_t* pkPtr = pk.getData();
Tox_Err_Bootstrap error; Tox_Err_Bootstrap error;
tox_bootstrap(tox.get(), address.constData(), dhtServer.port, pkPtr, &error); if (dhtServer.statusUdp) {
PARSE_ERR(error); tox_bootstrap(tox.get(), address.constData(), dhtServer.port, pkPtr, &error);
PARSE_ERR(error);
tox_add_tcp_relay(tox.get(), address.constData(), dhtServer.port, pkPtr, &error); }
PARSE_ERR(error); if (dhtServer.statusTcp) {
tox_add_tcp_relay(tox.get(), address.constData(), dhtServer.port, pkPtr, &error);
PARSE_ERR(error);
}
++j; ++j;
++i; ++i;

10
src/core/dhtserver.cpp

@ -26,8 +26,14 @@
*/ */
bool DhtServer::operator==(const DhtServer& other) const bool DhtServer::operator==(const DhtServer& other) const
{ {
return this == &other || (port == other.port && address == other.address return this == &other ||
&& userId == other.userId && name == other.name); (statusUdp == other.statusUdp
&& statusTcp == other.statusTcp
&& ipv4 == other.ipv4
&& ipv6 == other.ipv6
&& maintainer == other.maintainer
&& userId == other.userId
&& port == other.port);
} }
/** /**

7
src/core/dhtserver.h

@ -23,9 +23,12 @@
struct DhtServer struct DhtServer
{ {
QString name; bool statusUdp;
bool statusTcp;
QString ipv4;
QString ipv6;
QString maintainer;
QString userId; QString userId;
QString address;
quint16 port; quint16 port;
bool operator==(const DhtServer& other) const; bool operator==(const DhtServer& other) const;

243
src/net/bootstrapnodeupdater.cpp

@ -30,14 +30,6 @@
#include <QNetworkReply> #include <QNetworkReply>
#include <QRegularExpression> #include <QRegularExpression>
namespace {
const QUrl NodeListAddress{"https://nodes.tox.chat/json"};
const QLatin1String jsonNodeArrayName{"nodes"};
const QLatin1String emptyAddress{"-"};
const QRegularExpression ToxPkRegEx(QString("(^|\\s)[A-Fa-f0-9]{%1}($|\\s)").arg(64));
const QLatin1String builtinNodesFile{":/conf/nodes.json"};
} // namespace
namespace NodeFields { namespace NodeFields {
const QLatin1String status_udp{"status_udp"}; const QLatin1String status_udp{"status_udp"};
const QLatin1String status_tcp{"status_tcp"}; const QLatin1String status_tcp{"status_tcp"};
@ -51,95 +43,14 @@ const QLatin1String tcp_ports{"tcp_ports"};
const QStringList neededFields{status_udp, status_tcp, ipv4, ipv6, public_key, port, maintainer}; const QStringList neededFields{status_udp, status_tcp, ipv4, ipv6, public_key, port, maintainer};
} // namespace NodeFields } // namespace NodeFields
/** namespace {
* @brief Fetches a list of currently online bootstrap nodes from node.tox.chat const QUrl NodeListAddress{"https://nodes.tox.chat/json"};
* @param proxy Proxy to use for the lookup, must outlive this object const QLatin1String jsonNodeArrayName{"nodes"};
*/ const QLatin1String emptyAddress{"-"};
BootstrapNodeUpdater::BootstrapNodeUpdater(const QNetworkProxy& proxy, Paths& _paths, QObject* parent) const QRegularExpression ToxPkRegEx(QString("(^|\\s)[A-Fa-f0-9]{%1}($|\\s)").arg(64));
: proxy{proxy} const QLatin1String builtinNodesFile{":/conf/nodes.json"};
, paths{_paths}
, QObject{parent}
{}
QList<DhtServer> BootstrapNodeUpdater::getBootstrapnodes()
{
return loadDefaultBootstrapNodes();
}
void BootstrapNodeUpdater::requestBootstrapNodes()
{
nam.setProxy(proxy);
connect(&nam, &QNetworkAccessManager::finished, this, &BootstrapNodeUpdater::onRequestComplete);
QNetworkRequest request{NodeListAddress};
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
nam.get(request);
}
/**
* @brief Loads the list of built in boostrap nodes
* @return List of bootstrap nodes on success, empty list on error
*/
QList<DhtServer> BootstrapNodeUpdater::loadDefaultBootstrapNodes()
{
QFile nodesFile{builtinNodesFile};
if (!nodesFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
qWarning() << "Couldn't read bootstrap nodes";
return {};
}
QString nodesJson = nodesFile.readAll();
nodesFile.close();
QJsonDocument d = QJsonDocument::fromJson(nodesJson.toUtf8());
if (d.isNull()) {
qWarning() << "Failed to parse JSON document";
return {};
}
return jsonToNodeList(d);
}
QList<DhtServer> BootstrapNodeUpdater::loadUserBootrapNodes()
{
QFile nodesFile{builtinNodesFile};
if (!nodesFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
qWarning() << "Couldn't read bootstrap nodes";
return {};
}
QString nodesJson = nodesFile.readAll();
nodesFile.close();
QJsonDocument d = QJsonDocument::fromJson(nodesJson.toUtf8());
if (d.isNull()) {
qWarning() << "Failed to parse JSON document";
return {};
}
return jsonToNodeList(d);
}
void BootstrapNodeUpdater::onRequestComplete(QNetworkReply* reply)
{
if (reply->error() != QNetworkReply::NoError) {
nam.clearAccessCache();
emit availableBootstrapNodes({});
return;
}
// parse the reply JSON
QJsonDocument jsonDocument = QJsonDocument::fromJson(reply->readAll());
if (jsonDocument.isNull()) {
emit availableBootstrapNodes({});
return;
}
QList<DhtServer> result = jsonToNodeList(jsonDocument);
emit availableBootstrapNodes(result);
}
void BootstrapNodeUpdater::jsonNodeToDhtServer(const QJsonObject& node, QList<DhtServer>& outList) void jsonNodeToDhtServer(const QJsonObject& node, QList<DhtServer>& outList)
{ {
// first check if the node in question has all needed fields // first check if the node in question has all needed fields
bool found = true; bool found = true;
@ -148,6 +59,7 @@ void BootstrapNodeUpdater::jsonNodeToDhtServer(const QJsonObject& node, QList<Dh
} }
if (!found) { if (!found) {
qDebug() << "Node is missing required fields.";
return; return;
} }
@ -170,45 +82,47 @@ void BootstrapNodeUpdater::jsonNodeToDhtServer(const QJsonObject& node, QList<Dh
ipv4_address = QString{}; ipv4_address = QString{};
} }
if (ipv4_address.isEmpty() && ipv6_address.isEmpty()) {
qWarning() << "Both ipv4 and ipv4 addresses are empty for" << public_key;
}
const QString maintainer = node[NodeFields::maintainer].toString({}); const QString maintainer = node[NodeFields::maintainer].toString({});
if (port < 1 || port > std::numeric_limits<uint16_t>::max()) { if (port < 1 || port > std::numeric_limits<uint16_t>::max()) {
qDebug() << "Invalid port in nodes list:" << port;
return; return;
} }
const quint16 port_u16 = static_cast<quint16>(port); const quint16 port_u16 = static_cast<quint16>(port);
if (!public_key.contains(ToxPkRegEx)) { if (!public_key.contains(ToxPkRegEx)) {
qDebug() << "Invalid public key in nodes list" << public_key;
return; return;
} }
DhtServer server; DhtServer server;
server.statusUdp = true;
server.statusTcp = node[NodeFields::status_udp].toBool(false);
server.userId = public_key; server.userId = public_key;
server.port = port_u16; server.port = port_u16;
server.name = maintainer; server.maintainer = maintainer;
server.ipv4 = ipv4_address;
if (!ipv4_address.isEmpty()) { server.ipv6 = ipv6_address;
server.address = ipv4_address; outList.append(server);
outList.append(server);
}
// avoid adding the same server twice in case they use the same dns name for v6 and v4
if (!ipv6_address.isEmpty() && ipv4_address != ipv6_address) {
server.address = ipv6_address;
outList.append(server);
}
return; return;
} }
QList<DhtServer> BootstrapNodeUpdater::jsonToNodeList(const QJsonDocument& nodeList) QList<DhtServer> jsonToNodeList(const QJsonDocument& nodeList)
{ {
QList<DhtServer> result; QList<DhtServer> result;
if (!nodeList.isObject()) { if (!nodeList.isObject()) {
qWarning() << "Bootstrap JSON is missing root object";
return result; return result;
} }
QJsonObject rootObj = nodeList.object(); QJsonObject rootObj = nodeList.object();
if (!(rootObj.contains(jsonNodeArrayName) && rootObj[jsonNodeArrayName].isArray())) { if (!(rootObj.contains(jsonNodeArrayName) && rootObj[jsonNodeArrayName].isArray())) {
qWarning() << "Bootstrap JSON is missing nodes array";
return result; return result;
} }
QJsonArray nodes = rootObj[jsonNodeArrayName].toArray(); QJsonArray nodes = rootObj[jsonNodeArrayName].toArray();
@ -220,3 +134,114 @@ QList<DhtServer> BootstrapNodeUpdater::jsonToNodeList(const QJsonDocument& nodeL
return result; return result;
} }
QList<DhtServer> loadNodesFile(QString file)
{
QFile nodesFile{file};
if (!nodesFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
qWarning() << "Couldn't read bootstrap nodes";
return {};
}
QString nodesJson = nodesFile.readAll();
nodesFile.close();
auto jsonDoc = QJsonDocument::fromJson(nodesJson.toUtf8());
if (jsonDoc.isNull()) {
qWarning() << "Failed to parse JSON document";
return {};
}
return jsonToNodeList(jsonDoc);
}
QByteArray serialize(QList<DhtServer> nodes)
{
QJsonArray jsonNodes;
for (auto& node : nodes) {
QJsonObject nodeJson;
nodeJson.insert(NodeFields::status_udp, node.statusUdp);
nodeJson.insert(NodeFields::status_tcp, node.statusTcp);
nodeJson.insert(NodeFields::ipv4, node.ipv4);
nodeJson.insert(NodeFields::ipv6, node.ipv6);
nodeJson.insert(NodeFields::public_key, node.userId);
nodeJson.insert(NodeFields::port, node.port);
nodeJson.insert(NodeFields::maintainer, node.maintainer);
jsonNodes.append(nodeJson);
}
QJsonObject rootObj;
rootObj.insert("nodes", jsonNodes);
QJsonDocument doc{rootObj};
return doc.toJson(QJsonDocument::Indented);
}
} // namespace
/**
* @brief Fetches a list of currently online bootstrap nodes from node.tox.chat
* @param proxy Proxy to use for the lookup, must outlive this object
*/
BootstrapNodeUpdater::BootstrapNodeUpdater(const QNetworkProxy& proxy, Paths& _paths, QObject* parent)
: proxy{proxy}
, paths{_paths}
, QObject{parent}
{}
QList<DhtServer> BootstrapNodeUpdater::getBootstrapnodes()
{
auto userFilePath = paths.getUserNodesFilePath();
if (!QFile(userFilePath).exists()) {
qInfo() << "Bootstrap node list not found, creating one with default nodes.";
// deserialize and reserialize instead of just copying to strip out any unnecessary json, making it easier for
// users to edit
auto buildInNodes = loadNodesFile(builtinNodesFile);
auto serializedNodes = serialize(buildInNodes);
QFile outFile(userFilePath);
outFile.open(QIODevice::WriteOnly | QIODevice::Text);
outFile.write(serializedNodes.data(), serializedNodes.size());
outFile.close();
}
return loadNodesFile(userFilePath);
}
void BootstrapNodeUpdater::requestBootstrapNodes()
{
nam.setProxy(proxy);
connect(&nam, &QNetworkAccessManager::finished, this, &BootstrapNodeUpdater::onRequestComplete);
QNetworkRequest request{NodeListAddress};
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
nam.get(request);
}
/**
* @brief Loads the list of built in boostrap nodes
* @return List of bootstrap nodes on success, empty list on error
*/
QList<DhtServer> BootstrapNodeUpdater::loadDefaultBootstrapNodes()
{
return loadNodesFile(builtinNodesFile);
}
void BootstrapNodeUpdater::onRequestComplete(QNetworkReply* reply)
{
if (reply->error() != QNetworkReply::NoError) {
nam.clearAccessCache();
emit availableBootstrapNodes({});
return;
}
// parse the reply JSON
QJsonDocument jsonDocument = QJsonDocument::fromJson(reply->readAll());
if (jsonDocument.isNull()) {
emit availableBootstrapNodes({});
return;
}
QList<DhtServer> result = jsonToNodeList(jsonDocument);
emit availableBootstrapNodes(result);
}

2
src/net/bootstrapnodeupdater.h

@ -46,8 +46,6 @@ private slots:
void onRequestComplete(QNetworkReply* reply); void onRequestComplete(QNetworkReply* reply);
private: private:
static QList<DhtServer> jsonToNodeList(const QJsonDocument& nodeList);
static void jsonNodeToDhtServer(const QJsonObject& node, QList<DhtServer>& outList);
QList<DhtServer> loadUserBootrapNodes(); QList<DhtServer> loadUserBootrapNodes();
private: private:

7
src/persistence/paths.cpp

@ -344,4 +344,11 @@ QString Paths::getAppCacheDirPath() const
#endif #endif
} }
QString Paths::getUserNodesFilePath() const
{
QDir dir(getSettingsDirPath());
constexpr static char nodesFileName[] = "bootstrapNodes.json";
return dir.filePath(nodesFileName);
}
#endif // PATHS_VERSION_TCS_COMPLIANT #endif // PATHS_VERSION_TCS_COMPLIANT

1
src/persistence/paths.h

@ -49,6 +49,7 @@ public:
QString getSettingsDirPath() const; QString getSettingsDirPath() const;
QString getAppDataDirPath() const; QString getAppDataDirPath() const;
QString getAppCacheDirPath() const; QString getAppCacheDirPath() const;
QString getUserNodesFilePath() const;
#endif #endif

Loading…
Cancel
Save