From e43ad1f7540eb2fbefe7a8df347637d4eebf01b9 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Mon, 2 May 2011 11:54:15 +0000 Subject: [PATCH] Implement support for IRC peer discovery. Patch contributed by John Sample. --- src/com/google/bitcoin/core/IrcDiscovery.java | 208 ++++++++++++++++++ .../google/bitcoin/core/PeerDiscovery.java | 30 +++ .../bitcoin/core/PeerDiscoveryException.java | 41 ++++ src/com/google/bitcoin/core/Utils.java | 4 + src/com/google/bitcoin/examples/IRCPeers.java | 46 ++++ .../google/bitcoin/core/IrcDiscoveryTest.java | 49 +++++ 6 files changed, 378 insertions(+) create mode 100644 src/com/google/bitcoin/core/IrcDiscovery.java create mode 100644 src/com/google/bitcoin/core/PeerDiscovery.java create mode 100644 src/com/google/bitcoin/core/PeerDiscoveryException.java create mode 100644 src/com/google/bitcoin/examples/IRCPeers.java create mode 100644 tests/com/google/bitcoin/core/IrcDiscoveryTest.java diff --git a/src/com/google/bitcoin/core/IrcDiscovery.java b/src/com/google/bitcoin/core/IrcDiscovery.java new file mode 100644 index 00000000..baebe879 --- /dev/null +++ b/src/com/google/bitcoin/core/IrcDiscovery.java @@ -0,0 +1,208 @@ +/** + * Copyright 2011 John Sample + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.bitcoin.core; + +import java.io.*; +import java.net.*; +import java.util.*; + +/** + * IrcDiscovery provides a way to find network peers by joining a pre-agreed rendevouz point on the LFnet IRC network. + */ +public class IrcDiscovery implements PeerDiscovery { + private String channel; + private int port = 6667; + private String server; + + private BufferedWriter writer = null; + + /** + * Finds a list of peers by connecting to an IRC network, joining a channel, decoding the nicks and then + * disconnecting. + * + * @param channel The IRC channel to join, either "#bitcoin" or "#bitcoinTEST" for the production and test networks + * respectively. + */ + public IrcDiscovery(String channel) { + this(channel, "irc.lfnet.org", 6667); + } + + /** + * Finds a list of peers by connecting to an IRC network, joining a channel, decoding the nicks and then + * disconnecting. + * + * @param server Name or textual IP address of the IRC server to join. + * @param channel The IRC channel to join, either "#bitcoin" or "#bitcoinTEST" for the production and test networks + */ + public IrcDiscovery(String channel, String server, int port) { + this.channel = channel; + this.server = server; + this.port = port; + } + + protected void onIRCSend(String message) { + } + + protected void onIRCReceive(String message) { + } + + /** + * Returns a list of peers that were found in the IRC channel. Note that just because a peer appears in the list + * does not mean it is accepting connections. + */ + public InetSocketAddress[] getPeers() throws PeerDiscoveryException { + ArrayList addresses = new ArrayList(); + Socket connection = null; + try { + connection = new Socket(server, port); + writer = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream())); + BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); + + // Generate a random nick for the connection. This is chosen to be clearly identifiable as coming from + // BitCoinJ but not match the standard nick format, so full peers don't try and connect to us. + String nickRnd = String.format("bcj%d", new Random().nextInt(Integer.MAX_VALUE)); + String command = "NICK " + nickRnd; + logAndSend(command); + // USER (RFC 2812) + command = "USER " + nickRnd + " 8 *: " + nickRnd; + logAndSend(command); + writer.flush(); + + // Wait to be logged in. Worst case we end up blocked until the server PING/PONGs us out. + String currLine = null; + while ((currLine = reader.readLine()) != null) { + onIRCReceive(currLine); + // 004 tells us we are connected + // TODO: add common exception conditions (nick already in use, etc..) + // these aren't bullet proof checks but they should do for our purposes. + if (checkLineStatus("004", currLine)) { + break; + } + } + + // Join the channel. + logAndSend("JOIN " + channel); + writer.flush(); + + // A list of the users should be returned when we join. Look for code 353 and parse until code 366. + while ((currLine = reader.readLine()) != null) { + onIRCReceive(currLine); + if (checkLineStatus("353", currLine)) { + // Line contains users. List follows ":" (second ":" if line starts with ":") + int subIndex = 0; + if (currLine.startsWith(":")) { + subIndex = 1; + } + + String spacedList = currLine.substring(currLine.indexOf(":", subIndex)); + addresses.addAll(parseUserList(spacedList.split(" "))); + } else if (checkLineStatus("366", currLine)) { + // End of user list. + break; + } + } + + // Quit the server. + logAndSend("PART " + channel); + logAndSend("QUIT"); + writer.flush(); + } catch (Exception e) { + // Throw the original error wrapped in the discovery error. + throw new PeerDiscoveryException(e.getMessage(), e); + } finally { + try { + // No matter what try to close the connection. + connection.close(); + } catch (Exception e2) {} + } + return addresses.toArray(new InetSocketAddress[]{}); + } + + private void logAndSend(String command) throws Exception { + onIRCSend(command); + writer.write(command + "\n"); + } + + // Visible for testing. + static ArrayList parseUserList(String[] userNames) throws UnknownHostException { + ArrayList addresses = new ArrayList(); + for (String user : userNames) { + // All BitCoin peers start their nicknames with a 'u' character. + if (!user.startsWith("u")) { + continue; + } + + // After "u" is stripped from the beginning array contains unsigned chars of: + // 4 byte ip address, 2 byte port, 4 byte hash check (ipv4) + + byte[] addressBytes; + try { + // Strip off the "u" before decoding. Note that it's possible for anyone to join these IRC channels and + // so simply beginning with "u" does not imply this is a valid BitCoin encoded address. + // + // decodeChecked removes the checksum from the returned bytes. + addressBytes = Base58.decodeChecked(user.substring(1)); + } catch (AddressFormatException e) { + Utils.LOG("IRC nick does not parse as base58: " + user); + continue; + } + + // TODO: Handle IPv6 if one day the official client uses it. It may be that IRC discovery never does. + if (addressBytes.length != 6) { + continue; + } + + byte[] ipBytes = new byte[]{addressBytes[0], addressBytes[1], addressBytes[2], addressBytes[3]}; + int port = Utils.readUint16BE(addressBytes, 4); + + InetAddress ip; + try { + ip = InetAddress.getByAddress(ipBytes); + } catch (UnknownHostException e) { + // Bytes are not a valid IP address. + continue; + } + + InetSocketAddress address = new InetSocketAddress(ip, port); + addresses.add(address); + } + + return addresses; + } + + private static boolean checkLineStatus(String statusCode, String response) { + // Lines can either start with the status code or an optional : + // + // All the testing shows the servers for this purpose use : but plan for either. + // TODO: Consider whether regex would be worth it here. + if (response.startsWith(":")) { + // Look for first space. + int startIndex = response.indexOf(" ") + 1; + // Next part should be status code. + if (response.indexOf(statusCode + " ", startIndex) == startIndex) { + return true; + } else { + return false; + } + } else { + if (response.startsWith(statusCode + " ")) { + return true; + } + } + return false; + } +} diff --git a/src/com/google/bitcoin/core/PeerDiscovery.java b/src/com/google/bitcoin/core/PeerDiscovery.java new file mode 100644 index 00000000..76820ec0 --- /dev/null +++ b/src/com/google/bitcoin/core/PeerDiscovery.java @@ -0,0 +1,30 @@ +/** + * Copyright 2011 John Sample. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.bitcoin.core; + +import java.net.InetSocketAddress; + +/** + * A PeerDiscovery object is responsible for finding addresses of other nodes in the BitCoin P2P network. Note that + * the addresses returned may or may not be accepting connections. + */ +public interface PeerDiscovery { + // TODO: Flesh out this interface a lot more. + + /** Returns an array of addresses. This method may block. */ + InetSocketAddress[] getPeers() throws PeerDiscoveryException; +} diff --git a/src/com/google/bitcoin/core/PeerDiscoveryException.java b/src/com/google/bitcoin/core/PeerDiscoveryException.java new file mode 100644 index 00000000..0b63c93d --- /dev/null +++ b/src/com/google/bitcoin/core/PeerDiscoveryException.java @@ -0,0 +1,41 @@ +/** + * Copyright 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.bitcoin.core; + +public class PeerDiscoveryException extends Exception { + + private static final long serialVersionUID = -2863411151549391392L; + + public PeerDiscoveryException() { + super(); + } + + public PeerDiscoveryException(String message) { + super(message); + } + + public PeerDiscoveryException(Throwable arg0) + { + super(arg0); + } + + public PeerDiscoveryException(String message, Throwable arg0) { + super(message, arg0); + } + + +} diff --git a/src/com/google/bitcoin/core/Utils.java b/src/com/google/bitcoin/core/Utils.java index 43432a68..ea96d09f 100644 --- a/src/com/google/bitcoin/core/Utils.java +++ b/src/com/google/bitcoin/core/Utils.java @@ -190,6 +190,10 @@ public class Utils { ((bytes[offset + 2] & 0xFFL) << 8) | ((bytes[offset + 3] & 0xFFL) << 0); } + + public static int readUint16BE(byte[] bytes, int offset) { + return ((bytes[offset] & 0xff) << 8) | bytes[offset + 1] & 0xff; + } static void LOG(String msg) { // Set this to true to see debug prints from the library. diff --git a/src/com/google/bitcoin/examples/IRCPeers.java b/src/com/google/bitcoin/examples/IRCPeers.java new file mode 100644 index 00000000..66687d63 --- /dev/null +++ b/src/com/google/bitcoin/examples/IRCPeers.java @@ -0,0 +1,46 @@ +/** + * Copyright 2011 John Sample. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.bitcoin.examples; + +import java.net.InetSocketAddress; + +import com.google.bitcoin.core.*; + +/** + * Prints a list of IP addresses connected to the rendezvous point on the LFnet IRC channel. + */ +public class IRCPeers { + public static void main(String[] args) throws PeerDiscoveryException { + IrcDiscovery d = new IrcDiscovery("#bitcoin") { + @Override + protected void onIRCReceive(String message) { + System.out.println(message); + } + + @Override + protected void onIRCSend(String message) { + System.out.println(message); + } + }; + + InetSocketAddress[] addresses = d.getPeers(); + for (InetSocketAddress address : addresses) { + String hostAddress = address.getAddress().getHostAddress(); + System.out.println(String.format("%s:%d", hostAddress.toString(), address.getPort())); + } + } +} diff --git a/tests/com/google/bitcoin/core/IrcDiscoveryTest.java b/tests/com/google/bitcoin/core/IrcDiscoveryTest.java new file mode 100644 index 00000000..3070f092 --- /dev/null +++ b/tests/com/google/bitcoin/core/IrcDiscoveryTest.java @@ -0,0 +1,49 @@ +/** + * Copyright John Sample + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.bitcoin.core; + +import static org.junit.Assert.*; + +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; + +import org.junit.Test; + +public class IrcDiscoveryTest { + // TODO: Inject a mock IRC server and more thoroughly exercise this class. + + @Test + public void testParseUserList() throws UnknownHostException { + // Test some random addresses grabbed from the channel. + String[] userList = new String[]{ "x201500200","u4stwEBjT6FYyVV", "u5BKEqDApa8SbA7"}; + + ArrayList addresses = IrcDiscovery.parseUserList(userList); + + // Make sure the "x" address is excluded. + assertEquals("Too many addresses.", 2, addresses.size()); + + String[] ips = new String[]{"69.4.98.82:8333","74.92.222.129:8333"}; + InetSocketAddress[] decoded = addresses.toArray(new InetSocketAddress[]{}); + + for (int i = 0; i < decoded.length; i++) { + String formattedIP = decoded[0].getAddress().getHostAddress() + ":" + ((Integer)decoded[i].getPort()).toString(); + assertEquals("IPs decoded improperly", ips[0], formattedIP); + } + } + +}