Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Persist Proxy settings #433

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/interfaces/node.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
#include <tuple>
#include <vector>

static const char DEFAULT_PROXY_HOST[] = "127.0.0.1";
static constexpr uint16_t DEFAULT_PROXY_PORT = 9050;

class BanMan;
class CFeeRate;
class CNodeStats;
Expand Down Expand Up @@ -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;

Expand Down
22 changes: 22 additions & 0 deletions src/node/interfaces.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
#include <uint256.h>
#include <univalue.h>
#include <util/check.h>
#include <util/strencodings.h>
#include <util/translation.h>
#include <validation.h>
#include <validationinterface.h>
Expand Down Expand Up @@ -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;
Expand Down
31 changes: 25 additions & 6 deletions src/qml/components/ProxySettings.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -30,7 +35,9 @@ ColumnLayout {
} else {
defaultProxy.state = "FILLED"
}
optionsModel.setIsProxySet(checked)
}
checked: optionsModel.isProxySet
}
onClicked: {
loadedItem.toggle()
Expand All @@ -41,14 +48,19 @@ 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"
text: optionsModel.proxyAddress
activeFocusOnTab: true
onTextChanged: {
if (validInput = nodeModel.validateProxyAddress(text)) {
optionsModel.setProxyAddress(text);
}
}
}
onClicked: {
loadedItem.filled = true
Expand Down Expand Up @@ -78,7 +90,9 @@ ColumnLayout {
} else {
torProxy.state = "FILLED"
}
optionsModel.setIsTorProxySet(checked)
}
checked: optionsModel.isTorProxySet
}
onClicked: {
loadedItem.toggle()
Expand All @@ -89,14 +103,19 @@ 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"
text: optionsModel.torProxyAddress
activeFocusOnTab: true
onTextChanged: {
if (validInput = nodeModel.validateProxyAddress(text)) {
optionsModel.setTorProxyAddress(text);
}
}
}
onClicked: {
loadedItem.filled = true
Expand Down
30 changes: 2 additions & 28 deletions src/qml/controls/IPAddressValueInput.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}
}
}
10 changes: 10 additions & 0 deletions src/qml/models/nodemodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
3 changes: 3 additions & 0 deletions src/qml/models/nodemodel.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
98 changes: 98 additions & 0 deletions src/qml/models/options_model.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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;
}
24 changes: 24 additions & 0 deletions src/qml/models/options_model.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
Expand All @@ -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;
};
Expand Down
Loading