Skip to content

Commit 59e8f3e

Browse files
committed
MultiAddress now supports Onion addresses, and most of the tests from the Go implementation.
1 parent 7d6cd63 commit 59e8f3e

File tree

4 files changed

+248
-4
lines changed

4 files changed

+248
-4
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package org.ipfs.api;
2+
3+
import java.math.*;
4+
5+
/**
6+
* Based on RFC 4648
7+
*/
8+
public class Base32 {
9+
private static final String ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
10+
private static final BigInteger BASE = BigInteger.valueOf(32);
11+
12+
public static String encode(byte[] input) {
13+
// TODO: This could be a lot more efficient.
14+
BigInteger bi = new BigInteger(1, input);
15+
StringBuffer s = new StringBuffer();
16+
while (bi.compareTo(BASE) >= 0) {
17+
BigInteger mod = bi.mod(BASE);
18+
s.insert(0, ALPHABET.charAt(mod.intValue()));
19+
bi = bi.subtract(mod).divide(BASE);
20+
}
21+
s.insert(0, ALPHABET.charAt(bi.intValue()));
22+
// Convert leading zeros too.
23+
for (byte anInput : input) {
24+
if (anInput == 0)
25+
s.insert(0, ALPHABET.charAt(0));
26+
else
27+
break;
28+
}
29+
return s.toString();
30+
}
31+
32+
public static byte[] decode(String input) {
33+
byte[] bytes = decodeToBigInteger(input).toByteArray();
34+
// We may have got one more byte than we wanted, if the high bit of the next-to-last byte was not zero. This
35+
// is because BigIntegers are represented with twos-compliment notation, thus if the high bit of the last
36+
// byte happens to be 1 another 8 zero bits will be added to ensure the number parses as positive. Detect
37+
// that case here and chop it off.
38+
boolean stripSignByte = bytes.length > 1 && bytes[0] == 0 && bytes[1] < 0;
39+
// Count the leading zeros, if any.
40+
int leadingZeros = 0;
41+
for (int i = 0; input.charAt(i) == ALPHABET.charAt(0); i++) {
42+
leadingZeros++;
43+
}
44+
// Now cut/pad correctly. Java 6 has a convenience for this, but Android can't use it.
45+
byte[] tmp = new byte[bytes.length - (stripSignByte ? 1 : 0) + leadingZeros];
46+
System.arraycopy(bytes, stripSignByte ? 1 : 0, tmp, leadingZeros, tmp.length - leadingZeros);
47+
return tmp;
48+
}
49+
50+
public static BigInteger decodeToBigInteger(String input) {
51+
BigInteger bi = BigInteger.valueOf(0);
52+
// Work backwards through the string.
53+
for (int i = input.length() - 1; i >= 0; i--) {
54+
int alphaIndex = ALPHABET.indexOf(input.charAt(i));
55+
if (alphaIndex == -1) {
56+
throw new IllegalStateException("Illegal character " + input.charAt(i) + " at " + i);
57+
}
58+
bi = bi.add(BigInteger.valueOf(alphaIndex).multiply(BASE.pow(input.length() - 1 - i)));
59+
}
60+
return bi;
61+
}
62+
}

src/main/java/org/ipfs/api/Protocol.java

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ enum Type {
1818
UDT(302, 0, "udt"),
1919
IPFS(421, LENGTH_PREFIXED_VAR_SIZE, "ipfs"),
2020
HTTPS(443, 0, "https"),
21-
HTTP(480, 0, "http");
21+
HTTP(480, 0, "http"),
22+
ONION(444, 80, "onion");
2223

2324
public final int code, size;
2425
public final String name;
@@ -77,8 +78,8 @@ public byte[] addressToBytes(String addr) {
7778
case DCCP:
7879
case SCTP:
7980
int x = Integer.parseInt(addr);
80-
if (x > 65536)
81-
throw new IllegalStateException("Failed to parse "+type.name+" address "+addr + " (> 65536");
81+
if (x > 65535)
82+
throw new IllegalStateException("Failed to parse "+type.name+" address "+addr + " (> 65535");
8283
return new byte[]{(byte)(x >>8), (byte)x};
8384
case IPFS:
8485
Multihash hash = Multihash.fromBase58(addr);
@@ -89,6 +90,29 @@ public byte[] addressToBytes(String addr) {
8990
bout.write(varint);
9091
bout.write(hashBytes);
9192
return bout.toByteArray();
93+
case ONION:
94+
String[] split = addr.split(":");
95+
if (split.length != 2)
96+
throw new IllegalStateException("Onion address needs a port: "+addr);
97+
98+
// onion address without the ".onion" substring
99+
if (split[0].length() != 16)
100+
throw new IllegalStateException("failed to parse "+name()+" addr: "+addr+" not a Tor onion address.");
101+
102+
byte[] onionHostBytes = Base32.decode(split[0].toUpperCase());
103+
int port = Integer.parseInt(split[1]);
104+
if (port > 65535)
105+
throw new IllegalStateException("Port is > 65535: "+port);
106+
107+
if (port < 1)
108+
throw new IllegalStateException("Port is < 1: "+port);
109+
110+
ByteArrayOutputStream b = new ByteArrayOutputStream();
111+
DataOutputStream dout = new DataOutputStream(b);
112+
dout.write(onionHostBytes);
113+
dout.writeShort(port);
114+
dout.flush();
115+
return b.toByteArray();
92116
}
93117
} catch (IOException e) {
94118
throw new RuntimeException(e);
@@ -117,6 +141,11 @@ public String readAddress(InputStream in) throws IOException {
117141
buf = new byte[sizeForAddress];
118142
in.read(buf);
119143
return new Multihash(buf).toBase58();
144+
case ONION:
145+
byte[] host = new byte[10];
146+
in.read(host);
147+
String port = Integer.toString((in.read() << 8) | (in.read()));
148+
return Base32.encode(host)+":"+port;
120149
}
121150
throw new IllegalStateException("Unimplemented protocl type: "+type.name);
122151
}

src/test/java/org/ipfs/api/Test.java renamed to src/test/java/org/ipfs/api/APITests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import static org.junit.Assert.assertTrue;
88

9-
public class Test {
9+
public class APITests {
1010

1111
IPFS ipfs = new IPFS(new MultiAddress("/ip4/127.0.0.1/tcp/5001"));
1212
@org.junit.Test
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
package org.ipfs.api;
2+
3+
import org.junit.*;
4+
5+
import java.io.*;
6+
import java.util.*;
7+
import java.util.function.*;
8+
import java.util.stream.*;
9+
10+
public class MultiAddressTests {
11+
12+
@Test
13+
public void fails() {
14+
List<String> parsed = Stream.of(
15+
"/ip4",
16+
"/ip4/::1",
17+
"/ip4/fdpsofodsajfdoisa",
18+
"/ip6",
19+
"/udp",
20+
"/tcp",
21+
"/sctp",
22+
"/udp/65536",
23+
"/tcp/65536",
24+
"/onion/9imaq4ygg2iegci7:80",
25+
"/onion/aaimaq4ygg2iegci7:80",
26+
"/onion/timaq4ygg2iegci7:0",
27+
"/onion/timaq4ygg2iegci7:-1",
28+
"/onion/timaq4ygg2iegci7",
29+
"/onion/timaq4ygg2iegci@:666",
30+
"/udp/1234/sctp",
31+
"/udp/1234/udt/1234",
32+
"/udp/1234/utp/1234",
33+
"/ip4/127.0.0.1/udp/jfodsajfidosajfoidsa",
34+
"/ip4/127.0.0.1/udp",
35+
"/ip4/127.0.0.1/tcp/jfodsajfidosajfoidsa",
36+
"/ip4/127.0.0.1/tcp",
37+
"/ip4/127.0.0.1/ipfs",
38+
"/ip4/127.0.0.1/ipfs/tcp"
39+
).flatMap(s -> {
40+
try {
41+
new MultiAddress(s);
42+
return Stream.of(s);
43+
} catch (Exception e) {
44+
return Stream.empty();
45+
}
46+
}).collect(Collectors.toList());
47+
if (parsed.size() > 0)
48+
throw new IllegalStateException("Parsed invalid MultiAddresses: "+parsed);
49+
}
50+
51+
@Test
52+
public void succeeds() {
53+
List<String> failed = Stream.of(
54+
"/ip4/1.2.3.4",
55+
"/ip4/0.0.0.0",
56+
"/ip6/::1",
57+
"/ip6/2601:9:4f81:9700:803e:ca65:66e8:c21",
58+
"/onion/timaq4ygg2iegci7:1234",
59+
"/onion/timaq4ygg2iegci7:80/http",
60+
"/udp/0",
61+
"/tcp/0",
62+
"/sctp/0",
63+
"/udp/1234",
64+
"/tcp/1234",
65+
"/sctp/1234",
66+
"/udp/65535",
67+
"/tcp/65535",
68+
"/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC",
69+
"/udp/1234/sctp/1234",
70+
"/udp/1234/udt",
71+
"/udp/1234/utp",
72+
"/tcp/1234/http",
73+
"/tcp/1234/https",
74+
"/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC/tcp/1234",
75+
"/ip4/127.0.0.1/udp/1234",
76+
"/ip4/127.0.0.1/udp/0",
77+
"/ip4/127.0.0.1/tcp/1234",
78+
"/ip4/127.0.0.1/tcp/1234/",
79+
"/ip4/127.0.0.1/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC",
80+
"/ip4/127.0.0.1/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSupNKC/tcp/1234"
81+
).flatMap(s -> {
82+
try {
83+
new MultiAddress(s);
84+
return Stream.empty();
85+
} catch (Exception e) {
86+
e.printStackTrace();
87+
return Stream.of(s);
88+
}
89+
}).collect(Collectors.toList());
90+
if (failed.size() > 0)
91+
throw new IllegalStateException("Failed to construct MultiAddresses: "+failed);
92+
}
93+
94+
@Test
95+
public void equalsTests() {
96+
MultiAddress m1 = new MultiAddress("/ip4/127.0.0.1/udp/1234");
97+
MultiAddress m2 = new MultiAddress("/ip4/127.0.0.1/tcp/1234");
98+
MultiAddress m3 = new MultiAddress("/ip4/127.0.0.1/tcp/1234");
99+
MultiAddress m4 = new MultiAddress("/ip4/127.0.0.1/tcp/1234/");
100+
101+
if (m1.equals(m2))
102+
throw new IllegalStateException("Should be unequal!");
103+
104+
if (m2.equals(m1))
105+
throw new IllegalStateException("Should be unequal!");
106+
107+
if (!m2.equals(m3))
108+
throw new IllegalStateException("Should be equal!");
109+
110+
if (!m3.equals(m2))
111+
throw new IllegalStateException("Should be equal!");
112+
113+
if (!m1.equals(m1))
114+
throw new IllegalStateException("Should be equal!");
115+
116+
if (!m2.equals(m4))
117+
throw new IllegalStateException("Should be equal!");
118+
119+
if (!m4.equals(m3))
120+
throw new IllegalStateException("Should be equal!");
121+
}
122+
123+
@Test
124+
public void stringToBytes() {
125+
BiConsumer<String, String> test = (s, h) -> {
126+
if (!Arrays.equals(new MultiAddress(s).getBytes(), fromHex(h))) throw new IllegalStateException(s + " bytes != " + new MultiAddress(fromHex(h)));
127+
};
128+
129+
test.accept("/ip4/127.0.0.1/udp/1234", "047f0000011104d2");
130+
test.accept("/ip4/127.0.0.1/tcp/4321", "047f0000010610e1");
131+
test.accept("/ip4/127.0.0.1/udp/1234/ip4/127.0.0.1/tcp/4321", "047f0000011104d2047f0000010610e1");
132+
}
133+
134+
@Test
135+
public void bytesToString() {
136+
BiConsumer<String, String> test = (s, h) -> {
137+
if (!s.equals(new MultiAddress(fromHex(h)).toString())) throw new IllegalStateException(s + " != " + new MultiAddress(fromHex(h)));
138+
};
139+
140+
test.accept("/ip4/127.0.0.1/udp/1234", "047f0000011104d2");
141+
test.accept("/ip4/127.0.0.1/tcp/4321", "047f0000010610e1");
142+
test.accept("/ip4/127.0.0.1/udp/1234/ip4/127.0.0.1/tcp/4321", "047f0000011104d2047f0000010610e1");
143+
}
144+
145+
public static byte[] fromHex(String hex) {
146+
if (hex.length() % 2 != 0)
147+
throw new IllegalStateException("Uneven number of hex digits!");
148+
ByteArrayOutputStream bout = new ByteArrayOutputStream();
149+
for (int i=0; i < hex.length()-1; i+= 2)
150+
bout.write(Integer.valueOf(hex.substring(i, i+2), 16));
151+
return bout.toByteArray();
152+
}
153+
}

0 commit comments

Comments
 (0)