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.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Arrays; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import javax.net.ssl.SSLServerSocket; import javax.net.ssl.SSLServerSocketFactory; import gnu.getopt.Getopt; import net.i2p.I2PAppContext; import net.i2p.app.*; import static net.i2p.app.ClientAppState.*; import net.i2p.data.DataFormatException; import net.i2p.data.DataHelper; import net.i2p.data.Destination; import net.i2p.util.I2PAppThread; import net.i2p.util.I2PSSLSocketFactory; import net.i2p.util.Log; import net.i2p.util.OrderedProperties; import net.i2p.util.PortMapper; import net.i2p.util.SystemVersion; /** * SAM bridge implementation. * This is the main entry point for SAM. * * @author human */ public class SAMBridge implements Runnable, ClientApp { private final Log _log; private volatile ServerSocketChannel serverSocket; private final String _listenHost; private final int _listenPort; private final Properties i2cpProps; private final boolean _useSSL; private final File _configFile; private volatile Thread _runner; private final Object _v3DGServerLock = new Object(); private SAMv3DatagramServer _v3DGServer; /** * filename in which the name to private key mapping should * be stored (and loaded from) */ private final String persistFilename; /** * app designated destination name to the base64 of the I2P formatted * destination keys (Destination+PrivateKey+SigningPrivateKey) */ private final Map<String,String> nameToPrivKeys; private final Set<Handler> _handlers; private volatile boolean acceptConnections = true; private final ClientAppManager _mgr; private volatile ClientAppState _state = UNINITIALIZED; private static final int SAM_LISTENPORT = 7656; public static final String DEFAULT_SAM_KEYFILE = "sam.keys"; static final String DEFAULT_SAM_CONFIGFILE = "sam.config"; private static final String PROP_SAM_KEYFILE = "sam.keyfile"; private static final String PROP_SAM_SSL = "sam.useSSL"; public static final String PROP_TCP_HOST = "sam.tcp.host"; public static final String PROP_TCP_PORT = "sam.tcp.port"; public static final String PROP_AUTH = "sam.auth"; public static final String PROP_PW_PREFIX = "sam.auth."; public static final String PROP_PW_SUFFIX = ".shash"; protected static final String DEFAULT_TCP_HOST = "127.0.0.1"; protected static final String DEFAULT_TCP_PORT = "7656"; public static final String PROP_DATAGRAM_HOST = "sam.udp.host"; public static final String PROP_DATAGRAM_PORT = "sam.udp.port"; protected static final String DEFAULT_DATAGRAM_HOST = "127.0.0.1"; protected static final int DEFAULT_DATAGRAM_PORT_INT = 7655; protected static final String DEFAULT_DATAGRAM_PORT = Integer.toString(DEFAULT_DATAGRAM_PORT_INT); /** * For ClientApp interface. * Recommended constructor for external use. * Does NOT open the listener socket or start threads; caller must call startup() * * @param mgr may be null * @param args non-null * @throws Exception on bad args * @since 0.9.6 */ public SAMBridge(I2PAppContext context, ClientAppManager mgr, String[] args) throws Exception { _log = context.logManager().getLog(SAMBridge.class); _mgr = mgr; Options options = getOptions(args); _listenHost = options.host; _listenPort = options.port; _useSSL = options.isSSL; if (_useSSL && !SystemVersion.isJava7()) throw new IllegalArgumentException("SSL requires Java 7 or higher"); persistFilename = options.keyFile; _configFile = options.configFile; nameToPrivKeys = new HashMap<String,String>(8); _handlers = new HashSet<Handler>(8); this.i2cpProps = options.opts; _state = INITIALIZED; } /** * Build a new SAM bridge. * NOT recommended for external use. * * Opens the listener socket but does NOT start the thread, and there's no * way to do that externally. * Use main(), or use the other constructor and call startup(). * * Deprecated for external use, to be made private. * * @param listenHost hostname to listen for SAM connections on ("0.0.0.0" for all) * @param listenPort port number to listen for SAM connections on * @param i2cpProps set of I2CP properties for finding and communicating with the router * @param persistFile location to store/load named keys to/from * @throws RuntimeException if a server socket can't be opened */ public SAMBridge(String listenHost, int listenPort, boolean isSSL, Properties i2cpProps, String persistFile, File configFile) { _log = I2PAppContext.getGlobalContext().logManager().getLog(SAMBridge.class); _mgr = null; _listenHost = listenHost; _listenPort = listenPort; _useSSL = isSSL; if (_useSSL && !SystemVersion.isJava7()) throw new IllegalArgumentException("SSL requires Java 7 or higher"); this.i2cpProps = i2cpProps; persistFilename = persistFile; _configFile = configFile; nameToPrivKeys = new HashMap<String,String>(8); _handlers = new HashSet<Handler>(8); loadKeys(); try { openSocket(); } catch (IOException e) { if (_log.shouldLog(Log.ERROR)) _log.error("Error starting SAM bridge on " + (listenHost == null ? "0.0.0.0" : listenHost) + ":" + listenPort, e); throw new RuntimeException(e); } _state = INITIALIZED; } /** * @since 0.9.6 */ private void openSocket() throws IOException { if (_useSSL) { SSLServerSocketFactory fact = SSLUtil.initializeFactory(i2cpProps); InetAddress addr; if (_listenHost != null && !_listenHost.equals("0.0.0.0")) addr = InetAddress.getByName(_listenHost); else addr = null; SSLServerSocket sock = (SSLServerSocket) fact.createServerSocket(_listenPort, 0, addr); I2PSSLSocketFactory.setProtocolsAndCiphers(sock); serverSocket = new SSLServerSocketChannel(sock); } else { serverSocket = ServerSocketChannel.open(); if (_listenHost != null && !_listenHost.equals("0.0.0.0")) { serverSocket.socket().bind(new InetSocketAddress(_listenHost, _listenPort)); if (_log.shouldLog(Log.DEBUG)) _log.debug("SAM bridge listening on " + _listenHost + ":" + _listenPort); } else { serverSocket.socket().bind(new InetSocketAddress(_listenPort)); if (_log.shouldLog(Log.DEBUG)) _log.debug("SAM bridge listening on 0.0.0.0:" + _listenPort); } } } /** * Retrieve the destination associated with the given name * * @param name name of the destination * @return null if the name does not exist, or if it is improperly formatted */ /**** public Destination getDestination(String name) { synchronized (nameToPrivKeys) { String val = nameToPrivKeys.get(name); if (val == null) return null; try { Destination d = new Destination(); d.fromBase64(val); return d; } catch (DataFormatException dfe) { _log.error("Error retrieving the destination from " + name, dfe); nameToPrivKeys.remove(name); return null; } } } ****/ /** * Retrieve the I2P private keystream for the given name, formatted * as a base64 string (Destination+PrivateKey+SessionPrivateKey, as I2CP * stores it). * * @param name Name of the destination * @return null if the name does not exist, else the stream */ public String getKeystream(String name) { synchronized (nameToPrivKeys) { String val = nameToPrivKeys.get(name); if (val == null) return null; return val; } } /** * Specify that the given keystream should be used for the given name * * @param name Name of the destination * @param stream Name of the stream */ public void addKeystream(String name, String stream) { synchronized (nameToPrivKeys) { nameToPrivKeys.put(name, stream); } storeKeys(); } /** * Load up the keys from the persistFilename. */ @SuppressWarnings("unchecked") private void loadKeys() { synchronized (nameToPrivKeys) { nameToPrivKeys.clear(); File file = new File(persistFilename); // now in config dir but check base dir too... if (!file.exists()) { if (file.isAbsolute()) return; file = new File(I2PAppContext.getGlobalContext().getConfigDir(), persistFilename); if (!file.exists()) return; } try { Properties props = new Properties(); DataHelper.loadProps(props, file); // unchecked Map foo = props; nameToPrivKeys.putAll(foo); if (_log.shouldInfo()) _log.info("Loaded " + nameToPrivKeys.size() + " private keys from " + file); } catch (IOException ioe) { _log.error("Unable to read the keys from " + file, ioe); } } } /** * Store the current keys to disk in the location specified on creation. */ private void storeKeys() { synchronized (nameToPrivKeys) { File file = new File(persistFilename); // now in config dir but check base dir too... if (!file.exists() && !file.isAbsolute()) file = new File(I2PAppContext.getGlobalContext().getConfigDir(), persistFilename); try { Properties props = new OrderedProperties(); props.putAll(nameToPrivKeys); DataHelper.storeProps(props, file); if (_log.shouldInfo()) _log.info("Saved " + nameToPrivKeys.size() + " private keys to " + file); } catch (IOException ioe) { _log.error("Error writing out the SAM keys to " + file, ioe); } } } /** * Handlers must call on startup * @since 0.9.20 */ public void register(Handler handler) { if (_log.shouldInfo()) _log.info("Register " + handler); synchronized (_handlers) { _handlers.add(handler); } } /** * Handlers must call on stop * @since 0.9.20 */ public void unregister(Handler handler) { if (_log.shouldInfo()) _log.info("Unregister " + handler); synchronized (_handlers) { _handlers.remove(handler); } } /** * Stop all the handlers. * @since 0.9.20 */ private void stopHandlers() { List<Handler> handlers = null; synchronized (_handlers) { if (!_handlers.isEmpty()) { handlers = new ArrayList<Handler>(_handlers); _handlers.clear(); } } if (handlers != null) { for (Handler handler : handlers) { if (_log.shouldInfo()) _log.info("Stopping " + handler); handler.stopHandling(); } } } /** * Was a static singleton, now a singleton for this bridge. * Instantiate and start server if it doesn't exist. * We only listen on one host and port, as specified in the * sam.udp.host and sam.udp.port properties. * TODO we could have multiple servers on different hosts/ports in the future. * * @param props non-null instantiate and start server if it doesn't exist * @return non-null * @throws IOException if can't bind to host/port, or if different than existing * @since 0.9.24 */ SAMv3DatagramServer getV3DatagramServer(Properties props) throws IOException { String host = props.getProperty(PROP_DATAGRAM_HOST, DEFAULT_DATAGRAM_HOST); int port; String portStr = props.getProperty(PROP_DATAGRAM_PORT, DEFAULT_DATAGRAM_PORT); try { port = Integer.parseInt(portStr); } catch (NumberFormatException e) { port = DEFAULT_DATAGRAM_PORT_INT; } synchronized (_v3DGServerLock) { if (_v3DGServer == null) { _v3DGServer = new SAMv3DatagramServer(this, host, port, props); _v3DGServer.start(); } else { if (_v3DGServer.getPort() != port || !_v3DGServer.getHost().equals(host)) throw new IOException("Already have V3 DatagramServer with host=" + host + " port=" + port); } return _v3DGServer; } } ////// begin ClientApp interface, use only if using correct construtor /** * @since 0.9.6 */ public synchronized void startup() throws IOException { if (_state != INITIALIZED) return; changeState(STARTING); synchronized (_handlers) { _handlers.clear(); } loadKeys(); try { openSocket(); } catch (IOException e) { if (_log.shouldLog(Log.ERROR)) _log.error("Error starting SAM bridge on " + (_listenHost == null ? "0.0.0.0" : _listenHost) + ":" + _listenPort, e); changeState(START_FAILED, e); throw e; } startThread(); } /** * As of 0.9.20, stops running handlers and sessions. * * @since 0.9.6 */ public synchronized void shutdown(String[] args) { if (_state != RUNNING) return; changeState(STOPPING); acceptConnections = false; stopHandlers(); if (_runner != null) _runner.interrupt(); else changeState(STOPPED); } /** * @since 0.9.6 */ public ClientAppState getState() { return _state; } /** * @since 0.9.6 */ public String getName() { return "SAM"; } /** * @since 0.9.6 */ public String getDisplayName() { return "SAM " + _listenHost + ':' + _listenPort; } ////// end ClientApp interface ////// begin ClientApp helpers /** * @since 0.9.6 */ private void changeState(ClientAppState state) { changeState(state, null); } /** * @since 0.9.6 */ private synchronized void changeState(ClientAppState state, Exception e) { _state = state; if (_mgr != null) _mgr.notify(this, state, null, e); } ////// end ClientApp helpers private static class HelpRequestedException extends Exception {static final long serialVersionUID=0x1;} /** * Usage: * <pre>SAMBridge [ keyfile [listenHost ] listenPort [ name=val ]* ]</pre> * or: * <pre>SAMBridge [ name=val ]* </pre> * * name=val options are passed to the I2CP code to build a session, * allowing the bridge to specify an alternate I2CP host and port, tunnel * depth, etc. * @param args [ keyfile [ listenHost ] listenPort [ name=val ]* ] */ public static void main(String args[]) { try { Options options = getOptions(args); SAMBridge bridge = new SAMBridge(options.host, options.port, options.isSSL, options.opts, options.keyFile, options.configFile); bridge.startThread(); } catch (RuntimeException e) { e.printStackTrace(); usage(); throw e; } catch (Exception e) { e.printStackTrace(); usage(); throw new RuntimeException(e); } } /** * @since 0.9.6 */ private void startThread() { I2PAppThread t = new I2PAppThread(this, "SAMListener " + _listenPort); if (Boolean.parseBoolean(System.getProperty("sam.shutdownOnOOM"))) { t.addOOMEventThreadListener(new I2PAppThread.OOMEventListener() { public void outOfMemory(OutOfMemoryError err) { err.printStackTrace(); System.err.println("OOMed, die die die"); System.exit(-1); } }); } t.start(); _runner = t; } /** * @since 0.9.6 */ private static class Options { private final String host, keyFile; private final int port; private final Properties opts; private final boolean isSSL; private final File configFile; public Options(String host, int port, boolean isSSL, Properties opts, String keyFile, File configFile) { this.host = host; this.port = port; this.opts = opts; this.keyFile = keyFile; this.isSSL = isSSL; this.configFile = configFile; } } /** * Usage: * <pre>SAMBridge [ keyfile [listenHost ] listenPort [ name=val ]* ]</pre> * or: * <pre>SAMBridge [ name=val ]* </pre> * * name=val options are passed to the I2CP code to build a session, * allowing the bridge to specify an alternate I2CP host and port, tunnel * depth, etc. * @param args [ keyfile [ listenHost ] listenPort [ name=val ]* ] * @return non-null Options or throws Exception * @throws HelpRequestedException on command line problems * @throws IllegalArgumentException if specified config file does not exist * @throws IOException if specified config file cannot be read, or on SSL keystore problems * @since 0.9.6 */ private static Options getOptions(String args[]) throws Exception { String keyfile = null; int port = -1; String host = null; boolean isSSL = false; String cfile = null; Getopt g = new Getopt("SAM", args, "hsc:"); int c; while ((c = g.getopt()) != -1) { switch (c) { case 's': isSSL = true; break; case 'c': cfile = g.getOptarg(); break; case 'h': case '?': case ':': default: throw new HelpRequestedException(); } // switch } // while int startArgs = g.getOptind(); // possible args before ones containing '='; // (none) // key port // key host port int startOpts; for (startOpts = startArgs; startOpts < args.length; startOpts++) { if (args[startOpts].contains("=")) break; } int numArgs = startOpts - startArgs; switch (numArgs) { case 0: break; case 2: keyfile = args[startArgs]; try { port = Integer.parseInt(args[startArgs + 1]); } catch (NumberFormatException nfe) { throw new HelpRequestedException(); } break; case 3: keyfile = args[startArgs]; host = args[startArgs + 1]; try { port = Integer.parseInt(args[startArgs + 2]); } catch (NumberFormatException nfe) { throw new HelpRequestedException(); } break; default: throw new HelpRequestedException(); } String scfile = cfile != null ? cfile : DEFAULT_SAM_CONFIGFILE; File file = new File(scfile); if (!file.isAbsolute()) file = new File(I2PAppContext.getGlobalContext().getConfigDir(), scfile); Properties opts = new Properties(); if (file.exists()) { DataHelper.loadProps(opts, file); } else if (cfile != null) { // only throw if specified on command line throw new IllegalArgumentException("Config file not found: " + file); } // command line trumps config file trumps defaults if (host == null) host = opts.getProperty(PROP_TCP_HOST, DEFAULT_TCP_HOST); if (port < 0) { try { port = Integer.parseInt(opts.getProperty(PROP_TCP_PORT, DEFAULT_TCP_PORT)); } catch (NumberFormatException nfe) { throw new HelpRequestedException(); } } if (keyfile == null) keyfile = opts.getProperty(PROP_SAM_KEYFILE, DEFAULT_SAM_KEYFILE); if (!isSSL) isSSL = Boolean.parseBoolean(opts.getProperty(PROP_SAM_SSL)); if (isSSL) { // must do this before we add command line opts since we may be writing them back out boolean shouldSave = SSLUtil.verifyKeyStore(opts); if (shouldSave) DataHelper.storeProps(opts, file); } int remaining = args.length - startOpts; if (remaining > 0) { parseOptions(args, startOpts, opts); } return new Options(host, port, isSSL, opts, keyfile, file); } /** * Parse key=value options starting at startArgs. * @param props out parameter, any options found are added * @throws HelpRequestedException on any item not of the form key=value. */ private static void parseOptions(String args[], int startArgs, Properties props) throws HelpRequestedException { for (int i = startArgs; i < args.length; i++) { int eq = args[i].indexOf('='); if (eq <= 0) throw new HelpRequestedException(); if (eq >= args[i].length()-1) throw new HelpRequestedException(); String key = args[i].substring(0, eq); String val = args[i].substring(eq+1); key = key.trim(); val = val.trim(); if ( (key.length() > 0) && (val.length() > 0) ) props.setProperty(key, val); else throw new HelpRequestedException(); } } private static void usage() { System.err.println("Usage: SAMBridge [-s] [-c sam.config] [keyfile [listenHost] listenPortNum[ name=val]*]\n" + "or:\n" + " SAMBridge [ name=val ]*\n" + " -s: Use SSL\n" + " -c sam.config: Specify config file\n" + " keyfile: location to persist private keys (default sam.keys)\n" + " listenHost: interface to listen on (0.0.0.0 for all interfaces)\n" + " listenPort: port to listen for SAM connections on (default 7656)\n" + " name=val: options to pass when connecting via I2CP, such as \n" + " i2cp.host=localhost and i2cp.port=7654\n" + "\n" + "Host and ports of the SAM bridge can be specified with the alternate\n" + "form by specifying options "+SAMBridge.PROP_TCP_HOST+" and/or "+ SAMBridge.PROP_TCP_PORT + "\n" + "Options "+SAMBridge.PROP_DATAGRAM_HOST+" and "+SAMBridge.PROP_DATAGRAM_PORT+ " specify the listening ip\n" + "range and the port of SAM datagram server. This server is\n" + "only launched after a client creates the first SAM datagram\n" + "or raw session, after a handshake with SAM version >= 3.0.\n" + "\n" + "The option loglevel=[DEBUG|WARN|ERROR|CRIT] can be used\n" + "for tuning the log verbosity."); } public void run() { if (serverSocket == null) return; changeState(RUNNING); if (_mgr != null) _mgr.register(this); I2PAppContext.getGlobalContext().portMapper().register(_useSSL ? PortMapper.SVC_SAM_SSL : PortMapper.SVC_SAM, _listenHost != null ? _listenHost : "127.0.0.1", _listenPort); try { while (acceptConnections) { SocketChannel s = serverSocket.accept(); if (_log.shouldLog(Log.DEBUG)) _log.debug("New connection from " + s.socket().getInetAddress().toString() + ":" + s.socket().getPort()); class HelloHandler implements Runnable, Handler { private final SocketChannel s; private final SAMBridge parent; HelloHandler(SocketChannel s, SAMBridge parent) { this.s = s ; this.parent = parent ; } public void run() { parent.register(this); try { SAMHandler handler = SAMHandlerFactory.createSAMHandler(s, i2cpProps, parent); if (handler == null) { if (_log.shouldLog(Log.DEBUG)) _log.debug("SAM handler has not been instantiated"); try { s.close(); } catch (IOException e) {} return; } handler.startHandling(); } catch (SAMException e) { if (_log.shouldLog(Log.ERROR)) _log.error("SAM error: " + e.getMessage(), e); String reply = "HELLO REPLY RESULT=I2P_ERROR MESSAGE=\"" + e.getMessage() + "\"\n"; SAMHandler.writeString(reply, s); try { s.close(); } catch (IOException ioe) {} } catch (Exception ee) { try { s.close(); } catch (IOException ioe) {} _log.log(Log.CRIT, "Unexpected error handling SAM connection", ee); } finally { parent.unregister(this); } } /** @since 0.9.20 */ public void stopHandling() { try { s.close(); } catch (IOException ioe) {} } } new I2PAppThread(new HelloHandler(s,this), "SAM HelloHandler").start(); } changeState(STOPPING); } catch (Exception e) { if (acceptConnections) _log.error("Unexpected error while listening for connections", e); else e = null; changeState(STOPPING, e); } finally { try { if (_log.shouldLog(Log.DEBUG)) _log.debug("Shutting down, closing server socket"); if (serverSocket != null) serverSocket.close(); } catch (IOException e) {} I2PAppContext.getGlobalContext().portMapper().unregister(_useSSL ? PortMapper.SVC_SAM_SSL : PortMapper.SVC_SAM); stopHandlers(); changeState(STOPPED); } } /** @since 0.9.24 */ public void saveConfig() throws IOException { DataHelper.storeProps(i2cpProps, _configFile); } }