package net.i2p.sam.client; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.Properties; import java.util.StringTokenizer; import java.util.concurrent.atomic.AtomicInteger; import net.i2p.I2PAppContext; import net.i2p.client.I2PSession; import net.i2p.data.DataHelper; import net.i2p.util.I2PAppThread; import net.i2p.util.Log; /** * Read from a socket, producing events for any SAM message read * */ public class SAMReader { private final Log _log; private final InputStream _inRaw; private final SAMClientEventListener _listener; private volatile boolean _live; private Thread _thread; private static final AtomicInteger _count = new AtomicInteger(); public SAMReader(I2PAppContext context, InputStream samIn, SAMClientEventListener listener) { _log = context.logManager().getLog(SAMReader.class); _inRaw = samIn; _listener = listener; } public synchronized void startReading() { if (_live) throw new IllegalStateException(); _live = true; I2PAppThread t = new I2PAppThread(new Runner(), "SAM reader " + _count.incrementAndGet()); t.start(); _thread = t; } public synchronized void stopReading() { _live = false; if (_thread != null) { _thread.interrupt(); _thread = null; try { _inRaw.close(); } catch (IOException ioe) {} } } /** * Async event notification interface for SAM clients * */ public interface SAMClientEventListener { public static final String SESSION_STATUS_OK = "OK"; public static final String SESSION_STATUS_DUPLICATE_DEST = "DUPLICATE_DEST"; public static final String SESSION_STATUS_I2P_ERROR = "I2P_ERROR"; public static final String SESSION_STATUS_INVALID_KEY = "INVALID_KEY"; public static final String STREAM_STATUS_OK = "OK"; public static final String STREAM_STATUS_CANT_REACH_PEER = "CANT_REACH_PEER"; public static final String STREAM_STATUS_I2P_ERROR = "I2P_ERROR"; public static final String STREAM_STATUS_INVALID_KEY = "INVALID_KEY"; public static final String STREAM_STATUS_TIMEOUT = "TIMEOUT"; public static final String STREAM_CLOSED_OK = "OK"; public static final String STREAM_CLOSED_CANT_REACH_PEER = "CANT_REACH_PEER"; public static final String STREAM_CLOSED_I2P_ERROR = "I2P_ERROR"; public static final String STREAM_CLOSED_PEER_NOT_FOUND = "PEER_NOT_FOUND"; public static final String STREAM_CLOSED_TIMEOUT = "CLOSED"; public static final String NAMING_REPLY_OK = "OK"; public static final String NAMING_REPLY_INVALID_KEY = "INVALID_KEY"; public static final String NAMING_REPLY_KEY_NOT_FOUND = "KEY_NOT_FOUND"; public void helloReplyReceived(boolean ok, String version); public void sessionStatusReceived(String result, String destination, String message); public void streamStatusReceived(String result, String id, String message); public void streamConnectedReceived(String remoteDestination, String id); public void streamClosedReceived(String result, String id, String message); public void streamDataReceived(String id, byte data[], int offset, int length); public void namingReplyReceived(String name, String result, String value, String message); public void destReplyReceived(String publicKey, String privateKey); public void datagramReceived(String dest, byte[] data, int offset, int length, int fromPort, int toPort); public void rawReceived(byte[] data, int offset, int length, int fromPort, int toPort, int protocol); public void pingReceived(String data); public void pongReceived(String data); public void unknownMessageReceived(String major, String minor, Properties params); } private class Runner implements Runnable { public void run() { Properties params = new Properties(); ByteArrayOutputStream baos = new ByteArrayOutputStream(80); while (_live) { try { int c = -1; while ((c = _inRaw.read()) != -1) { if (c == '\n') { break; } baos.write(c); } if (c == -1) { _log.info("EOF reading from the SAM bridge"); break; } } catch (IOException ioe) { _log.error("Error reading from SAM", ioe); break; } String line = DataHelper.getUTF8(baos.toByteArray()); baos.reset(); if (_log.shouldDebug()) _log.debug("Line read from the bridge: " + line); StringTokenizer tok = new StringTokenizer(line); if (tok.countTokens() <= 0) { _log.error("Invalid SAM line: [" + line + "]"); break; } String major = tok.nextToken(); String minor = tok.hasMoreTokens() ? tok.nextToken() : ""; params.clear(); while (tok.hasMoreTokens()) { String pair = tok.nextToken(); int eq = pair.indexOf('='); if ( (eq > 0) && (eq < pair.length() - 1) ) { String name = pair.substring(0, eq); String val = pair.substring(eq+1); if (val.length() <= 0) { _log.error("Empty value for " + name); continue; } while ( (val.charAt(0) == '\"') && (val.length() > 0) ) val = val.substring(1); while ( (val.length() > 0) && (val.charAt(val.length()-1) == '\"') ) val = val.substring(0, val.length()-1); params.setProperty(name, val); } } processEvent(major, minor, params); } _live = false; if (_log.shouldWarn()) _log.warn("SAMReader exiting"); } } /** * Big ugly method parsing everything. If I cared, I'd factor this out into * a dozen tiny methods. * */ private void processEvent(String major, String minor, Properties params) { if ("HELLO".equals(major)) { if ("REPLY".equals(minor)) { String result = params.getProperty("RESULT"); String version= params.getProperty("VERSION"); if ("OK".equals(result) && version != null) _listener.helloReplyReceived(true, version); else _listener.helloReplyReceived(false, version); } else { _listener.unknownMessageReceived(major, minor, params); } } else if ("SESSION".equals(major)) { if ("STATUS".equals(minor)) { String result = params.getProperty("RESULT"); String dest = params.getProperty("DESTINATION"); String msg = params.getProperty("MESSAGE"); _listener.sessionStatusReceived(result, dest, msg); } else { _listener.unknownMessageReceived(major, minor, params); } } else if ("STREAM".equals(major)) { if ("STATUS".equals(minor)) { String result = params.getProperty("RESULT"); String id = params.getProperty("ID"); String msg = params.getProperty("MESSAGE"); // id is null in v3, so pass it through regardless //if (id != null) { _listener.streamStatusReceived(result, id, msg); //} else { // _listener.unknownMessageReceived(major, minor, params); //} } else if ("CONNECTED".equals(minor)) { String dest = params.getProperty("DESTINATION"); String id = params.getProperty("ID"); if (id != null) { _listener.streamConnectedReceived(dest, id); } else { _listener.unknownMessageReceived(major, minor, params); } } else if ("CLOSED".equals(minor)) { String result = params.getProperty("RESULT"); String id = params.getProperty("ID"); String msg = params.getProperty("MESSAGE"); if (id != null) { _listener.streamClosedReceived(result, id, msg); } else { _listener.unknownMessageReceived(major, minor, params); } } else if ("RECEIVED".equals(minor)) { String id = params.getProperty("ID"); String size = params.getProperty("SIZE"); if (id != null) { try { int sizeVal = Integer.parseInt(size); byte data[] = new byte[sizeVal]; int read = DataHelper.read(_inRaw, data); if (read != sizeVal) { _listener.unknownMessageReceived(major, minor, params); } else { _listener.streamDataReceived(id, data, 0, sizeVal); } } catch (NumberFormatException nfe) { _listener.unknownMessageReceived(major, minor, params); } catch (IOException ioe) { _live = false; _listener.unknownMessageReceived(major, minor, params); } } else { _listener.unknownMessageReceived(major, minor, params); } } else { _listener.unknownMessageReceived(major, minor, params); } } else if ("DATAGRAM".equals(major)) { if ("RECEIVED".equals(minor)) { String dest = params.getProperty("DESTINATION"); String size = params.getProperty("SIZE"); String fp = params.getProperty("FROM_PORT"); String tp = params.getProperty("TO_PORT"); int fromPort = 0; int toPort = 0; if (dest != null) { try { if (fp != null) fromPort = Integer.parseInt(fp); if (tp != null) toPort = Integer.parseInt(tp); int sizeVal = Integer.parseInt(size); byte data[] = new byte[sizeVal]; int read = DataHelper.read(_inRaw, data); if (read != sizeVal) { _listener.unknownMessageReceived(major, minor, params); } else { _listener.datagramReceived(dest, data, 0, sizeVal, fromPort, toPort); } } catch (NumberFormatException nfe) { _listener.unknownMessageReceived(major, minor, params); } catch (IOException ioe) { _live = false; _listener.unknownMessageReceived(major, minor, params); } } else { _listener.unknownMessageReceived(major, minor, params); } } else { _listener.unknownMessageReceived(major, minor, params); } } else if ("RAW".equals(major)) { if ("RECEIVED".equals(minor)) { String size = params.getProperty("SIZE"); String fp = params.getProperty("FROM_PORT"); String tp = params.getProperty("TO_PORT"); String pr = params.getProperty("PROTOCOL"); int fromPort = 0; int toPort = 0; int protocol = I2PSession.PROTO_DATAGRAM_RAW; try { if (fp != null) fromPort = Integer.parseInt(fp); if (tp != null) toPort = Integer.parseInt(tp); if (pr != null) protocol = Integer.parseInt(pr); int sizeVal = Integer.parseInt(size); byte data[] = new byte[sizeVal]; int read = DataHelper.read(_inRaw, data); if (read != sizeVal) { _listener.unknownMessageReceived(major, minor, params); } else { _listener.rawReceived(data, 0, sizeVal, fromPort, toPort, protocol); } } catch (NumberFormatException nfe) { _listener.unknownMessageReceived(major, minor, params); } catch (IOException ioe) { _live = false; _listener.unknownMessageReceived(major, minor, params); } } else { _listener.unknownMessageReceived(major, minor, params); } } else if ("NAMING".equals(major)) { if ("REPLY".equals(minor)) { String name = params.getProperty("NAME"); String result = params.getProperty("RESULT"); String value = params.getProperty("VALUE"); String msg = params.getProperty("MESSAGE"); _listener.namingReplyReceived(name, result, value, msg); } else { _listener.unknownMessageReceived(major, minor, params); } } else if ("DEST".equals(major)) { if ("REPLY".equals(minor)) { String pub = params.getProperty("PUB"); String priv = params.getProperty("PRIV"); _listener.destReplyReceived(pub, priv); } else { _listener.unknownMessageReceived(major, minor, params); } } else if ("PING".equals(major)) { // this omits anything after a space _listener.pingReceived(minor); } else if ("PONG".equals(major)) { // this omits anything after a space _listener.pongReceived(minor); } else { _listener.unknownMessageReceived(major, minor, params); } } }