From 4dea7c91a537710f3582d57b44f8908767ebb944 Mon Sep 17 00:00:00 2001 From: pablomartin4btc Date: Thu, 31 Oct 2024 10:29:08 -0300 Subject: [PATCH 1/2] qml, proxy: Allow IPv6 and move out UI validation Allowed to configure the proxy with an IPv6 address and moved the validation out from the UI/ control to the node interface. --- src/interfaces/node.h | 9 +++++++ src/node/interfaces.cpp | 22 +++++++++++++++++ src/qml/components/ProxySettings.qml | 23 +++++++++++++----- src/qml/controls/IPAddressValueInput.qml | 30 ++---------------------- src/qml/models/nodemodel.cpp | 10 ++++++++ src/qml/models/nodemodel.h | 3 +++ 6 files changed, 63 insertions(+), 34 deletions(-) diff --git a/src/interfaces/node.h b/src/interfaces/node.h index f6c79f0c1b..1aab36927b 100644 --- a/src/interfaces/node.h +++ b/src/interfaces/node.h @@ -22,6 +22,9 @@ #include #include +static const char DEFAULT_PROXY_HOST[] = "127.0.0.1"; +static constexpr uint16_t DEFAULT_PROXY_PORT = 9050; + class BanMan; class CFeeRate; class CNodeStats; @@ -126,6 +129,12 @@ class Node //! Get proxy. virtual bool getProxy(Network net, Proxy& proxy_info) = 0; + //! Get default proxy address. + virtual std::string defaultProxyAddress() = 0; + + //! Validate a proxy address. + virtual bool validateProxyAddress(const std::string& addr_port) = 0; + //! Get number of connections. virtual size_t getNodeCount(ConnectionDirection flags) = 0; diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index f1fe42206e..3dd867cb8d 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -45,6 +45,7 @@ #include #include #include +#include #include #include #include @@ -169,6 +170,27 @@ class NodeImpl : public Node } void mapPort(bool use_upnp, bool use_natpmp) override { StartMapPort(use_upnp, use_natpmp); } bool getProxy(Network net, Proxy& proxy_info) override { return GetProxy(net, proxy_info); } + std::string defaultProxyAddress() override + { + return std::string(DEFAULT_PROXY_HOST) + ":" + ToString(DEFAULT_PROXY_PORT); + } + bool validateProxyAddress(const std::string& addr_port) override + { + uint16_t port{0}; + std::string hostname; + // First, attempt to split the input address into hostname and port components. + // We call SplitHostPort to validate that a port is provided in addr_port. + // If either splitting fails or port is zero (not specified), return false. + if (!SplitHostPort(addr_port, port, hostname) || !port) return false; + + // Create a service endpoint (CService) from the address and port. + // If port is missing in addr_port, DEFAULT_PROXY_PORT is used as the fallback. + CService serv(LookupNumeric(addr_port, DEFAULT_PROXY_PORT)); + + // Construct the Proxy with the service endpoint and return if it's valid + Proxy addrProxy = Proxy(serv, true); + return addrProxy.IsValid(); + } size_t getNodeCount(ConnectionDirection flags) override { return m_context->connman ? m_context->connman->GetNodeCount(flags) : 0; diff --git a/src/qml/components/ProxySettings.qml b/src/qml/components/ProxySettings.qml index bc1467d0ff..6af147dd09 100644 --- a/src/qml/components/ProxySettings.qml +++ b/src/qml/components/ProxySettings.qml @@ -7,7 +7,12 @@ import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 import "../controls" +import org.bitcoincore.qt 1.0 + ColumnLayout { + property string ipAndPortHeader: qsTr("IP and Port") + property string invalidIpError: qsTr("Invalid IP address or port format. Use '255.255.255.255:65535' or '[ffff::]:65535'") + spacing: 4 Header { headerBold: true @@ -41,14 +46,17 @@ ColumnLayout { Setting { id: defaultProxy Layout.fillWidth: true - header: qsTr("IP and Port") - errorText: qsTr("Invalid IP address or port format. Please use the format '255.255.255.255:65535'.") + header: ipAndPortHeader + errorText: invalidIpError state: !defaultProxyEnable.loadedItem.checked ? "DISABLED" : "FILLED" showErrorText: !defaultProxy.loadedItem.validInput && defaultProxyEnable.loadedItem.checked actionItem: IPAddressValueInput { parentState: defaultProxy.state - description: "127.0.0.1:9050" + description: nodeModel.defaultProxyAddress() activeFocusOnTab: true + onTextChanged: { + validInput = nodeModel.validateProxyAddress(text); + } } onClicked: { loadedItem.filled = true @@ -89,14 +97,17 @@ ColumnLayout { Setting { id: torProxy Layout.fillWidth: true - header: qsTr("IP and Port") - errorText: qsTr("Invalid IP address or port format. Please use the format '255.255.255.255:65535'.") + header: ipAndPortHeader + errorText: invalidIpError state: !torProxyEnable.loadedItem.checked ? "DISABLED" : "FILLED" showErrorText: !torProxy.loadedItem.validInput && torProxyEnable.loadedItem.checked actionItem: IPAddressValueInput { parentState: torProxy.state - description: "127.0.0.1:9050" + description: nodeModel.defaultProxyAddress() activeFocusOnTab: true + onTextChanged: { + validInput = nodeModel.validateProxyAddress(text); + } } onClicked: { loadedItem.filled = true diff --git a/src/qml/controls/IPAddressValueInput.qml b/src/qml/controls/IPAddressValueInput.qml index d2ce4c7f16..01e9aa9021 100644 --- a/src/qml/controls/IPAddressValueInput.qml +++ b/src/qml/controls/IPAddressValueInput.qml @@ -16,9 +16,9 @@ TextInput { property bool validInput: true enabled: true state: root.parentState - validator: RegExpValidator { regExp: /[0-9.:]*/ } // Allow only digits, dots, and colons + validator: RegularExpressionValidator { regularExpression: /^[\][0-9a-f.:]+$/i } // Allow only IPv4/ IPv6 chars - maximumLength: 21 + maximumLength: 47 states: [ State { @@ -53,30 +53,4 @@ TextInput { Behavior on color { ColorAnimation { duration: 150 } } - - function isValidIPPort(input) - { - var parts = input.split(":"); - if (parts.length !== 2) return false; - if (parts[1].length === 0) return false; // port part is empty - var ipAddress = parts[0]; - var ipAddressParts = ipAddress.split("."); - if (ipAddressParts.length !== 4) return false; - for (var i = 0; (i < ipAddressParts.length); i++) { - if (ipAddressParts[i].length === 0) return false; // ip group number part is empty - if (parseInt(ipAddressParts[i]) > 255) return false; - } - var port = parseInt(parts[1]); - if (port < 1 || port > 65535) return false; - return true; - } - - // Connections element to ensure validation on editing finished - Connections { - target: root - function onTextChanged() { - // Validate the input whenever editing is finished - validInput = isValidIPPort(root.text); - } - } } diff --git a/src/qml/models/nodemodel.cpp b/src/qml/models/nodemodel.cpp index 521e5fa1c5..8ccc532016 100644 --- a/src/qml/models/nodemodel.cpp +++ b/src/qml/models/nodemodel.cpp @@ -166,3 +166,13 @@ void NodeModel::ConnectToNumConnectionsChangedSignal() setNumOutboundPeers(new_num_peers.outbound_full_relay + new_num_peers.block_relay); }); } + +bool NodeModel::validateProxyAddress(QString address_port) +{ + return m_node.validateProxyAddress(address_port.toStdString()); +} + +QString NodeModel::defaultProxyAddress() +{ + return QString::fromStdString(m_node.defaultProxyAddress()); +} diff --git a/src/qml/models/nodemodel.h b/src/qml/models/nodemodel.h index a17f9b0833..8603c44d36 100644 --- a/src/qml/models/nodemodel.h +++ b/src/qml/models/nodemodel.h @@ -62,6 +62,9 @@ class NodeModel : public QObject void startShutdownPolling(); void stopShutdownPolling(); + Q_INVOKABLE bool validateProxyAddress(QString addr_port); + Q_INVOKABLE QString defaultProxyAddress(); + public Q_SLOTS: void initializeResult(bool success, interfaces::BlockAndHeaderTipInfo tip_info); From 8ef413bee3bcbd797c83adac46a455b80a3b6049 Mon Sep 17 00:00:00 2001 From: pablomartin4btc Date: Thu, 14 Nov 2024 15:23:01 -0300 Subject: [PATCH 2/2] qml, proxy: Persist proxy settings Addding the logic to persist the ip address and port values of both the default proxy and the tor proxy configuration into settings.js. --- src/qml/components/ProxySettings.qml | 16 +++-- src/qml/models/options_model.cpp | 98 ++++++++++++++++++++++++++++ src/qml/models/options_model.h | 24 +++++++ 3 files changed, 134 insertions(+), 4 deletions(-) diff --git a/src/qml/components/ProxySettings.qml b/src/qml/components/ProxySettings.qml index 6af147dd09..aa53efde4d 100644 --- a/src/qml/components/ProxySettings.qml +++ b/src/qml/components/ProxySettings.qml @@ -35,7 +35,9 @@ ColumnLayout { } else { defaultProxy.state = "FILLED" } + optionsModel.setIsProxySet(checked) } + checked: optionsModel.isProxySet } onClicked: { loadedItem.toggle() @@ -52,10 +54,12 @@ ColumnLayout { showErrorText: !defaultProxy.loadedItem.validInput && defaultProxyEnable.loadedItem.checked actionItem: IPAddressValueInput { parentState: defaultProxy.state - description: nodeModel.defaultProxyAddress() + text: optionsModel.proxyAddress activeFocusOnTab: true onTextChanged: { - validInput = nodeModel.validateProxyAddress(text); + if (validInput = nodeModel.validateProxyAddress(text)) { + optionsModel.setProxyAddress(text); + } } } onClicked: { @@ -86,7 +90,9 @@ ColumnLayout { } else { torProxy.state = "FILLED" } + optionsModel.setIsTorProxySet(checked) } + checked: optionsModel.isTorProxySet } onClicked: { loadedItem.toggle() @@ -103,10 +109,12 @@ ColumnLayout { showErrorText: !torProxy.loadedItem.validInput && torProxyEnable.loadedItem.checked actionItem: IPAddressValueInput { parentState: torProxy.state - description: nodeModel.defaultProxyAddress() + text: optionsModel.torProxyAddress activeFocusOnTab: true onTextChanged: { - validInput = nodeModel.validateProxyAddress(text); + if (validInput = nodeModel.validateProxyAddress(text)) { + optionsModel.setTorProxyAddress(text); + } } } onClicked: { diff --git a/src/qml/models/options_model.cpp b/src/qml/models/options_model.cpp index 9e95152311..77771f89e0 100644 --- a/src/qml/models/options_model.cpp +++ b/src/qml/models/options_model.cpp @@ -45,6 +45,14 @@ OptionsQmlModel::OptionsQmlModel(interfaces::Node& node, bool is_onboarded) m_upnp = SettingToBool(m_node.getPersistentSetting("upnp"), DEFAULT_UPNP); m_dataDir = getDefaultDataDirString(); + + m_proxy_address = QString::fromStdString(SettingToString(m_node.getPersistentSetting("proxy"), "")); + + m_is_proxy_set = evaluateIfProxyIsSet(); + + m_tor_proxy_address = QString::fromStdString(SettingToString(m_node.getPersistentSetting("onion"), "")); + + m_is_tor_proxy_set = evaluateIfTorProxyIsSet(); } void OptionsQmlModel::setDbcacheSizeMiB(int new_dbcache_size_mib) @@ -225,5 +233,95 @@ void OptionsQmlModel::onboard() if (m_upnp) { m_node.updateRwSetting("upnp", m_upnp); } + if (m_is_proxy_set && !m_proxy_address.isEmpty() && m_node.validateProxyAddress(m_proxy_address.toStdString())) { + m_node.updateRwSetting("proxy", m_proxy_address.toStdString()); + } + if (m_is_tor_proxy_set && !m_tor_proxy_address.isEmpty() && m_node.validateProxyAddress(m_tor_proxy_address.toStdString())) { + m_node.updateRwSetting("onion", m_tor_proxy_address.toStdString()); + } m_onboarded = true; } + +void OptionsQmlModel::setProxyAddress(QString new_proxy_address) +{ + if (new_proxy_address != m_proxy_address) { + m_proxy_address = new_proxy_address; + if (m_onboarded && m_node.validateProxyAddress(new_proxy_address.toStdString())) { + m_node.updateRwSetting("proxy", new_proxy_address.toStdString()); + } + Q_EMIT proxyAddressChanged(new_proxy_address); + } +} + +void OptionsQmlModel::setIsProxySet(bool is_set) +{ + if (is_set != m_is_proxy_set) { + m_is_proxy_set = is_set; + if (m_onboarded && m_is_proxy_set && m_node.validateProxyAddress(m_proxy_address.toStdString())) { + m_node.updateRwSetting("proxy", m_proxy_address.toStdString()); + } + if (!m_is_proxy_set) { + m_node.updateRwSetting("proxy", {}); + } + Q_EMIT isProxySetChanged(is_set); + } +} + +bool OptionsQmlModel::evaluateIfProxyIsSet() +{ + bool proxyHasBeenSet; + + if (!m_proxy_address.isEmpty()) { + if (!m_node.validateProxyAddress(m_proxy_address.toStdString())) { + m_proxy_address = ""; + } + } + + proxyHasBeenSet = !m_proxy_address.isEmpty(); + if (!proxyHasBeenSet) { + m_proxy_address = QString::fromStdString(m_node.defaultProxyAddress()); + } + return proxyHasBeenSet; +} + +void OptionsQmlModel::setTorProxyAddress(QString new_proxy_address) +{ + if (new_proxy_address != m_tor_proxy_address) { + m_tor_proxy_address = new_proxy_address; + if (m_onboarded && m_node.validateProxyAddress(new_proxy_address.toStdString())) { + m_node.updateRwSetting("onion", new_proxy_address.toStdString()); + } + Q_EMIT torProxyAddressChanged(new_proxy_address); + } +} + +void OptionsQmlModel::setIsTorProxySet(bool is_set) +{ + if (is_set != m_is_tor_proxy_set) { + m_is_tor_proxy_set = is_set; + if (m_onboarded && m_is_proxy_set && m_node.validateProxyAddress(m_tor_proxy_address.toStdString())) { + m_node.updateRwSetting("onion", m_tor_proxy_address.toStdString()); + } + if (!m_is_proxy_set) { + m_node.updateRwSetting("onion", {}); + } + Q_EMIT isTorProxySetChanged(is_set); + } +} + +bool OptionsQmlModel::evaluateIfTorProxyIsSet() +{ + bool proxyHasBeenSet; + + if (!m_tor_proxy_address.isEmpty()) { + if (!m_node.validateProxyAddress(m_tor_proxy_address.toStdString())) { + m_tor_proxy_address = ""; + } + } + + proxyHasBeenSet = !m_tor_proxy_address.isEmpty(); + if (!proxyHasBeenSet) { + m_tor_proxy_address = QString::fromStdString(m_node.defaultProxyAddress()); + } + return proxyHasBeenSet; +} diff --git a/src/qml/models/options_model.h b/src/qml/models/options_model.h index 459d40b574..ab3db0a9ea 100644 --- a/src/qml/models/options_model.h +++ b/src/qml/models/options_model.h @@ -37,6 +37,14 @@ class OptionsQmlModel : public QObject Q_PROPERTY(QString dataDir READ dataDir WRITE setDataDir NOTIFY dataDirChanged) Q_PROPERTY(QString getDefaultDataDirString READ getDefaultDataDirString CONSTANT) Q_PROPERTY(QUrl getDefaultDataDirectory READ getDefaultDataDirectory CONSTANT) + Q_PROPERTY(QString proxyAddress READ proxyAddress WRITE setProxyAddress NOTIFY proxyAddressChanged) + Q_PROPERTY(bool isProxySet READ isProxySet WRITE setIsProxySet NOTIFY isProxySetChanged) + Q_PROPERTY(QString torProxyAddress READ torProxyAddress WRITE setTorProxyAddress NOTIFY proxyAddressChanged) + Q_PROPERTY(bool isTorProxySet READ isTorProxySet WRITE setIsTorProxySet NOTIFY isTorProxySetChanged) + +protected: + bool evaluateIfProxyIsSet(); + bool evaluateIfTorProxyIsSet(); public: explicit OptionsQmlModel(interfaces::Node& node, bool is_onboarded); @@ -67,6 +75,14 @@ class OptionsQmlModel : public QObject QUrl getDefaultDataDirectory(); Q_INVOKABLE bool setCustomDataDirArgs(QString path); Q_INVOKABLE QString getCustomDataDirString(); + QString proxyAddress() const { return m_proxy_address; } + Q_INVOKABLE void setProxyAddress(QString new_proxy_address); + bool isProxySet() const { return m_is_proxy_set; } + Q_INVOKABLE void setIsProxySet(bool is_set); + QString torProxyAddress() const { return m_tor_proxy_address; } + Q_INVOKABLE void setTorProxyAddress(QString new_proxy_address); + bool isTorProxySet() const { return m_is_tor_proxy_set; } + Q_INVOKABLE void setIsTorProxySet(bool is_set); public Q_SLOTS: void setCustomDataDirString(const QString &new_custom_datadir_string) { @@ -85,6 +101,10 @@ public Q_SLOTS: void upnpChanged(bool new_upnp); void customDataDirStringChanged(QString new_custom_datadir_string); void dataDirChanged(QString new_data_dir); + void proxyAddressChanged(QString new_proxy_address); + void isProxySetChanged(bool is_set); + void torProxyAddressChanged(QString new_proxy_address); + void isTorProxySetChanged(bool is_set); private: interfaces::Node& m_node; @@ -105,6 +125,10 @@ public Q_SLOTS: bool m_upnp; QString m_custom_datadir_string; QString m_dataDir; + QString m_proxy_address; + bool m_is_proxy_set; + QString m_tor_proxy_address; + bool m_is_tor_proxy_set; common::SettingsValue pruneSetting() const; };