diff --git a/apps/sam/java/src/net/i2p/sam/SAMBridge.java b/apps/sam/java/src/net/i2p/sam/SAMBridge.java index beb23235639dd30099c103c6e27f9415a06afb88..11332bdf223697321c19d61393bb05b7c9ce53fa 100644 --- a/apps/sam/java/src/net/i2p/sam/SAMBridge.java +++ b/apps/sam/java/src/net/i2p/sam/SAMBridge.java @@ -1,9 +1,9 @@ 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 + * 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. * */ @@ -63,17 +63,24 @@ public class SAMBridge implements Runnable, ClientApp { private volatile Thread _runner; private final Object _v3DGServerLock = new Object(); private SAMv3DatagramServer _v3DGServer; + /** + * Pluggable "Secure Session Manager" for interactive, GUI-based session + * confirmation. This will block the SAM Handler Factory at the HELLO phase. + * during the createSAMHandler call. If it's null, then no interactive session + * will be used and SAM will work without it. + */ + private final SAMSecureSessionInterface _secureSession; - /** - * filename in which the name to private key mapping should - * be stored (and loaded from) + /** + * 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 + /** + * app designated destination name to the base64 of the I2P formatted * destination keys (Destination+PrivateKey+SigningPrivateKey) */ - private final Map<String,String> nameToPrivKeys; + private final Map<String, String> nameToPrivKeys; private final Set<Handler> _handlers; private volatile boolean acceptConnections = true; @@ -82,7 +89,7 @@ public class SAMBridge implements Runnable, ClientApp { 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"; @@ -94,27 +101,28 @@ public class SAMBridge implements Runnable, ClientApp { 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() + * 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 + * @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; + _secureSession = null; Options options = getOptions(args); _listenHost = options.host; _listenPort = options.port; @@ -123,13 +131,12 @@ public class SAMBridge implements Runnable, ClientApp { throw new IllegalArgumentException("SSL requires Java 7 or higher"); persistFilename = options.keyFile; _configFile = options.configFile; - nameToPrivKeys = new HashMap<String,String>(8); + 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. @@ -140,25 +147,57 @@ public class SAMBridge implements Runnable, ClientApp { * * 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 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) { + String persistFile, File configFile) { + this(listenHost, listenPort, isSSL, i2cpProps, + persistFile, configFile, null); + + } + + /** + * 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 + * @param secureSession an instance of a Secure Session to use + * @throws RuntimeException if a server socket can't be opened + * + * @since 1.8.0 + */ + public SAMBridge(String listenHost, int listenPort, boolean isSSL, Properties i2cpProps, + String persistFile, File configFile, SAMSecureSessionInterface secureSession) { _log = I2PAppContext.getGlobalContext().logManager().getLog(SAMBridge.class); _mgr = null; _listenHost = listenHost; _listenPort = listenPort; _useSSL = isSSL; + _secureSession = secureSession; + 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); + nameToPrivKeys = new HashMap<String, String>(8); _handlers = new HashSet<Handler>(8); loadKeys(); try { @@ -166,15 +205,15 @@ public class SAMBridge implements Runnable, ClientApp { } catch (IOException e) { if (_log.shouldLog(Log.ERROR)) _log.error("Error starting SAM bridge on " - + (listenHost == null ? "0.0.0.0" : listenHost) - + ":" + listenPort, e); + + (listenHost == null ? "0.0.0.0" : listenHost) + + ":" + listenPort, e); throw new RuntimeException(e); } _state = INITIALIZED; } /** - * @since 0.9.6 + * @since 0.9.6 */ private void openSocket() throws IOException { if (_useSSL) { @@ -193,7 +232,7 @@ public class SAMBridge implements Runnable, ClientApp { serverSocket.socket().bind(new InetSocketAddress(_listenHost, _listenPort)); if (_log.shouldLog(Log.DEBUG)) _log.debug("SAM bridge listening on " - + _listenHost + ":" + _listenPort); + + _listenHost + ":" + _listenPort); } else { serverSocket.socket().bind(new InetSocketAddress(_listenPort)); if (_log.shouldLog(Log.DEBUG)) @@ -208,27 +247,27 @@ public class SAMBridge implements Runnable, ClientApp { * @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; - } - } - } -****/ - + /**** + * 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 + * as a base64 string (Destination+PrivateKey+SessionPrivateKey, as I2CP * stores it). * * @param name Name of the destination @@ -237,7 +276,8 @@ public class SAMBridge implements Runnable, ClientApp { public String getKeystream(String name) { synchronized (nameToPrivKeys) { String val = nameToPrivKeys.get(name); - if (val == null) return null; + if (val == null) + return null; return val; } } @@ -245,8 +285,8 @@ public class SAMBridge implements Runnable, ClientApp { /** * Specify that the given keystream should be used for the given name * - * @param name Name of the destination - * @param stream Name of the stream + * @param name Name of the destination + * @param stream Name of the stream */ public void addKeystream(String name, String stream) { synchronized (nameToPrivKeys) { @@ -254,7 +294,7 @@ public class SAMBridge implements Runnable, ClientApp { } storeKeys(); } - + /** * Load up the keys from the persistFilename. */ @@ -284,7 +324,7 @@ public class SAMBridge implements Runnable, ClientApp { } } } - + /** * Store the current keys to disk in the location specified on creation. */ @@ -305,9 +345,10 @@ public class SAMBridge implements Runnable, ClientApp { } } } - + /** * Handlers must call on startup + * * @since 0.9.20 */ public void register(Handler handler) { @@ -317,9 +358,10 @@ public class SAMBridge implements Runnable, ClientApp { _handlers.add(handler); } } - + /** * Handlers must call on stop + * * @since 0.9.20 */ public void unregister(Handler handler) { @@ -332,6 +374,7 @@ public class SAMBridge implements Runnable, ClientApp { /** * Stop all the handlers. + * * @since 0.9.20 */ private void stopHandlers() { @@ -384,11 +427,10 @@ public class SAMBridge implements Runnable, ClientApp { } } - ////// begin ClientApp interface, use only if using correct construtor /** - * @since 0.9.6 + * @since 0.9.6 */ public synchronized void startup() throws IOException { if (_state != INITIALIZED) @@ -403,8 +445,8 @@ public class SAMBridge implements Runnable, ClientApp { } catch (IOException e) { if (_log.shouldLog(Log.ERROR)) _log.error("Error starting SAM bridge on " - + (_listenHost == null ? "0.0.0.0" : _listenHost) - + ":" + _listenPort, e); + + (_listenHost == null ? "0.0.0.0" : _listenHost) + + ":" + _listenPort, e); changeState(START_FAILED, e); throw e; } @@ -412,9 +454,9 @@ public class SAMBridge implements Runnable, ClientApp { } /** - * As of 0.9.20, stops running handlers and sessions. + * As of 0.9.20, stops running handlers and sessions. * - * @since 0.9.6 + * @since 0.9.6 */ public synchronized void shutdown(String[] args) { if (_state != RUNNING) @@ -429,21 +471,21 @@ public class SAMBridge implements Runnable, ClientApp { } /** - * @since 0.9.6 + * @since 0.9.6 */ public ClientAppState getState() { return _state; } /** - * @since 0.9.6 + * @since 0.9.6 */ public String getName() { return "SAM"; } /** - * @since 0.9.6 + * @since 0.9.6 */ public String getDisplayName() { return "SAM " + _listenHost + ':' + _listenPort; @@ -453,14 +495,14 @@ public class SAMBridge implements Runnable, ClientApp { ////// begin ClientApp helpers /** - * @since 0.9.6 + * @since 0.9.6 */ private void changeState(ClientAppState state) { changeState(state, null); } /** - * @since 0.9.6 + * @since 0.9.6 */ private synchronized void changeState(ClientAppState state, Exception e) { _state = state; @@ -470,24 +512,34 @@ public class SAMBridge implements Runnable, ClientApp { ////// end ClientApp helpers - private static class HelpRequestedException extends Exception {static final long serialVersionUID=0x1;} + private static class HelpRequestedException extends Exception { + static final long serialVersionUID = 0x1; + } /** * Usage: - * <pre>SAMBridge [ keyfile [listenHost ] listenPort [ name=val ]* ]</pre> + * + * <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, + * + * <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); + options.keyFile, options.configFile); bridge.startThread(); } catch (RuntimeException e) { e.printStackTrace(); @@ -501,7 +553,7 @@ public class SAMBridge implements Runnable, ClientApp { } /** - * @since 0.9.6 + * @since 0.9.6 */ private void startThread() { I2PAppThread t = new I2PAppThread(this, "SAMListener " + _listenPort); @@ -517,9 +569,9 @@ public class SAMBridge implements Runnable, ClientApp { t.start(); _runner = t; } - + /** - * @since 0.9.6 + * @since 0.9.6 */ private static class Options { private final String host, keyFile; @@ -529,26 +581,38 @@ public class SAMBridge implements Runnable, ClientApp { 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.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> + * + * <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, + * + * <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 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 + * @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 { @@ -560,21 +624,21 @@ public class SAMBridge implements Runnable, ClientApp { 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 + 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(); @@ -642,7 +706,8 @@ public class SAMBridge implements Runnable, ClientApp { 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 + // 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); @@ -650,83 +715,85 @@ public class SAMBridge implements Runnable, ClientApp { int remaining = args.length - startOpts; if (remaining > 0) { - parseOptions(args, startOpts, opts); + 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. + * 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) + if (eq >= args[i].length() - 1) throw new HelpRequestedException(); String key = args[i].substring(0, eq); - String val = args[i].substring(eq+1); + String val = args[i].substring(eq + 1); key = key.trim(); val = val.trim(); - if ( (key.length() > 0) && (val.length() > 0) ) + 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."); + "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; + 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); + _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()); + + 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 ; + HelloHandler(SocketChannel s, SAMBridge parent) { + this.s = s; + this.parent = parent; } public void run() { @@ -738,7 +805,8 @@ public class SAMBridge implements Runnable, ClientApp { _log.debug("SAM handler has not been instantiated"); try { s.close(); - } catch (IOException e) {} + } catch (IOException e) { + } return; } handler.startHandling(); @@ -747,9 +815,15 @@ public class SAMBridge implements Runnable, ClientApp { _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) {} + try { + s.close(); + } catch (IOException ioe) { + } } catch (Exception ee) { - try { s.close(); } catch (IOException ioe) {} + try { + s.close(); + } catch (IOException ioe) { + } _log.log(Log.CRIT, "Unexpected error handling SAM connection", ee); } finally { parent.unregister(this); @@ -758,10 +832,13 @@ public class SAMBridge implements Runnable, ClientApp { /** @since 0.9.20 */ public void stopHandling() { - try { s.close(); } catch (IOException ioe) {} + try { + s.close(); + } catch (IOException ioe) { + } } } - new I2PAppThread(new HelloHandler(s,this), "SAM HelloHandler").start(); + new I2PAppThread(new HelloHandler(s, this), "SAM HelloHandler").start(); } changeState(STOPPING); } catch (Exception e) { @@ -776,8 +853,10 @@ public class SAMBridge implements Runnable, ClientApp { _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); + } catch (IOException e) { + } + I2PAppContext.getGlobalContext().portMapper() + .unregister(_useSSL ? PortMapper.SVC_SAM_SSL : PortMapper.SVC_SAM); stopHandlers(); changeState(STOPPED); } @@ -787,4 +866,26 @@ public class SAMBridge implements Runnable, ClientApp { public void saveConfig() throws IOException { DataHelper.storeProps(i2cpProps, _configFile); } + + /* + * Returns the interactive Secure Session manager which requires SAM + * applications to seek "approval" for their initial connections from the user + * before they can start the session. + * + * @since 1.8.0 + */ + public SAMSecureSessionInterface secureSession() { + if (_secureSession == null) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("SAMBridge.secureSession() called when secureSession is null, creating default I2CP auth"); + boolean attemptauth = Boolean.parseBoolean(i2cpProps.getProperty(SAMBridge.PROP_AUTH)); + if (attemptauth) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("SAMBridge.secureSession() called when authentication is enabled"); + SAMSecureSessionInterface secureSession = new SAMSecureSession(); + return secureSession; + } + } + return _secureSession; + } } diff --git a/apps/sam/java/src/net/i2p/sam/SAMHandlerFactory.java b/apps/sam/java/src/net/i2p/sam/SAMHandlerFactory.java index 8a0d0bd10c2213e05ebd0b6e805f9db8ca4fd0cb..4ead311cb3128c8c2177d0cfc2be0e47e42cd995 100644 --- a/apps/sam/java/src/net/i2p/sam/SAMHandlerFactory.java +++ b/apps/sam/java/src/net/i2p/sam/SAMHandlerFactory.java @@ -1,9 +1,9 @@ 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 + * 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. * */ @@ -15,9 +15,7 @@ import java.nio.channels.SocketChannel; import java.util.Properties; import net.i2p.I2PAppContext; -import net.i2p.data.DataHelper; import net.i2p.util.Log; -import net.i2p.util.PasswordManager; import net.i2p.util.VersionComparator; /** @@ -27,21 +25,24 @@ class SAMHandlerFactory { private static final String VERSION = "3.3"; - private static final int HELLO_TIMEOUT = 60*1000; + private static final int HELLO_TIMEOUT = 60 * 1000; /** * Return the right SAM handler depending on the protocol version * required by the client. * - * @param s Socket attached to SAM client + * @param s Socket attached to SAM client * @param i2cpProps config options for our i2cp connection - * @throws SAMException if the connection handshake (HELLO message) was malformed - * @return A SAM protocol handler, or null if the client closed before the handshake + * @throws SAMException if the connection handshake (HELLO message) was + * malformed + * @return A SAM protocol handler, or null if the client closed before the + * handshake */ public static SAMHandler createSAMHandler(SocketChannel s, Properties i2cpProps, - SAMBridge parent) throws SAMException { + SAMBridge parent) throws SAMException { String line; Log log = I2PAppContext.getGlobalContext().logManager().getLog(SAMHandlerFactory.class); + SAMSecureSessionInterface secureSession = parent.secureSession(); try { Socket sock = s.socket(); @@ -63,20 +64,20 @@ class SAMHandlerFactory { // Message format: HELLO VERSION [MIN=v1] [MAX=v2] Properties props = SAMUtils.parseParams(line); if (!"HELLO".equals(props.remove(SAMUtils.COMMAND)) || - !"VERSION".equals(props.remove(SAMUtils.OPCODE))) { + !"VERSION".equals(props.remove(SAMUtils.OPCODE))) { throw new SAMException("Must start with HELLO VERSION"); } String minVer = props.getProperty("MIN"); if (minVer == null) { - //throw new SAMException("Missing MIN parameter in HELLO VERSION message"); + // throw new SAMException("Missing MIN parameter in HELLO VERSION message"); // MIN optional as of 0.9.14 minVer = "1"; } String maxVer = props.getProperty("MAX"); if (maxVer == null) { - //throw new SAMException("Missing MAX parameter in HELLO VERSION message"); + // throw new SAMException("Missing MAX parameter in HELLO VERSION message"); // MAX optional as of 0.9.14 maxVer = "99.99"; } @@ -88,31 +89,16 @@ class SAMHandlerFactory { return null; } - if (Boolean.parseBoolean(i2cpProps.getProperty(SAMBridge.PROP_AUTH))) { - String user = props.getProperty("USER"); - String pw = props.getProperty("PASSWORD"); - if (user == null || pw == null) { - if (user == null) - log.logAlways(Log.WARN, "SAM authentication failed"); - else - log.logAlways(Log.WARN, "SAM authentication failed, user: " + user); - throw new SAMException("USER and PASSWORD required"); - } - String savedPW = i2cpProps.getProperty(SAMBridge.PROP_PW_PREFIX + user + SAMBridge.PROP_PW_SUFFIX); - if (savedPW == null) { - log.logAlways(Log.WARN, "SAM authentication failed, user: " + user); - throw new SAMException("Authorization failed"); - } - PasswordManager pm = new PasswordManager(I2PAppContext.getGlobalContext()); - if (!pm.checkHash(savedPW, pw)) { - log.logAlways(Log.WARN, "SAM authentication failed, user: " + user); - throw new SAMException("Authorization failed"); + if (secureSession != null) { + boolean approval = secureSession.approveOrDenySecureSession(i2cpProps, props); + if (!approval) { + throw new SAMException("SAM connection cancelled by user request"); } } // Let's answer positively if (!SAMHandler.writeString("HELLO REPLY RESULT=OK VERSION=" + ver + "\n", s)) - throw new SAMException("Error writing to socket"); + throw new SAMException("Error writing to socket"); // ...and instantiate the right SAM handler int verMajor = getMajor(ver); @@ -121,21 +107,21 @@ class SAMHandlerFactory { try { switch (verMajor) { - case 1: - handler = new SAMv1Handler(s, verMajor, verMinor, i2cpProps, parent); - break; - case 2: - handler = new SAMv2Handler(s, verMajor, verMinor, i2cpProps, parent); - break; - case 3: - handler = new SAMv3Handler(s, verMajor, verMinor, i2cpProps, parent); - break; - default: - log.error("BUG! Trying to initialize the wrong SAM version!"); - throw new SAMException("BUG! (in handler instantiation)"); + case 1: + handler = new SAMv1Handler(s, verMajor, verMinor, i2cpProps, parent); + break; + case 2: + handler = new SAMv2Handler(s, verMajor, verMinor, i2cpProps, parent); + break; + case 3: + handler = new SAMv3Handler(s, verMajor, verMinor, i2cpProps, parent); + break; + default: + log.error("BUG! Trying to initialize the wrong SAM version!"); + throw new SAMException("BUG! (in handler instantiation)"); } } catch (IOException e) { - log.error("Error creating the handler for version "+verMajor, e); + log.error("Error creating the handler for version " + verMajor, e); throw new SAMException("IOException caught during SAM handler instantiation"); } return handler; @@ -146,24 +132,24 @@ class SAMHandlerFactory { */ private static String chooseBestVersion(String minVer, String maxVer) { if (VersionComparator.comp(VERSION, minVer) >= 0 && - VersionComparator.comp(VERSION, maxVer) <= 0) + VersionComparator.comp(VERSION, maxVer) <= 0) return VERSION; if (VersionComparator.comp("3.2", minVer) >= 0 && - VersionComparator.comp("3.2", maxVer) <= 0) + VersionComparator.comp("3.2", maxVer) <= 0) return "3.2"; if (VersionComparator.comp("3.1", minVer) >= 0 && - VersionComparator.comp("3.1", maxVer) <= 0) + VersionComparator.comp("3.1", maxVer) <= 0) return "3.1"; // in VersionComparator, "3" < "3.0" so // use comparisons carefully if (VersionComparator.comp("3.0", minVer) >= 0 && - VersionComparator.comp("3", maxVer) <= 0) + VersionComparator.comp("3", maxVer) <= 0) return "3.0"; if (VersionComparator.comp("2.0", minVer) >= 0 && - VersionComparator.comp("2", maxVer) <= 0) + VersionComparator.comp("2", maxVer) <= 0) return "2.0"; if (VersionComparator.comp("1.0", minVer) >= 0 && - VersionComparator.comp("1", maxVer) <= 0) + VersionComparator.comp("1", maxVer) <= 0) return "1.0"; return null; } @@ -186,7 +172,7 @@ class SAMHandlerFactory { /* Get the minor protocol version from a string, or -1 */ private static int getMinor(String ver) { - if ( (ver == null) || (ver.indexOf('.') < 0) ) + if ((ver == null) || (ver.indexOf('.') < 0)) return -1; try { String major = ver.substring(ver.indexOf('.') + 1); diff --git a/apps/sam/java/src/net/i2p/sam/SAMSecureSession.java b/apps/sam/java/src/net/i2p/sam/SAMSecureSession.java new file mode 100644 index 0000000000000000000000000000000000000000..f814f55fe31feffb0670eaf19bf8d69c996b85e1 --- /dev/null +++ b/apps/sam/java/src/net/i2p/sam/SAMSecureSession.java @@ -0,0 +1,49 @@ +package net.i2p.sam; + +import java.util.Properties; + +import net.i2p.I2PAppContext; +import net.i2p.util.Log; +import net.i2p.util.PasswordManager; + +/** + * + * This is the "default" implementation of the SAMSecureSession @interface + * that behaves exactly like SAM without interactive authentication. It uses + * the i2cp username and password properties for authentication. Implementers + * can add their own means of authentication by substituting this interface + * for their own. + * + * @since 1.8.0 + */ +public class SAMSecureSession implements SAMSecureSessionInterface { + private final Log log = I2PAppContext.getGlobalContext().logManager().getLog(SAMHandlerFactory.class); + + /** + * Authenticate based on the i2cp username/password. + * + * @since 1.8.0 + */ + public boolean approveOrDenySecureSession(Properties i2cpProps, Properties props) throws SAMException { + String user = props.getProperty("USER"); + String pw = props.getProperty("PASSWORD"); + if (user == null || pw == null) { + if (user == null) + log.logAlways(Log.WARN, "SAM authentication failed"); + else + log.logAlways(Log.WARN, "SAM authentication failed, user: " + user); + throw new SAMException("USER and PASSWORD required"); + } + String savedPW = i2cpProps.getProperty(SAMBridge.PROP_PW_PREFIX + user + SAMBridge.PROP_PW_SUFFIX); + if (savedPW == null) { + log.logAlways(Log.WARN, "SAM authentication failed, user: " + user); + throw new SAMException("Authorization failed"); + } + PasswordManager pm = new PasswordManager(I2PAppContext.getGlobalContext()); + if (!pm.checkHash(savedPW, pw)) { + log.logAlways(Log.WARN, "SAM authentication failed, user: " + user); + throw new SAMException("Authorization failed"); + } + return true; + } +} diff --git a/apps/sam/java/src/net/i2p/sam/SAMSecureSessionInterface.java b/apps/sam/java/src/net/i2p/sam/SAMSecureSessionInterface.java new file mode 100644 index 0000000000000000000000000000000000000000..432224eef785ee79bb71f47b67abceb387808f30 --- /dev/null +++ b/apps/sam/java/src/net/i2p/sam/SAMSecureSessionInterface.java @@ -0,0 +1,27 @@ +package net.i2p.sam; + +import java.util.Properties; + +/** + * SAMSecureSessionInterface is used for implementing interactive authentication + * to SAM applications. It needs to be implemented by a class for Desktop and + * Android applications and passed to the SAM bridge when constructed. + * + * It is NOT required that a SAM API have this feature. It is recommended that + * it be implemented for platforms which have a very hostile malware landscape + * like Android. + * + * @since 1.8.0 + */ +public interface SAMSecureSessionInterface { + /** + * Within this function, read and accept input from a user to approve a SAM + * connection. Return false by default + * + * if the connection is approved by user input: + * + * @since 1.8.0 + * @return true + */ + public boolean approveOrDenySecureSession(Properties i2cpProps, Properties props) throws SAMException; +}