/*
* The MIT License
*
* Copyright 2014 sorrge.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.nyan.dch.communication.transport.tcpip;
import java.io.*;
import java.net.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import org.nyan.dch.communication.AcceptanceChecker;
import org.nyan.dch.communication.IAcceptance;
import org.nyan.dch.communication.IAddress;
import org.nyan.dch.communication.IDiscovery;
import org.nyan.dch.communication.IDiscoveryListener;
import org.nyan.dch.misc.AddressFormatException;
import org.nyan.dch.misc.Base58;
import org.nyan.dch.misc.IStoppable;
import org.nyan.dch.misc.RandomSet;
/**
* IRCDiscovery provides a way to find network peers by joining a pre-agreed rendezvous point on an IRC network.
* @author sorrge
*/
public class IRCDiscovery implements IDiscovery, Runnable, IStoppable
{
public static final String Server = "us.undernet.org", Channel = "dch";
public static final int ServerPort = 6667;
private static class IRCMessage
{
static final Pattern Space = Pattern.compile(" ");
final String prefix;
final IRCCommands command;
final String[] parameters;
public IRCMessage(String line)
{
String[] parts = Space.split(line);
int i = 0;
if(parts[i].charAt(0) == ':')
prefix = parts[i++].substring(1);
else
prefix = null;
command = IRCCommands.FromCode(parts[i++]);
ArrayList<String> params = new ArrayList<>(parts.length - i);
for(; i < Math.min(parts.length, 14) && parts[i].charAt(0) != ':'; ++i)
params.add(parts[i]);
if(i < parts.length)
{
String trailing = parts[i].charAt(0) == ':' ? parts[i].substring(1) : parts[i];
for(++i; i < parts.length; ++i)
trailing += " " + parts[i];
params.add(trailing);
}
parameters = new String[params.size()];
params.toArray(parameters);
}
}
private static enum IRCCommands
{
Ping("PING"), Pong("PONG"), Nick("NICK"), User("USER"), Join("JOIN"), Quit("QUIT"), Who("WHO"),
RplMyInfo("004"), RplNamReply("353"), RplWhoReply("352");
public final String code;
private static final HashMap<String, IRCCommands> codeMap = new HashMap<>();
private IRCCommands(String code)
{
this.code = code;
}
static
{
for (IRCCommands type : IRCCommands.values())
codeMap.put(type.code, type);
}
static IRCCommands FromCode(String code)
{
return codeMap.get(code.toUpperCase());
}
}
private final static Logger log = Logger.getLogger(IRCDiscovery.class.getName());
private BufferedWriter writer;
private BufferedReader reader;
private Socket connection;
private final Random rand;
private Thread thread;
private String myNick = null;
private IPAddress myAddress = null;
private final HashSet<String> checkedNicks = new HashSet<>();
private final RandomSet<String> uncheckedNicks = new RandomSet<>();
private final HashSet<IPAddress> sentAddresses = new HashSet<>(), unsentAddresses = new HashSet<>();
private final HashMap<String, IPAddress> resolvedNicks = new HashMap<>();
private IDiscoveryListener listener;
private boolean discovering = false;
private final int selectedPort;
private final AcceptanceChecker acceptance = new AcceptanceChecker();
private boolean portIsValid;
public IRCDiscovery(int serverPort, Random rand) throws IOException
{
this.rand = rand;
selectedPort = serverPort;
SetPort(selectedPort);
Connect();
}
private void Connect() throws IOException
{
InetAddress[] ips = InetAddress.getAllByName(Server);
Collections.shuffle(Arrays.asList(ips), rand);
for (InetAddress ip : ips)
{
try
{
connection = new Socket(ip, ServerPort);
connection.setSoTimeout(1000);
writer = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream(), "UTF-8"));
reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
Send(IRCCommands.Nick, myNick);
char[] userName = new char[7];
for (int i = 0; i < userName.length; ++i)
userName[i] = (char) ('a' + rand.nextInt(26));
Send(IRCCommands.User, String.valueOf(userName), "8", "*", String.valueOf(userName));
thread = new Thread(this, "IRCDiscovery");
thread.start();
return;
}
catch (IOException ex)
{
if(connection != null)
try { connection.close(); } catch (IOException exx) {}
}
}
throw new IOException("Could not connect to IRC");
}
private void Send(IRCCommands command, String... arguments) throws IOException
{
String commandString = command.code;
for(int i = 0; i < arguments.length - 1; ++i)
commandString += " " + arguments[i];
if(arguments.length > 0)
commandString += " :" + arguments[arguments.length - 1];
log.log(Level.FINE, "<{0}", commandString);
writer.write(commandString + "\r\n");
writer.flush();
}
public final void SetPort(int port) throws IOException
{
byte[] portBytesWithCookieAndChecksum = new byte[5];
rand.nextBytes(portBytesWithCookieAndChecksum);
portBytesWithCookieAndChecksum[0] = (byte)(port & 0xff);
portBytesWithCookieAndChecksum[1] = (byte)((port >> 8) & 0xff);
portBytesWithCookieAndChecksum[4] = Checksum(portBytesWithCookieAndChecksum);
synchronized(uncheckedNicks)
{
boolean checked = checkedNicks.remove(myNick);
uncheckedNicks.remove(myNick);
myNick = "d" + Base58.encode(portBytesWithCookieAndChecksum);
(checked ? checkedNicks : uncheckedNicks).add(myNick);
}
if(myAddress != null)
myAddress = new IPAddress(myAddress.address.getAddress(), port);
if(writer != null)
Send(IRCCommands.Nick, myNick);
portIsValid = port > 127 && port < 65536;
}
private static byte Checksum(byte[] bytes)
{
byte checkSum = 0;
for(int i = 0; i < 4; ++i)
checkSum = (byte)(checkSum ^ bytes[i]);
return checkSum;
}
private static int DecodeNick(String nick)
{
if(nick.charAt(0) != 'd')
return -1;
try
{
byte[] bytes = Base58.decode(nick.substring(1));
if(bytes.length != 5)
return -1;
byte checkSum = Checksum(bytes);
if(checkSum != bytes[4])
return -1;
int port = (((int)bytes[0]) & 0xff) | ((((int)bytes[1]) & 0xff) << 8);
if(port < 128 || port > 65535)
return -1;
return port;
}
catch(AddressFormatException ex)
{
return -1;
}
}
@Override
public void BeginDiscovery()
{
discovering = true;
}
@Override
public void EndDiscovery()
{
discovering = false;
}
@Override
public void SetDiscoveryListener(IDiscoveryListener listener)
{
this.listener = listener;
}
@Override
public void Close()
{
try
{
if (connection != null)
{
Send(IRCCommands.Quit);
connection.close();
}
}
catch (IOException ex)
{
}
acceptance.Close();
}
@Override
public void Restart() throws IOException
{
if(!IsRunning())
Connect();
}
@Override
public IAddress GetMyAddress()
{
return myAddress;
}
@Override
public void run()
{
boolean joinedChannel = false;
Date lastPing = new Date();
while (true)
try
{
String line = reader.readLine();
if(line == null)
break;
log.log(Level.FINE, ">{0}", line);
IRCMessage message = new IRCMessage(line);
lastPing = new Date();
if(message.command != null)
switch(message.command)
{
case Ping:
Send(IRCCommands.Pong, message.parameters);
break;
case Join:
String[] added = ParseUserSource(message.prefix);
if(added[0] != null)
{
if(added[1] != null)
RegisterNick(added[0], InetAddress.getByName(added[1]));
else
RegisterNick(added[0], null);
}
break;
case Quit:
String[] removed = ParseUserSource(message.prefix);
if(removed[0] != null)
UnregisterNick(removed[0]);
break;
case RplMyInfo:
if(!joinedChannel)
{
Send(IRCCommands.Join, "#" + Channel);
joinedChannel = true;
}
break;
case RplNamReply:
for(String nick : IRCMessage.Space.split(message.parameters[3]))
{
String n = nick.charAt(0) == '@' || nick.charAt(0) == '+' ? nick.substring(1) : nick;
RegisterNick(n, null);
}
break;
case RplWhoReply:
RegisterNick(message.parameters[5], InetAddress.getByName(message.parameters[3]));
break;
case Nick:
String[] changed = ParseUserSource(message.prefix);
if(changed[0] != null)
UnregisterNick(changed[0]);
if(changed[1] != null)
RegisterNick(message.parameters[0], InetAddress.getByName(changed[1]));
else
RegisterNick(message.parameters[0], null);
break;
default:
break;
}
if(discovering)
CheckNicks();
if(!acceptance.NeedToConfirmAcceptance())
if(acceptance.CanAcceptConnections() && !portIsValid)
SetPort(selectedPort);
else if(!acceptance.CanAcceptConnections() && portIsValid)
SetPort(0);
}
catch(SocketTimeoutException ste)
{
Date now = new Date();
if(now.getTime() - lastPing.getTime() > 60 * 1000)
{
try { Send(IRCCommands.Ping, "xxx"); } catch (IOException ex) {}
lastPing = now;
}
}
catch (IOException ex)
{
log.log(Level.WARNING, "Error during Irc communication: {0}", ex.getMessage());
if(connection.isClosed())
break;
}
log.info("Irc connection closed");
}
private void UnregisterNick(String nick)
{
synchronized(uncheckedNicks)
{
log.log(Level.FINE, "Removed nick: {0}", nick);
uncheckedNicks.remove(nick);
checkedNicks.remove(nick);
IPAddress addr = resolvedNicks.remove(nick);
if(addr != null)
unsentAddresses.remove(addr);
}
}
private static String[] ParseUserSource(String src)
{
String[] res = new String[2];
int nickLength = src.indexOf('!');
if(nickLength != -1)
{
res[0] = src.substring(0, nickLength);
int hostPos = src.indexOf('@', nickLength + 1);
if(hostPos != -1)
res[1] = src.substring(hostPos + 1);
}
return res;
}
public boolean IsRunning()
{
return thread != null && thread.isAlive();
}
private void RegisterNick(String nick, InetAddress address)
{
synchronized(uncheckedNicks)
{
if(checkedNicks.contains(nick) || uncheckedNicks.contains(nick) && address == null)
return;
uncheckedNicks.remove(nick);
int port = DecodeNick(nick);
if(port == -1)
checkedNicks.add(nick);
else if(address == null)
uncheckedNicks.add(nick);
else
{
checkedNicks.add(nick);
IPAddress addr = new IPAddress(address, port);
resolvedNicks.put(nick, addr);
log.log(Level.FINE, "Nick {0}: {1}", new Object[]{nick, addr});
if(nick.equals(myNick))
myAddress = addr;
else
{
if(discovering && listener != null)
{
if(!sentAddresses.contains(addr))
{
listener.AddAddress(addr);
sentAddresses.add(addr);
}
unsentAddresses.remove(addr);
}
else if(!sentAddresses.contains(addr))
unsentAddresses.add(addr);
}
}
}
}
private void CheckNicks() throws IOException
{
if(listener == null)
return;
synchronized(uncheckedNicks)
{
if(!unsentAddresses.isEmpty())
{
for(IPAddress addr : unsentAddresses)
listener.AddAddress(addr);
sentAddresses.addAll(unsentAddresses);
unsentAddresses.clear();
}
else
for(int i = 0; i < 10 && !uncheckedNicks.isEmpty(); ++i)
{
String toCheck = uncheckedNicks.pollRandom(rand);
Send(IRCCommands.Who, toCheck);
}
}
}
@Override
public IAcceptance GetAcceptance()
{
return acceptance;
}
}