From cdded6cef7710fb25093ea142edea3d3b7479447 Mon Sep 17 00:00:00 2001 From: Vladimir Golovnev Date: Tue, 21 Mar 2023 08:33:46 +0300 Subject: [PATCH] Add (experimental) I2P support PR #18717. Closes #16257. --- src/base/algorithm.h | 7 ++ src/base/bittorrent/session.h | 8 ++ src/base/bittorrent/sessionimpl.cpp | 84 +++++++++++++-- src/base/bittorrent/sessionimpl.h | 12 +++ src/gui/optionsdialog.cpp | 15 +++ src/gui/optionsdialog.ui | 91 +++++++++++++++- src/gui/properties/peerlistwidget.cpp | 147 +++++++++++++++----------- src/gui/properties/peerlistwidget.h | 3 +- 8 files changed, 295 insertions(+), 72 deletions(-) diff --git a/src/base/algorithm.h b/src/base/algorithm.h index 24ee3ab5f..85fb25ff8 100644 --- a/src/base/algorithm.h +++ b/src/base/algorithm.h @@ -63,4 +63,11 @@ namespace Algorithm while (it != set.end()) it = (p(*it) ? set.erase(it) : ++it); } + + template + List sorted(List list) + { + list.sort(); + return list; + } } diff --git a/src/base/bittorrent/session.h b/src/base/bittorrent/session.h index e4c9bdb8a..5dd6f7683 100644 --- a/src/base/bittorrent/session.h +++ b/src/base/bittorrent/session.h @@ -260,6 +260,14 @@ namespace BitTorrent virtual void setEncryption(int state) = 0; virtual int maxActiveCheckingTorrents() const = 0; virtual void setMaxActiveCheckingTorrents(int val) = 0; + virtual bool isI2PEnabled() const = 0; + virtual void setI2PEnabled(bool enabled) = 0; + virtual QString I2PAddress() const = 0; + virtual void setI2PAddress(const QString &address) = 0; + virtual int I2PPort() const = 0; + virtual void setI2PPort(int port) = 0; + virtual bool I2PMixedMode() const = 0; + virtual void setI2PMixedMode(bool enabled) = 0; virtual bool isProxyPeerConnectionsEnabled() const = 0; virtual void setProxyPeerConnectionsEnabled(bool enabled) = 0; virtual ChokingAlgorithm chokingAlgorithm() const = 0; diff --git a/src/base/bittorrent/sessionimpl.cpp b/src/base/bittorrent/sessionimpl.cpp index 0112577a2..ff76815b9 100644 --- a/src/base/bittorrent/sessionimpl.cpp +++ b/src/base/bittorrent/sessionimpl.cpp @@ -520,16 +520,12 @@ SessionImpl::SessionImpl(QObject *parent) , m_requestQueueSize(BITTORRENT_SESSION_KEY(u"RequestQueueSize"_qs), 500) , m_isExcludedFileNamesEnabled(BITTORRENT_KEY(u"ExcludedFileNamesEnabled"_qs), false) , m_excludedFileNames(BITTORRENT_SESSION_KEY(u"ExcludedFileNames"_qs)) - , m_bannedIPs(u"State/BannedIPs"_qs - , QStringList() - , [](const QStringList &value) - { - QStringList tmp = value; - tmp.sort(); - return tmp; - } - ) + , m_bannedIPs(u"State/BannedIPs"_qs, QStringList(), Algorithm::sorted) , m_resumeDataStorageType(BITTORRENT_SESSION_KEY(u"ResumeDataStorageType"_qs), ResumeDataStorageType::Legacy) + , m_isI2PEnabled {BITTORRENT_SESSION_KEY(u"I2P/Enabled"_qs), false} + , m_I2PAddress {BITTORRENT_SESSION_KEY(u"I2P/Address"_qs), u"127.0.0.1"_qs} + , m_I2PPort {BITTORRENT_SESSION_KEY(u"I2P/Port"_qs), 7656} + , m_I2PMixedMode {BITTORRENT_SESSION_KEY(u"I2P/MixedMode"_qs), false} , m_seedingLimitTimer {new QTimer(this)} , m_resumeDataTimer {new QTimer(this)} , m_ioThread {new QThread} @@ -1630,6 +1626,20 @@ lt::settings_pack SessionImpl::loadLTSettings() const settingsPack.set_int(lt::settings_pack::active_checking, maxActiveCheckingTorrents()); + // I2P + if (isI2PEnabled()) + { + settingsPack.set_str(lt::settings_pack::i2p_hostname, I2PAddress().toStdString()); + settingsPack.set_int(lt::settings_pack::i2p_port, I2PPort()); + settingsPack.set_bool(lt::settings_pack::allow_i2p_mixed, I2PMixedMode()); + } + else + { + settingsPack.set_str(lt::settings_pack::i2p_hostname, ""); + settingsPack.set_int(lt::settings_pack::i2p_port, 0); + settingsPack.set_bool(lt::settings_pack::allow_i2p_mixed, false); + } + // proxy settingsPack.set_int(lt::settings_pack::proxy_type, lt::settings_pack::none); if (Preferences::instance()->useProxyForBT()) @@ -3533,6 +3543,62 @@ void SessionImpl::setMaxActiveCheckingTorrents(const int val) configureDeferred(); } +bool SessionImpl::isI2PEnabled() const +{ + return m_isI2PEnabled; +} + +void SessionImpl::setI2PEnabled(const bool enabled) +{ + if (m_isI2PEnabled != enabled) + { + m_isI2PEnabled = enabled; + configureDeferred(); + } +} + +QString SessionImpl::I2PAddress() const +{ + return m_I2PAddress; +} + +void SessionImpl::setI2PAddress(const QString &address) +{ + if (m_I2PAddress != address) + { + m_I2PAddress = address; + configureDeferred(); + } +} + +int SessionImpl::I2PPort() const +{ + return m_I2PPort; +} + +void SessionImpl::setI2PPort(int port) +{ + if (m_I2PPort != port) + { + m_I2PPort = port; + configureDeferred(); + } +} + +bool SessionImpl::I2PMixedMode() const +{ + return m_I2PMixedMode; +} + +void SessionImpl::setI2PMixedMode(const bool enabled) +{ + if (m_I2PMixedMode != enabled) + { + m_I2PMixedMode = enabled; + configureDeferred(); + } +} + bool SessionImpl::isProxyPeerConnectionsEnabled() const { return m_isProxyPeerConnectionsEnabled; diff --git a/src/base/bittorrent/sessionimpl.h b/src/base/bittorrent/sessionimpl.h index 2b7e2ca7d..5a5bb6349 100644 --- a/src/base/bittorrent/sessionimpl.h +++ b/src/base/bittorrent/sessionimpl.h @@ -237,6 +237,14 @@ namespace BitTorrent void setEncryption(int state) override; int maxActiveCheckingTorrents() const override; void setMaxActiveCheckingTorrents(int val) override; + bool isI2PEnabled() const override; + void setI2PEnabled(bool enabled) override; + QString I2PAddress() const override; + void setI2PAddress(const QString &address) override; + int I2PPort() const override; + void setI2PPort(int port) override; + bool I2PMixedMode() const override; + void setI2PMixedMode(bool enabled) override; bool isProxyPeerConnectionsEnabled() const override; void setProxyPeerConnectionsEnabled(bool enabled) override; ChokingAlgorithm chokingAlgorithm() const override; @@ -675,6 +683,10 @@ namespace BitTorrent CachedSettingValue m_excludedFileNames; CachedSettingValue m_bannedIPs; CachedSettingValue m_resumeDataStorageType; + CachedSettingValue m_isI2PEnabled; + CachedSettingValue m_I2PAddress; + CachedSettingValue m_I2PPort; + CachedSettingValue m_I2PMixedMode; bool m_isRestored = false; diff --git a/src/gui/optionsdialog.cpp b/src/gui/optionsdialog.cpp index 7617e2822..92455e7b1 100644 --- a/src/gui/optionsdialog.cpp +++ b/src/gui/optionsdialog.cpp @@ -782,6 +782,11 @@ void OptionsDialog::loadConnectionTabOptions() m_ui->spinMaxUploadsPerTorrent->setEnabled(false); } + m_ui->textI2PHost->setText(session->I2PAddress()); + m_ui->spinI2PPort->setValue(session->I2PPort()); + m_ui->checkI2PMixed->setChecked(session->I2PMixedMode()); + m_ui->groupI2P->setChecked(session->isI2PEnabled()); + const auto *proxyConfigManager = Net::ProxyConfigurationManager::instance(); const Net::ProxyConfiguration proxyConf = proxyConfigManager->proxyConfiguration(); @@ -831,6 +836,11 @@ void OptionsDialog::loadConnectionTabOptions() connect(m_ui->textProxyIP, &QLineEdit::textChanged, this, &ThisType::enableApplyButton); connect(m_ui->spinProxyPort, qSpinBoxValueChanged, this, &ThisType::enableApplyButton); + connect(m_ui->textI2PHost, &QLineEdit::textChanged, this, &ThisType::enableApplyButton); + connect(m_ui->spinI2PPort, qSpinBoxValueChanged, this, &ThisType::enableApplyButton); + connect(m_ui->checkI2PMixed, &QCheckBox::toggled, this, &ThisType::enableApplyButton); + connect(m_ui->groupI2P, &QGroupBox::toggled, this, &ThisType::enableApplyButton); + connect(m_ui->checkProxyBitTorrent, &QGroupBox::toggled, this, &ThisType::enableApplyButton); connect(m_ui->checkProxyBitTorrent, &QGroupBox::toggled, this, &ThisType::adjustProxyOptions); connect(m_ui->checkProxyPeerConnections, &QAbstractButton::toggled, this, &ThisType::enableApplyButton); @@ -862,6 +872,11 @@ void OptionsDialog::saveConnectionTabOptions() const session->setMaxUploads(getMaxUploads()); session->setMaxUploadsPerTorrent(getMaxUploadsPerTorrent()); + session->setI2PEnabled(m_ui->groupI2P->isChecked()); + session->setI2PAddress(m_ui->textI2PHost->text().trimmed()); + session->setI2PPort(m_ui->spinI2PPort->value()); + session->setI2PMixedMode(m_ui->checkI2PMixed->isChecked()); + auto proxyConfigManager = Net::ProxyConfigurationManager::instance(); Net::ProxyConfiguration proxyConf; proxyConf.type = getProxyType(); diff --git a/src/gui/optionsdialog.ui b/src/gui/optionsdialog.ui index 58e18f7ab..a1d1c0520 100644 --- a/src/gui/optionsdialog.ui +++ b/src/gui/optionsdialog.ui @@ -1572,8 +1572,8 @@ readme[0-9].txt: filter 'readme1.txt', 'readme2.txt' but not 'readme10.txt'. 0 0 - 377 - 756 + 504 + 848 @@ -1788,6 +1788,78 @@ readme[0-9].txt: filter 'readme1.txt', 'readme2.txt' but not 'readme10.txt'. + + + + I2P (experimental) + + + true + + + true + + + + + + + + Host: + + + + + + + + + + Port: + + + + + + + 1 + + + 65535 + + + 8080 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + <html><head/><body><p>If &quot;mixed mode&quot; is enabled I2P torrents are allowed to also get peers from other sources than the tracker, and connect to regular IPs, not providing any anonymization. This may be useful if the user is not interested in the anonymization of I2P, but still wants to be able to connect to I2P peers.</p></body></html> + + + Mixed mode + + + + + + @@ -1839,6 +1911,19 @@ readme[0-9].txt: filter 'readme1.txt', 'readme2.txt' but not 'readme10.txt'. + + + + Qt::Horizontal + + + + 20 + 20 + + + + @@ -1859,7 +1944,7 @@ readme[0-9].txt: filter 'readme1.txt', 'readme2.txt' but not 'readme10.txt'.If checked, hostname lookups are done via the proxy - Use proxy for hostname lookups + Perform hostname lookup via proxy diff --git a/src/gui/properties/peerlistwidget.cpp b/src/gui/properties/peerlistwidget.cpp index 8ff554179..e0b57eaee 100644 --- a/src/gui/properties/peerlistwidget.cpp +++ b/src/gui/properties/peerlistwidget.cpp @@ -1,5 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2023 Vladimir Golovnev * Copyright (C) 2006 Christophe Dumez * * This program is free software; you can redistribute it and/or @@ -85,6 +86,21 @@ uint qHash(const PeerEndpoint &peerEndpoint, const uint seed = 0) } #endif +namespace +{ + void setModelData(QStandardItemModel *model, const int row, const int column, const QString &displayData + , const QVariant &underlyingData, const Qt::Alignment textAlignmentData = {}, const QString &toolTip = {}) + { + const QMap data = { + {Qt::DisplayRole, displayData}, + {PeerListSortModel::UnderlyingDataRole, underlyingData}, + {Qt::TextAlignmentRole, QVariant {textAlignmentData}}, + {Qt::ToolTipRole, toolTip}}; + + model->setItemData(model->index(row, column), data); + } +} + PeerListWidget::PeerListWidget(PropertiesWidget *parent) : QTreeView(parent) , m_properties(parent) @@ -404,23 +420,58 @@ void PeerListWidget::loadPeers(const BitTorrent::Torrent *torrent) if (torrent != m_properties->getCurrentTorrent()) return; + // Remove I2P peers since they will be completely reloaded. + for (QStandardItem *item : asConst(m_I2PPeerItems)) + m_listModel->removeRow(item->row()); + m_I2PPeerItems.clear(); + QSet existingPeers; existingPeers.reserve(m_peerItems.size()); for (auto i = m_peerItems.cbegin(); i != m_peerItems.cend(); ++i) existingPeers.insert(i.key()); + const bool hideZeroValues = Preferences::instance()->getHideZeroValues(); for (const BitTorrent::PeerInfo &peer : peers) { if (peer.address().ip.isNull()) continue; - bool isNewPeer = false; - updatePeer(torrent, peer, isNewPeer); - if (!isNewPeer) + const PeerEndpoint peerEndpoint {peer.address(), peer.connectionType()}; + + auto itemIter = m_peerItems.find(peerEndpoint); + const bool isNewPeer = (itemIter == m_peerItems.end()); + const int row = isNewPeer ? m_listModel->rowCount() : (*itemIter)->row(); + if (isNewPeer) + { + m_listModel->insertRow(row); + + const bool useI2PSocket = peer.useI2PSocket(); + + const QString peerIPString = useI2PSocket ? tr("N/A") : peerEndpoint.address.ip.toString(); + setModelData(m_listModel, row, PeerListColumns::IP, peerIPString, peerIPString, {}, peerIPString); + + const QString peerIPHiddenString = useI2PSocket ? QString() : peerEndpoint.address.ip.toString(); + setModelData(m_listModel, row, PeerListColumns::IP_HIDDEN, peerIPHiddenString, peerIPHiddenString); + + const QString peerPortString = useI2PSocket ? tr("N/A") : QString::number(peer.address().port); + setModelData(m_listModel, row, PeerListColumns::PORT, peerPortString, peer.address().port, (Qt::AlignRight | Qt::AlignVCenter)); + + if (useI2PSocket) + { + m_I2PPeerItems.append(m_listModel->item(row, PeerListColumns::IP)); + } + else + { + itemIter = m_peerItems.insert(peerEndpoint, m_listModel->item(row, PeerListColumns::IP)); + m_itemsByIP[peerEndpoint.address.ip].insert(itemIter.value()); + } + } + else { - const PeerEndpoint peerEndpoint {peer.address(), peer.connectionType()}; existingPeers.remove(peerEndpoint); } + + updatePeer(row, torrent, peer, hideZeroValues); } // Remove peers that are gone @@ -438,73 +489,51 @@ void PeerListWidget::loadPeers(const BitTorrent::Torrent *torrent) }); } -void PeerListWidget::updatePeer(const BitTorrent::Torrent *torrent, const BitTorrent::PeerInfo &peer, bool &isNewPeer) +void PeerListWidget::updatePeer(const int row, const BitTorrent::Torrent *torrent, const BitTorrent::PeerInfo &peer, const bool hideZeroValues) { - const PeerEndpoint peerEndpoint {peer.address(), peer.connectionType()}; - const QString peerIp = peerEndpoint.address.ip.toString(); const Qt::Alignment intDataTextAlignment = Qt::AlignRight | Qt::AlignVCenter; - const auto setModelData = - [this](const int row, const int column, const QString &displayData - , const QVariant &underlyingData, const Qt::Alignment textAlignmentData = {} - , const QString &toolTip = {}) - { - const QMap data = - { - {Qt::DisplayRole, displayData}, - {PeerListSortModel::UnderlyingDataRole, underlyingData}, - {Qt::TextAlignmentRole, QVariant {textAlignmentData}}, - {Qt::ToolTipRole, toolTip} - }; - m_listModel->setItemData(m_listModel->index(row, column), data); - }; - - auto itemIter = m_peerItems.find(peerEndpoint); - isNewPeer = (itemIter == m_peerItems.end()); - if (isNewPeer) - { - // new item - const int row = m_listModel->rowCount(); - m_listModel->insertRow(row); - - setModelData(row, PeerListColumns::IP, peerIp, peerIp, {}, peerIp); - setModelData(row, PeerListColumns::PORT, QString::number(peer.address().port), peer.address().port, intDataTextAlignment); - setModelData(row, PeerListColumns::IP_HIDDEN, peerIp, peerIp); - - itemIter = m_peerItems.insert(peerEndpoint, m_listModel->item(row, PeerListColumns::IP)); - m_itemsByIP[peerEndpoint.address.ip].insert(itemIter.value()); - } - - const int row = (*itemIter)->row(); - const bool hideValues = Preferences::instance()->getHideZeroValues(); - - setModelData(row, PeerListColumns::CONNECTION, peer.connectionType(), peer.connectionType()); - setModelData(row, PeerListColumns::FLAGS, peer.flags(), peer.flags(), {}, peer.flagsDescription()); const QString client = peer.client().toHtmlEscaped(); - setModelData(row, PeerListColumns::CLIENT, client, client, {}, client); + setModelData(m_listModel, row, PeerListColumns::CLIENT, client, client, {}, client); + const QString peerIdClient = peer.peerIdClient().toHtmlEscaped(); - setModelData(row, PeerListColumns::PEERID_CLIENT, peerIdClient, peerIdClient); - setModelData(row, PeerListColumns::PROGRESS, (Utils::String::fromDouble(peer.progress() * 100, 1) + u'%'), peer.progress(), intDataTextAlignment); - const QString downSpeed = (hideValues && (peer.payloadDownSpeed() <= 0)) ? QString {} : Utils::Misc::friendlyUnit(peer.payloadDownSpeed(), true); - setModelData(row, PeerListColumns::DOWN_SPEED, downSpeed, peer.payloadDownSpeed(), intDataTextAlignment); - const QString upSpeed = (hideValues && (peer.payloadUpSpeed() <= 0)) ? QString {} : Utils::Misc::friendlyUnit(peer.payloadUpSpeed(), true); - setModelData(row, PeerListColumns::UP_SPEED, upSpeed, peer.payloadUpSpeed(), intDataTextAlignment); - const QString totalDown = (hideValues && (peer.totalDownload() <= 0)) ? QString {} : Utils::Misc::friendlyUnit(peer.totalDownload()); - setModelData(row, PeerListColumns::TOT_DOWN, totalDown, peer.totalDownload(), intDataTextAlignment); - const QString totalUp = (hideValues && (peer.totalUpload() <= 0)) ? QString {} : Utils::Misc::friendlyUnit(peer.totalUpload()); - setModelData(row, PeerListColumns::TOT_UP, totalUp, peer.totalUpload(), intDataTextAlignment); - setModelData(row, PeerListColumns::RELEVANCE, (Utils::String::fromDouble(peer.relevance() * 100, 1) + u'%'), peer.relevance(), intDataTextAlignment); + setModelData(m_listModel, row, PeerListColumns::PEERID_CLIENT, peerIdClient, peerIdClient); + + const QString downSpeed = (hideZeroValues && (peer.payloadDownSpeed() <= 0)) + ? QString() : Utils::Misc::friendlyUnit(peer.payloadDownSpeed(), true); + setModelData(m_listModel, row, PeerListColumns::DOWN_SPEED, downSpeed, peer.payloadDownSpeed(), intDataTextAlignment); + + const QString upSpeed = (hideZeroValues && (peer.payloadUpSpeed() <= 0)) + ? QString() : Utils::Misc::friendlyUnit(peer.payloadUpSpeed(), true); + setModelData(m_listModel, row, PeerListColumns::UP_SPEED, upSpeed, peer.payloadUpSpeed(), intDataTextAlignment); + + const QString totalDown = (hideZeroValues && (peer.totalDownload() <= 0)) + ? QString() : Utils::Misc::friendlyUnit(peer.totalDownload()); + setModelData(m_listModel, row, PeerListColumns::TOT_DOWN, totalDown, peer.totalDownload(), intDataTextAlignment); + + const QString totalUp = (hideZeroValues && (peer.totalUpload() <= 0)) + ? QString() : Utils::Misc::friendlyUnit(peer.totalUpload()); + setModelData(m_listModel, row, PeerListColumns::TOT_UP, totalUp, peer.totalUpload(), intDataTextAlignment); + + setModelData(m_listModel, row, PeerListColumns::CONNECTION, peer.connectionType(), peer.connectionType()); + setModelData(m_listModel, row, PeerListColumns::FLAGS, peer.flags(), peer.flags(), {}, peer.flagsDescription()); + setModelData(m_listModel, row, PeerListColumns::PROGRESS, (Utils::String::fromDouble(peer.progress() * 100, 1) + u'%') + , peer.progress(), intDataTextAlignment); + setModelData(m_listModel, row, PeerListColumns::RELEVANCE, (Utils::String::fromDouble(peer.relevance() * 100, 1) + u'%') + , peer.relevance(), intDataTextAlignment); const PathList filePaths = torrent->info().filesForPiece(peer.downloadingPieceIndex()); QStringList downloadingFiles; downloadingFiles.reserve(filePaths.size()); for (const Path &filePath : filePaths) downloadingFiles.append(filePath.toString()); - const QString downloadingFilesDisplayValue = downloadingFiles.join(u';'); - setModelData(row, PeerListColumns::DOWNLOADING_PIECE, downloadingFilesDisplayValue, downloadingFilesDisplayValue, {}, downloadingFiles.join(u'\n')); - if (m_resolver) - m_resolver->resolve(peerEndpoint.address.ip); + const QString downloadingFilesDisplayValue = downloadingFiles.join(u';'); + setModelData(m_listModel, row, PeerListColumns::DOWNLOADING_PIECE, downloadingFilesDisplayValue + , downloadingFilesDisplayValue, {}, downloadingFiles.join(u'\n')); + + if (!peer.useI2PSocket() && m_resolver) + m_resolver->resolve(peer.address().ip); if (m_resolveCountries) { diff --git a/src/gui/properties/peerlistwidget.h b/src/gui/properties/peerlistwidget.h index 2276437c1..83e733829 100644 --- a/src/gui/properties/peerlistwidget.h +++ b/src/gui/properties/peerlistwidget.h @@ -98,7 +98,7 @@ private slots: void handleResolved(const QHostAddress &ip, const QString &hostname) const; private: - void updatePeer(const BitTorrent::Torrent *torrent, const BitTorrent::PeerInfo &peer, bool &isNewPeer); + void updatePeer(int row, const BitTorrent::Torrent *torrent, const BitTorrent::PeerInfo &peer, bool hideZeroValues); int visibleColumnsCount() const; void wheelEvent(QWheelEvent *event) override; @@ -108,6 +108,7 @@ private: PropertiesWidget *m_properties = nullptr; Net::ReverseResolution *m_resolver = nullptr; QHash m_peerItems; + QList m_I2PPeerItems; QHash> m_itemsByIP; // must be kept in sync with `m_peerItems` bool m_resolveCountries; };