Add (experimental) I2P support

PR #18717.
Closes #16257.
This commit is contained in:
Vladimir Golovnev 2023-03-21 08:33:46 +03:00 committed by GitHub
parent 8cbe4a571c
commit cdded6cef7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 295 additions and 72 deletions

View file

@ -63,4 +63,11 @@ namespace Algorithm
while (it != set.end())
it = (p(*it) ? set.erase(it) : ++it);
}
template <typename List>
List sorted(List list)
{
list.sort();
return list;
}
}

View file

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

View file

@ -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<QStringList>)
, 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;

View file

@ -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<QStringList> m_excludedFileNames;
CachedSettingValue<QStringList> m_bannedIPs;
CachedSettingValue<ResumeDataStorageType> m_resumeDataStorageType;
CachedSettingValue<bool> m_isI2PEnabled;
CachedSettingValue<QString> m_I2PAddress;
CachedSettingValue<int> m_I2PPort;
CachedSettingValue<bool> m_I2PMixedMode;
bool m_isRestored = false;

View file

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

View file

@ -1572,8 +1572,8 @@ readme[0-9].txt: filter 'readme1.txt', 'readme2.txt' but not 'readme10.txt'.</st
<rect>
<x>0</x>
<y>0</y>
<width>377</width>
<height>756</height>
<width>504</width>
<height>848</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_20">
@ -1788,6 +1788,78 @@ readme[0-9].txt: filter 'readme1.txt', 'readme2.txt' but not 'readme10.txt'.</st
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupI2P">
<property name="title">
<string>I2P (experimental)</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout_40">
<item>
<layout class="QHBoxLayout" name="i2pLayout">
<item>
<widget class="QLabel" name="labelI2PHost">
<property name="text">
<string>Host:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="textI2PHost"/>
</item>
<item>
<widget class="QLabel" name="labelI2PPort">
<property name="text">
<string>Port:</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinI2PPort">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>65535</number>
</property>
<property name="value">
<number>8080</number>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_22">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="checkI2PMixed">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If &amp;quot;mixed mode&amp;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.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Mixed mode</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupProxy">
<property name="enabled">
@ -1839,6 +1911,19 @@ readme[0-9].txt: filter 'readme1.txt', 'readme2.txt' but not 'readme10.txt'.</st
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_24">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
@ -1859,7 +1944,7 @@ readme[0-9].txt: filter 'readme1.txt', 'readme2.txt' but not 'readme10.txt'.</st
<string>If checked, hostname lookups are done via the proxy</string>
</property>
<property name="text">
<string>Use proxy for hostname lookups</string>
<string>Perform hostname lookup via proxy</string>
</property>
</widget>
</item>

View file

@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* 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<int, QVariant> 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<PeerEndpoint> 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<int, QVariant> 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)
{

View file

@ -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<PeerEndpoint, QStandardItem *> m_peerItems;
QList<QStandardItem *> m_I2PPeerItems;
QHash<QHostAddress, QSet<QStandardItem *>> m_itemsByIP; // must be kept in sync with `m_peerItems`
bool m_resolveCountries;
};