1919
2020import bisq .common .proto .NetworkProto ;
2121import 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 ;
2424import lombok .EqualsAndHashCode ;
2525import lombok .Getter ;
2626import lombok .extern .slf4j .Slf4j ;
2727
28- import java .util .Locale ;
29- import java .util .StringTokenizer ;
30-
3128import 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
0 commit comments