3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-02-14 11:15:51 +00:00

Implement support for IRC peer discovery. Patch contributed by John Sample.

This commit is contained in:
Mike Hearn 2011-05-02 11:54:15 +00:00
parent 37cb9cb6e5
commit e43ad1f754
6 changed files with 378 additions and 0 deletions

View File

@ -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<InetSocketAddress> addresses = new ArrayList<InetSocketAddress>();
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 <user> <mode> <unused> <realname> (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<InetSocketAddress> parseUserList(String[] userNames) throws UnknownHostException {
ArrayList<InetSocketAddress> addresses = new ArrayList<InetSocketAddress>();
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 :<source>
//
// All the testing shows the servers for this purpose use :<source> 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;
}
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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.

View File

@ -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()));
}
}
}

View File

@ -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<InetSocketAddress> 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);
}
}
}