Compare commits

...

9 Commits

4 changed files with 360 additions and 197 deletions

View File

@ -1,9 +1,9 @@
package net.i2p.sam; package net.i2p.sam;
/* /*
* free (adj.): unencumbered; not under the control of others * free (adj.): unencumbered; not under the control of others
* Written by human in 2004 and released into the public domain * Written by human in 2004 and released into the public domain
* with no warranty of any kind, either expressed or implied. * with no warranty of any kind, either expressed or implied.
* It probably won't make your computer catch on fire, or eat * It probably won't make your computer catch on fire, or eat
* your children, but it might. Use at your own risk. * 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 volatile Thread _runner;
private final Object _v3DGServerLock = new Object(); private final Object _v3DGServerLock = new Object();
private SAMv3DatagramServer _v3DGServer; 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 * filename in which the name to private key mapping should
* be stored (and loaded from) * be stored (and loaded from)
*/ */
private final String persistFilename; 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) * destination keys (Destination+PrivateKey+SigningPrivateKey)
*/ */
private final Map<String,String> nameToPrivKeys; private final Map<String, String> nameToPrivKeys;
private final Set<Handler> _handlers; private final Set<Handler> _handlers;
private volatile boolean acceptConnections = true; private volatile boolean acceptConnections = true;
@ -82,7 +89,7 @@ public class SAMBridge implements Runnable, ClientApp {
private volatile ClientAppState _state = UNINITIALIZED; private volatile ClientAppState _state = UNINITIALIZED;
private static final int SAM_LISTENPORT = 7656; private static final int SAM_LISTENPORT = 7656;
public static final String DEFAULT_SAM_KEYFILE = "sam.keys"; public static final String DEFAULT_SAM_KEYFILE = "sam.keys";
static final String DEFAULT_SAM_CONFIGFILE = "sam.config"; static final String DEFAULT_SAM_CONFIGFILE = "sam.config";
private static final String PROP_SAM_KEYFILE = "sam.keyfile"; 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"; 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_HOST = "127.0.0.1";
protected static final String DEFAULT_TCP_PORT = "7656"; protected static final String DEFAULT_TCP_PORT = "7656";
public static final String PROP_DATAGRAM_HOST = "sam.udp.host"; public static final String PROP_DATAGRAM_HOST = "sam.udp.host";
public static final String PROP_DATAGRAM_PORT = "sam.udp.port"; public static final String PROP_DATAGRAM_PORT = "sam.udp.port";
protected static final String DEFAULT_DATAGRAM_HOST = "127.0.0.1"; protected static final String DEFAULT_DATAGRAM_HOST = "127.0.0.1";
protected static final int DEFAULT_DATAGRAM_PORT_INT = 7655; protected static final int DEFAULT_DATAGRAM_PORT_INT = 7655;
protected static final String DEFAULT_DATAGRAM_PORT = Integer.toString(DEFAULT_DATAGRAM_PORT_INT); protected static final String DEFAULT_DATAGRAM_PORT = Integer.toString(DEFAULT_DATAGRAM_PORT_INT);
/** /**
* For ClientApp interface. * For ClientApp interface.
* Recommended constructor for external use. * Recommended constructor for external use.
* Does NOT open the listener socket or start threads; caller must call startup() * Does NOT open the listener socket or start threads; caller must call
* startup()
* *
* @param mgr may be null * @param mgr may be null
* @param args non-null * @param args non-null
* @throws Exception on bad args * @throws Exception on bad args
* @since 0.9.6 * @since 0.9.6
*/ */
public SAMBridge(I2PAppContext context, ClientAppManager mgr, String[] args) throws Exception { public SAMBridge(I2PAppContext context, ClientAppManager mgr, String[] args) throws Exception {
_log = context.logManager().getLog(SAMBridge.class); _log = context.logManager().getLog(SAMBridge.class);
_mgr = mgr; _mgr = mgr;
_secureSession = null;
Options options = getOptions(args); Options options = getOptions(args);
_listenHost = options.host; _listenHost = options.host;
_listenPort = options.port; _listenPort = options.port;
@ -123,13 +131,12 @@ public class SAMBridge implements Runnable, ClientApp {
throw new IllegalArgumentException("SSL requires Java 7 or higher"); throw new IllegalArgumentException("SSL requires Java 7 or higher");
persistFilename = options.keyFile; persistFilename = options.keyFile;
_configFile = options.configFile; _configFile = options.configFile;
nameToPrivKeys = new HashMap<String,String>(8); nameToPrivKeys = new HashMap<String, String>(8);
_handlers = new HashSet<Handler>(8); _handlers = new HashSet<Handler>(8);
this.i2cpProps = options.opts; this.i2cpProps = options.opts;
_state = INITIALIZED; _state = INITIALIZED;
} }
/** /**
* Build a new SAM bridge. * Build a new SAM bridge.
* NOT recommended for external use. * NOT recommended for external use.
@ -140,25 +147,57 @@ public class SAMBridge implements Runnable, ClientApp {
* *
* Deprecated for external use, to be made private. * Deprecated for external use, to be made private.
* *
* @param listenHost hostname to listen for SAM connections on ("0.0.0.0" for all) * @param listenHost hostname to listen for SAM connections on ("0.0.0.0" for
* @param listenPort port number to listen for SAM connections on * all)
* @param i2cpProps set of I2CP properties for finding and communicating with the router * @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 persistFile location to store/load named keys to/from
* @throws RuntimeException if a server socket can't be opened * @throws RuntimeException if a server socket can't be opened
*/ */
public SAMBridge(String listenHost, int listenPort, boolean isSSL, Properties i2cpProps, 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); _log = I2PAppContext.getGlobalContext().logManager().getLog(SAMBridge.class);
_mgr = null; _mgr = null;
_listenHost = listenHost; _listenHost = listenHost;
_listenPort = listenPort; _listenPort = listenPort;
_useSSL = isSSL; _useSSL = isSSL;
_secureSession = secureSession;
if (_useSSL && !SystemVersion.isJava7()) if (_useSSL && !SystemVersion.isJava7())
throw new IllegalArgumentException("SSL requires Java 7 or higher"); throw new IllegalArgumentException("SSL requires Java 7 or higher");
this.i2cpProps = i2cpProps; this.i2cpProps = i2cpProps;
persistFilename = persistFile; persistFilename = persistFile;
_configFile = configFile; _configFile = configFile;
nameToPrivKeys = new HashMap<String,String>(8); nameToPrivKeys = new HashMap<String, String>(8);
_handlers = new HashSet<Handler>(8); _handlers = new HashSet<Handler>(8);
loadKeys(); loadKeys();
try { try {
@ -166,15 +205,15 @@ public class SAMBridge implements Runnable, ClientApp {
} catch (IOException e) { } catch (IOException e) {
if (_log.shouldLog(Log.ERROR)) if (_log.shouldLog(Log.ERROR))
_log.error("Error starting SAM bridge on " _log.error("Error starting SAM bridge on "
+ (listenHost == null ? "0.0.0.0" : listenHost) + (listenHost == null ? "0.0.0.0" : listenHost)
+ ":" + listenPort, e); + ":" + listenPort, e);
throw new RuntimeException(e); throw new RuntimeException(e);
} }
_state = INITIALIZED; _state = INITIALIZED;
} }
/** /**
* @since 0.9.6 * @since 0.9.6
*/ */
private void openSocket() throws IOException { private void openSocket() throws IOException {
if (_useSSL) { if (_useSSL) {
@ -193,7 +232,7 @@ public class SAMBridge implements Runnable, ClientApp {
serverSocket.socket().bind(new InetSocketAddress(_listenHost, _listenPort)); serverSocket.socket().bind(new InetSocketAddress(_listenHost, _listenPort));
if (_log.shouldLog(Log.DEBUG)) if (_log.shouldLog(Log.DEBUG))
_log.debug("SAM bridge listening on " _log.debug("SAM bridge listening on "
+ _listenHost + ":" + _listenPort); + _listenHost + ":" + _listenPort);
} else { } else {
serverSocket.socket().bind(new InetSocketAddress(_listenPort)); serverSocket.socket().bind(new InetSocketAddress(_listenPort));
if (_log.shouldLog(Log.DEBUG)) if (_log.shouldLog(Log.DEBUG))
@ -208,27 +247,27 @@ public class SAMBridge implements Runnable, ClientApp {
* @param name name of the destination * @param name name of the destination
* @return null if the name does not exist, or if it is improperly formatted * @return null if the name does not exist, or if it is improperly formatted
*/ */
/**** /****
public Destination getDestination(String name) { * public Destination getDestination(String name) {
synchronized (nameToPrivKeys) { * synchronized (nameToPrivKeys) {
String val = nameToPrivKeys.get(name); * String val = nameToPrivKeys.get(name);
if (val == null) return null; * if (val == null) return null;
try { * try {
Destination d = new Destination(); * Destination d = new Destination();
d.fromBase64(val); * d.fromBase64(val);
return d; * return d;
} catch (DataFormatException dfe) { * } catch (DataFormatException dfe) {
_log.error("Error retrieving the destination from " + name, dfe); * _log.error("Error retrieving the destination from " + name, dfe);
nameToPrivKeys.remove(name); * nameToPrivKeys.remove(name);
return null; * return null;
} * }
} * }
} * }
****/ ****/
/** /**
* Retrieve the I2P private keystream for the given name, formatted * 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). * stores it).
* *
* @param name Name of the destination * @param name Name of the destination
@ -237,7 +276,8 @@ public class SAMBridge implements Runnable, ClientApp {
public String getKeystream(String name) { public String getKeystream(String name) {
synchronized (nameToPrivKeys) { synchronized (nameToPrivKeys) {
String val = nameToPrivKeys.get(name); String val = nameToPrivKeys.get(name);
if (val == null) return null; if (val == null)
return null;
return val; return val;
} }
} }
@ -245,8 +285,8 @@ public class SAMBridge implements Runnable, ClientApp {
/** /**
* Specify that the given keystream should be used for the given name * Specify that the given keystream should be used for the given name
* *
* @param name Name of the destination * @param name Name of the destination
* @param stream Name of the stream * @param stream Name of the stream
*/ */
public void addKeystream(String name, String stream) { public void addKeystream(String name, String stream) {
synchronized (nameToPrivKeys) { synchronized (nameToPrivKeys) {
@ -254,7 +294,7 @@ public class SAMBridge implements Runnable, ClientApp {
} }
storeKeys(); storeKeys();
} }
/** /**
* Load up the keys from the persistFilename. * 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. * 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 * Handlers must call on startup
*
* @since 0.9.20 * @since 0.9.20
*/ */
public void register(Handler handler) { public void register(Handler handler) {
@ -317,9 +358,10 @@ public class SAMBridge implements Runnable, ClientApp {
_handlers.add(handler); _handlers.add(handler);
} }
} }
/** /**
* Handlers must call on stop * Handlers must call on stop
*
* @since 0.9.20 * @since 0.9.20
*/ */
public void unregister(Handler handler) { public void unregister(Handler handler) {
@ -332,6 +374,7 @@ public class SAMBridge implements Runnable, ClientApp {
/** /**
* Stop all the handlers. * Stop all the handlers.
*
* @since 0.9.20 * @since 0.9.20
*/ */
private void stopHandlers() { private void stopHandlers() {
@ -384,11 +427,10 @@ public class SAMBridge implements Runnable, ClientApp {
} }
} }
////// begin ClientApp interface, use only if using correct construtor ////// begin ClientApp interface, use only if using correct construtor
/** /**
* @since 0.9.6 * @since 0.9.6
*/ */
public synchronized void startup() throws IOException { public synchronized void startup() throws IOException {
if (_state != INITIALIZED) if (_state != INITIALIZED)
@ -403,8 +445,8 @@ public class SAMBridge implements Runnable, ClientApp {
} catch (IOException e) { } catch (IOException e) {
if (_log.shouldLog(Log.ERROR)) if (_log.shouldLog(Log.ERROR))
_log.error("Error starting SAM bridge on " _log.error("Error starting SAM bridge on "
+ (_listenHost == null ? "0.0.0.0" : _listenHost) + (_listenHost == null ? "0.0.0.0" : _listenHost)
+ ":" + _listenPort, e); + ":" + _listenPort, e);
changeState(START_FAILED, e); changeState(START_FAILED, e);
throw 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) { public synchronized void shutdown(String[] args) {
if (_state != RUNNING) if (_state != RUNNING)
@ -429,21 +471,21 @@ public class SAMBridge implements Runnable, ClientApp {
} }
/** /**
* @since 0.9.6 * @since 0.9.6
*/ */
public ClientAppState getState() { public ClientAppState getState() {
return _state; return _state;
} }
/** /**
* @since 0.9.6 * @since 0.9.6
*/ */
public String getName() { public String getName() {
return "SAM"; return "SAM";
} }
/** /**
* @since 0.9.6 * @since 0.9.6
*/ */
public String getDisplayName() { public String getDisplayName() {
return "SAM " + _listenHost + ':' + _listenPort; return "SAM " + _listenHost + ':' + _listenPort;
@ -453,14 +495,14 @@ public class SAMBridge implements Runnable, ClientApp {
////// begin ClientApp helpers ////// begin ClientApp helpers
/** /**
* @since 0.9.6 * @since 0.9.6
*/ */
private void changeState(ClientAppState state) { private void changeState(ClientAppState state) {
changeState(state, null); changeState(state, null);
} }
/** /**
* @since 0.9.6 * @since 0.9.6
*/ */
private synchronized void changeState(ClientAppState state, Exception e) { private synchronized void changeState(ClientAppState state, Exception e) {
_state = state; _state = state;
@ -470,24 +512,34 @@ public class SAMBridge implements Runnable, ClientApp {
////// end ClientApp helpers ////// 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: * Usage:
* <pre>SAMBridge [ keyfile [listenHost ] listenPort [ name=val ]* ]</pre> *
* <pre>
* SAMBridge [ keyfile [listenHost ] listenPort [ name=val ]* ]
* </pre>
*
* or: * or:
* <pre>SAMBridge [ name=val ]* </pre> *
* * <pre>
* name=val options are passed to the I2CP code to build a session, * 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 * allowing the bridge to specify an alternate I2CP host and port, tunnel
* depth, etc. * depth, etc.
*
* @param args [ keyfile [ listenHost ] listenPort [ name=val ]* ] * @param args [ keyfile [ listenHost ] listenPort [ name=val ]* ]
*/ */
public static void main(String args[]) { public static void main(String args[]) {
try { try {
Options options = getOptions(args); Options options = getOptions(args);
SAMBridge bridge = new SAMBridge(options.host, options.port, options.isSSL, options.opts, SAMBridge bridge = new SAMBridge(options.host, options.port, options.isSSL, options.opts,
options.keyFile, options.configFile); options.keyFile, options.configFile);
bridge.startThread(); bridge.startThread();
} catch (RuntimeException e) { } catch (RuntimeException e) {
e.printStackTrace(); e.printStackTrace();
@ -501,7 +553,7 @@ public class SAMBridge implements Runnable, ClientApp {
} }
/** /**
* @since 0.9.6 * @since 0.9.6
*/ */
private void startThread() { private void startThread() {
I2PAppThread t = new I2PAppThread(this, "SAMListener " + _listenPort); I2PAppThread t = new I2PAppThread(this, "SAMListener " + _listenPort);
@ -517,9 +569,9 @@ public class SAMBridge implements Runnable, ClientApp {
t.start(); t.start();
_runner = t; _runner = t;
} }
/** /**
* @since 0.9.6 * @since 0.9.6
*/ */
private static class Options { private static class Options {
private final String host, keyFile; private final String host, keyFile;
@ -529,26 +581,38 @@ public class SAMBridge implements Runnable, ClientApp {
private final File configFile; private final File configFile;
public Options(String host, int port, boolean isSSL, Properties opts, String keyFile, 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.isSSL = isSSL;
this.configFile = configFile; this.configFile = configFile;
} }
} }
/** /**
* Usage: * Usage:
* <pre>SAMBridge [ keyfile [listenHost ] listenPort [ name=val ]* ]</pre> *
* <pre>
* SAMBridge [ keyfile [listenHost ] listenPort [ name=val ]* ]
* </pre>
*
* or: * or:
* <pre>SAMBridge [ name=val ]* </pre> *
* * <pre>
* name=val options are passed to the I2CP code to build a session, * 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 * allowing the bridge to specify an alternate I2CP host and port, tunnel
* depth, etc. * depth, etc.
*
* @param args [ keyfile [ listenHost ] listenPort [ name=val ]* ] * @param args [ keyfile [ listenHost ] listenPort [ name=val ]* ]
* @return non-null Options or throws Exception * @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 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 * @since 0.9.6
*/ */
private static Options getOptions(String args[]) throws Exception { 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:"); Getopt g = new Getopt("SAM", args, "hsc:");
int c; int c;
while ((c = g.getopt()) != -1) { while ((c = g.getopt()) != -1) {
switch (c) { switch (c) {
case 's': case 's':
isSSL = true; isSSL = true;
break; break;
case 'c': case 'c':
cfile = g.getOptarg(); cfile = g.getOptarg();
break; break;
case 'h': case 'h':
case '?': case '?':
case ':': case ':':
default: default:
throw new HelpRequestedException(); throw new HelpRequestedException();
} // switch } // switch
} // while } // while
int startArgs = g.getOptind(); int startArgs = g.getOptind();
@ -642,7 +706,8 @@ public class SAMBridge implements Runnable, ClientApp {
if (!isSSL) if (!isSSL)
isSSL = Boolean.parseBoolean(opts.getProperty(PROP_SAM_SSL)); isSSL = Boolean.parseBoolean(opts.getProperty(PROP_SAM_SSL));
if (isSSL) { 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); boolean shouldSave = SSLUtil.verifyKeyStore(opts);
if (shouldSave) if (shouldSave)
DataHelper.storeProps(opts, file); DataHelper.storeProps(opts, file);
@ -650,83 +715,85 @@ public class SAMBridge implements Runnable, ClientApp {
int remaining = args.length - startOpts; int remaining = args.length - startOpts;
if (remaining > 0) { if (remaining > 0) {
parseOptions(args, startOpts, opts); parseOptions(args, startOpts, opts);
} }
return new Options(host, port, isSSL, opts, keyfile, file); return new Options(host, port, isSSL, opts, keyfile, file);
} }
/** /**
* Parse key=value options starting at startArgs. * 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. * @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 { private static void parseOptions(String args[], int startArgs, Properties props) throws HelpRequestedException {
for (int i = startArgs; i < args.length; i++) { for (int i = startArgs; i < args.length; i++) {
int eq = args[i].indexOf('='); int eq = args[i].indexOf('=');
if (eq <= 0) if (eq <= 0)
throw new HelpRequestedException(); throw new HelpRequestedException();
if (eq >= args[i].length()-1) if (eq >= args[i].length() - 1)
throw new HelpRequestedException(); throw new HelpRequestedException();
String key = args[i].substring(0, eq); String key = args[i].substring(0, eq);
String val = args[i].substring(eq+1); String val = args[i].substring(eq + 1);
key = key.trim(); key = key.trim();
val = val.trim(); val = val.trim();
if ( (key.length() > 0) && (val.length() > 0) ) if ((key.length() > 0) && (val.length() > 0))
props.setProperty(key, val); props.setProperty(key, val);
else else
throw new HelpRequestedException(); throw new HelpRequestedException();
} }
} }
private static void usage() { private static void usage() {
System.err.println("Usage: SAMBridge [-s] [-c sam.config] [keyfile [listenHost] listenPortNum[ name=val]*]\n" + System.err.println("Usage: SAMBridge [-s] [-c sam.config] [keyfile [listenHost] listenPortNum[ name=val]*]\n" +
"or:\n" + "or:\n" +
" SAMBridge [ name=val ]*\n" + " SAMBridge [ name=val ]*\n" +
" -s: Use SSL\n" + " -s: Use SSL\n" +
" -c sam.config: Specify config file\n" + " -c sam.config: Specify config file\n" +
" keyfile: location to persist private keys (default sam.keys)\n" + " keyfile: location to persist private keys (default sam.keys)\n" +
" listenHost: interface to listen on (0.0.0.0 for all interfaces)\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" + " listenPort: port to listen for SAM connections on (default 7656)\n" +
" name=val: options to pass when connecting via I2CP, such as \n" + " name=val: options to pass when connecting via I2CP, such as \n" +
" i2cp.host=localhost and i2cp.port=7654\n" + " i2cp.host=localhost and i2cp.port=7654\n" +
"\n" + "\n" +
"Host and ports of the SAM bridge can be specified with the alternate\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 "+ "form by specifying options " + SAMBridge.PROP_TCP_HOST + " and/or " +
SAMBridge.PROP_TCP_PORT + SAMBridge.PROP_TCP_PORT +
"\n" + "\n" +
"Options "+SAMBridge.PROP_DATAGRAM_HOST+" and "+SAMBridge.PROP_DATAGRAM_PORT+ "Options " + SAMBridge.PROP_DATAGRAM_HOST + " and " + SAMBridge.PROP_DATAGRAM_PORT +
" specify the listening ip\n" + " specify the listening ip\n" +
"range and the port of SAM datagram server. This server is\n" + "range and the port of SAM datagram server. This server is\n" +
"only launched after a client creates the first SAM datagram\n" + "only launched after a client creates the first SAM datagram\n" +
"or raw session, after a handshake with SAM version >= 3.0.\n" + "or raw session, after a handshake with SAM version >= 3.0.\n" +
"\n" + "\n" +
"The option loglevel=[DEBUG|WARN|ERROR|CRIT] can be used\n" + "The option loglevel=[DEBUG|WARN|ERROR|CRIT] can be used\n" +
"for tuning the log verbosity."); "for tuning the log verbosity.");
} }
public void run() { public void run() {
if (serverSocket == null) return; if (serverSocket == null)
return;
changeState(RUNNING); changeState(RUNNING);
if (_mgr != null) if (_mgr != null)
_mgr.register(this); _mgr.register(this);
I2PAppContext.getGlobalContext().portMapper().register(_useSSL ? PortMapper.SVC_SAM_SSL : PortMapper.SVC_SAM, I2PAppContext.getGlobalContext().portMapper().register(_useSSL ? PortMapper.SVC_SAM_SSL : PortMapper.SVC_SAM,
_listenHost != null ? _listenHost : "127.0.0.1", _listenHost != null ? _listenHost : "127.0.0.1",
_listenPort); _listenPort);
try { try {
while (acceptConnections) { while (acceptConnections) {
SocketChannel s = serverSocket.accept(); SocketChannel s = serverSocket.accept();
if (_log.shouldLog(Log.DEBUG)) if (_log.shouldLog(Log.DEBUG))
_log.debug("New connection from " _log.debug("New connection from "
+ s.socket().getInetAddress().toString() + ":" + s.socket().getInetAddress().toString() + ":"
+ s.socket().getPort()); + s.socket().getPort());
class HelloHandler implements Runnable, Handler { class HelloHandler implements Runnable, Handler {
private final SocketChannel s; private final SocketChannel s;
private final SAMBridge parent; private final SAMBridge parent;
HelloHandler(SocketChannel s, SAMBridge parent) { HelloHandler(SocketChannel s, SAMBridge parent) {
this.s = s ; this.s = s;
this.parent = parent ; this.parent = parent;
} }
public void run() { public void run() {
@ -738,7 +805,8 @@ public class SAMBridge implements Runnable, ClientApp {
_log.debug("SAM handler has not been instantiated"); _log.debug("SAM handler has not been instantiated");
try { try {
s.close(); s.close();
} catch (IOException e) {} } catch (IOException e) {
}
return; return;
} }
handler.startHandling(); handler.startHandling();
@ -747,9 +815,15 @@ public class SAMBridge implements Runnable, ClientApp {
_log.error("SAM error: " + e.getMessage(), e); _log.error("SAM error: " + e.getMessage(), e);
String reply = "HELLO REPLY RESULT=I2P_ERROR MESSAGE=\"" + e.getMessage() + "\"\n"; String reply = "HELLO REPLY RESULT=I2P_ERROR MESSAGE=\"" + e.getMessage() + "\"\n";
SAMHandler.writeString(reply, s); SAMHandler.writeString(reply, s);
try { s.close(); } catch (IOException ioe) {} try {
s.close();
} catch (IOException ioe) {
}
} catch (Exception ee) { } 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); _log.log(Log.CRIT, "Unexpected error handling SAM connection", ee);
} finally { } finally {
parent.unregister(this); parent.unregister(this);
@ -758,10 +832,13 @@ public class SAMBridge implements Runnable, ClientApp {
/** @since 0.9.20 */ /** @since 0.9.20 */
public void stopHandling() { 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); changeState(STOPPING);
} catch (Exception e) { } catch (Exception e) {
@ -776,8 +853,10 @@ public class SAMBridge implements Runnable, ClientApp {
_log.debug("Shutting down, closing server socket"); _log.debug("Shutting down, closing server socket");
if (serverSocket != null) if (serverSocket != null)
serverSocket.close(); serverSocket.close();
} catch (IOException e) {} } catch (IOException e) {
I2PAppContext.getGlobalContext().portMapper().unregister(_useSSL ? PortMapper.SVC_SAM_SSL : PortMapper.SVC_SAM); }
I2PAppContext.getGlobalContext().portMapper()
.unregister(_useSSL ? PortMapper.SVC_SAM_SSL : PortMapper.SVC_SAM);
stopHandlers(); stopHandlers();
changeState(STOPPED); changeState(STOPPED);
} }
@ -787,4 +866,26 @@ public class SAMBridge implements Runnable, ClientApp {
public void saveConfig() throws IOException { public void saveConfig() throws IOException {
DataHelper.storeProps(i2cpProps, _configFile); 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;
}
} }

View File

@ -1,9 +1,9 @@
package net.i2p.sam; package net.i2p.sam;
/* /*
* free (adj.): unencumbered; not under the control of others * free (adj.): unencumbered; not under the control of others
* Written by human in 2004 and released into the public domain * Written by human in 2004 and released into the public domain
* with no warranty of any kind, either expressed or implied. * with no warranty of any kind, either expressed or implied.
* It probably won't make your computer catch on fire, or eat * It probably won't make your computer catch on fire, or eat
* your children, but it might. Use at your own risk. * your children, but it might. Use at your own risk.
* *
*/ */
@ -15,9 +15,7 @@ import java.nio.channels.SocketChannel;
import java.util.Properties; import java.util.Properties;
import net.i2p.I2PAppContext; import net.i2p.I2PAppContext;
import net.i2p.data.DataHelper;
import net.i2p.util.Log; import net.i2p.util.Log;
import net.i2p.util.PasswordManager;
import net.i2p.util.VersionComparator; import net.i2p.util.VersionComparator;
/** /**
@ -27,21 +25,24 @@ class SAMHandlerFactory {
private static final String VERSION = "3.3"; 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 * Return the right SAM handler depending on the protocol version
* required by the client. * 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 * @param i2cpProps config options for our i2cp connection
* @throws SAMException if the connection handshake (HELLO message) was malformed * @throws SAMException if the connection handshake (HELLO message) was
* @return A SAM protocol handler, or null if the client closed before the handshake * malformed
* @return A SAM protocol handler, or null if the client closed before the
* handshake
*/ */
public static SAMHandler createSAMHandler(SocketChannel s, Properties i2cpProps, public static SAMHandler createSAMHandler(SocketChannel s, Properties i2cpProps,
SAMBridge parent) throws SAMException { SAMBridge parent) throws SAMException {
String line; String line;
Log log = I2PAppContext.getGlobalContext().logManager().getLog(SAMHandlerFactory.class); Log log = I2PAppContext.getGlobalContext().logManager().getLog(SAMHandlerFactory.class);
SAMSecureSessionInterface secureSession = parent.secureSession();
try { try {
Socket sock = s.socket(); Socket sock = s.socket();
@ -63,20 +64,20 @@ class SAMHandlerFactory {
// Message format: HELLO VERSION [MIN=v1] [MAX=v2] // Message format: HELLO VERSION [MIN=v1] [MAX=v2]
Properties props = SAMUtils.parseParams(line); Properties props = SAMUtils.parseParams(line);
if (!"HELLO".equals(props.remove(SAMUtils.COMMAND)) || 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"); throw new SAMException("Must start with HELLO VERSION");
} }
String minVer = props.getProperty("MIN"); String minVer = props.getProperty("MIN");
if (minVer == null) { 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 // MIN optional as of 0.9.14
minVer = "1"; minVer = "1";
} }
String maxVer = props.getProperty("MAX"); String maxVer = props.getProperty("MAX");
if (maxVer == null) { 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 // MAX optional as of 0.9.14
maxVer = "99.99"; maxVer = "99.99";
} }
@ -88,31 +89,16 @@ class SAMHandlerFactory {
return null; return null;
} }
if (Boolean.parseBoolean(i2cpProps.getProperty(SAMBridge.PROP_AUTH))) { if (secureSession != null) {
String user = props.getProperty("USER"); boolean approval = secureSession.approveOrDenySecureSession(i2cpProps, props);
String pw = props.getProperty("PASSWORD"); if (!approval) {
if (user == null || pw == null) { throw new SAMException("SAM connection cancelled by user request");
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");
} }
} }
// Let's answer positively // Let's answer positively
if (!SAMHandler.writeString("HELLO REPLY RESULT=OK VERSION=" + ver + "\n", s)) 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 // ...and instantiate the right SAM handler
int verMajor = getMajor(ver); int verMajor = getMajor(ver);
@ -121,21 +107,21 @@ class SAMHandlerFactory {
try { try {
switch (verMajor) { switch (verMajor) {
case 1: case 1:
handler = new SAMv1Handler(s, verMajor, verMinor, i2cpProps, parent); handler = new SAMv1Handler(s, verMajor, verMinor, i2cpProps, parent);
break; break;
case 2: case 2:
handler = new SAMv2Handler(s, verMajor, verMinor, i2cpProps, parent); handler = new SAMv2Handler(s, verMajor, verMinor, i2cpProps, parent);
break; break;
case 3: case 3:
handler = new SAMv3Handler(s, verMajor, verMinor, i2cpProps, parent); handler = new SAMv3Handler(s, verMajor, verMinor, i2cpProps, parent);
break; break;
default: default:
log.error("BUG! Trying to initialize the wrong SAM version!"); log.error("BUG! Trying to initialize the wrong SAM version!");
throw new SAMException("BUG! (in handler instantiation)"); throw new SAMException("BUG! (in handler instantiation)");
} }
} catch (IOException e) { } 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"); throw new SAMException("IOException caught during SAM handler instantiation");
} }
return handler; return handler;
@ -146,24 +132,24 @@ class SAMHandlerFactory {
*/ */
private static String chooseBestVersion(String minVer, String maxVer) { private static String chooseBestVersion(String minVer, String maxVer) {
if (VersionComparator.comp(VERSION, minVer) >= 0 && if (VersionComparator.comp(VERSION, minVer) >= 0 &&
VersionComparator.comp(VERSION, maxVer) <= 0) VersionComparator.comp(VERSION, maxVer) <= 0)
return VERSION; return VERSION;
if (VersionComparator.comp("3.2", minVer) >= 0 && if (VersionComparator.comp("3.2", minVer) >= 0 &&
VersionComparator.comp("3.2", maxVer) <= 0) VersionComparator.comp("3.2", maxVer) <= 0)
return "3.2"; return "3.2";
if (VersionComparator.comp("3.1", minVer) >= 0 && if (VersionComparator.comp("3.1", minVer) >= 0 &&
VersionComparator.comp("3.1", maxVer) <= 0) VersionComparator.comp("3.1", maxVer) <= 0)
return "3.1"; return "3.1";
// in VersionComparator, "3" < "3.0" so // in VersionComparator, "3" < "3.0" so
// use comparisons carefully // use comparisons carefully
if (VersionComparator.comp("3.0", minVer) >= 0 && if (VersionComparator.comp("3.0", minVer) >= 0 &&
VersionComparator.comp("3", maxVer) <= 0) VersionComparator.comp("3", maxVer) <= 0)
return "3.0"; return "3.0";
if (VersionComparator.comp("2.0", minVer) >= 0 && if (VersionComparator.comp("2.0", minVer) >= 0 &&
VersionComparator.comp("2", maxVer) <= 0) VersionComparator.comp("2", maxVer) <= 0)
return "2.0"; return "2.0";
if (VersionComparator.comp("1.0", minVer) >= 0 && if (VersionComparator.comp("1.0", minVer) >= 0 &&
VersionComparator.comp("1", maxVer) <= 0) VersionComparator.comp("1", maxVer) <= 0)
return "1.0"; return "1.0";
return null; return null;
} }
@ -186,7 +172,7 @@ class SAMHandlerFactory {
/* Get the minor protocol version from a string, or -1 */ /* Get the minor protocol version from a string, or -1 */
private static int getMinor(String ver) { private static int getMinor(String ver) {
if ( (ver == null) || (ver.indexOf('.') < 0) ) if ((ver == null) || (ver.indexOf('.') < 0))
return -1; return -1;
try { try {
String major = ver.substring(ver.indexOf('.') + 1); String major = ver.substring(ver.indexOf('.') + 1);

View File

@ -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;
}
}

View File

@ -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;
}