Skip to content

Commit 765f608

Browse files
committed
Merge #430: Allow IPv6 in Proxy settings and moving validation out from the UI into the model/ interface
4dea7c9 qml, proxy: Allow IPv6 and move out UI validation (pablomartin4btc) Pull request description: The main purposes of this PR are: - Allow configure proxy server with IPv6 (after testing exhaustively in current `bitcoin-qt` [repo](https://github.com/bitcoin-core/gui/) and `bitcoind` - e.g. [IPv6 on Qt](bitcoin-core/gui#836 (comment)); [IP validation in Qt](bitcoin-core/gui#813)) - More importantly: moving the validation logic out from the UI (component/ control/ widget) into the back-end (model/ interfaces). It seemed currently in [Qt](https://github.com/bitcoin-core/gui/) all this logic (validation, network classes) was coupled to the UI since the beginning (more than [12y ago](https://github.com/bitcoin-core/gui/blame/c4443c2be141e5f45bb10376056f3083e97cde50/src/qt/optionsmodel.cpp)) instead of using interfaces (introduced much later) that would be the correct thing to do. Feedback and suggestions are very welcome and will help establish a foundation for leaving business logic out of the UI going forward. --- <details> <summary>In order to test it, if it's the first time you run <code>./src/qt/bitcoin-qt</code> you need to go thru the on-boarding flow (<i>start->next->next->next->next->connection settings->Proxy settings</i>) otherwise you need to go to the settings (top right gear icon) then <i>Connection->Proxy settings</i>.</summary> ![image](https://github.com/user-attachments/assets/9f10c4b7-b7e4-4807-b895-1c03d9c00037) </details> Before this change, only IPv4 address were allow in the value input, now also IPv6 can be entered. --- There are some [look and feel kind of issues](#430 (comment)) that will be handled on follow-ups (issues: #437 and #438). ACKs for top commit: D33r-Gee: tACK [4dea7c9](4dea7c9) on Ubuntu 20.04 works as expected. vasild: ACK 4dea7c9 johnny9: ACK 4dea7c9 Tree-SHA512: f633f5729f4ca2a8433560b15722bccd5938bfa55643cd3d925b75ed01006dbc7a56fe3a20766a6bb9e2bb601c8a3828c177de3cccc7728959eb48987838e90c
2 parents b4bebc3 + 4dea7c9 commit 765f608

File tree

6 files changed

+63
-34
lines changed

6 files changed

+63
-34
lines changed

src/interfaces/node.h

+9
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
#include <tuple>
2323
#include <vector>
2424

25+
static const char DEFAULT_PROXY_HOST[] = "127.0.0.1";
26+
static constexpr uint16_t DEFAULT_PROXY_PORT = 9050;
27+
2528
class BanMan;
2629
class CFeeRate;
2730
class CNodeStats;
@@ -126,6 +129,12 @@ class Node
126129
//! Get proxy.
127130
virtual bool getProxy(Network net, Proxy& proxy_info) = 0;
128131

132+
//! Get default proxy address.
133+
virtual std::string defaultProxyAddress() = 0;
134+
135+
//! Validate a proxy address.
136+
virtual bool validateProxyAddress(const std::string& addr_port) = 0;
137+
129138
//! Get number of connections.
130139
virtual size_t getNodeCount(ConnectionDirection flags) = 0;
131140

src/node/interfaces.cpp

+22
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
#include <uint256.h>
4646
#include <univalue.h>
4747
#include <util/check.h>
48+
#include <util/strencodings.h>
4849
#include <util/translation.h>
4950
#include <validation.h>
5051
#include <validationinterface.h>
@@ -169,6 +170,27 @@ class NodeImpl : public Node
169170
}
170171
void mapPort(bool use_upnp, bool use_natpmp) override { StartMapPort(use_upnp, use_natpmp); }
171172
bool getProxy(Network net, Proxy& proxy_info) override { return GetProxy(net, proxy_info); }
173+
std::string defaultProxyAddress() override
174+
{
175+
return std::string(DEFAULT_PROXY_HOST) + ":" + ToString(DEFAULT_PROXY_PORT);
176+
}
177+
bool validateProxyAddress(const std::string& addr_port) override
178+
{
179+
uint16_t port{0};
180+
std::string hostname;
181+
// First, attempt to split the input address into hostname and port components.
182+
// We call SplitHostPort to validate that a port is provided in addr_port.
183+
// If either splitting fails or port is zero (not specified), return false.
184+
if (!SplitHostPort(addr_port, port, hostname) || !port) return false;
185+
186+
// Create a service endpoint (CService) from the address and port.
187+
// If port is missing in addr_port, DEFAULT_PROXY_PORT is used as the fallback.
188+
CService serv(LookupNumeric(addr_port, DEFAULT_PROXY_PORT));
189+
190+
// Construct the Proxy with the service endpoint and return if it's valid
191+
Proxy addrProxy = Proxy(serv, true);
192+
return addrProxy.IsValid();
193+
}
172194
size_t getNodeCount(ConnectionDirection flags) override
173195
{
174196
return m_context->connman ? m_context->connman->GetNodeCount(flags) : 0;

src/qml/components/ProxySettings.qml

+17-6
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ import QtQuick.Controls 2.15
77
import QtQuick.Layouts 1.15
88
import "../controls"
99

10+
import org.bitcoincore.qt 1.0
11+
1012
ColumnLayout {
13+
property string ipAndPortHeader: qsTr("IP and Port")
14+
property string invalidIpError: qsTr("Invalid IP address or port format. Use '255.255.255.255:65535' or '[ffff::]:65535'")
15+
1116
spacing: 4
1217
Header {
1318
headerBold: true
@@ -41,14 +46,17 @@ ColumnLayout {
4146
Setting {
4247
id: defaultProxy
4348
Layout.fillWidth: true
44-
header: qsTr("IP and Port")
45-
errorText: qsTr("Invalid IP address or port format. Please use the format '255.255.255.255:65535'.")
49+
header: ipAndPortHeader
50+
errorText: invalidIpError
4651
state: !defaultProxyEnable.loadedItem.checked ? "DISABLED" : "FILLED"
4752
showErrorText: !defaultProxy.loadedItem.validInput && defaultProxyEnable.loadedItem.checked
4853
actionItem: IPAddressValueInput {
4954
parentState: defaultProxy.state
50-
description: "127.0.0.1:9050"
55+
description: nodeModel.defaultProxyAddress()
5156
activeFocusOnTab: true
57+
onTextChanged: {
58+
validInput = nodeModel.validateProxyAddress(text);
59+
}
5260
}
5361
onClicked: {
5462
loadedItem.filled = true
@@ -89,14 +97,17 @@ ColumnLayout {
8997
Setting {
9098
id: torProxy
9199
Layout.fillWidth: true
92-
header: qsTr("IP and Port")
93-
errorText: qsTr("Invalid IP address or port format. Please use the format '255.255.255.255:65535'.")
100+
header: ipAndPortHeader
101+
errorText: invalidIpError
94102
state: !torProxyEnable.loadedItem.checked ? "DISABLED" : "FILLED"
95103
showErrorText: !torProxy.loadedItem.validInput && torProxyEnable.loadedItem.checked
96104
actionItem: IPAddressValueInput {
97105
parentState: torProxy.state
98-
description: "127.0.0.1:9050"
106+
description: nodeModel.defaultProxyAddress()
99107
activeFocusOnTab: true
108+
onTextChanged: {
109+
validInput = nodeModel.validateProxyAddress(text);
110+
}
100111
}
101112
onClicked: {
102113
loadedItem.filled = true

src/qml/controls/IPAddressValueInput.qml

+2-28
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ TextInput {
1616
property bool validInput: true
1717
enabled: true
1818
state: root.parentState
19-
validator: RegExpValidator { regExp: /[0-9.:]*/ } // Allow only digits, dots, and colons
19+
validator: RegularExpressionValidator { regularExpression: /^[\][0-9a-f.:]+$/i } // Allow only IPv4/ IPv6 chars
2020

21-
maximumLength: 21
21+
maximumLength: 47
2222

2323
states: [
2424
State {
@@ -53,30 +53,4 @@ TextInput {
5353
Behavior on color {
5454
ColorAnimation { duration: 150 }
5555
}
56-
57-
function isValidIPPort(input)
58-
{
59-
var parts = input.split(":");
60-
if (parts.length !== 2) return false;
61-
if (parts[1].length === 0) return false; // port part is empty
62-
var ipAddress = parts[0];
63-
var ipAddressParts = ipAddress.split(".");
64-
if (ipAddressParts.length !== 4) return false;
65-
for (var i = 0; (i < ipAddressParts.length); i++) {
66-
if (ipAddressParts[i].length === 0) return false; // ip group number part is empty
67-
if (parseInt(ipAddressParts[i]) > 255) return false;
68-
}
69-
var port = parseInt(parts[1]);
70-
if (port < 1 || port > 65535) return false;
71-
return true;
72-
}
73-
74-
// Connections element to ensure validation on editing finished
75-
Connections {
76-
target: root
77-
function onTextChanged() {
78-
// Validate the input whenever editing is finished
79-
validInput = isValidIPPort(root.text);
80-
}
81-
}
8256
}

src/qml/models/nodemodel.cpp

+10
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,13 @@ void NodeModel::ConnectToNumConnectionsChangedSignal()
166166
setNumOutboundPeers(new_num_peers.outbound_full_relay + new_num_peers.block_relay);
167167
});
168168
}
169+
170+
bool NodeModel::validateProxyAddress(QString address_port)
171+
{
172+
return m_node.validateProxyAddress(address_port.toStdString());
173+
}
174+
175+
QString NodeModel::defaultProxyAddress()
176+
{
177+
return QString::fromStdString(m_node.defaultProxyAddress());
178+
}

src/qml/models/nodemodel.h

+3
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ class NodeModel : public QObject
6262
void startShutdownPolling();
6363
void stopShutdownPolling();
6464

65+
Q_INVOKABLE bool validateProxyAddress(QString addr_port);
66+
Q_INVOKABLE QString defaultProxyAddress();
67+
6568
public Q_SLOTS:
6669
void initializeResult(bool success, interfaces::BlockAndHeaderTipInfo tip_info);
6770

0 commit comments

Comments
 (0)