Skip to content

Commit 55aaae0

Browse files
Merge pull request #3829 from HenrikJannsen/various-improvements
Add specific Address subclasses
2 parents e532e0a + e9c12fe commit 55aaae0

17 files changed

Lines changed: 484 additions & 104 deletions

File tree

apps/desktop/desktop/src/main/java/bisq/desktop/main/content/settings/network/NetworkSettingsController.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import bisq.bonded_roles.security_manager.difficulty_adjustment.DifficultyAdjustmentService;
2424
import bisq.common.application.DevMode;
2525
import bisq.common.network.Address;
26+
import bisq.common.network.ClearnetAddress;
2627
import bisq.common.network.TransportType;
2728
import bisq.common.observable.Pin;
2829
import bisq.desktop.ServiceProvider;
@@ -103,8 +104,8 @@ public void onActivate() {
103104

104105
Config i2pConfig = networkConfig.getConfig("configByTransportType.i2p");
105106
model.getUseEmbeddedI2PRouter().set(i2pConfig.getBoolean("embeddedRouter"));
106-
model.getI2cpAddress().set(new Address(i2pConfig.getString("i2cpHost"), i2pConfig.getInt("i2cpPort")));
107-
model.getBi2pGrpcAddress().set(new Address(i2pConfig.getString("bi2pGrpcHost"), i2pConfig.getInt("bi2pGrpcPort")));
107+
model.getI2cpAddress().set(new ClearnetAddress(i2pConfig.getString("i2cpHost"), i2pConfig.getInt("i2cpPort")));
108+
model.getBi2pGrpcAddress().set(new ClearnetAddress(i2pConfig.getString("bi2pGrpcHost"), i2pConfig.getInt("bi2pGrpcPort")));
108109

109110
subscriptions.add(EasyBind.subscribe(model.getI2cpAddress(), e -> onDataChanged()));
110111
subscriptions.add(EasyBind.subscribe(model.getBi2pGrpcAddress(), e -> onDataChanged()));

apps/desktop/desktop/src/main/java/bisq/desktop/main/content/settings/network/NetworkSettingsModel.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package bisq.desktop.main.content.settings.network;
1919

2020
import bisq.common.network.Address;
21+
import bisq.common.network.ClearnetAddress;
2122
import bisq.desktop.common.converters.AddressStringConverter;
2223
import bisq.desktop.common.converters.DoubleStringConverter;
2324
import bisq.desktop.common.view.Model;
@@ -45,8 +46,8 @@
4546
@Slf4j
4647
@Getter
4748
class NetworkSettingsModel implements Model {
48-
final static Address DEFAULT_I2CP_ADDRESS = new Address(DEFAULT_I2CP_HOST, DEFAULT_I2CP_PORT);
49-
final static Address DEFAULT_BI2P_GRPC_ADDRESS = new Address(DEFAULT_BI2P_GRPC_HOST, DEFAULT_BI2P_GRPC_PORT);
49+
final static Address DEFAULT_I2CP_ADDRESS = new ClearnetAddress(DEFAULT_I2CP_HOST, DEFAULT_I2CP_PORT);
50+
final static Address DEFAULT_BI2P_GRPC_ADDRESS = new ClearnetAddress(DEFAULT_BI2P_GRPC_HOST, DEFAULT_BI2P_GRPC_PORT);
5051

5152
private final ObjectProperty<TransportOption> selectedTransportOption = new SimpleObjectProperty<>();
5253
private final BooleanProperty useEmbeddedI2PRouter = new SimpleBooleanProperty();

common/src/main/java/bisq/common/network/Address.java

Lines changed: 74 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -19,45 +19,72 @@
1919

2020
import bisq.common.proto.NetworkProto;
2121
import bisq.common.util.StringUtils;
22-
import bisq.common.validation.NetworkDataValidation;
23-
import com.google.common.net.InetAddresses;
22+
import bisq.common.validation.NetworkPortValidation;
23+
import com.google.common.annotations.VisibleForTesting;
2424
import lombok.EqualsAndHashCode;
2525
import lombok.Getter;
2626
import lombok.extern.slf4j.Slf4j;
2727

28-
import java.util.Locale;
29-
import java.util.StringTokenizer;
30-
3128
import static com.google.common.base.Preconditions.checkArgument;
3229

30+
// We do not change the proto with subclasses to avoid breaking old clients.
3331
@Slf4j
3432
@EqualsAndHashCode
35-
@Getter
36-
public final class Address implements NetworkProto, Comparable<Address> {
37-
public static Address fromFullAddress(String fullAddress) {
33+
public abstract class Address implements NetworkProto, Comparable<Address> {
34+
35+
public static Address from(String host, int port) {
36+
if (TorAddress.isTorAddress(host)) {
37+
return new TorAddress(host, port);
38+
} else if (I2PAddress.isBase64Destination(host)) {
39+
return new I2PAddress(host, port);
40+
} else {
41+
return new ClearnetAddress(host, port);
42+
}
43+
}
44+
45+
public static Address fromFullAddress(String socketAddress) {
46+
String original = socketAddress;
47+
checkArgument(StringUtils.isNotEmpty(socketAddress), "SocketAddress must not be null or empty");
3848
try {
39-
fullAddress = fullAddress.replaceFirst("^https?://", "");
40-
StringTokenizer st = new StringTokenizer(fullAddress, ":");
41-
String hostToken = st.nextToken();
42-
String host = maybeConvertLocalHost(hostToken);
43-
checkArgument(st.hasMoreTokens(), "Full address need to contain the port after the ':'. fullAddress=" + fullAddress);
44-
String portToken = st.nextToken();
49+
socketAddress = removeProtocolPrefix(socketAddress.trim());
50+
checkArgument(!socketAddress.isEmpty(), "SocketAddress must not be empty");
51+
// IPv6 bracketed form: [host]:port
52+
if (socketAddress.startsWith("[")) {
53+
int end = socketAddress.indexOf(']');
54+
checkArgument(end > 0 && end + 1 < socketAddress.length() && socketAddress.charAt(end + 1) == ':',
55+
"Invalid IPv6 socket address, expected [host]:port");
56+
String hostToken = socketAddress.substring(1, end);
57+
String portToken = socketAddress.substring(end + 2).trim();
58+
int port = Integer.parseInt(portToken);
59+
return Address.from(hostToken, port);
60+
}
61+
62+
// IPv4/hostname: split at last colon
63+
checkArgument(socketAddress.split(":").length == 2, "Socket address must be of form host:port");
64+
int sep = socketAddress.lastIndexOf(':');
65+
checkArgument(sep > 0 && sep < socketAddress.length() - 1, "Socket address must be of form host:port");
66+
String hostToken = socketAddress.substring(0, sep).trim();
67+
String portToken = socketAddress.substring(sep + 1).trim();
4568
int port = Integer.parseInt(portToken);
46-
return new Address(host, port);
69+
return Address.from(hostToken, port);
4770
} catch (Exception e) {
48-
log.error("Could not resolve address from {}", fullAddress, e);
71+
log.error("Could not resolve address from {}", original, e);
4972
throw e;
5073
}
5174
}
5275

53-
private final String host;
54-
private final int port;
76+
@Getter
77+
protected final String host;
78+
@Getter
79+
protected final int port;
5580

56-
public Address(String host, int port) {
81+
protected Address(String host, int port) {
5782
try {
58-
this.host = maybeConvertLocalHost(host);
83+
checkArgument(StringUtils.isNotEmpty(host), "Host must not be null/blank");
84+
host = host.trim();
85+
checkArgument(NetworkPortValidation.isValid(port), "Invalid port: "+port);
86+
this.host = host;
5987
this.port = port;
60-
6188
verify();
6289
} catch (Exception e) {
6390
log.error("Could not resolve address from {}:{}", host, port, e);
@@ -70,18 +97,6 @@ public Address(String host, int port) {
7097
// Protobuf
7198
/* --------------------------------------------------------------------- */
7299

73-
@Override
74-
public void verify() {
75-
if (isTorAddress()) {
76-
NetworkDataValidation.validateText(host, 62);
77-
} else if (isClearNetAddress()) {
78-
NetworkDataValidation.validateText(host, 45);
79-
} else {
80-
// I2P
81-
NetworkDataValidation.validateText(host, 600);
82-
}
83-
}
84-
85100
@Override
86101
public bisq.common.protobuf.Address toProto(boolean serializeForHash) {
87102
return resolveProto(serializeForHash);
@@ -94,63 +109,52 @@ public bisq.common.protobuf.Address.Builder getBuilder(boolean serializeForHash)
94109
.setPort(port);
95110
}
96111

112+
abstract public TransportType getTransportType();
113+
97114
public static Address fromProto(bisq.common.protobuf.Address proto) {
98-
return new Address(proto.getHost(), proto.getPort());
115+
return Address.from(proto.getHost(), proto.getPort());
99116
}
100117

101118
public boolean isClearNetAddress() {
102-
return InetAddresses.isInetAddress(host);
119+
return this instanceof ClearnetAddress;
103120
}
104121

105122
public boolean isTorAddress() {
106-
return host.endsWith(".onion");
123+
return this instanceof TorAddress;
107124
}
108125

109126
public boolean isI2pAddress() {
110-
String lowerHost = host.toLowerCase(Locale.ROOT);
111-
// Base32: always 60 characters
112-
// Base64: ~512–528 characters
113-
return lowerHost.matches("^[a-z2-7]{52}\\.b32\\.i2p$")
114-
|| lowerHost.endsWith(".i2p")
115-
|| lowerHost.matches("^[a-z0-9~\\-=]{500,600}(:\\d{1,5})?$");
116-
}
117-
118-
public boolean isLocalhost() {
119-
return host.equals("127.0.0.1");
120-
}
121-
122-
public TransportType getTransportType() {
123-
if (isClearNetAddress()) {
124-
return TransportType.CLEAR;
125-
} else if (isTorAddress()) {
126-
return TransportType.TOR;
127-
} else if (isI2pAddress()) {
128-
return TransportType.I2P;
129-
} else {
130-
throw new IllegalArgumentException("Could not derive TransportType from address: " + getFullAddress());
131-
}
127+
return this instanceof I2PAddress;
132128
}
133129

134130
public String getFullAddress() {
135131
return host + ":" + port;
136132
}
137133

138134
@Override
139-
public String toString() {
140-
if (isLocalhost()) {
141-
return "[" + port + "]";
142-
} else {
143-
return StringUtils.truncate(host, 1000) + ":" + port;
144-
}
135+
public int compareTo(Address o) {
136+
return getFullAddress().compareTo(o.getFullAddress());
145137
}
146138

147-
private static String maybeConvertLocalHost(String host) {
148-
return host.equals("localhost") ? "127.0.0.1" : host;
149-
}
139+
@VisibleForTesting
140+
static String removeProtocolPrefix(String fullAddress) {
141+
// Match leading scheme
142+
// RFC 3986 scheme: ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
143+
String schemePattern = "^[a-zA-Z][a-zA-Z0-9+.-]*://";
144+
if (fullAddress.matches("(?i)^://.*")) {
145+
throw new IllegalArgumentException("Address has missing scheme before ://: " + fullAddress);
146+
}
150147

151-
@Override
152-
public int compareTo(Address o) {
153-
return getFullAddress().compareTo(o.getFullAddress());
148+
String withoutScheme = fullAddress.replaceFirst("(?i)" + schemePattern, "");
149+
// After removing scheme, ensure the remaining string is not another scheme
150+
if (withoutScheme.matches("(?i)^[a-zA-Z][a-zA-Z0-9+.-]*://.*")) {
151+
throw new IllegalArgumentException("Address has repeated scheme: " + fullAddress);
152+
}
153+
154+
if (withoutScheme.isEmpty()) {
155+
throw new IllegalArgumentException("Address is empty after removing scheme: " + fullAddress);
156+
}
157+
return withoutScheme;
154158
}
155159
}
156160

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* This file is part of Bisq.
3+
*
4+
* Bisq is free software: you can redistribute it and/or modify it
5+
* under the terms of the GNU Affero General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or (at
7+
* your option) any later version.
8+
*
9+
* Bisq is distributed in the hope that it will be useful, but WITHOUT
10+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
12+
* License for more details.
13+
*
14+
* You should have received a copy of the GNU Affero General Public License
15+
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
package bisq.common.network;
19+
20+
import bisq.common.application.DevMode;
21+
import bisq.common.validation.NetworkDataValidation;
22+
import bisq.common.validation.NetworkPortValidation;
23+
import com.google.common.net.InetAddresses;
24+
import lombok.EqualsAndHashCode;
25+
26+
import static com.google.common.base.Preconditions.checkArgument;
27+
28+
@EqualsAndHashCode(callSuper = true)
29+
public class ClearnetAddress extends Address {
30+
private static final int MIN_HOST_LENGTH = 2;
31+
// IPv4: 7-15 characters, IPv6: 2-39 characters, FQDNs can be up to 253
32+
private static final int MAX_HOST_LENGTH = 253;
33+
34+
public ClearnetAddress(String host, int port) {
35+
super(maybeConvertLocalHost(host), port);
36+
}
37+
38+
@Override
39+
public void verify() {
40+
checkArgument(NetworkPortValidation.isValid(port), "Invalid port: "+port);
41+
NetworkDataValidation.validateText(host, MIN_HOST_LENGTH, MAX_HOST_LENGTH);
42+
checkArgument(InetAddresses.isInetAddress(host), "Invalid inetAddress");
43+
}
44+
45+
@Override
46+
public TransportType getTransportType() {
47+
return TransportType.CLEAR;
48+
}
49+
50+
@Override
51+
public String toString() {
52+
return DevMode.isDevMode() && isLocalhost() ? "[" + port + "]" : host + ":" + port;
53+
}
54+
55+
public boolean isLocalhost() {
56+
return host.equals("127.0.0.1");
57+
}
58+
59+
private static String maybeConvertLocalHost(String host) {
60+
return host.equals("localhost") ? "127.0.0.1" : host;
61+
}
62+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* This file is part of Bisq.
3+
*
4+
* Bisq is free software: you can redistribute it and/or modify it
5+
* under the terms of the GNU Affero General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or (at
7+
* your option) any later version.
8+
*
9+
* Bisq is distributed in the hope that it will be useful, but WITHOUT
10+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
12+
* License for more details.
13+
*
14+
* You should have received a copy of the GNU Affero General Public License
15+
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
package bisq.common.network;
19+
20+
import bisq.common.util.StringUtils;
21+
import bisq.common.validation.NetworkDataValidation;
22+
import bisq.common.validation.NetworkPortValidation;
23+
import lombok.EqualsAndHashCode;
24+
import lombok.Getter;
25+
import lombok.extern.slf4j.Slf4j;
26+
27+
import java.util.Optional;
28+
import java.util.regex.Pattern;
29+
30+
import static com.google.common.base.Preconditions.checkArgument;
31+
32+
@Slf4j
33+
@EqualsAndHashCode(callSuper = true)
34+
public class I2PAddress extends Address {
35+
private static final int MIN_DESTINATION_LENGTH = 60;
36+
private static final int MAX_DESTINATION_LENGTH = 700;
37+
38+
private static final Pattern I2P_B32 = Pattern.compile("^[a-z2-7]{52}\\.b32\\.i2p$", Pattern.CASE_INSENSITIVE);
39+
// Base64: length vary by Signing Key Type used. 516 (DSA_SHA1) - 616 (ECDSA_SHA256_P256).
40+
// We use EdDSA_SHA512_Ed25519 which has a length of about 524 (+/-padding variance)
41+
// I2P source code check only for >= 516. To be on the safe side we use 516-700
42+
// `.i2p` suffix is supported in the base64 format. We use it only internally and there it is not expected.
43+
// Base 64 encoded string using the I2P alphabet A-Z, a-z, 0-9, -, ~ (See: https://docs.i2p-projekt.de/net/i2p/data/Base64.html)
44+
private static final Pattern I2P_B64 =
45+
Pattern.compile("^[A-Za-z0-9\\-~]{516,700}={0,2}$", Pattern.CASE_INSENSITIVE);
46+
47+
public static boolean isBase32Destination(String destination) {
48+
return I2P_B32.matcher(destination).matches();
49+
}
50+
51+
public static boolean isBase64Destination(String destination) {
52+
return I2P_B64.matcher(destination).matches();
53+
}
54+
55+
@Getter
56+
private Optional<String> destinationBase32 = Optional.empty();
57+
58+
public I2PAddress(String host, int port) {
59+
super(host, port);
60+
61+
if (!isBase64Destination(host)) {
62+
throw new IllegalArgumentException("I2P host must be in base 64 destination format. " + host);
63+
}
64+
}
65+
66+
public I2PAddress(String destinationBase64, String destinationBase32, int port) {
67+
super(destinationBase64, port);
68+
checkArgument(isBase32Destination(destinationBase32), "destinationBase32: " + destinationBase32);
69+
this.destinationBase32 = Optional.of(destinationBase32);
70+
}
71+
72+
@Override
73+
public void verify() {
74+
checkArgument(NetworkPortValidation.isValid(port), "Invalid port: " + port);
75+
NetworkDataValidation.validateText(host, MIN_DESTINATION_LENGTH, MAX_DESTINATION_LENGTH);
76+
checkArgument(isBase64Destination(host), "Host must be a I2P base 64 destination");
77+
}
78+
79+
@Override
80+
public TransportType getTransportType() {
81+
return TransportType.I2P;
82+
}
83+
84+
@Override
85+
public String toString() {
86+
String destination = destinationBase32.orElseGet(() -> StringUtils.truncate(host, 30) + ".i2p");
87+
return destination + ":" + port;
88+
}
89+
90+
public String getDestinationBase64() {
91+
return host;
92+
}
93+
}

0 commit comments

Comments
 (0)