diff --git a/apps/sam/java/src/net/i2p/sam/SAMBridge.java b/apps/sam/java/src/net/i2p/sam/SAMBridge.java new file mode 100644 index 0000000000000000000000000000000000000000..7f0678101c2a9578e20463f5dcae81f9285ff0ff --- /dev/null +++ b/apps/sam/java/src/net/i2p/sam/SAMBridge.java @@ -0,0 +1,105 @@ +package net.i2p.sam; +/* + * free (adj.): unencumbered; not under the control of others + * Written by human in 2004 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.IOException; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; + +import net.i2p.util.I2PThread; +import net.i2p.util.Log; + +/** + * SAM bridge implementation. + * + * @author human + */ +public class SAMBridge implements Runnable { + + private final static Log _log = new Log(SAMBridge.class); + private ServerSocket serverSocket; + + private boolean acceptConnections = true; + + private final static int SAM_LISTENPORT = 7656; + + /** + * Build a new SAM bridge listening on 127.0.0.1. + * + * @param listenPort The port to listen on + */ + public SAMBridge(int listenPort) { + this((String)null, listenPort); + } + + /** + * Build a new SAM bridge. + * + * @param listenHost The network interface to listen on + * @param listenPort The port to listen on + */ + public SAMBridge(String listenHost, int listenPort) { + try { + if (listenHost != null) { + serverSocket = new ServerSocket(listenPort, 0, + InetAddress.getByName(listenHost)); + _log.debug("SAM bridge listening on " + + listenHost + ":" + listenPort); + } else { + serverSocket = new ServerSocket(listenPort); + _log.debug("SAM bridge listening on 0.0.0.0:" + listenPort); + } + } catch (Exception e) { + _log.error("Error starting SAM bridge on " + + (listenHost == null ? "0.0.0.0" : listenHost) + + ":" + listenPort, e); + } + + } + + public static void main(String args[]) { + SAMBridge bridge = new SAMBridge(SAM_LISTENPORT); + I2PThread t = new I2PThread(bridge, "SAMListener"); + t.start(); + } + + public void run() { + try { + while (acceptConnections) { + Socket s = serverSocket.accept(); + _log.debug("New connection from " + + s.getInetAddress().toString() + ":" + + s.getPort()); + + try { + SAMHandler handler = SAMHandlerFactory.createSAMHandler(s); + if (handler == null) { + _log.debug("SAM handler has not been instantiated"); + try { + s.close(); + } catch (IOException e) {} + continue; + } + handler.startHandling(); + } catch (SAMException e) { + _log.error("SAM error: " + e.getMessage()); + s.close(); + } + } + } catch (Exception e) { + _log.error("Unexpected error while listening for connections", e); + } finally { + try { + _log.debug("Shutting down, closing server socket"); + serverSocket.close(); + } catch (IOException e) {} + } + } +} diff --git a/apps/sam/java/src/net/i2p/sam/SAMException.java b/apps/sam/java/src/net/i2p/sam/SAMException.java new file mode 100644 index 0000000000000000000000000000000000000000..e51e35ea4fcf8aad07dd23727022e4c1a606a932 --- /dev/null +++ b/apps/sam/java/src/net/i2p/sam/SAMException.java @@ -0,0 +1,25 @@ +package net.i2p.sam; +/* + * free (adj.): unencumbered; not under the control of others + * Written by human in 2004 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +/** + * Exception thrown by SAM methods + * + * @author human + */ +public class SAMException extends Exception { + + public SAMException() { + super(); + } + + public SAMException(String s) { + super(s); + } +} diff --git a/apps/sam/java/src/net/i2p/sam/SAMHandler.java b/apps/sam/java/src/net/i2p/sam/SAMHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..d5d4453cc45b03109c637a02e36aaabcb2bd724b --- /dev/null +++ b/apps/sam/java/src/net/i2p/sam/SAMHandler.java @@ -0,0 +1,104 @@ +package net.i2p.sam; +/* + * free (adj.): unencumbered; not under the control of others + * Written by human in 2004 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.IOException; +import java.io.OutputStream; +import java.net.Socket; + +import net.i2p.util.I2PThread; +import net.i2p.util.Log; + +/** + * Base class for SAM protocol handlers. It implements common + * methods, but is not able to actually parse the protocol itself: + * this task is delegated to subclasses. + * + * @author human + */ +public abstract class SAMHandler implements Runnable { + + private final static Log _log = new Log(SAMHandler.class); + + protected I2PThread thread = null; + + private Object socketWLock = new Object(); // Guards writings on socket + private OutputStream socketOS = null; // Stream associated to socket + protected Socket socket = null; + + protected int verMajor = 0; + protected int verMinor = 0; + + private boolean stopHandler = false; + private Object stopLock = new Object(); + + /** + * Start handling the SAM connection, detaching an handling thread. + * + */ + public void startHandling() { + thread = new I2PThread(this, "SAMHandler"); + thread.start(); + } + + /** + * Actually handle the SAM protocol. + * + */ + protected abstract void handle(); + + /** + * Write a byte array on the handler's socket. This method must + * always be used when writing data, unless you really know what + * you're doing. + * + * @param data A byte array to be written + */ + protected void writeBytes(byte[] data) throws IOException { + synchronized (socketWLock) { + if (socketOS == null) { + socketOS = socket.getOutputStream(); + } + socketOS.write(data); + socketOS.flush(); + } + } + + /** + * Stop the SAM handler + * + */ + public void stopHandling() { + synchronized (stopLock) { + stopHandler = true; + } + } + + /** + * Should the handler be stopped? + * + * @return True if the handler should be stopped, false otherwise + */ + protected boolean shouldStop() { + synchronized (stopLock) { + return stopHandler; + } + } + + /** + * Get a string describing the handler. + * + * @return A String describing the handler; + */ + public abstract String toString(); + + public final void run() { + handle(); + } +} diff --git a/apps/sam/java/src/net/i2p/sam/SAMHandlerFactory.java b/apps/sam/java/src/net/i2p/sam/SAMHandlerFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..c598a0d501fda2ef606ed62d47d3e3ea86e60e00 --- /dev/null +++ b/apps/sam/java/src/net/i2p/sam/SAMHandlerFactory.java @@ -0,0 +1,179 @@ +package net.i2p.sam; +/* + * free (adj.): unencumbered; not under the control of others + * Written by human in 2004 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.Socket; +import java.util.Properties; +import java.util.StringTokenizer; + +import net.i2p.util.Log; + +/** + * SAM handler factory class. + */ +public class SAMHandlerFactory { + + private final static Log _log = new Log(SAMHandlerFactory.class); + + /** + * Return the right SAM handler depending on the protocol version + * required by the client. + * + * @param s Socket attached to SAM client + * + * @return A SAM protocol handler + */ + public static SAMHandler createSAMHandler(Socket s) throws SAMException { + BufferedReader br; + StringTokenizer tok; + + try { + br = new BufferedReader(new InputStreamReader(s.getInputStream(), + "ISO-8859-1")); + tok = new StringTokenizer(br.readLine(), " "); + } catch (IOException e) { + throw new SAMException("Error reading from socket: " + + e.getMessage()); + } catch (Exception e) { + throw new SAMException("Unexpected error: " + + e.getMessage()); + } + + // Message format: HELLO VERSION MIN=v1 MAX=v2 + if (tok.countTokens() != 4) { + throw new SAMException("Bad format in HELLO message"); + } + if (!tok.nextToken().equals("HELLO")) { + throw new SAMException("Bad domain in HELLO message"); + } + { + String opcode; + if (!(opcode = tok.nextToken()).equals("VERSION")) { + throw new SAMException("Unrecognized HELLO message opcode: \"" + + opcode + "\""); + } + } + + Properties props; + props = SAMUtils.parseParams(tok); + if (props == null) { + throw new SAMException("No parameters in HELLO VERSION message"); + } + + String minVer = props.getProperty("MIN"); + if (minVer == null) { + throw new SAMException("Missing MIN parameter in HELLO VERSION message"); + } + + String maxVer = props.getProperty("MAX"); + if (maxVer == null) { + throw new SAMException("Missing MAX parameter in HELLO VERSION message"); + } + + String ver = chooseBestVersion(minVer, maxVer); + if (ver == null) { + // Let's answer negatively + try { + OutputStream out = s.getOutputStream(); + out.write("HELLO REPLY RESULT=NOVERSION\n".getBytes("ISO-8859-1")); + return null; + } catch (UnsupportedEncodingException e) { + _log.error("Caught UnsupportedEncodingException (" + + e.getMessage() + ")"); + throw new SAMException("Character encoding error: " + + e.getMessage()); + } catch (IOException e) { + throw new SAMException("Error reading from socket: " + + e.getMessage()); + } + } + + // Let's answer positively + try { + OutputStream out = s.getOutputStream(); + out.write(("HELLO REPLY RESULT=OK VERSION=" + + ver + "\n").getBytes("ISO-8859-1")); + } catch (UnsupportedEncodingException e) { + _log.error("Caught UnsupportedEncodingException (" + + e.getMessage() + ")"); + throw new SAMException("Character encoding error: " + + e.getMessage()); + } catch (IOException e) { + throw new SAMException("Error writing to socket: " + + e.getMessage()); + } + + // ...and instantiate the right SAM handler + int verMajor = getMajor(ver); + int verMinor = getMinor(ver); + SAMHandler handler; + switch (verMajor) { + case 1: + handler = new SAMv1Handler(s, verMajor, verMinor); + break; + default: + _log.error("BUG! Trying to initialize the wrong SAM version!"); + throw new SAMException("BUG triggered! (handler instantiation)"); + } + + return handler; + } + + /* Return the best version we can use, or null on failure */ + private static String chooseBestVersion(String minVer, String maxVer) { + int minMajor = getMajor(minVer), minMinor = getMinor(minVer); + int maxMajor = getMajor(maxVer), maxMinor = getMinor(maxVer); + + // Consistency checks + if ((minMajor == -1) || (minMinor == -1) + || (maxMajor == -1) || (maxMinor == -1)) { + return null; + } + if (minMajor > maxMajor) { + return null; + } else if ((minMajor == maxMajor) && (minMinor > maxMinor)) { + return null; + } + + if ((minMajor >= 1) && (minMinor >= 0)) { + return "1.0"; + } + + return null; + } + + /* Get the major protocol version from a string */ + private static int getMajor(String ver) { + try { + String major = ver.substring(0, ver.indexOf(".")); + return Integer.parseInt(major); + } catch (NumberFormatException e) { + return -1; + } catch (ArrayIndexOutOfBoundsException e) { + return -1; + } + } + + /* Get the minor protocol version from a string */ + private static int getMinor(String ver) { + try { + String major = ver.substring(ver.indexOf(".") + 1); + return Integer.parseInt(major); + } catch (NumberFormatException e) { + return -1; + } catch (ArrayIndexOutOfBoundsException e) { + return -1; + } + } +} diff --git a/apps/sam/java/src/net/i2p/sam/SAMRawReceiver.java b/apps/sam/java/src/net/i2p/sam/SAMRawReceiver.java new file mode 100644 index 0000000000000000000000000000000000000000..95a0e9df7e89ca1d1e19166fbac46d065e530757 --- /dev/null +++ b/apps/sam/java/src/net/i2p/sam/SAMRawReceiver.java @@ -0,0 +1,31 @@ +package net.i2p.sam; +/* + * free (adj.): unencumbered; not under the control of others + * Written by human in 2004 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.IOException; + +/** + * Interface for sending raw data to a SAM client + */ +public interface SAMRawReceiver { + + /** + * Send a byte array to a SAM client, without informations + * regarding the sender. + * + * @param data Byte array to be written + */ + public void receiveRawBytes(byte data[]) throws IOException; + + /** + * Stop receiving data. + * + */ + public void stopReceiving(); +} diff --git a/apps/sam/java/src/net/i2p/sam/SAMRawSession.java b/apps/sam/java/src/net/i2p/sam/SAMRawSession.java new file mode 100644 index 0000000000000000000000000000000000000000..1078ba6bb9a096ec913db38f189c1f432d88cd1e --- /dev/null +++ b/apps/sam/java/src/net/i2p/sam/SAMRawSession.java @@ -0,0 +1,213 @@ +package net.i2p.sam; +/* + * free (adj.): unencumbered; not under the control of others + * Written by human in 2004 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.IOException; +import java.util.Properties; + +import net.i2p.client.I2PClient; +import net.i2p.client.I2PClientFactory; +import net.i2p.client.I2PSession; +import net.i2p.client.I2PSessionException; +import net.i2p.client.I2PSessionListener; +import net.i2p.data.Base64; +import net.i2p.data.DataFormatException; +import net.i2p.data.Destination; +import net.i2p.util.HexDump; +import net.i2p.util.I2PThread; +import net.i2p.util.Log; + +/** + * SAM RAW session class. + * + * @author human + */ +public class SAMRawSession { + + private final static Log _log = new Log(SAMRawSession.class); + + private I2PSession session = null; + + private SAMRawReceiver recv = null; + + private SAMRawSessionHandler handler = null; + + /** + * Create a new SAM RAW session. + * + * @param dest Base64-encoded destination (private key) + * @param props Properties to setup the I2P session + * @param recv Object that will receive incoming data + */ + public SAMRawSession(String dest, Properties props, + SAMRawReceiver recv) throws DataFormatException, I2PSessionException { + ByteArrayInputStream bais; + + bais = new ByteArrayInputStream(Base64.decode(dest)); + + initSAMRawSession(bais, props, recv); + } + + /** + * Create a new SAM RAW session. + * + * @param destStream Input stream containing the destination keys + * @param props Properties to setup the I2P session + * @param recv Object that will receive incoming data + */ + public SAMRawSession(InputStream destStream, Properties props, + SAMRawReceiver recv) throws I2PSessionException { + initSAMRawSession(destStream, props, recv); + } + + private void initSAMRawSession(InputStream destStream, Properties props, + SAMRawReceiver recv) throws I2PSessionException { + this.recv = recv; + + _log.debug("SAM RAW session instantiated"); + + handler = new SAMRawSessionHandler(destStream, props); + Thread t = new I2PThread(handler, "SAMRawSessionHandler"); + + t.start(); + } + + /** + * Send bytes through a SAM RAW session. + * + * @param data Bytes to be sent + * + * @return True if the data was sent, false otherwise + */ + public boolean sendBytes(String dest, byte[] data) throws DataFormatException { + Destination d = new Destination(); + d.fromBase64(dest); + + try { + return session.sendMessage(d, data); + } catch (I2PSessionException e) { + _log.error("I2PSessionException while sending data", e); + return false; + } + } + + /** + * Close a SAM RAW session. + * + */ + public void close() { + handler.stopRunning(); + } + + /** + * SAM RAW session handler, running in its own thread + * + * @author human + */ + public class SAMRawSessionHandler implements Runnable, I2PSessionListener { + + private Object runningLock = new Object(); + private boolean stillRunning = true; + + /** + * Create a new SAM RAW session handler + * + * @param destStream Input stream containing the destination keys + * @param props Properties to setup the I2P session + */ + public SAMRawSessionHandler(InputStream destStream, Properties props) throws I2PSessionException { + _log.debug("Instantiating new SAM RAW session handler"); + + I2PClient client = I2PClientFactory.createClient(); + session = client.createSession(destStream, props); + + _log.debug("Connecting I2P session..."); + session.connect(); + _log.debug("I2P session connected"); + + session.setSessionListener(this); + } + + /** + * Stop a SAM RAW session handling thread + * + */ + public void stopRunning() { + synchronized (runningLock) { + stillRunning = false; + runningLock.notify(); + } + } + + public void run() { + + _log.debug("SAM RAW session handler running"); + + synchronized (runningLock) { + while (stillRunning) { + try { + runningLock.wait(); + } catch (InterruptedException ie) {} + } + _log.debug("Shutting down SAM RAW session handler"); + + recv.stopReceiving(); + + try { + _log.debug("Destroying I2P session..."); + session.destroySession(); + _log.debug("I2P session destroyed"); + } catch (I2PSessionException e) { + _log.error("Error destroying I2P session", e); + } + } + } + + public void disconnected(I2PSession session) { + _log.debug("I2P session disconnected"); + stopRunning(); + } + + public void errorOccurred(I2PSession session, String message, + Throwable error) { + _log.debug("I2P error: " + message, error); + stopRunning(); + } + + + public void messageAvailable(I2PSession session, int msgId, long size){ + _log.debug("I2P message available (id: " + msgId + + "; size: " + size + ")"); + try { + byte msg[] = session.receiveMessage(msgId); + if (_log.shouldLog(Log.DEBUG)) { + _log.debug("Content of message " + msgId + ":\n" + + HexDump.dump(msg)); + } + + recv.receiveRawBytes(msg); + } catch (IOException e) { + _log.error("Error forwarding message to receiver", e); + stopRunning(); + } catch (I2PSessionException e) { + _log.error("Error fetching I2P message", e); + stopRunning(); + } + + } + + public void reportAbuse(I2PSession session, int severity) { + _log.warn("Abuse reported (severity: " + severity + ")"); + stopRunning(); + } + } +} diff --git a/apps/sam/java/src/net/i2p/sam/SAMUtils.java b/apps/sam/java/src/net/i2p/sam/SAMUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..c22d0cec0bd3d924e7fee1a3826335723b9e2b51 --- /dev/null +++ b/apps/sam/java/src/net/i2p/sam/SAMUtils.java @@ -0,0 +1,158 @@ +package net.i2p.sam; +/* + * free (adj.): unencumbered; not under the control of others + * Written by human in 2004 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Enumeration; +import java.util.Properties; +import java.util.StringTokenizer; + +import net.i2p.I2PException; +import net.i2p.client.I2PClient; +import net.i2p.client.I2PClientFactory; +import net.i2p.client.naming.NamingService; +import net.i2p.data.Base64; +import net.i2p.data.DataFormatException; +import net.i2p.data.Destination; +import net.i2p.util.Log; + +/** + * Miscellaneous utility methods used by SAM protocol handlers. + * + * @author human + */ +public class SAMUtils { + + private final static Log _log = new Log(SAMUtils.class); + + /** + * Generate a random destination key + * + * @param priv Stream used to write the private key + * @param pub Stream used to write the public key (may be null) + */ + public static void genRandomKey(OutputStream priv, OutputStream pub) { + _log.debug("Generating random keys..."); + try { + I2PClient c = I2PClientFactory.createClient(); + Destination d = c.createDestination(priv); + priv.flush(); + + if (pub != null) { + d.writeBytes(pub); + pub.flush(); + } + } catch (I2PException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * Check whether a base64-encoded dest is valid + * + * @param dest The base64-encoded destination to be checked + * + * @return True if the destination is valid, false otherwise + */ + public static boolean checkDestination(String dest) { + try { + Destination d = new Destination(); + d.fromBase64(dest); + + return true; + } catch (DataFormatException e) { + return false; + } + } + + /** + * Resolved the specified hostname. + * + * @param name Hostname to be resolved + * @param pubKey A stream to write the Destination public key (may be null) + * + * @return the Destination for the specified hostname, or null if not found + */ + public static Destination lookupHost(String name, OutputStream pubKey) { + NamingService ns = NamingService.getInstance(); + Destination dest = ns.lookup(name); + + if ((pubKey != null) && (dest != null)) { + try { + dest.writeBytes(pubKey); + } catch (IOException e) { + e.printStackTrace(); + return null; + } catch (DataFormatException e) { + e.printStackTrace(); + return null; + } + } + + return dest; + } + + /** + * Parse SAM parameters, and put them into a Propetries object + * + * @param tok A StringTokenizer pointing to the SAM parameters + * + * @return A Properties object with the parsed SAM parameters + */ + public static Properties parseParams(StringTokenizer tok) { + int pos, ntoks = tok.countTokens(); + String token, param, value; + Properties props = new Properties(); + + for (int i = 0; i < ntoks; ++i) { + token = tok.nextToken(); + + pos = token.indexOf("="); + if (pos == -1) { + _log.debug("Error in params format"); + return null; + } + param = token.substring(0, pos); + value = token.substring(pos + 1); + + props.setProperty(param, value); + } + + if (_log.shouldLog(Log.DEBUG)) { + _log.debug("Parsed properties: " + dumpProperties(props)); + } + + return props; + } + + /* Dump a Properties object in an human-readable form */ + private static String dumpProperties(Properties props) { + Enumeration enum = props.propertyNames(); + String msg = ""; + String key, val; + boolean firstIter = true; + + while (enum.hasMoreElements()) { + key = (String)enum.nextElement(); + val = props.getProperty(key); + + if (!firstIter) { + msg += ";"; + } else { + firstIter = false; + } + msg += " \"" + key + "\" -> \"" + val + "\""; + } + + return msg; + } +} diff --git a/apps/sam/java/src/net/i2p/sam/SAMv1Handler.java b/apps/sam/java/src/net/i2p/sam/SAMv1Handler.java new file mode 100644 index 0000000000000000000000000000000000000000..bc02b5054e1f1296634fab171e527912119e2c4f --- /dev/null +++ b/apps/sam/java/src/net/i2p/sam/SAMv1Handler.java @@ -0,0 +1,420 @@ +package net.i2p.sam; +/* + * free (adj.): unencumbered; not under the control of others + * Written by human in 2004 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.EOFException; +import java.io.InputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.Socket; +import java.util.Enumeration; +import java.util.Properties; +import java.util.StringTokenizer; + +import net.i2p.client.I2PSessionException; +import net.i2p.data.Base64; +import net.i2p.data.DataFormatException; +import net.i2p.data.Destination; +import net.i2p.util.Log; + +/** + * Class able to handle a SAM version 1 client connections. + * + * @author human + */ +public class SAMv1Handler extends SAMHandler implements SAMRawReceiver { + + private final static Log _log = new Log(SAMv1Handler.class); + + private final static int IN_BUFSIZE = 2048; + + private SAMRawSession rawSession = null; + private SAMRawSession datagramSession = null; + private SAMRawSession streamSession = null; + + /** + * Create a new SAM version 1 handler. This constructor expects + * that the SAM HELLO message has been still answered (and + * stripped) from the socket input stream. + * + * @param s Socket attached to a SAM client + */ + public SAMv1Handler(Socket s, int verMajor, int verMinor) throws SAMException{ + _log.debug("SAM version 1 handler instantiated"); + + this.verMajor = verMajor; + this.verMinor = verMinor; + + if ((this.verMajor != 1) || (this.verMinor != 0)) { + throw new SAMException("BUG! Wrong protocol version!"); + } + + this.socket = s; + this.verMajor = verMajor; + this.verMinor = verMinor; + } + + public void handle() { + String msg, domain, opcode; + boolean canContinue = false; + ByteArrayOutputStream buf = new ByteArrayOutputStream(IN_BUFSIZE); + StringTokenizer tok; + + this.thread.setName("SAMv1Handler"); + _log.debug("SAM handling started"); + + try { + InputStream in = socket.getInputStream(); + int b = -1; + + while (true) { + if (shouldStop()) { + _log.debug("Stop request found"); + break; + } + + while ((b = in.read()) != -1) { + if (b == '\n') { + break; + } + buf.write(b); + } + if (b == -1) { + _log.debug("Connection closed by client"); + break; + } + + msg = buf.toString("ISO-8859-1"); + if (_log.shouldLog(Log.DEBUG)) { + _log.debug("New message received: " + msg); + } + buf.reset(); + + tok = new StringTokenizer(msg, " "); + if (tok.countTokens() < 2) { + // This is not a correct message, for sure + _log.debug("Error in message format"); + break; + } + domain = tok.nextToken(); + opcode = tok.nextToken(); + + _log.debug("Parsing (domain: \"" + domain + "\"; opcode: \"" + + opcode + "\")"); + if (domain.equals("RAW")) { + canContinue = execRawMessage(opcode, tok); + } else if (domain.equals("SESSION")) { + canContinue = execSessionMessage(opcode, tok); + } else if (domain.equals("DEST")) { + canContinue = execDestMessage(opcode, tok); + } else if (domain.equals("NAMING")) { + canContinue = execNamingMessage(opcode, tok); + } else { + _log.debug("Unrecognized message domain: \"" + + domain + "\""); + break; + } + + if (!canContinue) { + break; + } + } + } catch (UnsupportedEncodingException e) { + _log.error("Caught UnsupportedEncodingException (" + + e.getMessage() + ")"); + } catch (IOException e) { + _log.debug("Caught IOException (" + + e.getMessage() + ")"); + } catch (Exception e) { + _log.error("Unexpected exception", e); + } finally { + _log.debug("Stopping handler"); + try { + this.socket.close(); + } catch (IOException e) { + _log.error("Error closing socket: " + e.getMessage()); + } + if (rawSession != null) { + rawSession.close(); + } + if (datagramSession != null) { + datagramSession.close(); + } + if (streamSession != null) { + streamSession.close(); + } + } + } + + /* Parse and execute a SESSION message */ + private boolean execSessionMessage(String opcode, StringTokenizer tok) { + Properties props = null; + + if (opcode.equals("CREATE")) { + + if ((rawSession != null) || (datagramSession != null) + || (streamSession != null)) { + _log.debug("Trying to create a session, but one still exists"); + return false; + } + props = SAMUtils.parseParams(tok); + if (props == null) { + return false; + } + + String dest = props.getProperty("DESTINATION"); + if (dest == null) { + _log.debug("SESSION DESTINATION parameter not specified"); + return false; + } + props.remove("DESTINATION"); + + String style = props.getProperty("STYLE"); + if (style == null) { + _log.debug("SESSION STYLE parameter not specified"); + return false; + } + props.remove("STYLE"); + + try { + if (style.equals("RAW")) { + try { + if (dest.equals("TRANSIENT")) { + _log.debug("TRANSIENT destination requested"); + ByteArrayOutputStream priv = new ByteArrayOutputStream(); + SAMUtils.genRandomKey(priv, null); + + dest = Base64.encode(priv.toByteArray()); + } + rawSession = new SAMRawSession (dest, props, this); + writeBytes(("SESSION STATUS RESULT=OK DESTINATION=" + dest + "\n").getBytes("ISO-8859-1")); + } catch (DataFormatException e) { + _log.debug("Invalid destination specified"); + writeBytes(("SESSION STATUS RESULT=INVALID_KEY DESTINATION=" + dest + "\n").getBytes("ISO-8859-1")); + return true; + } catch (I2PSessionException e) { + _log.debug("I2P error when instantiating RAW session", e); + writeBytes(("SESSION STATUS RESULT=I2P_ERROR DESTINATION=" + dest + "\n").getBytes("ISO-8859-1")); + return true; + } + } else { + _log.debug("Unrecognized SESSION STYLE: \"" + style + "\""); + return false; + } + } catch (UnsupportedEncodingException e) { + _log.error("Caught UnsupportedEncodingException (" + + e.getMessage() + ")"); + return false; + } catch (IOException e) { + _log.error("Caught IOException while parsing SESSION message (" + + e.getMessage() + ")"); + return false; + } + + return true; + } else { + _log.debug("Unrecognized SESSION message opcode: \"" + + opcode + "\""); + return false; + } + } + + /* Parse and execute a DEST message*/ + private boolean execDestMessage(String opcode, StringTokenizer tok) { + + if (opcode.equals("GENERATE")) { + if (tok.countTokens() > 0) { + _log.debug("Bad format in DEST GENERATE message"); + return false; + } + + try { + ByteArrayOutputStream priv = new ByteArrayOutputStream(); + ByteArrayOutputStream pub = new ByteArrayOutputStream(); + + SAMUtils.genRandomKey(priv, pub); + writeBytes(("DEST REPLY" + + " PUB=" + + Base64.encode(pub.toByteArray()) + + " PRIV=" + + Base64.encode(priv.toByteArray()) + + "\n").getBytes("ISO-8859-1")); + } catch (UnsupportedEncodingException e) { + _log.error("Caught UnsupportedEncodingException (" + + e.getMessage() + ")"); + return false; + } catch (IOException e) { + _log.debug("IOException while executing DEST message", e); + return false; + } + } else { + _log.debug("Unrecognized DEST message opcode: \"" + opcode + "\""); + return false; + } + + return true; + } + + /* Parse and execute a NAMING message */ + private boolean execNamingMessage(String opcode, StringTokenizer tok) { + Properties props = null; + + if (opcode.equals("LOOKUP")) { + props = SAMUtils.parseParams(tok); + if (props == null) { + return false; + } + + String name = props.getProperty("NAME"); + if (name == null) { + _log.debug("Name to resolve not specified"); + return false; + } + + try { + ByteArrayOutputStream pubKey = new ByteArrayOutputStream(); + Destination dest = SAMUtils.lookupHost(name, pubKey); + + if (dest == null) { + writeBytes("NAMING REPLY RESULT=KEY_NOT_FOUND\n".getBytes("ISP-8859-1")); + return true; + } + + writeBytes(("NAMING REPLY RESULT=OK NAME=" + name + + " VALUE=" + Base64.encode(pubKey.toByteArray()) + + "\n").getBytes("ISO-8859-1")); + return true; + } catch (UnsupportedEncodingException e) { + _log.error("Caught UnsupportedEncodingException (" + + e.getMessage() + ")"); + return false; + } catch (IOException e) { + _log.debug("Caught IOException while parsing NAMING message", + e); + return false; + } + } else { + _log.debug("Unrecognized NAMING message opcode: \"" + + opcode + "\""); + return false; + } + } + + public String toString() { + return "SAM v1 handler (client: " + + this.socket.getInetAddress().toString() + ":" + + this.socket.getPort() + ")"; + } + + /* Parse and execute a RAW message */ + private boolean execRawMessage(String opcode, StringTokenizer tok) { + Properties props = null; + + if (rawSession == null) { + _log.debug("RAW message received, but no RAW session exists"); + return false; + } + + if (opcode.equals("SEND")) { + props = SAMUtils.parseParams(tok); + if (props == null) { + return false; + } + + String dest = props.getProperty("DESTINATION"); + if (dest == null) { + _log.debug("Destination not specified in RAW SEND message"); + return false; + } + + int size; + { + String strsize = props.getProperty("SIZE"); + if (strsize == null) { + _log.debug("Size not specified in RAW SEND message"); + return false; + } + try { + size = Integer.parseInt(strsize); + } catch (NumberFormatException e) { + _log.debug("Invalid RAW SEND size specified: " + strsize); + return false; + } + if (!checkSize(size)) { + _log.debug("Specified size (" + size + + ") is out of protocol limits"); + return false; + } + } + + try { + DataInputStream in = new DataInputStream(socket.getInputStream()); + byte[] data = new byte[size]; + + in.readFully(data); + + if (!rawSession.sendBytes(dest, data)) { + _log.error("RAW SEND failed"); + return false; + } + + return true; + } catch (EOFException e) { + _log.debug("Too few bytes with RAW SEND message (expected: " + + size); + return false; + } catch (IOException e) { + _log.debug("Caught IOException while parsing RAW SEND message", + e); + return false; + } catch (DataFormatException e) { + _log.debug("Invalid key specified with RAW SEND message", + e); + return false; + } + } else { + _log.debug("Unrecognized RAW message opcode: \"" + + opcode + "\""); + return false; + } + } + + /* Check whether a size is inside the limits allowed by this protocol */ + private boolean checkSize(int size) { + return ((size >= 1) && (size <= 32768)); + } + + // SAMRawReceiver implementation + public void receiveRawBytes(byte data[]) throws IOException { + if (rawSession == null) { + _log.error("BUG! Trying to write raw bytes, but session is null!"); + throw new NullPointerException("BUG! RAW session is null!"); + } + + ByteArrayOutputStream msg = new ByteArrayOutputStream(); + + msg.write(("RAW RECEIVED SIZE=" + data.length + "\n").getBytes()); + msg.write(data); + + writeBytes(msg.toByteArray()); + } + + public void stopReceiving() { + _log.debug("stopReceiving() invoked"); + try { + this.socket.close(); + } catch (IOException e) { + _log.error("Error closing socket: " + e.getMessage()); + } + } +}