|
|
@ -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); |
|
|
|
|
|
|
|
} |
|
|
|