diff --git a/apps/sam/doc/README-test.txt b/apps/sam/doc/README-test.txt new file mode 100644 index 0000000000000000000000000000000000000000..c1936274d9d8e8265396d7e6266666b229e91b4e --- /dev/null +++ b/apps/sam/doc/README-test.txt @@ -0,0 +1,25 @@ +To run tests: + +Build and run standalone Java I2CP (no router): +ant buildTest +java -cp build/i2ptest.jar:build/routertest.jar -Djava.library.path=. net.i2p.router.client.LocalClientManager + + +Build and run standalone SAM server: +ant buildSAM +java -cp build/i2p.jar:build/mstreaming.jar:build/streaming.jar:build/sam.jar -Djava.library.path=. net.i2p.sam.SAMBridge + + +Build Java test clients: +cd apps/sam/java +ant clientjar +cd ../../.. + +Run sink client: +mkdir samsinkdir +java -cp build/i2p.jar:apps/sam/java/build/samclient.jar net.i2p.sam.client.SAMStreamSink samdest.txt samsinkdir -v 3.2 +run with no args to see usage + +Run send client: +java -cp build/i2p.jar:apps/sam/java/build/samclient.jar net.i2p.sam.client.SAMStreamSend samdest.txt samtestdata -v 3.2 +run with no args to see usage diff --git a/apps/sam/java/build.xml b/apps/sam/java/build.xml index 5d93ae4768eb3d4f5957a181e6cb5a913d468a76..9ecc58b8c20f241abf90b09be514f52c856a23ef 100644 --- a/apps/sam/java/build.xml +++ b/apps/sam/java/build.xml @@ -21,7 +21,9 @@ </depend> </target> - <property name="javac.compilerargs" value="" /> + <!-- ignored for now, we require java 7 here --> + <property name="javac.compilerargs7" value="" /> + <!-- ignored for now, we require java 7 here --> <property name="javac.version" value="1.6" /> <!-- compile everything including client classes --> @@ -30,22 +32,22 @@ <mkdir dir="./build/obj" /> <javac srcdir="./src" - debug="true" deprecation="on" source="${javac.version}" target="${javac.version}" + debug="true" deprecation="on" source="1.7" target="1.7" includeAntRuntime="false" destdir="./build/obj" classpath="../../../core/java/build/i2p.jar:../../ministreaming/java/build/mstreaming.jar" > - <compilerarg line="${javac.compilerargs}" /> + <compilerarg line="${javac.compilerargs7}" /> </javac> </target> <target name="compileTest" depends="compile"> <javac srcdir="./test" - debug="true" deprecation="on" source="${javac.version}" target="${javac.version}" + debug="true" deprecation="on" source="1.7" target="1.7" includeAntRuntime="false" destdir="./build/obj" classpath="../../../core/java/build/i2p.jar:../../ministreaming/java/build/mstreaming.jar" > - <compilerarg line="${javac.compilerargs}" /> + <compilerarg line="${javac.compilerargs7}" /> </javac> </target> diff --git a/apps/sam/java/src/net/i2p/sam/ReadLine.java b/apps/sam/java/src/net/i2p/sam/ReadLine.java new file mode 100644 index 0000000000000000000000000000000000000000..2af5f19d31e105dfedf39bb96fc42ab10d58da2d --- /dev/null +++ b/apps/sam/java/src/net/i2p/sam/ReadLine.java @@ -0,0 +1,73 @@ +package net.i2p.sam; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.Socket; +import java.net.SocketException; +import java.net.SocketTimeoutException; + +/** + * Modified from I2PTunnelHTTPServer + * + * @since 0.9.24 + */ +class ReadLine { + + private static final int MAX_LINE_LENGTH = 8*1024; + + /** + * Read a line teriminated by newline, with a total read timeout. + * + * Warning - strips \n but not \r + * Warning - 8KB line length limit as of 0.7.13, @throws IOException if exceeded + * + * @param buf output + * @param timeout throws SocketTimeoutException immediately if zero or negative + * @throws SocketTimeoutException if timeout is reached before newline + * @throws EOFException if EOF is reached before newline + * @throws LineTooLongException if too long + * @throws IOException on other errors in the underlying stream + */ + public static void readLine(Socket socket, StringBuilder buf, int timeout) throws IOException { + if (timeout <= 0) + throw new SocketTimeoutException(); + long expires = System.currentTimeMillis() + timeout; + // this reads and buffers extra bytes, so we can't use it + // unless we're going to decode UTF-8 on-the-fly, we're stuck with ASCII + //InputStreamReader in = new InputStreamReader(socket.getInputStream(), "UTF-8"); + InputStream in = socket.getInputStream(); + int c; + int i = 0; + socket.setSoTimeout(timeout); + while ( (c = in.read()) != -1) { + if (++i > MAX_LINE_LENGTH) + throw new LineTooLongException("Line too long - max " + MAX_LINE_LENGTH); + if (c == '\n') + break; + int newTimeout = (int) (expires - System.currentTimeMillis()); + if (newTimeout <= 0) + throw new SocketTimeoutException(); + buf.append((char)c); + if (newTimeout != timeout) { + timeout = newTimeout; + socket.setSoTimeout(timeout); + } + } + if (c == -1) { + if (System.currentTimeMillis() >= expires) + throw new SocketTimeoutException(); + else + throw new EOFException(); + } + } + + private static class LineTooLongException extends IOException { + public LineTooLongException(String s) { + super(s); + } + } +} + + diff --git a/apps/sam/java/src/net/i2p/sam/SAMBridge.java b/apps/sam/java/src/net/i2p/sam/SAMBridge.java index 383f44fb574b50a8ce6803eea3143a778143e67d..905ae808d0724f4329180247c76e23928b5455d5 100644 --- a/apps/sam/java/src/net/i2p/sam/SAMBridge.java +++ b/apps/sam/java/src/net/i2p/sam/SAMBridge.java @@ -9,15 +9,16 @@ package net.i2p.sam; */ 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.nio.ByteBuffer; import java.util.Arrays; import java.util.ArrayList; import java.util.HashMap; @@ -27,14 +28,22 @@ 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.PortMapper; +import net.i2p.util.SystemVersion; /** * SAM bridge implementation. @@ -48,7 +57,11 @@ public class SAMBridge implements Runnable, ClientApp { 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 @@ -65,21 +78,27 @@ public class SAMBridge implements Runnable, ClientApp { private volatile boolean acceptConnections = true; private final ClientAppManager _mgr; - private final String[] _args; 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"; - protected static final String DEFAULT_TCP_HOST = "0.0.0.0"; + 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 = "0.0.0.0"; - protected static final String DEFAULT_DATAGRAM_PORT = "7655"; + 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); /** @@ -95,11 +114,14 @@ public class SAMBridge implements Runnable, ClientApp { public SAMBridge(I2PAppContext context, ClientAppManager mgr, String[] args) throws Exception { _log = context.logManager().getLog(SAMBridge.class); _mgr = mgr; - _args = args; 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; @@ -123,13 +145,18 @@ public class SAMBridge implements Runnable, ClientApp { * @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, Properties i2cpProps, String persistFile) { + public SAMBridge(String listenHost, int listenPort, boolean isSSL, Properties i2cpProps, + String persistFile, File configFile) { _log = I2PAppContext.getGlobalContext().logManager().getLog(SAMBridge.class); _mgr = null; - _args = new String[] {listenHost, Integer.toString(listenPort) }; // placeholder _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(); @@ -142,7 +169,6 @@ public class SAMBridge implements Runnable, ClientApp { + ":" + listenPort, e); throw new RuntimeException(e); } - this.i2cpProps = i2cpProps; _state = INITIALIZED; } @@ -150,17 +176,28 @@ public class SAMBridge implements Runnable, ClientApp { * @since 0.9.6 */ private void openSocket() throws IOException { - if ( (_listenHost != null) && !("0.0.0.0".equals(_listenHost)) ) { - serverSocket = ServerSocketChannel.open(); - serverSocket.socket().bind(new InetSocketAddress(_listenHost, _listenPort)); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("SAM bridge listening on " - + _listenHost + ":" + _listenPort); + 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(); - serverSocket.socket().bind(new InetSocketAddress(_listenPort)); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("SAM bridge listening on 0.0.0.0:" + _listenPort); + 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); + } } } @@ -320,6 +357,40 @@ public class SAMBridge implements Runnable, ClientApp { } } + /** + * 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 + * @param 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 /** @@ -381,7 +452,7 @@ public class SAMBridge implements Runnable, ClientApp { * @since 0.9.6 */ public String getDisplayName() { - return "SAM " + Arrays.toString(_args); + return "SAM " + _listenHost + ':' + _listenPort; } ////// end ClientApp interface @@ -421,7 +492,8 @@ public class SAMBridge implements Runnable, ClientApp { public static void main(String args[]) { try { Options options = getOptions(args); - SAMBridge bridge = new SAMBridge(options.host, options.port, options.opts, options.keyFile); + SAMBridge bridge = new SAMBridge(options.host, options.port, options.isSSL, options.opts, + options.keyFile, options.configFile); bridge.startThread(); } catch (RuntimeException e) { e.printStackTrace(); @@ -459,9 +531,13 @@ public class SAMBridge implements Runnable, ClientApp { 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, Properties opts, String keyFile) { + 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; } } @@ -476,73 +552,162 @@ public class SAMBridge implements Runnable, ClientApp { * 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 = DEFAULT_SAM_KEYFILE; - int port = SAM_LISTENPORT; - String host = DEFAULT_TCP_HOST; - Properties opts = null; - if (args.length > 0) { - opts = parseOptions(args, 0); - keyfile = args[0]; - int portIndex = 1; - try { - if (args.length>portIndex) port = Integer.parseInt(args[portIndex]); - } catch (NumberFormatException nfe) { - host = args[portIndex]; - portIndex++; - try { - if (args.length>portIndex) port = Integer.parseInt(args[portIndex]); - } catch (NumberFormatException nfe1) { - port = Integer.parseInt(opts.getProperty(SAMBridge.PROP_TCP_PORT, SAMBridge.DEFAULT_TCP_PORT)); - host = opts.getProperty(SAMBridge.PROP_TCP_HOST, SAMBridge.DEFAULT_TCP_HOST); - } - } + 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); } - return new Options(host, port, opts, keyfile); + + int remaining = args.length - startOpts; + if (remaining > 0) { + parseOptions(args, startOpts, opts); + } + return new Options(host, port, isSSL, opts, keyfile, file); } - private static Properties parseOptions(String args[], int startArgs) throws HelpRequestedException { - Properties props = new Properties(); - // skip over first few options + /** + * 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++) { - if (args[i].equals("-h")) throw new HelpRequestedException(); int eq = args[i].indexOf('='); - if (eq <= 0) continue; - if (eq >= args[i].length()-1) continue; + 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(); } - return props; } private static void usage() { - System.err.println("Usage: SAMBridge [keyfile [listenHost] listenPortNum[ name=val]*]"); - System.err.println("or:"); - System.err.println(" SAMBridge [ name=val ]*"); - System.err.println(" keyfile: location to persist private keys (default sam.keys)"); - System.err.println(" listenHost: interface to listen on (0.0.0.0 for all interfaces)"); - System.err.println(" listenPort: port to listen for SAM connections on (default 7656)"); - System.err.println(" name=val: options to pass when connecting via I2CP, such as "); - System.err.println(" i2cp.host=localhost and i2cp.port=7654"); - System.err.println(""); - System.err.println("Host and ports of the SAM bridge can be specified with the alternate"); - System.err.println("form by specifying options "+SAMBridge.PROP_TCP_HOST+" and/or "+ - SAMBridge.PROP_TCP_PORT); - System.err.println(""); - System.err.println("Options "+SAMBridge.PROP_DATAGRAM_HOST+" and "+SAMBridge.PROP_DATAGRAM_PORT+ - " specify the listening ip"); - System.err.println("range and the port of SAM datagram server. This server is"); - System.err.println("only launched after a client creates the first SAM datagram"); - System.err.println("or raw session, after a handshake with SAM version >= 3.0."); - System.err.println(""); - System.err.println("The option loglevel=[DEBUG|WARN|ERROR|CRIT] can be used"); - System.err.println("for tuning the log verbosity.\n"); + 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() { @@ -621,4 +786,9 @@ public class SAMBridge implements Runnable, ClientApp { changeState(STOPPED); } } + + /** @since 0.9.24 */ + public void saveConfig() throws IOException { + DataHelper.storeProps(i2cpProps, _configFile); + } } diff --git a/apps/sam/java/src/net/i2p/sam/SAMDatagramReceiver.java b/apps/sam/java/src/net/i2p/sam/SAMDatagramReceiver.java index 68971af31a46a208277ab550064144c2fd325a6b..b07ff8fbed71097a33238aed793137c7a917870d 100644 --- a/apps/sam/java/src/net/i2p/sam/SAMDatagramReceiver.java +++ b/apps/sam/java/src/net/i2p/sam/SAMDatagramReceiver.java @@ -22,9 +22,12 @@ interface SAMDatagramReceiver { * * @param sender Destination * @param data Byte array to be received + * @param proto I2CP protocol + * @param fromPort I2CP from port + * @param toPort I2CP to port * @throws IOException */ - public void receiveDatagramBytes(Destination sender, byte data[]) throws IOException; + public void receiveDatagramBytes(Destination sender, byte data[], int proto, int fromPort, int toPort) throws IOException; /** * Stop receiving data. diff --git a/apps/sam/java/src/net/i2p/sam/SAMDatagramSession.java b/apps/sam/java/src/net/i2p/sam/SAMDatagramSession.java index ce3c1803c31b326e3c057109d0c73d9b74ee95b8..b25d6ad0b3b1911b11f7a75c5f8422f1a855b2b5 100644 --- a/apps/sam/java/src/net/i2p/sam/SAMDatagramSession.java +++ b/apps/sam/java/src/net/i2p/sam/SAMDatagramSession.java @@ -78,24 +78,24 @@ class SAMDatagramSession extends SAMMessageSession { * * @param dest Destination * @param data Bytes to be sent + * @param proto ignored, will always use PROTO_DATAGRAM (17) * * @return True if the data was sent, false otherwise * @throws DataFormatException on unknown / bad dest * @throws I2PSessionException on serious error, probably session closed */ - public boolean sendBytes(String dest, byte[] data) throws DataFormatException, I2PSessionException { + public boolean sendBytes(String dest, byte[] data, int proto, + int fromPort, int toPort) throws DataFormatException, I2PSessionException { if (data.length > DGRAM_SIZE_MAX) throw new DataFormatException("Datagram size exceeded (" + data.length + ")"); byte[] dgram ; synchronized (dgramMaker) { dgram = dgramMaker.makeI2PDatagram(data); } - // TODO pass ports through - return sendBytesThroughMessageSession(dest, dgram, I2PSession.PROTO_DATAGRAM, - I2PSession.PORT_UNSPECIFIED, I2PSession.PORT_UNSPECIFIED); + return sendBytesThroughMessageSession(dest, dgram, I2PSession.PROTO_DATAGRAM, fromPort, toPort); } - protected void messageReceived(byte[] msg) { + protected void messageReceived(byte[] msg, int proto, int fromPort, int toPort) { byte[] payload; Destination sender; try { @@ -106,18 +106,18 @@ class SAMDatagramSession extends SAMMessageSession { } } catch (DataFormatException e) { if (_log.shouldLog(Log.DEBUG)) { - _log.debug("Dropping ill-formatted I2P repliable datagram"); + _log.debug("Dropping ill-formatted I2P repliable datagram", e); } return; } catch (I2PInvalidDatagramException e) { if (_log.shouldLog(Log.DEBUG)) { - _log.debug("Dropping ill-signed I2P repliable datagram"); + _log.debug("Dropping ill-signed I2P repliable datagram", e); } return; } try { - recv.receiveDatagramBytes(sender, payload); + recv.receiveDatagramBytes(sender, payload, proto, fromPort, toPort); } catch (IOException e) { _log.error("Error forwarding message to receiver", e); close(); diff --git a/apps/sam/java/src/net/i2p/sam/SAMHandler.java b/apps/sam/java/src/net/i2p/sam/SAMHandler.java index 56d68878c612ec998fdc6a80f56e5b298f03f7e0..e2bcb84fb709d4ad2d3005fa0a9c57c02f5d3df9 100644 --- a/apps/sam/java/src/net/i2p/sam/SAMHandler.java +++ b/apps/sam/java/src/net/i2p/sam/SAMHandler.java @@ -35,8 +35,8 @@ abstract class SAMHandler implements Runnable, Handler { private final Object socketWLock = new Object(); // Guards writings on socket protected final SocketChannel socket; - protected final int verMajor; - protected final int verMinor; + public final int verMajor; + public final int verMinor; /** I2CP options configuring the I2CP connection (port, host, numHops, etc) */ protected final Properties i2cpProps; @@ -102,7 +102,10 @@ abstract class SAMHandler implements Runnable, Handler { } } - static public void writeBytes(ByteBuffer data, SocketChannel out) throws IOException { + /** + * Caller must synch + */ + private static void writeBytes(ByteBuffer data, SocketChannel out) throws IOException { while (data.hasRemaining()) out.write(data); out.socket().getOutputStream().flush(); } @@ -132,7 +135,10 @@ abstract class SAMHandler implements Runnable, Handler { } } - /** @return success */ + /** + * Unsynchronized, use with caution + * @return success + */ public static boolean writeString(String str, SocketChannel out) { try { @@ -158,6 +164,8 @@ abstract class SAMHandler implements Runnable, Handler { * unregister with the bridge. */ public void stopHandling() { + if (_log.shouldInfo()) + _log.info("Stopping: " + this, new Exception("I did it")); synchronized (stopLock) { stopHandler = true; } diff --git a/apps/sam/java/src/net/i2p/sam/SAMHandlerFactory.java b/apps/sam/java/src/net/i2p/sam/SAMHandlerFactory.java index 582854d870617970e18ea5efbeb4dba2c11c7bb6..71a617e25fc8ad436f19e304277cae3487c12577 100644 --- a/apps/sam/java/src/net/i2p/sam/SAMHandlerFactory.java +++ b/apps/sam/java/src/net/i2p/sam/SAMHandlerFactory.java @@ -18,6 +18,7 @@ import java.util.StringTokenizer; import net.i2p.I2PAppContext; import net.i2p.data.DataHelper; import net.i2p.util.Log; +import net.i2p.util.PasswordManager; import net.i2p.util.VersionComparator; /** @@ -25,7 +26,7 @@ import net.i2p.util.VersionComparator; */ class SAMHandlerFactory { - private static final String VERSION = "3.1"; + private static final String VERSION = "3.2"; private static final int HELLO_TIMEOUT = 60*1000; @@ -45,20 +46,17 @@ class SAMHandlerFactory { try { Socket sock = s.socket(); - sock.setSoTimeout(HELLO_TIMEOUT); sock.setKeepAlive(true); - String line = DataHelper.readLine(sock.getInputStream()); + StringBuilder buf = new StringBuilder(128); + ReadLine.readLine(sock, buf, HELLO_TIMEOUT); + String line = buf.toString(); sock.setSoTimeout(0); - if (line == null) { - log.debug("Connection closed by client"); - return null; - } tok = new StringTokenizer(line.trim(), " "); } catch (SocketTimeoutException e) { throw new SAMException("Timeout waiting for HELLO VERSION", e); } catch (IOException e) { throw new SAMException("Error reading from socket", e); - } catch (Exception e) { + } catch (RuntimeException e) { throw new SAMException("Unexpected error", e); } @@ -93,6 +91,20 @@ class SAMHandlerFactory { SAMHandler.writeString("HELLO REPLY RESULT=NOVERSION\n", s); 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) + throw new SAMException("USER and PASSWORD required"); + String savedPW = i2cpProps.getProperty(SAMBridge.PROP_PW_PREFIX + user + SAMBridge.PROP_PW_SUFFIX); + if (savedPW == null) + throw new SAMException("Authorization failed"); + PasswordManager pm = new PasswordManager(I2PAppContext.getGlobalContext()); + if (!pm.checkHash(savedPW, pw)) + throw new SAMException("Authorization failed"); + } + // Let's answer positively if (!SAMHandler.writeString("HELLO REPLY RESULT=OK VERSION=" + ver + "\n", s)) throw new SAMException("Error writing to socket"); @@ -131,6 +143,9 @@ class SAMHandlerFactory { if (VersionComparator.comp(VERSION, minVer) >= 0 && VersionComparator.comp(VERSION, maxVer) <= 0) return VERSION; + if (VersionComparator.comp("3.1", minVer) >= 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 && diff --git a/apps/sam/java/src/net/i2p/sam/SAMMessageSession.java b/apps/sam/java/src/net/i2p/sam/SAMMessageSession.java index e8e502da8b2c5b5cf51cbc5b01343f6b68fd52d2..c95b6a9ab22c8faff38daeb7c99d507096662ec8 100644 --- a/apps/sam/java/src/net/i2p/sam/SAMMessageSession.java +++ b/apps/sam/java/src/net/i2p/sam/SAMMessageSession.java @@ -19,11 +19,12 @@ import net.i2p.client.I2PClient; import net.i2p.client.I2PClientFactory; import net.i2p.client.I2PSession; import net.i2p.client.I2PSessionException; -import net.i2p.client.I2PSessionListener; +import net.i2p.client.I2PSessionMuxedListener; +import net.i2p.client.SendMessageOptions; import net.i2p.data.Base64; import net.i2p.data.DataFormatException; import net.i2p.data.Destination; -import net.i2p.util.HexDump; +//import net.i2p.util.HexDump; import net.i2p.util.I2PAppThread; import net.i2p.util.Log; @@ -97,7 +98,8 @@ abstract class SAMMessageSession implements Closeable { * @throws DataFormatException on unknown / bad dest * @throws I2PSessionException on serious error, probably session closed */ - public abstract boolean sendBytes(String dest, byte[] data) throws DataFormatException, I2PSessionException; + public abstract boolean sendBytes(String dest, byte[] data, int proto, + int fromPort, int toPort) throws DataFormatException, I2PSessionException; /** * Actually send bytes through the SAM message-based session I2PSession @@ -125,6 +127,40 @@ abstract class SAMMessageSession implements Closeable { return session.sendMessage(d, data, proto, fromPort, toPort); } + /** + * Actually send bytes through the SAM message-based session I2PSession. + * TODO unused, umimplemented in the sessions and handlers + * + * @param dest Destination + * @param data Bytes to be sent + * @param proto I2CP protocol + * @param fromPort I2CP from port + * @param toPort I2CP to port + * + * @return True if the data was sent, false otherwise + * @throws DataFormatException on unknown / bad dest + * @throws I2PSessionException on serious error, probably session closed + * @since 0.9.24 + */ + protected boolean sendBytesThroughMessageSession(String dest, byte[] data, + int proto, int fromPort, int toPort, + boolean sendLeaseSet, int sendTags, + int tagThreshold, long expires) + throws DataFormatException, I2PSessionException { + Destination d = SAMUtils.getDest(dest); + + if (_log.shouldLog(Log.DEBUG)) { + _log.debug("Sending " + data.length + " bytes to " + dest); + } + SendMessageOptions opts = new SendMessageOptions(); + opts.setSendLeaseSet(sendLeaseSet); + opts.setTagsToSend(sendTags); + opts.setTagThreshold(tagThreshold); + opts.setDate(expires); + + return session.sendMessage(d, data, 0, data.length, proto, fromPort, toPort, opts); + } + /** * Close a SAM message-based session. */ @@ -136,7 +172,7 @@ abstract class SAMMessageSession implements Closeable { * Handle a new received message * @param msg Message payload */ - protected abstract void messageReceived(byte[] msg); + protected abstract void messageReceived(byte[] msg, int proto, int fromPort, int toPort); /** * Do whatever is needed to shutdown the SAM session @@ -158,7 +194,7 @@ abstract class SAMMessageSession implements Closeable { * * @author human */ - class SAMMessageSessionHandler implements Runnable, I2PSessionListener { + class SAMMessageSessionHandler implements Runnable, I2PSessionMuxedListener { private final Object runningLock = new Object(); private volatile boolean stillRunning = true; @@ -187,7 +223,7 @@ abstract class SAMMessageSession implements Closeable { if (_log.shouldLog(Log.DEBUG)) _log.debug("I2P session connected"); - session.setSessionListener(this); + session.addMuxedSessionListener(this, I2PSession.PROTO_ANY, I2PSession.PORT_ANY); } /** @@ -218,6 +254,7 @@ abstract class SAMMessageSession implements Closeable { _log.debug("Shutting down SAM message-based session handler"); shutDown(); + session.removeListener(I2PSession.PROTO_ANY, I2PSession.PORT_ANY); try { if (_log.shouldLog(Log.DEBUG)) @@ -243,7 +280,15 @@ abstract class SAMMessageSession implements Closeable { stopRunning(); } - public void messageAvailable(I2PSession session, int msgId, long size){ + public void messageAvailable(I2PSession session, int msgId, long size) { + messageAvailable(session, msgId, size, I2PSession.PROTO_UNSPECIFIED, + I2PSession.PORT_UNSPECIFIED, I2PSession.PORT_UNSPECIFIED); + } + + /** @since 0.9.24 */ + public void messageAvailable(I2PSession session, int msgId, long size, + int proto, int fromPort, int toPort) { + if (_log.shouldLog(Log.DEBUG)) { _log.debug("I2P message available (id: " + msgId + "; size: " + size + ")"); @@ -252,12 +297,12 @@ abstract class SAMMessageSession implements Closeable { byte msg[] = session.receiveMessage(msgId); if (msg == null) return; - if (_log.shouldLog(Log.DEBUG)) { - _log.debug("Content of message " + msgId + ":\n" - + HexDump.dump(msg)); - } + //if (_log.shouldLog(Log.DEBUG)) { + // _log.debug("Content of message " + msgId + ":\n" + // + HexDump.dump(msg)); + //} - messageReceived(msg); + messageReceived(msg, proto, fromPort, toPort); } catch (I2PSessionException e) { _log.error("Error fetching I2P message", e); stopRunning(); diff --git a/apps/sam/java/src/net/i2p/sam/SAMRawReceiver.java b/apps/sam/java/src/net/i2p/sam/SAMRawReceiver.java index 96ebe45d082102ffd98e4a35d1062a498fcfd5cd..564f95c3d8b39e53b2a20c07f3f4b1d5eac95d1f 100644 --- a/apps/sam/java/src/net/i2p/sam/SAMRawReceiver.java +++ b/apps/sam/java/src/net/i2p/sam/SAMRawReceiver.java @@ -20,9 +20,12 @@ interface SAMRawReceiver { * regarding the sender. * * @param data Byte array to be received + * @param proto I2CP protocol + * @param fromPort I2CP from port + * @param toPort I2CP to port * @throws IOException */ - public void receiveRawBytes(byte data[]) throws IOException; + public void receiveRawBytes(byte data[], int proto, int fromPort, int toPort) throws IOException; /** * Stop receiving data. diff --git a/apps/sam/java/src/net/i2p/sam/SAMRawSession.java b/apps/sam/java/src/net/i2p/sam/SAMRawSession.java index e3c42160834a7f46fd736f33d779b4472ce9bae4..ab1cd76322e36d6ca305510d14b9f0b1f82fffb9 100644 --- a/apps/sam/java/src/net/i2p/sam/SAMRawSession.java +++ b/apps/sam/java/src/net/i2p/sam/SAMRawSession.java @@ -67,22 +67,24 @@ class SAMRawSession extends SAMMessageSession { * Send bytes through a SAM RAW session. * * @param data Bytes to be sent + * @param proto if 0, will use PROTO_DATAGRAM_RAW (18) * * @return True if the data was sent, false otherwise * @throws DataFormatException on unknown / bad dest * @throws I2PSessionException on serious error, probably session closed */ - public boolean sendBytes(String dest, byte[] data) throws DataFormatException, I2PSessionException { + public boolean sendBytes(String dest, byte[] data, int proto, + int fromPort, int toPort) throws DataFormatException, I2PSessionException { if (data.length > RAW_SIZE_MAX) throw new DataFormatException("Data size limit exceeded (" + data.length + ")"); - // TODO pass ports through - return sendBytesThroughMessageSession(dest, data, I2PSession.PROTO_DATAGRAM_RAW, - I2PSession.PORT_UNSPECIFIED, I2PSession.PORT_UNSPECIFIED); + if (proto == I2PSession.PROTO_UNSPECIFIED) + proto = I2PSession.PROTO_DATAGRAM_RAW; + return sendBytesThroughMessageSession(dest, data, proto, fromPort, toPort); } - protected void messageReceived(byte[] msg) { + protected void messageReceived(byte[] msg, int proto, int fromPort, int toPort) { try { - recv.receiveRawBytes(msg); + recv.receiveRawBytes(msg, proto, fromPort, toPort); } catch (IOException e) { _log.error("Error forwarding message to receiver", e); close(); diff --git a/apps/sam/java/src/net/i2p/sam/SAMv1Handler.java b/apps/sam/java/src/net/i2p/sam/SAMv1Handler.java index 588d5f5e5786d0c6d3e228ec1949cd84f4abcaf5..5b594701686bea2de347ec5dba1471d466117c4e 100644 --- a/apps/sam/java/src/net/i2p/sam/SAMv1Handler.java +++ b/apps/sam/java/src/net/i2p/sam/SAMv1Handler.java @@ -23,6 +23,7 @@ import java.util.concurrent.atomic.AtomicLong; import net.i2p.I2PException; import net.i2p.client.I2PClient; +import net.i2p.client.I2PSession; import net.i2p.client.I2PSessionException; import net.i2p.crypto.SigType; import net.i2p.data.Base64; @@ -186,7 +187,9 @@ class SAMv1Handler extends SAMHandler implements SAMRawReceiver, SAMDatagramRece } catch (IOException e) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Caught IOException for message [" + msg + "]", e); - } catch (Exception e) { + } catch (SAMException e) { + _log.error("Unexpected exception for message [" + msg + "]", e); + } catch (RuntimeException e) { _log.error("Unexpected exception for message [" + msg + "]", e); } finally { if (_log.shouldLog(Log.DEBUG)) @@ -438,25 +441,44 @@ class SAMv1Handler extends SAMHandler implements SAMRawReceiver, SAMDatagramRece } int size; - { - String strsize = props.getProperty("SIZE"); - if (strsize == null) { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Size not specified in DATAGRAM SEND message"); - return false; - } + String strsize = props.getProperty("SIZE"); + if (strsize == null) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Size not specified in DATAGRAM SEND message"); + return false; + } + try { + size = Integer.parseInt(strsize); + } catch (NumberFormatException e) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Invalid DATAGRAM SEND size specified: " + strsize); + return false; + } + if (!checkDatagramSize(size)) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Specified size (" + size + + ") is out of protocol limits"); + return false; + } + int proto = I2PSession.PROTO_DATAGRAM; + int fromPort = I2PSession.PORT_UNSPECIFIED; + int toPort = I2PSession.PORT_UNSPECIFIED; + String s = props.getProperty("FROM_PORT"); + if (s != null) { try { - size = Integer.parseInt(strsize); + fromPort = Integer.parseInt(s); } catch (NumberFormatException e) { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Invalid DATAGRAM SEND size specified: " + strsize); - return false; + if (_log.shouldLog(Log.WARN)) + _log.warn("Invalid DATAGRAM SEND port specified: " + s); } - if (!checkDatagramSize(size)) { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Specified size (" + size - + ") is out of protocol limits"); - return false; + } + s = props.getProperty("TO_PORT"); + if (s != null) { + try { + toPort = Integer.parseInt(s); + } catch (NumberFormatException e) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Invalid RAW SEND port specified: " + s); } } @@ -466,7 +488,7 @@ class SAMv1Handler extends SAMHandler implements SAMRawReceiver, SAMDatagramRece in.readFully(data); - if (!getDatagramSession().sendBytes(dest, data)) { + if (!getDatagramSession().sendBytes(dest, data, proto, fromPort, toPort)) { _log.error("DATAGRAM SEND failed"); // a message send failure is no reason to drop the SAM session // for raw and repliable datagrams, just carry on our merry way @@ -523,25 +545,53 @@ class SAMv1Handler extends SAMHandler implements SAMRawReceiver, SAMDatagramRece } int size; - { - String strsize = props.getProperty("SIZE"); - if (strsize == null) { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Size not specified in RAW SEND message"); - return false; + String strsize = props.getProperty("SIZE"); + if (strsize == null) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Size not specified in RAW SEND message"); + return false; + } + try { + size = Integer.parseInt(strsize); + } catch (NumberFormatException e) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Invalid RAW SEND size specified: " + strsize); + return false; + } + if (!checkSize(size)) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Specified size (" + size + + ") is out of protocol limits"); + return false; + } + int proto = I2PSession.PROTO_DATAGRAM_RAW; + int fromPort = I2PSession.PORT_UNSPECIFIED; + int toPort = I2PSession.PORT_UNSPECIFIED; + String s = props.getProperty("PROTOCOL"); + if (s != null) { + try { + proto = Integer.parseInt(s); + } catch (NumberFormatException e) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Invalid RAW SEND protocol specified: " + s); } + } + s = props.getProperty("FROM_PORT"); + if (s != null) { try { - size = Integer.parseInt(strsize); + fromPort = Integer.parseInt(s); } catch (NumberFormatException e) { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Invalid RAW SEND size specified: " + strsize); - return false; + if (_log.shouldLog(Log.WARN)) + _log.warn("Invalid RAW SEND port specified: " + s); } - if (!checkSize(size)) { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Specified size (" + size - + ") is out of protocol limits"); - return false; + } + s = props.getProperty("TO_PORT"); + if (s != null) { + try { + toPort = Integer.parseInt(s); + } catch (NumberFormatException e) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Invalid RAW SEND port specified: " + s); } } @@ -551,7 +601,7 @@ class SAMv1Handler extends SAMHandler implements SAMRawReceiver, SAMDatagramRece in.readFully(data); - if (!getRawSession().sendBytes(dest, data)) { + if (!getRawSession().sendBytes(dest, data, proto, fromPort, toPort)) { _log.error("RAW SEND failed"); // a message send failure is no reason to drop the SAM session // for raw and repliable datagrams, just carry on our merry way @@ -796,16 +846,21 @@ class SAMv1Handler extends SAMHandler implements SAMRawReceiver, SAMDatagramRece } // SAMRawReceiver implementation - public void receiveRawBytes(byte data[]) throws IOException { + public void receiveRawBytes(byte data[], int proto, int fromPort, int toPort) throws IOException { if (getRawSession() == null) { _log.error("BUG! Received raw bytes, but session is null!"); return; } - ByteArrayOutputStream msg = new ByteArrayOutputStream(); + ByteArrayOutputStream msg = new ByteArrayOutputStream(64 + data.length); - String msgText = "RAW RECEIVED SIZE=" + data.length + "\n"; + String msgText = "RAW RECEIVED SIZE=" + data.length; msg.write(DataHelper.getASCII(msgText)); + if ((verMajor == 3 && verMinor >= 2) || verMajor > 3) { + msgText = " PROTOCOL=" + proto + " FROM_PORT=" + fromPort + " TO_PORT=" + toPort; + msg.write(DataHelper.getASCII(msgText)); + } + msg.write((byte) '\n'); msg.write(data); if (_log.shouldLog(Log.DEBUG)) @@ -832,17 +887,23 @@ class SAMv1Handler extends SAMHandler implements SAMRawReceiver, SAMDatagramRece } // SAMDatagramReceiver implementation - public void receiveDatagramBytes(Destination sender, byte data[]) throws IOException { + public void receiveDatagramBytes(Destination sender, byte data[], int proto, + int fromPort, int toPort) throws IOException { if (getDatagramSession() == null) { _log.error("BUG! Received datagram bytes, but session is null!"); return; } - ByteArrayOutputStream msg = new ByteArrayOutputStream(); + ByteArrayOutputStream msg = new ByteArrayOutputStream(100 + data.length); String msgText = "DATAGRAM RECEIVED DESTINATION=" + sender.toBase64() - + " SIZE=" + data.length + "\n"; + + " SIZE=" + data.length; msg.write(DataHelper.getASCII(msgText)); + if ((verMajor == 3 && verMinor >= 2) || verMajor > 3) { + msgText = " FROM_PORT=" + fromPort + " TO_PORT=" + toPort; + msg.write(DataHelper.getASCII(msgText)); + } + msg.write((byte) '\n'); if (_log.shouldLog(Log.DEBUG)) _log.debug("sending to client: " + msgText); diff --git a/apps/sam/java/src/net/i2p/sam/SAMv3DatagramServer.java b/apps/sam/java/src/net/i2p/sam/SAMv3DatagramServer.java new file mode 100644 index 0000000000000000000000000000000000000000..a482318fa66b69cea6d94e28b179747747eeb81e --- /dev/null +++ b/apps/sam/java/src/net/i2p/sam/SAMv3DatagramServer.java @@ -0,0 +1,223 @@ +package net.i2p.sam; +/* + * free (adj.): unencumbered; not under the control of others + * Written by human in 2004 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.DatagramChannel; +import java.util.Properties; +import java.util.StringTokenizer; + +import net.i2p.I2PAppContext; +import net.i2p.client.I2PSession; +import net.i2p.data.DataHelper; +import net.i2p.util.I2PAppThread; +import net.i2p.util.Log; + +/** + * This is the thread listening on 127.0.0.1:7655 or as specified by + * sam.udp.host and sam.udp.port properties. + * This is used for both repliable and raw datagrams. + * + * @since 0.9.24 moved from SAMv3Handler + */ +class SAMv3DatagramServer implements Handler { + + private final DatagramChannel _server; + private final Thread _listener; + private final SAMBridge _parent; + private final String _host; + private final int _port; + + /** + * Does not start listener. + * Caller must call start(). + * + * @param parent may be null + * @param props ignored for now + */ + public SAMv3DatagramServer(SAMBridge parent, String host, int port, Properties props) throws IOException { + _parent = parent; + _server = DatagramChannel.open(); + + _server.socket().bind(new InetSocketAddress(host, port)); + _listener = new I2PAppThread(new Listener(_server), "SAM DatagramListener " + port); + _host = host; + _port = port; + } + + /** + * Only call once. + * @since 0.9.22 + */ + public synchronized void start() { + _listener.start(); + if (_parent != null) + _parent.register(this); + } + + /** + * Cannot be restarted. + * @since 0.9.22 + */ + public synchronized void stopHandling() { + try { + _server.close(); + } catch (IOException ioe) {} + _listener.interrupt(); + if (_parent != null) + _parent.unregister(this); + } + + public void send(SocketAddress addr, ByteBuffer msg) throws IOException { + _server.send(msg, addr); + } + + /** @since 0.9.24 */ + public String getHost() { return _host; } + + /** @since 0.9.24 */ + public int getPort() { return _port; } + + private static class Listener implements Runnable { + + private final DatagramChannel server; + + public Listener(DatagramChannel server) + { + this.server = server ; + } + public void run() + { + ByteBuffer inBuf = ByteBuffer.allocateDirect(SAMRawSession.RAW_SIZE_MAX+1024); + + while (!Thread.interrupted()) + { + inBuf.clear(); + try { + server.receive(inBuf); + } catch (IOException e) { + break ; + } + inBuf.flip(); + ByteBuffer outBuf = ByteBuffer.wrap(new byte[inBuf.remaining()]); + outBuf.put(inBuf); + outBuf.flip(); + // A new thread for every message is wildly inefficient... + //new I2PAppThread(new MessageDispatcher(outBuf.array()), "MessageDispatcher").start(); + // inline + // Even though we could be sending messages through multiple sessions, + // that isn't a common use case, and blocking should be rare. + // Inside router context, I2CP drops on overflow. + (new MessageDispatcher(outBuf.array())).run(); + } + } + } + + private static class MessageDispatcher implements Runnable { + private final ByteArrayInputStream is; + + public MessageDispatcher(byte[] buf) { + this.is = new ByteArrayInputStream(buf) ; + } + + public void run() { + try { + String header = DataHelper.readLine(is).trim(); + StringTokenizer tok = new StringTokenizer(header, " "); + if (tok.countTokens() < 3) { + // This is not a correct message, for sure + warn("Bad datagram header received"); + return; + } + String version = tok.nextToken(); + if (!version.startsWith("3.")) { + warn("Bad datagram header received"); + return; + } + String nick = tok.nextToken(); + String dest = tok.nextToken(); + + SAMv3Handler.SessionRecord rec = SAMv3Handler.sSessionsHash.get(nick); + if (rec!=null) { + Properties sprops = rec.getProps(); + String pr = sprops.getProperty("PROTOCOL"); + String fp = sprops.getProperty("FROM_PORT"); + String tp = sprops.getProperty("TO_PORT"); + while (tok.hasMoreTokens()) { + String t = tok.nextToken(); + if (t.startsWith("PROTOCOL=")) + pr = t.substring("PROTOCOL=".length()); + else if (t.startsWith("FROM_PORT=")) + fp = t.substring("FROM_PORT=".length()); + else if (t.startsWith("TO_PORT=")) + tp = t.substring("TO_PORT=".length()); + } + + int proto = I2PSession.PROTO_UNSPECIFIED; + int fromPort = I2PSession.PORT_UNSPECIFIED; + int toPort = I2PSession.PORT_UNSPECIFIED; + if (pr != null) { + try { + proto = Integer.parseInt(pr); + } catch (NumberFormatException nfe) { + warn("Bad datagram header received"); + return; + } + } + if (fp != null) { + try { + fromPort = Integer.parseInt(fp); + } catch (NumberFormatException nfe) { + warn("Bad datagram header received"); + return; + } + } + if (tp != null) { + try { + toPort = Integer.parseInt(tp); + } catch (NumberFormatException nfe) { + warn("Bad datagram header received"); + return; + } + } + // TODO too many allocations and copies. One here and one in Listener above. + byte[] data = new byte[is.available()]; + is.read(data); + SAMv3Handler.Session sess = rec.getHandler().getSession(); + if (sess != null) + sess.sendBytes(dest, data, proto, fromPort, toPort); + else + warn("Dropping datagram, no session for " + nick); + } else { + warn("Dropping datagram, no session for " + nick); + } + } catch (Exception e) { + warn("Error handling datagram", e); + } + } + + /** @since 0.9.22 */ + private static void warn(String s) { + warn(s, null); + } + + /** @since 0.9.22 */ + private static void warn(String s, Throwable t) { + Log log = I2PAppContext.getGlobalContext().logManager().getLog(SAMv3DatagramServer.class); + if (log.shouldLog(Log.WARN)) + log.warn(s, t); + } + } +} diff --git a/apps/sam/java/src/net/i2p/sam/SAMv3DatagramSession.java b/apps/sam/java/src/net/i2p/sam/SAMv3DatagramSession.java index a16c92327fe23cc2f055d850a3ab48cd08a546a9..ee2782559266ceb1bb820069d796155a1fd02666 100644 --- a/apps/sam/java/src/net/i2p/sam/SAMv3DatagramSession.java +++ b/apps/sam/java/src/net/i2p/sam/SAMv3DatagramSession.java @@ -21,7 +21,7 @@ import java.nio.ByteBuffer; class SAMv3DatagramSession extends SAMDatagramSession implements SAMv3Handler.Session, SAMDatagramReceiver { private final SAMv3Handler handler; - private final SAMv3Handler.DatagramServer server; + private final SAMv3DatagramServer server; private final String nick; private final SocketAddress clientAddress; @@ -30,52 +30,58 @@ class SAMv3DatagramSession extends SAMDatagramSession implements SAMv3Handler.Se /** * build a DatagramSession according to informations registered * with the given nickname + * * @param nick nickname of the session * @throws IOException * @throws DataFormatException * @throws I2PSessionException */ - public SAMv3DatagramSession(String nick) - throws IOException, DataFormatException, I2PSessionException, SAMException { - + public SAMv3DatagramSession(String nick, SAMv3DatagramServer dgServer) + throws IOException, DataFormatException, I2PSessionException, SAMException { super(SAMv3Handler.sSessionsHash.get(nick).getDest(), SAMv3Handler.sSessionsHash.get(nick).getProps(), null // to be replaced by this ); - this.nick = nick ; - this.recv = this ; // replacement - this.server = SAMv3Handler.DatagramServer.getInstance() ; + this.nick = nick; + this.recv = this; // replacement + this.server = dgServer; SAMv3Handler.SessionRecord rec = SAMv3Handler.sSessionsHash.get(nick); - if ( rec==null ) throw new SAMException("Record disappeared for nickname : \""+nick+"\"") ; + if (rec == null) + throw new SAMException("Record disappeared for nickname : \""+nick+"\""); - this.handler = rec.getHandler(); + this.handler = rec.getHandler(); - Properties props = rec.getProps(); - String portStr = props.getProperty("PORT") ; - if ( portStr==null ) { - _log.debug("receiver port not specified. Current socket will be used."); - this.clientAddress = null; - } - else { - int port = Integer.parseInt(portStr); - - String host = props.getProperty("HOST"); - if ( host==null ) { - host = rec.getHandler().getClientIP(); - _log.debug("no host specified. Taken from the client socket : " + host+':'+port); - } - - - this.clientAddress = new InetSocketAddress(host,port); - } + Properties props = rec.getProps(); + String portStr = props.getProperty("PORT"); + if (portStr == null) { + if (_log.shouldDebug()) + _log.debug("receiver port not specified. Current socket will be used."); + this.clientAddress = null; + } else { + int port = Integer.parseInt(portStr); + String host = props.getProperty("HOST"); + if (host == null) { + host = rec.getHandler().getClientIP(); + if (_log.shouldDebug()) + _log.debug("no host specified. Taken from the client socket : " + host+':'+port); + } + this.clientAddress = new InetSocketAddress(host, port); + } } - public void receiveDatagramBytes(Destination sender, byte[] data) throws IOException { + public void receiveDatagramBytes(Destination sender, byte[] data, int proto, + int fromPort, int toPort) throws IOException { if (this.clientAddress==null) { - this.handler.receiveDatagramBytes(sender, data); + this.handler.receiveDatagramBytes(sender, data, proto, fromPort, toPort); } else { - String msg = sender.toBase64()+"\n"; + StringBuilder buf = new StringBuilder(600); + buf.append(sender.toBase64()); + if ((handler.verMajor == 3 && handler.verMinor >= 2) || handler.verMajor > 3) { + buf.append(" FROM_PORT=").append(fromPort).append(" TO_PORT=").append(toPort); + } + buf.append('\n'); + String msg = buf.toString(); ByteBuffer msgBuf = ByteBuffer.allocate(msg.length()+data.length); msgBuf.put(DataHelper.getASCII(msg)); msgBuf.put(data); diff --git a/apps/sam/java/src/net/i2p/sam/SAMv3Handler.java b/apps/sam/java/src/net/i2p/sam/SAMv3Handler.java index 5e75dcf661d485c1fa1b94e8a54683b122dfa849..9cc4a500c752a710125ec09d1db5d3ab601e3fed 100644 --- a/apps/sam/java/src/net/i2p/sam/SAMv3Handler.java +++ b/apps/sam/java/src/net/i2p/sam/SAMv3Handler.java @@ -10,15 +10,16 @@ package net.i2p.sam; import java.io.ByteArrayOutputStream; -import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InterruptedIOException; import java.net.ConnectException; import java.net.InetSocketAddress; +import java.net.Socket; import java.net.SocketAddress; +import java.net.SocketException; +import java.net.SocketTimeoutException; import java.net.NoRouteToHostException; -import java.nio.channels.DatagramChannel; import java.nio.channels.SocketChannel; import java.nio.ByteBuffer; import java.util.Properties; @@ -28,6 +29,7 @@ import java.util.StringTokenizer; import net.i2p.I2PAppContext; import net.i2p.I2PException; import net.i2p.client.I2PClient; +import net.i2p.client.I2PSession; import net.i2p.client.I2PSessionException; import net.i2p.crypto.SigType; import net.i2p.data.Base64; @@ -36,6 +38,7 @@ import net.i2p.data.DataHelper; import net.i2p.data.Destination; import net.i2p.util.Log; import net.i2p.util.I2PAppThread; +import net.i2p.util.PasswordManager; /** * Class able to handle a SAM version 3 client connection. @@ -50,12 +53,15 @@ class SAMv3Handler extends SAMv1Handler public static final SessionsDB sSessionsHash = new SessionsDB(); private volatile boolean stolenSocket; private volatile boolean streamForwardingSocket; - + private final boolean sendPorts; + private long _lastPing; + private static final int READ_TIMEOUT = 3*60*1000; interface Session { String getNick(); void close(); - boolean sendBytes(String dest, byte[] data) throws DataFormatException, I2PSessionException; + boolean sendBytes(String dest, byte[] data, int proto, + int fromPort, int toPort) throws DataFormatException, I2PSessionException; } /** @@ -88,6 +94,7 @@ class SAMv3Handler extends SAMv1Handler Properties i2cpProps, SAMBridge parent) throws SAMException, IOException { super(s, verMajor, verMinor, i2cpProps, parent); + sendPorts = (verMajor == 3 && verMinor >= 2) || verMajor > 3; if (_log.shouldLog(Log.DEBUG)) _log.debug("SAM version 3 handler instantiated"); } @@ -97,124 +104,6 @@ class SAMv3Handler extends SAMv1Handler { return (verMajor == 3); } - - public static class DatagramServer { - - private static DatagramServer _instance; - private static DatagramChannel server; - - public static DatagramServer getInstance() throws IOException { - return getInstance(new Properties()); - } - - public static DatagramServer getInstance(Properties props) throws IOException { - synchronized(DatagramServer.class) { - if (_instance==null) - _instance = new DatagramServer(props); - return _instance ; - } - } - - public DatagramServer(Properties props) throws IOException { - synchronized(DatagramServer.class) { - if (server==null) - server = DatagramChannel.open(); - } - - String host = props.getProperty(SAMBridge.PROP_DATAGRAM_HOST, SAMBridge.DEFAULT_DATAGRAM_HOST); - String portStr = props.getProperty(SAMBridge.PROP_DATAGRAM_PORT, SAMBridge.DEFAULT_DATAGRAM_PORT); - int port ; - try { - port = Integer.parseInt(portStr); - } catch (NumberFormatException e) { - port = Integer.parseInt(SAMBridge.DEFAULT_DATAGRAM_PORT); - } - - server.socket().bind(new InetSocketAddress(host, port)); - new I2PAppThread(new Listener(server), "DatagramListener").start(); - } - - public void send(SocketAddress addr, ByteBuffer msg) throws IOException { - server.send(msg, addr); - } - - static class Listener implements Runnable { - - private final DatagramChannel server; - - public Listener(DatagramChannel server) - { - this.server = server ; - } - public void run() - { - ByteBuffer inBuf = ByteBuffer.allocateDirect(SAMRawSession.RAW_SIZE_MAX+1024); - - while (!Thread.interrupted()) - { - inBuf.clear(); - try { - server.receive(inBuf); - } catch (IOException e) { - break ; - } - inBuf.flip(); - ByteBuffer outBuf = ByteBuffer.wrap(new byte[inBuf.remaining()]); - outBuf.put(inBuf); - outBuf.flip(); - // A new thread for every message is wildly inefficient... - //new I2PAppThread(new MessageDispatcher(outBuf.array()), "MessageDispatcher").start(); - // inline - // Even though we could be sending messages through multiple sessions, - // that isn't a common use case, and blocking should be rare. - // Inside router context, I2CP drops on overflow. - (new MessageDispatcher(outBuf.array())).run(); - } - } - } - } - - private static class MessageDispatcher implements Runnable - { - private final ByteArrayInputStream is; - - public MessageDispatcher(byte[] buf) - { - this.is = new java.io.ByteArrayInputStream(buf) ; - } - - public void run() { - try { - String header = DataHelper.readLine(is).trim(); - StringTokenizer tok = new StringTokenizer(header, " "); - if (tok.countTokens() != 3) { - // This is not a correct message, for sure - //_log.debug("Error in message format"); - // FIXME log? throw? - return; - } - String version = tok.nextToken(); - if (!"3.0".equals(version)) return ; - String nick = tok.nextToken(); - String dest = tok.nextToken(); - - byte[] data = new byte[is.available()]; - is.read(data); - SessionRecord rec = sSessionsHash.get(nick); - if (rec!=null) { - rec.getHandler().session.sendBytes(dest,data); - } else { - Log log = I2PAppContext.getGlobalContext().logManager().getLog(SAMv3Handler.class); - if (log.shouldLog(Log.WARN)) - log.warn("Dropping datagram, no session for " + nick); - } - } catch (Exception e) { - Log log = I2PAppContext.getGlobalContext().logManager().getLog(SAMv3Handler.class); - if (log.shouldLog(Log.WARN)) - log.warn("Error handling datagram", e); - } - } - } /** * The values in the SessionsDB @@ -342,6 +231,11 @@ class SAMv3Handler extends SAMv1Handler public void stealSocket() { stolenSocket = true ; + if (sendPorts) { + try { + socket.socket().setSoTimeout(0); + } catch (SocketException se) {} + } this.stopHandling(); } @@ -353,6 +247,16 @@ class SAMv3Handler extends SAMv1Handler return bridge; } + /** + * For SAMv3DatagramServer + * @return may be null + * @since 0.9.24 + */ + Session getSession() { + return session; + } + + @Override public void handle() { String msg = null; String domain = null; @@ -366,15 +270,72 @@ class SAMv3Handler extends SAMv1Handler _log.debug("SAMv3 handling started"); try { - InputStream in = getClientSocket().socket().getInputStream(); + Socket socket = getClientSocket().socket(); + InputStream in = socket.getInputStream(); + StringBuilder buf = new StringBuilder(1024); while (true) { if (shouldStop()) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Stop request found"); break; } - String line = DataHelper.readLine(in) ; + String line; + if (sendPorts) { + // client supports PING + try { + ReadLine.readLine(socket, buf, READ_TIMEOUT); + line = buf.toString(); + buf.setLength(0); + } catch (SocketTimeoutException ste) { + long now = System.currentTimeMillis(); + if (buf.length() <= 0) { + if (_lastPing > 0) { + if (now - _lastPing >= READ_TIMEOUT) { + if (_log.shouldWarn()) + _log.warn("Failed to respond to PING"); + writeString("SESSION STATUS RESULT=I2P_ERROR MESSAGE=\"PONG timeout\"\n"); + break; + } + } else { + if (_log.shouldDebug()) + _log.debug("Sendng PING " + now); + _lastPing = now; + if (!writeString("PING " + now + '\n')) + break; + } + } else { + if (_lastPing > 0) { + if (now - _lastPing >= 2*READ_TIMEOUT) { + if (_log.shouldWarn()) + _log.warn("Failed to respond to PING"); + writeString("SESSION STATUS RESULT=I2P_ERROR MESSAGE=\"PONG timeout\"\n"); + break; + } + } else if (_lastPing < 0) { + if (_log.shouldWarn()) + _log.warn("2nd timeout"); + writeString("SESSION STATUS RESULT=I2P_ERROR MESSAGE=\"command timeout, bye\"\n"); + break; + } else { + // don't clear buffer, don't send ping, + // go around again + _lastPing = -1; + if (_log.shouldWarn()) + _log.warn("timeout after partial: " + buf); + } + } + if (_log.shouldDebug()) + _log.debug("loop after timeout"); + continue; + } + } else { + buf.setLength(0); + if (DataHelper.readLine(in, buf)) + line = buf.toString(); + else + line = null; + } if (line==null) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Connection closed by client (line read : null)"); @@ -394,13 +355,33 @@ class SAMv3Handler extends SAMv1Handler } tok = new StringTokenizer(msg, " "); - if (tok.countTokens() < 2) { + int count = tok.countTokens(); + if (count <= 0) { // This is not a correct message, for sure if (_log.shouldLog(Log.DEBUG)) - _log.debug("Error in message format"); - break; + _log.debug("Ignoring whitespace"); + continue; } domain = tok.nextToken(); + // these may not have a second token + if (domain.equals("PING")) { + execPingMessage(tok); + continue; + } else if (domain.equals("PONG")) { + execPongMessage(tok); + continue; + } else if (domain.equals("QUIT") || domain.equals("STOP") || + domain.equals("EXIT")) { + writeString(domain + " STATUS RESULT=OK MESSAGE=bye\n"); + break; + } + if (count <= 1) { + // This is not a correct message, for sure + if (writeString(domain + " STATUS RESULT=I2P_ERROR MESSAGE=\"command not specified\"\n")) + continue; + else + break; + } opcode = tok.nextToken(); if (_log.shouldLog(Log.DEBUG)) { _log.debug("Parsing (domain: \"" + domain @@ -424,6 +405,8 @@ class SAMv3Handler extends SAMv1Handler } else if (domain.equals("RAW")) { // TODO not yet overridden, ID is ignored, most recent RAW session is used canContinue = execRawMessage(opcode, props); + } else if (domain.equals("AUTH")) { + canContinue = execAuthMessage(opcode, props); } else { if (_log.shouldLog(Log.DEBUG)) _log.debug("Unrecognized message domain: \"" @@ -434,12 +417,14 @@ class SAMv3Handler extends SAMv1Handler if (!canContinue) { break; } - } + } // while } catch (IOException e) { if (_log.shouldLog(Log.DEBUG)) - _log.debug("Caught IOException for message [" + msg + "]", e); - } catch (Exception e) { - _log.error("Unexpected exception for message [" + msg + "]", e); + _log.debug("Caught IOException in handler", e); + } catch (SAMException e) { + _log.error("Unexpected exception for message [" + msg + ']', e); + } catch (RuntimeException e) { + _log.error("Unexpected exception for message [" + msg + ']', e); } finally { if (_log.shouldLog(Log.DEBUG)) _log.debug("Stopping handler"); @@ -481,6 +466,8 @@ class SAMv3Handler extends SAMv1Handler */ @Override public void stopHandling() { + if (_log.shouldInfo()) + _log.info("Stopping (stolen? " + stolenSocket + "): " + this, new Exception("I did it")); synchronized (stopLock) { stopHandler = true; } @@ -609,13 +596,13 @@ class SAMv3Handler extends SAMv1Handler // Create the session if (style.equals("RAW")) { - DatagramServer.getInstance(i2cpProps); - SAMv3RawSession v3 = newSAMRawSession(nick); + SAMv3DatagramServer dgs = bridge.getV3DatagramServer(props); + SAMv3RawSession v3 = new SAMv3RawSession(nick, dgs); rawSession = v3; this.session = v3; } else if (style.equals("DATAGRAM")) { - DatagramServer.getInstance(i2cpProps); - SAMv3DatagramSession v3 = newSAMDatagramSession(nick); + SAMv3DatagramServer dgs = bridge.getV3DatagramServer(props); + SAMv3DatagramSession v3 = new SAMv3DatagramSession(nick, dgs); datagramSession = v3; this.session = v3; } else if (style.equals("STREAM")) { @@ -669,18 +656,6 @@ class SAMv3Handler extends SAMv1Handler return new SAMv3StreamSession( login ) ; } - private static SAMv3RawSession newSAMRawSession(String login ) - throws IOException, DataFormatException, SAMException, I2PSessionException - { - return new SAMv3RawSession( login ) ; - } - - private static SAMv3DatagramSession newSAMDatagramSession(String login ) - throws IOException, DataFormatException, SAMException, I2PSessionException - { - return new SAMv3DatagramSession( login ) ; - } - /* Parse and execute a STREAM message */ @Override protected boolean execStreamMessage ( String opcode, Properties props ) @@ -757,14 +732,16 @@ class SAMv3Handler extends SAMv1Handler @Override protected boolean execStreamConnect( Properties props) { + // Messages are NOT sent if SILENT=true, + // The specs said that they were. + boolean verbose = !Boolean.parseBoolean(props.getProperty("SILENT")); try { if (props.isEmpty()) { - notifyStreamResult(true,"I2P_ERROR","No parameters specified in STREAM CONNECT message"); + notifyStreamResult(verbose, "I2P_ERROR","No parameters specified in STREAM CONNECT message"); if (_log.shouldLog(Log.DEBUG)) _log.debug("No parameters specified in STREAM CONNECT message"); return false; } - boolean verbose = props.getProperty("SILENT","false").equals("false"); String dest = props.getProperty("DESTINATION"); if (dest == null) { @@ -804,11 +781,14 @@ class SAMv3Handler extends SAMv1Handler return false ; } - protected boolean execStreamForwardIncoming( Properties props ) { + private boolean execStreamForwardIncoming( Properties props ) { + // Messages ARE sent if SILENT=true, + // which is different from CONNECT and ACCEPT. + // But this matched the specs. try { try { streamForwardingSocket = true ; - ((SAMv3StreamSession)streamSession).startForwardingIncoming(props); + ((SAMv3StreamSession)streamSession).startForwardingIncoming(props, sendPorts); notifyStreamResult( true, "OK", null ); return true ; } catch (SAMException e) { @@ -821,9 +801,11 @@ class SAMv3Handler extends SAMv1Handler return false ; } - protected boolean execStreamAccept( Properties props ) + private boolean execStreamAccept( Properties props ) { - boolean verbose = props.getProperty( "SILENT", "false").equals("false"); + // Messages are NOT sent if SILENT=true, + // The specs said that they were. + boolean verbose = !Boolean.parseBoolean(props.getProperty("SILENT")); try { try { notifyStreamResult(verbose, "OK", null); @@ -858,13 +840,18 @@ class SAMv3Handler extends SAMv1Handler } } - public void notifyStreamIncomingConnection(Destination d) throws IOException { + public void notifyStreamIncomingConnection(Destination d, int fromPort, int toPort) throws IOException { if (getStreamSession() == null) { _log.error("BUG! Received stream connection, but session is null!"); throw new NullPointerException("BUG! STREAM session is null!"); } - - if (!writeString(d.toBase64() + "\n")) { + StringBuilder buf = new StringBuilder(600); + buf.append(d.toBase64()); + if (sendPorts) { + buf.append(" FROM_PORT=").append(fromPort).append(" TO_PORT=").append(toPort); + } + buf.append('\n'); + if (!writeString(buf.toString())) { throw new IOException("Error notifying connection to SAM client"); } } @@ -874,6 +861,91 @@ class SAMv3Handler extends SAMv1Handler throw new IOException("Error notifying connection to SAM client"); } } + + /** @since 0.9.24 */ + public static void notifyStreamIncomingConnection(SocketChannel client, Destination d, + int fromPort, int toPort) throws IOException { + if (!writeString(d.toBase64() + " FROM_PORT=" + fromPort + " TO_PORT=" + toPort + '\n', client)) { + throw new IOException("Error notifying connection to SAM client"); + } + } + /** @since 0.9.24 */ + private boolean execAuthMessage(String opcode, Properties props) { + if (opcode.equals("ENABLE")) { + i2cpProps.setProperty(SAMBridge.PROP_AUTH, "true"); + } else if (opcode.equals("DISABLE")) { + i2cpProps.setProperty(SAMBridge.PROP_AUTH, "false"); + } else if (opcode.equals("ADD")) { + String user = props.getProperty("USER"); + String pw = props.getProperty("PASSWORD"); + if (user == null || pw == null) + return writeString("AUTH STATUS RESULT=I2P_ERROR MESSAGE=\"USER and PASSWORD required\"\n"); + String prop = SAMBridge.PROP_PW_PREFIX + user + SAMBridge.PROP_PW_SUFFIX; + if (i2cpProps.containsKey(prop)) + return writeString("AUTH STATUS RESULT=I2P_ERROR MESSAGE=\"user " + user + " already exists\"\n"); + PasswordManager pm = new PasswordManager(I2PAppContext.getGlobalContext()); + String shash = pm.createHash(pw); + i2cpProps.setProperty(prop, shash); + } else if (opcode.equals("REMOVE")) { + String user = props.getProperty("USER"); + if (user == null) + return writeString("AUTH STATUS RESULT=I2P_ERROR MESSAGE=\"USER required\"\n"); + String prop = SAMBridge.PROP_PW_PREFIX + user + SAMBridge.PROP_PW_SUFFIX; + if (!i2cpProps.containsKey(prop)) + return writeString("AUTH STATUS RESULT=I2P_ERROR MESSAGE=\"user " + user + " not found\"\n"); + i2cpProps.remove(prop); + } else { + return writeString("AUTH STATUS RESULT=I2P_ERROR MESSAGE=\"Unknown AUTH command\"\n"); + } + try { + bridge.saveConfig(); + return writeString("AUTH STATUS RESULT=OK\n"); + } catch (IOException ioe) { + return writeString("AUTH STATUS RESULT=I2P_ERROR MESSAGE=\"Config save failed: " + ioe + "\"\n"); + } + } + + /** + * Handle a PING. + * Send a PONG. + * @since 0.9.24 + */ + private void execPingMessage(StringTokenizer tok) { + StringBuilder buf = new StringBuilder(); + buf.append("PONG"); + while (tok.hasMoreTokens()) { + buf.append(' ').append(tok.nextToken()); + } + buf.append('\n'); + writeString(buf.toString()); + } + + /** + * Handle a PONG. + * @since 0.9.24 + */ + private void execPongMessage(StringTokenizer tok) { + String s; + if (tok.hasMoreTokens()) { + s = tok.nextToken(); + } else { + s = ""; + } + if (_lastPing > 0) { + String expected = Long.toString(_lastPing); + if (expected.equals(s)) { + _lastPing = 0; + if (_log.shouldInfo()) + _log.warn("Got expected pong: " + s); + } else { + if (_log.shouldInfo()) + _log.warn("Got unexpected pong: " + s); + } + } else { + if (_log.shouldWarn()) + _log.warn("Pong received without a ping: " + s); + } + } } diff --git a/apps/sam/java/src/net/i2p/sam/SAMv3RawSession.java b/apps/sam/java/src/net/i2p/sam/SAMv3RawSession.java index 90eae76f9315f5e71bf4e7d1574bb1b332c78b1f..b68f3a74a151f7d5126b68e3be8234fc18906002 100644 --- a/apps/sam/java/src/net/i2p/sam/SAMv3RawSession.java +++ b/apps/sam/java/src/net/i2p/sam/SAMv3RawSession.java @@ -12,6 +12,7 @@ import java.util.Properties; import net.i2p.client.I2PSessionException; import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; import net.i2p.util.Log; /** @@ -22,8 +23,9 @@ class SAMv3RawSession extends SAMRawSession implements SAMv3Handler.Session, SA private final String nick; private final SAMv3Handler handler; - private final SAMv3Handler.DatagramServer server; + private final SAMv3DatagramServer server; private final SocketAddress clientAddress; + private final boolean _sendHeader; public String getNick() { return nick; } @@ -36,52 +38,57 @@ class SAMv3RawSession extends SAMRawSession implements SAMv3Handler.Session, SA * @throws DataFormatException * @throws I2PSessionException */ - public SAMv3RawSession(String nick) - throws IOException, DataFormatException, I2PSessionException { - + public SAMv3RawSession(String nick, SAMv3DatagramServer dgServer) + throws IOException, DataFormatException, I2PSessionException { super(SAMv3Handler.sSessionsHash.get(nick).getDest(), - SAMv3Handler.sSessionsHash.get(nick).getProps(), - SAMv3Handler.sSessionsHash.get(nick).getHandler() // to be replaced by this - ); + SAMv3Handler.sSessionsHash.get(nick).getProps(), + SAMv3Handler.sSessionsHash.get(nick).getHandler() // to be replaced by this + ); this.nick = nick ; this.recv = this ; // replacement - this.server = SAMv3Handler.DatagramServer.getInstance() ; + this.server = dgServer; SAMv3Handler.SessionRecord rec = SAMv3Handler.sSessionsHash.get(nick); - if ( rec==null ) throw new InterruptedIOException() ; - - this.handler = rec.getHandler(); - - Properties props = rec.getProps(); - - - String portStr = props.getProperty("PORT") ; - if ( portStr==null ) { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("receiver port not specified. Current socket will be used."); - this.clientAddress = null; - } - else { - int port = Integer.parseInt(portStr); - - String host = props.getProperty("HOST"); - if ( host==null ) { - host = rec.getHandler().getClientIP(); - + if (rec == null) + throw new InterruptedIOException() ; + this.handler = rec.getHandler(); + Properties props = rec.getProps(); + String portStr = props.getProperty("PORT") ; + if (portStr == null) { if (_log.shouldLog(Log.DEBUG)) - _log.debug("no host specified. Taken from the client socket : " + host +':'+port); - } - - - this.clientAddress = new InetSocketAddress(host,port); - } + _log.debug("receiver port not specified. Current socket will be used."); + this.clientAddress = null; + } else { + int port = Integer.parseInt(portStr); + String host = props.getProperty("HOST"); + if ( host==null ) { + host = rec.getHandler().getClientIP(); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("no host specified. Taken from the client socket : " + host +':'+port); + } + this.clientAddress = new InetSocketAddress(host, port); + } + _sendHeader = ((handler.verMajor == 3 && handler.verMinor >= 2) || handler.verMajor > 3) && + Boolean.parseBoolean(props.getProperty("HEADER")); } - public void receiveRawBytes(byte[] data) throws IOException { + public void receiveRawBytes(byte[] data, int proto, int fromPort, int toPort) throws IOException { if (this.clientAddress==null) { - this.handler.receiveRawBytes(data); + this.handler.receiveRawBytes(data, proto, fromPort, toPort); } else { - ByteBuffer msgBuf = ByteBuffer.allocate(data.length); + ByteBuffer msgBuf; + if (_sendHeader) { + StringBuilder buf = new StringBuilder(64); + buf.append("PROTOCOL=").append(proto) + .append(" FROM_PORT=").append(fromPort) + .append(" TO_PORT=").append(toPort) + .append('\n'); + String msg = buf.toString(); + msgBuf = ByteBuffer.allocate(msg.length()+data.length); + msgBuf.put(DataHelper.getASCII(msg)); + } else { + msgBuf = ByteBuffer.allocate(data.length); + } msgBuf.put(data); msgBuf.flip(); this.server.send(this.clientAddress, msgBuf); diff --git a/apps/sam/java/src/net/i2p/sam/SAMv3StreamSession.java b/apps/sam/java/src/net/i2p/sam/SAMv3StreamSession.java index 7770d4075e251436712742e20721df201e535ad8..54f58054f6b7e74e66bafed57f05ae60fb4dbe37 100644 --- a/apps/sam/java/src/net/i2p/sam/SAMv3StreamSession.java +++ b/apps/sam/java/src/net/i2p/sam/SAMv3StreamSession.java @@ -11,9 +11,22 @@ package net.i2p.sam; import java.io.IOException; import java.io.InterruptedIOException; import java.net.ConnectException; +import java.net.InetSocketAddress; import java.net.NoRouteToHostException; +import java.net.SocketTimeoutException; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.security.GeneralSecurityException; import java.util.Properties; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSocket; +import net.i2p.I2PAppContext; import net.i2p.I2PException; import net.i2p.client.streaming.I2PServerSocket; import net.i2p.client.streaming.I2PSocket; @@ -21,12 +34,8 @@ import net.i2p.client.streaming.I2PSocketOptions; import net.i2p.data.DataFormatException; import net.i2p.data.Destination; import net.i2p.util.I2PAppThread; +import net.i2p.util.I2PSSLSocketFactory; import net.i2p.util.Log; -import java.nio.channels.Channels; -import java.nio.channels.ReadableByteChannel; -import java.nio.channels.WritableByteChannel; -import java.nio.ByteBuffer; -import java.nio.channels.SocketChannel; /** * SAMv3 STREAM session class. @@ -40,7 +49,12 @@ class SAMv3StreamSession extends SAMStreamSession implements SAMv3Handler.Sessi private static final int BUFFER_SIZE = 1024 ; private final Object socketServerLock = new Object(); + /** this is ONLY set for FORWARD, not for ACCEPT */ private I2PServerSocket socketServer; + /** this is the count of active ACCEPT sockets */ + private final AtomicInteger _acceptors = new AtomicInteger(); + + private static I2PSSLSocketFactory _sslSocketFactory; private final String nick ; @@ -91,12 +105,28 @@ class SAMv3StreamSession extends SAMStreamSession implements SAMv3Handler.Sessi throws I2PException, ConnectException, NoRouteToHostException, DataFormatException, InterruptedIOException, IOException { - boolean verbose = (props.getProperty("SILENT", "false").equals("false")); + boolean verbose = !Boolean.parseBoolean(props.getProperty("SILENT")); Destination d = SAMUtils.getDest(dest); I2PSocketOptions opts = socketMgr.buildOptions(props); if (props.getProperty(I2PSocketOptions.PROP_CONNECT_TIMEOUT) == null) opts.setConnectTimeout(60 * 1000); + String fromPort = props.getProperty("FROM_PORT"); + if (fromPort != null) { + try { + opts.setLocalPort(Integer.parseInt(fromPort)); + } catch (NumberFormatException nfe) { + throw new I2PException("Bad port " + fromPort); + } + } + String toPort = props.getProperty("TO_PORT"); + if (toPort != null) { + try { + opts.setPort(Integer.parseInt(toPort)); + } catch (NumberFormatException nfe) { + throw new I2PException("Bad port " + toPort); + } + } if (_log.shouldLog(Log.DEBUG)) _log.debug("Connecting new I2PSocket..."); @@ -129,6 +159,8 @@ class SAMv3StreamSession extends SAMStreamSession implements SAMv3Handler.Sessi /** * Accept a single incoming STREAM on the socket stolen from the handler. + * As of version 3.2 (0.9.24), multiple simultaneous accepts are allowed. + * Accepts and forwarding may not be done at the same time. * * @param handler The handler that communicates with the requesting client * @param verbose If true, SAM will send the Base64-encoded peer Destination of an @@ -145,30 +177,30 @@ class SAMv3StreamSession extends SAMStreamSession implements SAMv3Handler.Sessi public void accept(SAMv3Handler handler, boolean verbose) throws I2PException, InterruptedIOException, IOException, SAMException { - synchronized( this.socketServerLock ) - { - if (this.socketServer!=null) { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("a socket server is already defined for this destination"); - throw new SAMException("a socket server is already defined for this destination"); - } - this.socketServer = this.socketMgr.getServerSocket(); - } - - I2PSocket i2ps = this.socketServer.accept(); + synchronized(this.socketServerLock) { + if (this.socketServer != null) { + if (_log.shouldWarn()) + _log.warn("a forwarding server is already defined for this destination"); + throw new SAMException("a forwarding server is already defined for this destination"); + } + } + + I2PSocket i2ps; + _acceptors.incrementAndGet(); + try { + i2ps = socketMgr.getServerSocket().accept(); + } finally { + _acceptors.decrementAndGet(); + } - synchronized( this.socketServerLock ) - { - this.socketServer = null ; - } - SAMv3Handler.SessionRecord rec = SAMv3Handler.sSessionsHash.get(nick); if ( rec==null || i2ps==null ) throw new InterruptedIOException() ; - if (verbose) - handler.notifyStreamIncomingConnection(i2ps.getPeerDestination()) ; - + if (verbose) { + handler.notifyStreamIncomingConnection(i2ps.getPeerDestination(), + i2ps.getPort(), i2ps.getLocalPort()); + } handler.stealSocket() ; ReadableByteChannel fromClient = handler.getClientSocket(); ReadableByteChannel fromI2P = Channels.newChannel(i2ps.getInputStream()); @@ -185,10 +217,14 @@ class SAMv3StreamSession extends SAMStreamSession implements SAMv3Handler.Sessi } - public void startForwardingIncoming( Properties props ) throws SAMException, InterruptedIOException + /** + * Forward sockets from I2P to the host/port provided. + * Accepts and forwarding may not be done at the same time. + */ + public void startForwardingIncoming(Properties props, boolean sendPorts) throws SAMException, InterruptedIOException { SAMv3Handler.SessionRecord rec = SAMv3Handler.sSessionsHash.get(nick); - boolean verbose = props.getProperty("SILENT", "false").equals("false"); + boolean verbose = !Boolean.parseBoolean(props.getProperty("SILENT")); if ( rec==null ) throw new InterruptedIOException() ; @@ -206,34 +242,43 @@ class SAMv3StreamSession extends SAMStreamSession implements SAMv3Handler.Sessi if (_log.shouldLog(Log.DEBUG)) _log.debug("no host specified. Taken from the client socket : " + host +':'+port); } - - - synchronized( this.socketServerLock ) - { - if (this.socketServer!=null) { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("a socket server is already defined for this destination"); - throw new SAMException("a socket server is already defined for this destination"); - } + boolean isSSL = Boolean.parseBoolean(props.getProperty("SSL")); + if (_acceptors.get() > 0) { + if (_log.shouldWarn()) + _log.warn("an accepting server is already defined for this destination"); + throw new SAMException("an accepting server is already defined for this destination"); + } + synchronized(this.socketServerLock) { + if (this.socketServer!=null) { + if (_log.shouldWarn()) + _log.warn("a forwarding server is already defined for this destination"); + throw new SAMException("a forwarding server is already defined for this destination"); + } this.socketServer = this.socketMgr.getServerSocket(); } - SocketForwarder forwarder = new SocketForwarder(host, port, this, verbose); + SocketForwarder forwarder = new SocketForwarder(host, port, isSSL, this, verbose, sendPorts); (new I2PAppThread(rec.getThreadGroup(), forwarder, "SAMV3StreamForwarder")).start(); } + /** + * Forward sockets from I2P to the host/port provided + */ private static class SocketForwarder implements Runnable { private final String host; private final int port; private final SAMv3StreamSession session; - private final boolean verbose; + private final boolean isSSL, verbose, sendPorts; - SocketForwarder(String host, int port, SAMv3StreamSession session, boolean verbose) { + SocketForwarder(String host, int port, boolean isSSL, + SAMv3StreamSession session, boolean verbose, boolean sendPorts) { this.host = host ; this.port = port ; this.session = session ; this.verbose = verbose ; + this.sendPorts = sendPorts; + this.isSSL = isSSL; } public void run() @@ -241,32 +286,77 @@ class SAMv3StreamSession extends SAMStreamSession implements SAMv3Handler.Sessi while (session.getSocketServer()!=null) { // wait and accept a connection from I2P side - I2PSocket i2ps = null ; + I2PSocket i2ps; try { i2ps = session.getSocketServer().accept(); - } catch (Exception e) {} - - if (i2ps==null) { - continue ; - } + if (i2ps == null) + continue; + } catch (SocketTimeoutException ste) { + continue; + } catch (ConnectException ce) { + Log log = I2PAppContext.getGlobalContext().logManager().getLog(SAMv3StreamSession.class); + if (log.shouldLog(Log.WARN)) + log.warn("Error accepting", ce); + try { Thread.sleep(50); } catch (InterruptedException ie) {} + continue; + } catch (I2PException ipe) { + Log log = I2PAppContext.getGlobalContext().logManager().getLog(SAMv3StreamSession.class); + if (log.shouldLog(Log.WARN)) + log.warn("Error accepting", ipe); + break; + } // open a socket towards client - java.net.InetSocketAddress addr = new java.net.InetSocketAddress(host,port); - SocketChannel clientServerSock = null ; + SocketChannel clientServerSock; try { - clientServerSock = SocketChannel.open(addr) ; - } - catch ( IOException e ) { - continue ; + if (isSSL) { + I2PAppContext ctx = I2PAppContext.getGlobalContext(); + synchronized(SAMv3StreamSession.class) { + if (_sslSocketFactory == null) { + try { + _sslSocketFactory = new I2PSSLSocketFactory( + ctx, true, "certificates/sam"); + } catch (GeneralSecurityException gse) { + Log log = ctx.logManager().getLog(SAMv3StreamSession.class); + log.error("SSL error", gse); + try { + i2ps.close(); + } catch (IOException ee) {} + throw new RuntimeException("SSL error", gse); + } + } + } + SSLSocket sock = (SSLSocket) _sslSocketFactory.createSocket(host, port); + I2PSSLSocketFactory.verifyHostname(ctx, sock, host); + clientServerSock = new SSLSocketChannel(sock); + } else { + InetSocketAddress addr = new InetSocketAddress(host, port); + clientServerSock = SocketChannel.open(addr) ; + } + } catch (IOException ioe) { + Log log = I2PAppContext.getGlobalContext().logManager().getLog(SAMv3StreamSession.class); + if (log.shouldLog(Log.WARN)) + log.warn("Error forwarding", ioe); + try { + i2ps.close(); + } catch (IOException ee) {} + continue; } // build pipes between both sockets try { clientServerSock.socket().setKeepAlive(true); - if (this.verbose) - SAMv3Handler.notifyStreamIncomingConnection( + if (this.verbose) { + if (sendPorts) { + SAMv3Handler.notifyStreamIncomingConnection( + clientServerSock, i2ps.getPeerDestination(), + i2ps.getPort(), i2ps.getLocalPort()); + } else { + SAMv3Handler.notifyStreamIncomingConnection( clientServerSock, i2ps.getPeerDestination()); + } + } ReadableByteChannel fromClient = clientServerSock ; ReadableByteChannel fromI2P = Channels.newChannel(i2ps.getInputStream()); WritableByteChannel toClient = clientServerSock ; @@ -347,7 +437,7 @@ class SAMv3StreamSession extends SAMStreamSession implements SAMv3Handler.Sessi } } - public I2PServerSocket getSocketServer() + private I2PServerSocket getSocketServer() { synchronized ( this.socketServerLock ) { return this.socketServer ; @@ -390,7 +480,11 @@ class SAMv3StreamSession extends SAMStreamSession implements SAMv3Handler.Sessi socketMgr.destroySocketManager(); } - public boolean sendBytes(String s, byte[] b) throws DataFormatException + /** + * Unsupported + * @throws DataFormatException always + */ + public boolean sendBytes(String s, byte[] b, int pr, int fp, int tp) throws DataFormatException { throw new DataFormatException(null); } diff --git a/apps/sam/java/src/net/i2p/sam/SSLServerSocketChannel.java b/apps/sam/java/src/net/i2p/sam/SSLServerSocketChannel.java new file mode 100644 index 0000000000000000000000000000000000000000..546955ceefb56375b2f49c49fe666164ba2b40e8 --- /dev/null +++ b/apps/sam/java/src/net/i2p/sam/SSLServerSocketChannel.java @@ -0,0 +1,77 @@ +package net.i2p.sam; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.SocketAddress; +/* requires Java 7 */ +import java.net.SocketOption; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.spi.SelectorProvider; +import java.util.Collections; +import java.util.Set; + +import javax.net.ssl.SSLServerSocket; +import javax.net.ssl.SSLSocket; + +/** + * Simple wrapper for a SSLServerSocket. + * Cannot be used for asynch ops. + * + * @since 0.9.24 + */ +class SSLServerSocketChannel extends ServerSocketChannel { + + private final SSLServerSocket _socket; + + public SSLServerSocketChannel(SSLServerSocket socket) { + super(SelectorProvider.provider()); + _socket = socket; + } + + //// ServerSocketChannel abstract methods + + public SocketChannel accept() throws IOException { + return new SSLSocketChannel((SSLSocket)_socket.accept()); + } + + public ServerSocket socket() { + return _socket; + } + + /** requires Java 7 */ + public ServerSocketChannel bind(SocketAddress local, int backlog) { + throw new UnsupportedOperationException(); + } + + /** requires Java 7 */ + public <T> ServerSocketChannel setOption(SocketOption<T> name, T value) { + return this; + } + + //// AbstractSelectableChannel abstract methods + + public void implCloseSelectableChannel() throws IOException { + _socket.close(); + } + + public void implConfigureBlocking(boolean block) throws IOException { + if (!block) + throw new UnsupportedOperationException(); + } + + //// NetworkChannel interface methods + + public SocketAddress getLocalAddress() { + return _socket.getLocalSocketAddress(); + } + + public <T> T getOption(SocketOption<T> name) { + return null; + } + + public Set<SocketOption<?>> supportedOptions() { + return Collections.emptySet(); + } +} diff --git a/apps/sam/java/src/net/i2p/sam/SSLSocketChannel.java b/apps/sam/java/src/net/i2p/sam/SSLSocketChannel.java new file mode 100644 index 0000000000000000000000000000000000000000..7b956a16e3d1381c388801e2d288e0dc2db676f6 --- /dev/null +++ b/apps/sam/java/src/net/i2p/sam/SSLSocketChannel.java @@ -0,0 +1,140 @@ +package net.i2p.sam; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketAddress; +/* requires Java 7 */ +import java.net.SocketOption; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.nio.channels.spi.SelectorProvider; +import java.util.Collections; +import java.util.Set; + +import javax.net.ssl.SSLSocket; + +/** + * Simple wrapper for a SSLSocket. + * Cannot be used for asynch ops. + * + * @since 0.9.24 + */ +class SSLSocketChannel extends SocketChannel { + + private final SSLSocket _socket; + + public SSLSocketChannel(SSLSocket socket) { + super(SelectorProvider.provider()); + _socket = socket; + } + + //// SocketChannel abstract methods + + public Socket socket() { + return _socket; + } + + public boolean connect(SocketAddress remote) { + throw new UnsupportedOperationException(); + } + + public boolean finishConnect() { + return true; + } + + public boolean isConnected() { + return _socket.isConnected(); + } + + public boolean isConnectionPending() { + return false; + } + + /** new in Java 7 */ + public SocketAddress getRemoteAddress() { + return _socket.getRemoteSocketAddress(); + } + + /** new in Java 7 */ + public SocketChannel shutdownInput() throws IOException { + _socket.getInputStream().close(); + return this; + } + + /** new in Java 7 */ + public SocketChannel shutdownOutput() throws IOException { + _socket.getOutputStream().close(); + return this; + } + + /** requires Java 7 */ + public <T> SocketChannel setOption(SocketOption<T> name, T value) { + return this; + } + + /** requires Java 7 */ + public SocketChannel bind(SocketAddress local) { + throw new UnsupportedOperationException(); + } + + //// SocketChannel abstract methods + + public int read(ByteBuffer src) throws IOException { + if (!src.hasArray()) + throw new UnsupportedOperationException(); + int pos = src.position(); + int len = src.remaining(); + int read = _socket.getInputStream().read(src.array(), src.arrayOffset() + pos, len); + if (read > 0) + src.position(pos + read); + return read; + } + + public long read(ByteBuffer[] srcs, int offset, int length) { + throw new UnsupportedOperationException(); + } + + public int write(ByteBuffer src) throws IOException { + if (!src.hasArray()) + throw new UnsupportedOperationException(); + int pos = src.position(); + int len = src.remaining(); + _socket.getOutputStream().write(src.array(), src.arrayOffset() + pos, len); + src.position(pos + len); + return len; + } + + public long write(ByteBuffer[] srcs, int offset, int length) { + throw new UnsupportedOperationException(); + } + + //// AbstractSelectableChannel abstract methods + + public void implCloseSelectableChannel() throws IOException { + _socket.close(); + } + + public void implConfigureBlocking(boolean block) throws IOException { + if (!block) + throw new UnsupportedOperationException(); + } + + + //// NetworkChannel interface methods + + public SocketAddress getLocalAddress() { + return _socket.getLocalSocketAddress(); + } + + public <T> T getOption(SocketOption<T> name) { + return null; + } + + public Set<SocketOption<?>> supportedOptions() { + return Collections.emptySet(); + } + + + +} diff --git a/apps/sam/java/src/net/i2p/sam/SSLUtil.java b/apps/sam/java/src/net/i2p/sam/SSLUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..bf4ebf1e42b3fa1b80f64295f38d4d629193266f --- /dev/null +++ b/apps/sam/java/src/net/i2p/sam/SSLUtil.java @@ -0,0 +1,188 @@ +package net.i2p.sam; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyStore; +import java.security.GeneralSecurityException; +import java.util.Properties; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLServerSocketFactory; +import javax.net.ssl.SSLContext; + +import net.i2p.I2PAppContext; +import net.i2p.crypto.KeyStoreUtil; +import net.i2p.util.Log; +import net.i2p.util.SecureDirectory; + +/** + * Utilities for SAM SSL server sockets. + * + * @since 0.9.24 adopted from net.i2p.i2ptunnel.SSLClientUtil + */ +class SSLUtil { + + private static final String PROP_KEYSTORE_PASSWORD = "sam.keystorePassword"; + private static final String DEFAULT_KEYSTORE_PASSWORD = "changeit"; + private static final String PROP_KEY_PASSWORD = "sam.keyPassword"; + private static final String PROP_KEY_ALIAS = "sam.keyAlias"; + private static final String ASCII_KEYFILE_SUFFIX = ".local.crt"; + private static final String PROP_KS_NAME = "sam.keystoreFile"; + private static final String KS_DIR = "keystore"; + private static final String PREFIX = "sam-"; + private static final String KS_SUFFIX = ".ks"; + private static final String CERT_DIR = "certificates/sam"; + + /** + * Create a new selfsigned cert and keystore and pubkey cert if they don't exist. + * May take a while. + * + * @param opts in/out, updated if rv is true + * @return false if it already exists; if true, caller must save opts + * @throws IOException on creation fail + */ + public static boolean verifyKeyStore(Properties opts) throws IOException { + String name = opts.getProperty(PROP_KEY_ALIAS); + if (name == null) { + name = KeyStoreUtil.randomString(); + opts.setProperty(PROP_KEY_ALIAS, name); + } + String ksname = opts.getProperty(PROP_KS_NAME); + if (ksname == null) { + ksname = PREFIX + name + KS_SUFFIX; + opts.setProperty(PROP_KS_NAME, ksname); + } + File ks = new File(ksname); + if (!ks.isAbsolute()) { + ks = new File(I2PAppContext.getGlobalContext().getConfigDir(), KS_DIR); + ks = new File(ks, ksname); + } + if (ks.exists()) + return false; + File dir = ks.getParentFile(); + if (!dir.exists()) { + File sdir = new SecureDirectory(dir.getAbsolutePath()); + if (!sdir.mkdirs()) + throw new IOException("Unable to create keystore " + ks); + } + boolean rv = createKeyStore(ks, name, opts); + if (!rv) + throw new IOException("Unable to create keystore " + ks); + + // Now read it back out of the new keystore and save it in ascii form + // where the clients can get to it. + // Failure of this part is not fatal. + exportCert(ks, name, opts); + return true; + } + + + /** + * Call out to keytool to create a new keystore with a keypair in it. + * + * @param name used in CNAME + * @param opts in/out, updated if rv is true, must contain PROP_KEY_ALIAS + * @return success, if true, opts will have password properties added to be saved + */ + private static boolean createKeyStore(File ks, String name, Properties opts) { + // make a random 48 character password (30 * 8 / 5) + String keyPassword = KeyStoreUtil.randomString(); + // and one for the cname + String cname = name + ".sam.i2p.net"; + + String keyName = opts.getProperty(PROP_KEY_ALIAS); + boolean success = KeyStoreUtil.createKeys(ks, keyName, cname, "SAM", keyPassword); + if (success) { + success = ks.exists(); + if (success) { + opts.setProperty(PROP_KEYSTORE_PASSWORD, DEFAULT_KEYSTORE_PASSWORD); + opts.setProperty(PROP_KEY_PASSWORD, keyPassword); + } + } + if (success) { + logAlways("Created self-signed certificate for " + cname + " in keystore: " + ks.getAbsolutePath() + "\n" + + "The certificate name was generated randomly, and is not associated with your " + + "IP address, host name, router identity, or destination keys."); + } else { + error("Failed to create SAM SSL keystore.\n" + + "If you create the keystore manually, you must add " + PROP_KEYSTORE_PASSWORD + " and " + PROP_KEY_PASSWORD + + " to " + (new File(I2PAppContext.getGlobalContext().getConfigDir(), SAMBridge.DEFAULT_SAM_CONFIGFILE)).getAbsolutePath()); + } + return success; + } + + /** + * Pull the cert back OUT of the keystore and save it as ascii + * so the clients can get to it. + * + * @param name used to generate output file name + * @param opts must contain PROP_KEY_ALIAS + */ + private static void exportCert(File ks, String name, Properties opts) { + File sdir = new SecureDirectory(I2PAppContext.getGlobalContext().getConfigDir(), CERT_DIR); + if (sdir.exists() || sdir.mkdirs()) { + String keyAlias = opts.getProperty(PROP_KEY_ALIAS); + String ksPass = opts.getProperty(PROP_KEYSTORE_PASSWORD, DEFAULT_KEYSTORE_PASSWORD); + File out = new File(sdir, PREFIX + name + ASCII_KEYFILE_SUFFIX); + boolean success = KeyStoreUtil.exportCert(ks, ksPass, keyAlias, out); + if (!success) + error("Error getting SSL cert to save as ASCII"); + } else { + error("Error saving ASCII SSL keys"); + } + } + + /** + * Sets up the SSLContext and sets the socket factory. + * No option prefix allowed. + * + * @throws IOException; GeneralSecurityExceptions are wrapped in IOE for convenience + * @return factory, throws on all errors + */ + public static SSLServerSocketFactory initializeFactory(Properties opts) throws IOException { + String ksPass = opts.getProperty(PROP_KEYSTORE_PASSWORD, DEFAULT_KEYSTORE_PASSWORD); + String keyPass = opts.getProperty(PROP_KEY_PASSWORD); + if (keyPass == null) { + throw new IOException("No key password, set " + PROP_KEY_PASSWORD + " in " + + (new File(I2PAppContext.getGlobalContext().getConfigDir(), SAMBridge.DEFAULT_SAM_CONFIGFILE)).getAbsolutePath()); + } + String ksname = opts.getProperty(PROP_KS_NAME); + if (ksname == null) { + throw new IOException("No keystore, set " + PROP_KS_NAME + " in " + + (new File(I2PAppContext.getGlobalContext().getConfigDir(), SAMBridge.DEFAULT_SAM_CONFIGFILE)).getAbsolutePath()); + } + File ks = new File(ksname); + if (!ks.isAbsolute()) { + ks = new File(I2PAppContext.getGlobalContext().getConfigDir(), KS_DIR); + ks = new File(ks, ksname); + } + + InputStream fis = null; + try { + SSLContext sslc = SSLContext.getInstance("TLS"); + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + fis = new FileInputStream(ks); + keyStore.load(fis, ksPass.toCharArray()); + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(keyStore, keyPass.toCharArray()); + sslc.init(kmf.getKeyManagers(), null, I2PAppContext.getGlobalContext().random()); + return sslc.getServerSocketFactory(); + } catch (GeneralSecurityException gse) { + IOException ioe = new IOException("keystore error"); + ioe.initCause(gse); + throw ioe; + } finally { + if (fis != null) try { fis.close(); } catch (IOException ioe) {} + } + } + + private static void error(String s) { + I2PAppContext.getGlobalContext().logManager().getLog(SSLUtil.class).error(s); + } + + private static void logAlways(String s) { + I2PAppContext.getGlobalContext().logManager().getLog(SSLUtil.class).logAlways(Log.INFO, s); + } +} diff --git a/apps/sam/java/src/net/i2p/sam/client/SAMClientEventListenerImpl.java b/apps/sam/java/src/net/i2p/sam/client/SAMClientEventListenerImpl.java index 5388f5dc2336f831aca586628ca1a6105553bcd2..1b7cdc99adffc516863998a11943d2bfc297d59c 100644 --- a/apps/sam/java/src/net/i2p/sam/client/SAMClientEventListenerImpl.java +++ b/apps/sam/java/src/net/i2p/sam/client/SAMClientEventListenerImpl.java @@ -7,12 +7,16 @@ import java.util.Properties; */ public class SAMClientEventListenerImpl implements SAMReader.SAMClientEventListener { public void destReplyReceived(String publicKey, String privateKey) {} - public void helloReplyReceived(boolean ok) {} + public void helloReplyReceived(boolean ok, String version) {} public void namingReplyReceived(String name, String result, String value, String message) {} public void sessionStatusReceived(String result, String destination, String message) {} - public void streamClosedReceived(String result, int id, String message) {} - public void streamConnectedReceived(String remoteDestination, int id) {} - public void streamDataReceived(int id, byte[] data, int offset, int length) {} - public void streamStatusReceived(String result, int id, String message) {} + public void streamClosedReceived(String result, String id, String message) {} + public void streamConnectedReceived(String remoteDestination, String id) {} + public void streamDataReceived(String id, byte[] data, int offset, int length) {} + public void streamStatusReceived(String result, String id, String message) {} + public void datagramReceived(String dest, byte[] data, int offset, int length, int fromPort, int toPort) {} + public void rawReceived(byte[] data, int offset, int length, int fromPort, int toPort, int protocol) {} + public void pingReceived(String data) {} + public void pongReceived(String data) {} public void unknownMessageReceived(String major, String minor, Properties params) {} } diff --git a/apps/sam/java/src/net/i2p/sam/client/SAMEventHandler.java b/apps/sam/java/src/net/i2p/sam/client/SAMEventHandler.java index 2d1a5b63b90a770b227e132a5e6c9cb2968e21e1..dc90d3fb2423dd31688e7e7cbcef8a0411158617 100644 --- a/apps/sam/java/src/net/i2p/sam/client/SAMEventHandler.java +++ b/apps/sam/java/src/net/i2p/sam/client/SAMEventHandler.java @@ -13,31 +13,35 @@ import net.i2p.util.Log; */ public class SAMEventHandler extends SAMClientEventListenerImpl { //private I2PAppContext _context; - private Log _log; + private final Log _log; private Boolean _helloOk; - private Object _helloLock = new Object(); + private String _version; + private final Object _helloLock = new Object(); private Boolean _sessionCreateOk; - private Object _sessionCreateLock = new Object(); - private Object _namingReplyLock = new Object(); - private Map<String,String> _namingReplies = new HashMap<String,String>(); + private Boolean _streamStatusOk; + private final Object _sessionCreateLock = new Object(); + private final Object _namingReplyLock = new Object(); + private final Object _streamStatusLock = new Object(); + private final Map<String,String> _namingReplies = new HashMap<String,String>(); public SAMEventHandler(I2PAppContext ctx) { //_context = ctx; _log = ctx.logManager().getLog(getClass()); } - @Override - public void helloReplyReceived(boolean ok) { + @Override + public void helloReplyReceived(boolean ok, String version) { synchronized (_helloLock) { if (ok) _helloOk = Boolean.TRUE; else _helloOk = Boolean.FALSE; + _version = version; _helloLock.notifyAll(); } } - @Override + @Override public void sessionStatusReceived(String result, String destination, String msg) { synchronized (_sessionCreateLock) { if (SAMReader.SAMClientEventListener.SESSION_STATUS_OK.equals(result)) @@ -48,7 +52,7 @@ public class SAMEventHandler extends SAMClientEventListenerImpl { } } - @Override + @Override public void namingReplyReceived(String name, String result, String value, String msg) { synchronized (_namingReplyLock) { if (SAMReader.SAMClientEventListener.NAMING_REPLY_OK.equals(result)) @@ -59,9 +63,20 @@ public class SAMEventHandler extends SAMClientEventListenerImpl { } } - @Override + @Override + public void streamStatusReceived(String result, String id, String message) { + synchronized (_streamStatusLock) { + if (SAMReader.SAMClientEventListener.SESSION_STATUS_OK.equals(result)) + _streamStatusOk = Boolean.TRUE; + else + _streamStatusOk = Boolean.FALSE; + _streamStatusLock.notifyAll(); + } + } + + @Override public void unknownMessageReceived(String major, String minor, Properties params) { - _log.error("wrt, [" + major + "] [" + minor + "] [" + params + "]"); + _log.error("Unhandled message: [" + major + "] [" + minor + "] [" + params + "]"); } @@ -70,20 +85,20 @@ public class SAMEventHandler extends SAMClientEventListenerImpl { // /** - * Wait for the connection to be established, returning true if everything + * Wait for the connection to be established, returning the server version if everything * went ok - * @return true if everything ok + * @return SAM server version if everything ok, or null on failure */ - public boolean waitForHelloReply() { + public String waitForHelloReply() { while (true) { try { synchronized (_helloLock) { if (_helloOk == null) _helloLock.wait(); else - return _helloOk.booleanValue(); + return _helloOk.booleanValue() ? _version : null; } - } catch (InterruptedException ie) {} + } catch (InterruptedException ie) { return null; } } } @@ -101,7 +116,25 @@ public class SAMEventHandler extends SAMClientEventListenerImpl { else return _sessionCreateOk.booleanValue(); } - } catch (InterruptedException ie) {} + } catch (InterruptedException ie) { return false; } + } + } + + /** + * Wait for the stream to be created, returning true if everything went ok + * + * @return true if everything ok + */ + public boolean waitForStreamStatusReply() { + while (true) { + try { + synchronized (_streamStatusLock) { + if (_streamStatusOk == null) + _streamStatusLock.wait(); + else + return _streamStatusOk.booleanValue(); + } + } catch (InterruptedException ie) { return false; } } } @@ -128,7 +161,7 @@ public class SAMEventHandler extends SAMClientEventListenerImpl { return val; } } - } catch (InterruptedException ie) {} + } catch (InterruptedException ie) { return null; } } } } diff --git a/apps/sam/java/src/net/i2p/sam/client/SAMReader.java b/apps/sam/java/src/net/i2p/sam/client/SAMReader.java index f39394901754eae7c1dbbb8cb6453a8722b84866..c82dff7219ea995ff26686cf284adce8d84d11d1 100644 --- a/apps/sam/java/src/net/i2p/sam/client/SAMReader.java +++ b/apps/sam/java/src/net/i2p/sam/client/SAMReader.java @@ -16,10 +16,11 @@ import net.i2p.util.Log; * */ public class SAMReader { - private Log _log; - private InputStream _inRaw; - private SAMClientEventListener _listener; - private boolean _live; + private final Log _log; + private final InputStream _inRaw; + private final SAMClientEventListener _listener; + private volatile boolean _live; + private Thread _thread; public SAMReader(I2PAppContext context, InputStream samIn, SAMClientEventListener listener) { _log = context.logManager().getLog(SAMReader.class); @@ -27,12 +28,23 @@ public class SAMReader { _listener = listener; } - public void startReading() { + public synchronized void startReading() { + if (_live) + throw new IllegalStateException(); _live = true; I2PAppThread t = new I2PAppThread(new Runner(), "SAM reader"); t.start(); + _thread = t; + } + + public synchronized void stopReading() { + _live = false; + if (_thread != null) { + _thread.interrupt(); + _thread = null; + try { _inRaw.close(); } catch (IOException ioe) {} + } } - public void stopReading() { _live = false; } /** * Async event notification interface for SAM clients @@ -60,14 +72,18 @@ public class SAMReader { public static final String NAMING_REPLY_INVALID_KEY = "INVALID_KEY"; public static final String NAMING_REPLY_KEY_NOT_FOUND = "KEY_NOT_FOUND"; - public void helloReplyReceived(boolean ok); + public void helloReplyReceived(boolean ok, String version); public void sessionStatusReceived(String result, String destination, String message); - public void streamStatusReceived(String result, int id, String message); - public void streamConnectedReceived(String remoteDestination, int id); - public void streamClosedReceived(String result, int id, String message); - public void streamDataReceived(int id, byte data[], int offset, int length); + public void streamStatusReceived(String result, String id, String message); + public void streamConnectedReceived(String remoteDestination, String id); + public void streamClosedReceived(String result, String id, String message); + public void streamDataReceived(String id, byte data[], int offset, int length); public void namingReplyReceived(String name, String result, String value, String message); public void destReplyReceived(String publicKey, String privateKey); + public void datagramReceived(String dest, byte[] data, int offset, int length, int fromPort, int toPort); + public void rawReceived(byte[] data, int offset, int length, int fromPort, int toPort, int protocol); + public void pingReceived(String data); + public void pongReceived(String data); public void unknownMessageReceived(String major, String minor, Properties params); } @@ -87,33 +103,29 @@ public class SAMReader { baos.write(c); } if (c == -1) { - _log.error("Error reading from the SAM bridge"); - return; + _log.info("EOF reading from the SAM bridge"); + break; } } catch (IOException ioe) { _log.error("Error reading from SAM", ioe); + break; } String line = DataHelper.getUTF8(baos.toByteArray()); baos.reset(); - if (line == null) { - _log.info("No more data from the SAM bridge"); - break; - } - - _log.debug("Line read from the bridge: " + line); + if (_log.shouldDebug()) + _log.debug("Line read from the bridge: " + line); StringTokenizer tok = new StringTokenizer(line); - if (tok.countTokens() < 2) { + if (tok.countTokens() <= 0) { _log.error("Invalid SAM line: [" + line + "]"); - _live = false; - return; + break; } String major = tok.nextToken(); - String minor = tok.nextToken(); + String minor = tok.hasMoreTokens() ? tok.nextToken() : ""; params.clear(); while (tok.hasMoreTokens()) { @@ -132,6 +144,9 @@ public class SAMReader { processEvent(major, minor, params); } + _live = false; + if (_log.shouldWarn()) + _log.warn("SAMReader exiting"); } } @@ -144,10 +159,11 @@ public class SAMReader { if ("HELLO".equals(major)) { if ("REPLY".equals(minor)) { String result = params.getProperty("RESULT"); - if ("OK".equals(result)) - _listener.helloReplyReceived(true); + String version= params.getProperty("VERSION"); + if ("OK".equals(result) && version != null) + _listener.helloReplyReceived(true, version); else - _listener.helloReplyReceived(false); + _listener.helloReplyReceived(false, version); } else { _listener.unknownMessageReceived(major, minor, params); } @@ -165,24 +181,17 @@ public class SAMReader { String result = params.getProperty("RESULT"); String id = params.getProperty("ID"); String msg = params.getProperty("MESSAGE"); - if (id != null) { - try { - _listener.streamStatusReceived(result, Integer.parseInt(id), msg); - } catch (NumberFormatException nfe) { - _listener.unknownMessageReceived(major, minor, params); - } - } else { - _listener.unknownMessageReceived(major, minor, params); - } + // id is null in v3, so pass it through regardless + //if (id != null) { + _listener.streamStatusReceived(result, id, msg); + //} else { + // _listener.unknownMessageReceived(major, minor, params); + //} } else if ("CONNECTED".equals(minor)) { String dest = params.getProperty("DESTINATION"); String id = params.getProperty("ID"); if (id != null) { - try { - _listener.streamConnectedReceived(dest, Integer.parseInt(id)); - } catch (NumberFormatException nfe) { - _listener.unknownMessageReceived(major, minor, params); - } + _listener.streamConnectedReceived(dest, id); } else { _listener.unknownMessageReceived(major, minor, params); } @@ -191,11 +200,7 @@ public class SAMReader { String id = params.getProperty("ID"); String msg = params.getProperty("MESSAGE"); if (id != null) { - try { - _listener.streamClosedReceived(result, Integer.parseInt(id), msg); - } catch (NumberFormatException nfe) { - _listener.unknownMessageReceived(major, minor, params); - } + _listener.streamClosedReceived(result, id, msg); } else { _listener.unknownMessageReceived(major, minor, params); } @@ -204,7 +209,6 @@ public class SAMReader { String size = params.getProperty("SIZE"); if (id != null) { try { - int idVal = Integer.parseInt(id); int sizeVal = Integer.parseInt(size); byte data[] = new byte[sizeVal]; @@ -212,7 +216,7 @@ public class SAMReader { if (read != sizeVal) { _listener.unknownMessageReceived(major, minor, params); } else { - _listener.streamDataReceived(idVal, data, 0, sizeVal); + _listener.streamDataReceived(id, data, 0, sizeVal); } } catch (NumberFormatException nfe) { _listener.unknownMessageReceived(major, minor, params); @@ -226,6 +230,73 @@ public class SAMReader { } else { _listener.unknownMessageReceived(major, minor, params); } + } else if ("DATAGRAM".equals(major)) { + if ("RECEIVED".equals(minor)) { + String dest = params.getProperty("DESTINATION"); + String size = params.getProperty("SIZE"); + String fp = params.getProperty("FROM_PORT"); + String tp = params.getProperty("TO_PORT"); + int fromPort = 0; + int toPort = 0; + if (dest != null) { + try { + if (fp != null) + fromPort = Integer.parseInt(fp); + if (tp != null) + toPort = Integer.parseInt(tp); + int sizeVal = Integer.parseInt(size); + byte data[] = new byte[sizeVal]; + int read = DataHelper.read(_inRaw, data); + if (read != sizeVal) { + _listener.unknownMessageReceived(major, minor, params); + } else { + _listener.datagramReceived(dest, data, 0, sizeVal, fromPort, toPort); + } + } catch (NumberFormatException nfe) { + _listener.unknownMessageReceived(major, minor, params); + } catch (IOException ioe) { + _live = false; + _listener.unknownMessageReceived(major, minor, params); + } + } else { + _listener.unknownMessageReceived(major, minor, params); + } + } else { + _listener.unknownMessageReceived(major, minor, params); + } + } else if ("RAW".equals(major)) { + if ("RECEIVED".equals(minor)) { + String size = params.getProperty("SIZE"); + String fp = params.getProperty("FROM_PORT"); + String tp = params.getProperty("TO_PORT"); + String pr = params.getProperty("PROTOCOL"); + int fromPort = 0; + int toPort = 0; + int protocol = 18; + try { + if (fp != null) + fromPort = Integer.parseInt(fp); + if (tp != null) + toPort = Integer.parseInt(tp); + if (pr != null) + protocol = Integer.parseInt(pr); + int sizeVal = Integer.parseInt(size); + byte data[] = new byte[sizeVal]; + int read = DataHelper.read(_inRaw, data); + if (read != sizeVal) { + _listener.unknownMessageReceived(major, minor, params); + } else { + _listener.rawReceived(data, 0, sizeVal, fromPort, toPort, protocol); + } + } catch (NumberFormatException nfe) { + _listener.unknownMessageReceived(major, minor, params); + } catch (IOException ioe) { + _live = false; + _listener.unknownMessageReceived(major, minor, params); + } + } else { + _listener.unknownMessageReceived(major, minor, params); + } } else if ("NAMING".equals(major)) { if ("REPLY".equals(minor)) { String name = params.getProperty("NAME"); @@ -244,6 +315,12 @@ public class SAMReader { } else { _listener.unknownMessageReceived(major, minor, params); } + } else if ("PING".equals(major)) { + // this omits anything after a space + _listener.pingReceived(minor); + } else if ("PONG".equals(major)) { + // this omits anything after a space + _listener.pongReceived(minor); } else { _listener.unknownMessageReceived(major, minor, params); } diff --git a/apps/sam/java/src/net/i2p/sam/client/SAMStreamSend.java b/apps/sam/java/src/net/i2p/sam/client/SAMStreamSend.java index 2ce8b3b02f81f86ee9a48c9f5dd2dbf290559a0c..d8f7432086fcfd53dbce375a8322f4640cd411df 100644 --- a/apps/sam/java/src/net/i2p/sam/client/SAMStreamSend.java +++ b/apps/sam/java/src/net/i2p/sam/client/SAMStreamSend.java @@ -1,50 +1,141 @@ package net.i2p.sam.client; +import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetSocketAddress; import java.net.Socket; +import java.security.GeneralSecurityException; import java.util.HashMap; import java.util.Map; +import javax.net.ssl.SSLSocket; + +import gnu.getopt.Getopt; import net.i2p.I2PAppContext; +import net.i2p.data.Base32; import net.i2p.data.DataHelper; import net.i2p.util.I2PAppThread; +import net.i2p.util.I2PSSLSocketFactory; import net.i2p.util.Log; +import net.i2p.util.VersionComparator; /** - * Send a file to a peer + * Swiss army knife tester. + * Sends a file (datafile) to a peer (b64 dest in peerDestFile). + * + * Usage: SAMStreamSend [options] peerDestFile dataFile * - * Usage: SAMStreamSend samHost samPort peerDestFile dataFile + * See apps/sam/doc/README-test.txt for info on test setup. + * Sends data in one of 5 modes. + * Optionally uses SSL. + * Configurable SAM client version. * */ public class SAMStreamSend { - private I2PAppContext _context; - private Log _log; - private String _samHost; - private String _samPort; - private String _destFile; - private String _dataFile; + private final I2PAppContext _context; + private final Log _log; + private final String _samHost; + private final String _samPort; + private final String _destFile; + private final String _dataFile; private String _conOptions; - private Socket _samSocket; - private OutputStream _samOut; - private InputStream _samIn; - private SAMReader _reader; + private SAMReader _reader, _reader2; + private boolean _isV3; + private boolean _isV32; + private String _v3ID; //private boolean _dead; - private SAMEventHandler _eventHandler; /** Connection id (Integer) to peer (Flooder) */ - private Map<Integer, Sender> _remotePeers; + private final Map<String, Sender> _remotePeers; + private static I2PSSLSocketFactory _sslSocketFactory; + private static final int STREAM=0, DG=1, V1DG=2, RAW=3, V1RAW=4; + private static final String USAGE = "Usage: SAMStreamSend [-s] [-m mode] [-v version] [-b samHost] [-p samPort] [-o opt=val] [-u user] [-w password] peerDestFile dataDir\n" + + " modes: stream: 0; datagram: 1; v1datagram: 2; raw: 3; v1raw: 4\n" + + " -s: use SSL\n" + + " multiple -o session options are allowed"; + public static void main(String args[]) { - if (args.length < 4) { - System.err.println("Usage: SAMStreamSend samHost samPort peerDestFile dataFile"); + Getopt g = new Getopt("SAM", args, "sb:m:o:p:u:v:w:"); + boolean isSSL = false; + int mode = STREAM; + String version = "1.0"; + String host = "127.0.0.1"; + String port = "7656"; + String user = null; + String password = null; + String opts = ""; + int c; + while ((c = g.getopt()) != -1) { + switch (c) { + case 's': + isSSL = true; + break; + + case 'm': + mode = Integer.parseInt(g.getOptarg()); + if (mode < 0 || mode > V1RAW) { + System.err.println(USAGE); + return; + } + break; + + case 'v': + version = g.getOptarg(); + break; + + case 'b': + host = g.getOptarg(); + break; + + case 'o': + opts = opts + ' ' + g.getOptarg(); + break; + + case 'p': + port = g.getOptarg(); + break; + + case 'u': + user = g.getOptarg(); + break; + + case 'w': + password = g.getOptarg(); + break; + + case 'h': + case '?': + case ':': + default: + System.err.println(USAGE); + return; + } // switch + } // while + + int startArgs = g.getOptind(); + if (args.length - startArgs != 2) { + System.err.println(USAGE); + return; + } + if ((user == null && password != null) || + (user != null && password == null)) { + System.err.println("both user and password or neither"); + return; + } + if (user != null && password != null && VersionComparator.comp(version, "3.2") < 0) { + System.err.println("user/password require 3.2"); return; } - I2PAppContext ctx = new I2PAppContext(); - //String files[] = new String[args.length - 3]; - SAMStreamSend sender = new SAMStreamSend(ctx, args[0], args[1], args[2], args[3]); - sender.startup(); + I2PAppContext ctx = I2PAppContext.getGlobalContext(); + SAMStreamSend sender = new SAMStreamSend(ctx, host, port, + args[startArgs], args[startArgs + 1]); + sender.startup(version, isSSL, mode, user, password, opts); } public SAMStreamSend(I2PAppContext ctx, String samHost, String samPort, String destFile, String dataFile) { @@ -56,86 +147,141 @@ public class SAMStreamSend { _destFile = destFile; _dataFile = dataFile; _conOptions = ""; - _eventHandler = new SendEventHandler(_context); - _remotePeers = new HashMap<Integer,Sender>(); + _remotePeers = new HashMap<String, Sender>(); } - public void startup() { + public void startup(String version, boolean isSSL, int mode, String user, String password, String sessionOpts) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Starting up"); - boolean ok = connect(); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Connected: " + ok); - if (ok) { - _reader = new SAMReader(_context, _samIn, _eventHandler); + try { + Socket sock = connect(isSSL); + SAMEventHandler eventHandler = new SendEventHandler(_context); + _reader = new SAMReader(_context, sock.getInputStream(), eventHandler); _reader.startReading(); if (_log.shouldLog(Log.DEBUG)) _log.debug("Reader created"); - String ourDest = handshake(); + OutputStream out = sock.getOutputStream(); + String ourDest = handshake(out, version, true, eventHandler, mode, user, password, sessionOpts); + if (ourDest == null) + throw new IOException("handshake failed"); if (_log.shouldLog(Log.DEBUG)) _log.debug("Handshake complete. we are " + ourDest); - if (ourDest != null) { - send(); + if (_isV3 && mode == STREAM) { + Socket sock2 = connect(isSSL); + eventHandler = new SendEventHandler(_context); + _reader2 = new SAMReader(_context, sock2.getInputStream(), eventHandler); + _reader2.startReading(); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Reader2 created"); + out = sock2.getOutputStream(); + String ok = handshake(out, version, false, eventHandler, mode, user, password, ""); + if (ok == null) + throw new IOException("2nd handshake failed"); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Handshake2 complete."); } + if (mode == DG || mode == RAW) + out = null; + send(out, eventHandler, mode); + } catch (IOException e) { + _log.error("Unable to connect to SAM at " + _samHost + ":" + _samPort, e); + if (_reader != null) + _reader.stopReading(); + if (_reader2 != null) + _reader2.stopReading(); } } private class SendEventHandler extends SAMEventHandler { public SendEventHandler(I2PAppContext ctx) { super(ctx); } - public void streamClosedReceived(String result, int id, String message) { + + @Override + public void streamClosedReceived(String result, String id, String message) { Sender sender = null; synchronized (_remotePeers) { - sender = _remotePeers.remove(Integer.valueOf(id)); + sender = _remotePeers.remove(id); } if (sender != null) { sender.closed(); if (_log.shouldLog(Log.DEBUG)) _log.debug("Connection " + sender.getConnectionId() + " closed to " + sender.getDestination()); } else { - _log.error("wtf, not connected to " + id + " but we were just closed?"); + _log.error("not connected to " + id + " but we were just closed?"); } } } - private boolean connect() { - try { - _samSocket = new Socket(_samHost, Integer.parseInt(_samPort)); - _samOut = _samSocket.getOutputStream(); - _samIn = _samSocket.getInputStream(); - return true; - } catch (Exception e) { - _log.error("Unable to connect to SAM at " + _samHost + ":" + _samPort, e); - return false; + private Socket connect(boolean isSSL) throws IOException { + int port = Integer.parseInt(_samPort); + if (!isSSL) + return new Socket(_samHost, port); + synchronized(SAMStreamSink.class) { + if (_sslSocketFactory == null) { + try { + _sslSocketFactory = new I2PSSLSocketFactory( + _context, true, "certificates/sam"); + } catch (GeneralSecurityException gse) { + throw new IOException("SSL error", gse); + } + } } + SSLSocket sock = (SSLSocket) _sslSocketFactory.createSocket(_samHost, port); + I2PSSLSocketFactory.verifyHostname(_context, sock, _samHost); + return sock; } - private String handshake() { - synchronized (_samOut) { + /** @return our b64 dest or null */ + private String handshake(OutputStream samOut, String version, boolean isMaster, + SAMEventHandler eventHandler, int mode, String user, String password, + String opts) { + synchronized (samOut) { try { - _samOut.write("HELLO VERSION MIN=1.0 MAX=1.0\n".getBytes()); - _samOut.flush(); + if (user != null && password != null) + samOut.write(("HELLO VERSION MIN=1.0 MAX=" + version + " USER=" + user + " PASSWORD=" + password + '\n').getBytes()); + else + samOut.write(("HELLO VERSION MIN=1.0 MAX=" + version + '\n').getBytes()); + samOut.flush(); if (_log.shouldLog(Log.DEBUG)) _log.debug("Hello sent"); - boolean ok = _eventHandler.waitForHelloReply(); + String hisVersion = eventHandler.waitForHelloReply(); if (_log.shouldLog(Log.DEBUG)) - _log.debug("Hello reply found: " + ok); - if (!ok) - throw new IOException("wtf, hello failed?"); - String req = "SESSION CREATE STYLE=STREAM DESTINATION=TRANSIENT " + _conOptions + "\n"; - _samOut.write(req.getBytes()); - _samOut.flush(); + _log.debug("Hello reply found: " + hisVersion); + if (hisVersion == null) + throw new IOException("Hello failed"); + if (!isMaster) + return "OK"; + _isV3 = VersionComparator.comp(hisVersion, "3") >= 0; + if (_isV3) { + _isV32 = VersionComparator.comp(hisVersion, "3.2") >= 0; + byte[] id = new byte[5]; + _context.random().nextBytes(id); + _v3ID = Base32.encode(id); + _conOptions = "ID=" + _v3ID; + } + String style; + if (mode == STREAM) + style = "STREAM"; + else if (mode == DG || mode == V1DG) + style = "DATAGRAM"; + else + style = "RAW"; + String req = "SESSION CREATE STYLE=" + style + " DESTINATION=TRANSIENT " + _conOptions + ' ' + opts + '\n'; + samOut.write(req.getBytes()); + samOut.flush(); if (_log.shouldLog(Log.DEBUG)) _log.debug("Session create sent"); - ok = _eventHandler.waitForSessionCreateReply(); + boolean ok = eventHandler.waitForSessionCreateReply(); + if (!ok) + throw new IOException("Session create failed"); if (_log.shouldLog(Log.DEBUG)) _log.debug("Session create reply found: " + ok); req = "NAMING LOOKUP NAME=ME\n"; - _samOut.write(req.getBytes()); - _samOut.flush(); + samOut.write(req.getBytes()); + samOut.flush(); if (_log.shouldLog(Log.DEBUG)) _log.debug("Naming lookup sent"); - String destination = _eventHandler.waitForNamingReply("ME"); + String destination = eventHandler.waitForNamingReply("ME"); if (_log.shouldLog(Log.DEBUG)) _log.debug("Naming lookup reply found: " + destination); if (destination == null) { @@ -145,32 +291,56 @@ public class SAMStreamSend { _log.info("We are " + destination); } return destination; - } catch (Exception e) { + } catch (IOException e) { _log.error("Error handshaking", e); return null; } } } - private void send() { - Sender sender = new Sender(); + private void send(OutputStream samOut, SAMEventHandler eventHandler, int mode) throws IOException { + Sender sender = new Sender(samOut, eventHandler, mode); boolean ok = sender.openConnection(); if (ok) { I2PAppThread t = new I2PAppThread(sender, "Sender"); t.start(); + } else { + throw new IOException("Sender failed to connect"); } } private class Sender implements Runnable { - private int _connectionId; + private final String _connectionId; private String _remoteDestination; private InputStream _in; - private boolean _closed; + private volatile boolean _closed; private long _started; private long _totalSent; + private final OutputStream _samOut; + private final SAMEventHandler _eventHandler; + private final int _mode; + private final DatagramSocket _dgSock; + private final InetSocketAddress _dgSAM; - public Sender() { - _closed = false; + public Sender(OutputStream samOut, SAMEventHandler eventHandler, int mode) throws IOException { + _samOut = samOut; + _eventHandler = eventHandler; + _mode = mode; + if (mode == DG || mode == RAW) { + // samOut will be null + _dgSock = new DatagramSocket(); + _dgSAM = new InetSocketAddress(_samHost, 7655); + } else { + _dgSock = null; + _dgSAM = null; + } + synchronized (_remotePeers) { + if (_v3ID != null) + _connectionId = _v3ID; + else + _connectionId = Integer.toString(_remotePeers.size() + 1); + _remotePeers.put(_connectionId, Sender.this); + } } public boolean openConnection() { @@ -181,19 +351,27 @@ public class SAMStreamSend { int read = DataHelper.read(fin, dest); _remoteDestination = DataHelper.getUTF8(dest, 0, read); - synchronized (_remotePeers) { - _connectionId = _remotePeers.size() + 1; - _remotePeers.put(Integer.valueOf(_connectionId), Sender.this); - } _context.statManager().createRateStat("send." + _connectionId + ".totalSent", "Data size sent", "swarm", new long[] { 30*1000, 60*1000, 5*60*1000 }); _context.statManager().createRateStat("send." + _connectionId + ".started", "When we start", "swarm", new long[] { 5*60*1000 }); _context.statManager().createRateStat("send." + _connectionId + ".lifetime", "How long we talk to a peer", "swarm", new long[] { 5*60*1000 }); - byte msg[] = ("STREAM CONNECT ID=" + _connectionId + " DESTINATION=" + _remoteDestination + "\n").getBytes(); - synchronized (_samOut) { - _samOut.write(msg); - _samOut.flush(); + if (_mode == STREAM) { + StringBuilder buf = new StringBuilder(1024); + buf.append("STREAM CONNECT ID=").append(_connectionId).append(" DESTINATION=").append(_remoteDestination); + // not supported until 3.2 but 3.0-3.1 will ignore + if (_isV3) + buf.append(" FROM_PORT=1234 TO_PORT=5678"); + buf.append('\n'); + byte[] msg = DataHelper.getASCII(buf.toString()); + synchronized (_samOut) { + _samOut.write(msg); + _samOut.flush(); + } + _log.debug("STREAM CONNECT sent, waiting for STREAM STATUS..."); + boolean ok = _eventHandler.waitForStreamStatusReply(); + if (!ok) + throw new IOException("STREAM CONNECT failed"); } _in = new FileInputStream(_dataFile); @@ -210,7 +388,7 @@ public class SAMStreamSend { } } - public int getConnectionId() { return _connectionId; } + public String getConnectionId() { return _connectionId; } public String getDestination() { return _remoteDestination; } public void closed() { @@ -224,7 +402,8 @@ public class SAMStreamSend { public void run() { _started = _context.clock().now(); _context.statManager().addRateData("send." + _connectionId + ".started", 1, 0); - byte data[] = new byte[1024]; + final long toSend = (new File(_dataFile)).length(); + byte data[] = new byte[8192]; long lastSend = _context.clock().now(); while (!_closed) { try { @@ -239,11 +418,45 @@ public class SAMStreamSend { _log.debug("Sending " + read + " on " + _connectionId + " after " + (now-lastSend)); lastSend = now; - byte msg[] = ("STREAM SEND ID=" + _connectionId + " SIZE=" + read + "\n").getBytes(); - synchronized (_samOut) { - _samOut.write(msg); - _samOut.write(data, 0, read); - _samOut.flush(); + if (_samOut != null) { + synchronized (_samOut) { + if (!_isV3 || _mode == V1DG || _mode == V1RAW) { + String m; + if (_mode == STREAM) { + m = "STREAM SEND ID=" + _connectionId + " SIZE=" + read + "\n"; + } else if (_mode == V1DG) { + m = "DATAGRAM SEND DESTINATION=" + _remoteDestination + " SIZE=" + read + "\n"; + } else if (_mode == V1RAW) { + m = "RAW SEND DESTINATION=" + _remoteDestination + " SIZE=" + read + "\n"; + } else { + throw new IOException("unsupported mode " + _mode); + } + byte msg[] = DataHelper.getASCII(m); + _samOut.write(msg); + } + _samOut.write(data, 0, read); + _samOut.flush(); + } + } else { + // real datagrams + ByteArrayOutputStream baos = new ByteArrayOutputStream(read + 1024); + baos.write(DataHelper.getASCII("3.0 ")); + baos.write(DataHelper.getASCII(_v3ID)); + baos.write((byte) ' '); + baos.write(DataHelper.getASCII(_remoteDestination)); + if (_isV32) { + // only set TO_PORT to test session setting of FROM_PORT + if (_mode == RAW) + baos.write(DataHelper.getASCII(" PROTOCOL=123 TO_PORT=5678")); + else + baos.write(DataHelper.getASCII(" TO_PORT=5678")); + } + baos.write((byte) '\n'); + baos.write(data, 0, read); + byte[] pkt = baos.toByteArray(); + DatagramPacket p = new DatagramPacket(pkt, pkt.length, _dgSAM); + _dgSock.send(p); + try { Thread.sleep(25); } catch (InterruptedException ie) {} } _totalSent += read; @@ -251,20 +464,49 @@ public class SAMStreamSend { } } catch (IOException ioe) { _log.error("Error sending", ioe); + break; } } - byte msg[] = ("STREAM CLOSE ID=" + _connectionId + "\n").getBytes(); - try { - synchronized (_samOut) { - _samOut.write(msg); - _samOut.flush(); + if (_samOut != null) { + if (_isV3) { + try { + _samOut.close(); + } catch (IOException ioe) { + _log.info("Error closing", ioe); + } + } else { + byte msg[] = ("STREAM CLOSE ID=" + _connectionId + "\n").getBytes(); + try { + synchronized (_samOut) { + _samOut.write(msg); + _samOut.flush(); + _samOut.close(); + } + } catch (IOException ioe) { + _log.info("Error closing", ioe); + } } - } catch (IOException ioe) { - _log.error("Error closing", ioe); + } else if (_dgSock != null) { + _dgSock.close(); } closed(); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Runner exiting"); + if (toSend != _totalSent) + _log.error("Only sent " + _totalSent + " of " + toSend + " bytes"); + if (_reader2 != null) + _reader2.stopReading(); + // stop the reader, since we're only doing this once for testing + // you wouldn't do this in a real application + if (_isV3) { + // closing the master socket too fast will kill the data socket flushing through + try { + Thread.sleep(10000); + } catch (InterruptedException ie) {} + } + _reader.stopReading(); } } } diff --git a/apps/sam/java/src/net/i2p/sam/client/SAMStreamSink.java b/apps/sam/java/src/net/i2p/sam/client/SAMStreamSink.java index 666b7116d8bf4fb3a7022981c6ec1102a3579467..ef865afcc7067b7fd4fc11a7c8684bc38882846a 100644 --- a/apps/sam/java/src/net/i2p/sam/client/SAMStreamSink.java +++ b/apps/sam/java/src/net/i2p/sam/client/SAMStreamSink.java @@ -1,184 +1,636 @@ package net.i2p.sam.client; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.ServerSocket; import java.net.Socket; +import java.security.GeneralSecurityException; import java.util.HashMap; import java.util.Map; +import java.util.Properties; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLServerSocket; + +import gnu.getopt.Getopt; import net.i2p.I2PAppContext; +import net.i2p.data.Base32; +import net.i2p.data.DataHelper; +import net.i2p.util.I2PAppThread; +import net.i2p.util.I2PSSLSocketFactory; import net.i2p.util.Log; +import net.i2p.util.VersionComparator; /** - * Sit around on a SAM destination, receiving lots of data and - * writing it to disk + * Swiss army knife tester. + * Saves our transient b64 destination to myKeyFile where SAMStreamSend can get it. + * Saves received data to a file (in sinkDir). + * + * Usage: SAMStreamSink [options] myKeyFile sinkDir * - * Usage: SAMStreamSink samHost samPort myKeyFile sinkDir + * See apps/sam/doc/README-test.txt for info on test setup. + * Receives data in one of 7 modes. + * Optionally uses SSL. + * Configurable SAM client version. * */ public class SAMStreamSink { - private I2PAppContext _context; - private Log _log; - private String _samHost; - private String _samPort; - private String _destFile; - private String _sinkDir; + private final I2PAppContext _context; + private final Log _log; + private final String _samHost; + private final String _samPort; + private final String _destFile; + private final String _sinkDir; private String _conOptions; - private Socket _samSocket; - private OutputStream _samOut; - private InputStream _samIn; - private SAMReader _reader; - //private boolean _dead; - private SAMEventHandler _eventHandler; + private SAMReader _reader, _reader2; + private boolean _isV3; + private boolean _isV32; + private String _v3ID; /** Connection id (Integer) to peer (Flooder) */ - private Map<Integer, Sink> _remotePeers; + private final Map<String, Sink> _remotePeers; + private static I2PSSLSocketFactory _sslSocketFactory; + private static final int STREAM=0, DG=1, V1DG=2, RAW=3, V1RAW=4, RAWHDR = 5, FORWARD = 6; + private static final String USAGE = "Usage: SAMStreamSink [-s] [-m mode] [-v version] [-b samHost] [-p samPort] [-o opt=val] [-u user] [-w password] myDestFile sinkDir\n" + + " modes: stream: 0; datagram: 1; v1datagram: 2; raw: 3; v1raw: 4; raw-with-headers: 5; stream-forward: 6\n" + + " -s: use SSL\n" + + " multiple -o session options are allowed"; + private static final int V3FORWARDPORT=9998; + private static final int V3DGPORT=9999; + public static void main(String args[]) { - if (args.length < 4) { - System.err.println("Usage: SAMStreamSink samHost samPort myDestFile sinkDir"); + Getopt g = new Getopt("SAM", args, "sb:m:p:u:v:w:"); + boolean isSSL = false; + int mode = STREAM; + String version = "1.0"; + String host = "127.0.0.1"; + String port = "7656"; + String user = null; + String password = null; + String opts = ""; + int c; + while ((c = g.getopt()) != -1) { + switch (c) { + case 's': + isSSL = true; + break; + + case 'm': + mode = Integer.parseInt(g.getOptarg()); + if (mode < 0 || mode > FORWARD) { + System.err.println(USAGE); + return; + } + break; + + case 'v': + version = g.getOptarg(); + break; + + case 'b': + host = g.getOptarg(); + break; + + case 'o': + opts = opts + ' ' + g.getOptarg(); + break; + + case 'p': + port = g.getOptarg(); + break; + + case 'u': + user = g.getOptarg(); + break; + + case 'w': + password = g.getOptarg(); + break; + + case 'h': + case '?': + case ':': + default: + System.err.println(USAGE); + return; + } // switch + } // while + + int startArgs = g.getOptind(); + if (args.length - startArgs != 2) { + System.err.println(USAGE); return; } - I2PAppContext ctx = new I2PAppContext(); - SAMStreamSink sink = new SAMStreamSink(ctx, args[0], args[1], args[2], args[3]); - sink.startup(); + if ((user == null && password != null) || + (user != null && password == null)) { + System.err.println("both user and password or neither"); + return; + } + if (user != null && password != null && VersionComparator.comp(version, "3.2") < 0) { + System.err.println("user/password require 3.2"); + return; + } + I2PAppContext ctx = I2PAppContext.getGlobalContext(); + SAMStreamSink sink = new SAMStreamSink(ctx, host, port, + args[startArgs], args[startArgs + 1]); + sink.startup(version, isSSL, mode, user, password, opts); } public SAMStreamSink(I2PAppContext ctx, String samHost, String samPort, String destFile, String sinkDir) { _context = ctx; _log = ctx.logManager().getLog(SAMStreamSink.class); - //_dead = false; _samHost = samHost; _samPort = samPort; _destFile = destFile; _sinkDir = sinkDir; _conOptions = ""; - _eventHandler = new SinkEventHandler(_context); - _remotePeers = new HashMap<Integer,Sink>(); + _remotePeers = new HashMap<String, Sink>(); } - public void startup() { + public void startup(String version, boolean isSSL, int mode, String user, String password, String sessionOpts) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Starting up"); - boolean ok = connect(); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Connected: " + ok); - if (ok) { - _reader = new SAMReader(_context, _samIn, _eventHandler); + try { + Socket sock = connect(isSSL); + OutputStream out = sock.getOutputStream(); + SAMEventHandler eventHandler = new SinkEventHandler(_context, out); + _reader = new SAMReader(_context, sock.getInputStream(), eventHandler); _reader.startReading(); if (_log.shouldLog(Log.DEBUG)) _log.debug("Reader created"); - String ourDest = handshake(); + String ourDest = handshake(out, version, true, eventHandler, mode, user, password, sessionOpts); + if (ourDest == null) + throw new IOException("handshake failed"); if (_log.shouldLog(Log.DEBUG)) _log.debug("Handshake complete. we are " + ourDest); - if (ourDest != null) { - //boolean written = - writeDest(ourDest); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Dest written"); + if (_isV32) { + _log.debug("Starting pinger"); + Thread t = new Pinger(out); + t.start(); + } + if (_isV3 && (mode == STREAM || mode == FORWARD)) { + // test multiple acceptors, only works in 3.2 + int acceptors = (_isV32 && mode == STREAM) ? 4 : 1; + for (int i = 0; i < acceptors; i++) { + Socket sock2 = connect(isSSL); + out = sock2.getOutputStream(); + eventHandler = new SinkEventHandler2(_context, sock2.getInputStream(), out); + _reader2 = new SAMReader(_context, sock2.getInputStream(), eventHandler); + _reader2.startReading(); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Reader " + (2 + i) + " created"); + String ok = handshake(out, version, false, eventHandler, mode, user, password, ""); + if (ok == null) + throw new IOException("handshake " + (2 + i) + " failed"); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Handshake " + (2 + i) + " complete."); + } + if (mode == FORWARD) { + // set up a listening ServerSocket + (new FwdRcvr(isSSL)).start(); + } + } else if (_isV3 && (mode == DG || mode == RAW || mode == RAWHDR)) { + // set up a listening DatagramSocket + (new DGRcvr(mode)).start(); + } + writeDest(ourDest); + } catch (IOException e) { + _log.error("Unable to connect to SAM at " + _samHost + ":" + _samPort, e); + } + } + + private class DGRcvr extends I2PAppThread { + private final int _mode; + + public DGRcvr(int mode) { _mode = mode; } + + public void run() { + byte[] buf = new byte[32768]; + try { + Sink sink = new Sink("FAKE", "FAKEFROM"); + DatagramSocket dg = new DatagramSocket(V3DGPORT); + while (true) { + DatagramPacket p = new DatagramPacket(buf, 32768); + dg.receive(p); + int len = p.getLength(); + int off = p.getOffset(); + byte[] data = p.getData(); + _log.info("Got datagram length " + len); + if (_mode == DG || _mode == RAWHDR) { + ByteArrayInputStream bais = new ByteArrayInputStream(data, off, len); + String line = DataHelper.readLine(bais); + if (line == null) { + _log.error("DGRcvr no header line"); + continue; + } + if (_mode == DG && line.length() < 516) { + _log.error("DGRcvr line too short: \"" + line + '\n'); + continue; + } + String[] parts = line.split(" "); + int i = 0; + if (_mode == DG) { + String dest = parts[0]; + _log.info("DG is from " + dest); + i++; + } + for ( ; i < parts.length; i++) { + _log.info("Parameter: " + parts[i]); + } + int left = bais.available(); + sink.received(data, off + len - left, left); + } else { + sink.received(data, off, len); + } + } + } catch (IOException ioe) { + _log.error("DGRcvr", ioe); + } + } + } + + private class FwdRcvr extends I2PAppThread { + private final boolean _isSSL; + + public FwdRcvr(boolean isSSL) { + if (isSSL) + throw new UnsupportedOperationException("TODO"); + _isSSL = isSSL; + } + + public void run() { + try { + ServerSocket ss; + if (_isSSL) { + throw new UnsupportedOperationException("TODO"); + } else { + ss = new ServerSocket(V3FORWARDPORT); + } + while (true) { + Socket s = ss.accept(); + Sink sink = new Sink("FAKE", "FAKEFROM"); + try { + InputStream in = s.getInputStream(); + byte[] buf = new byte[32768]; + int len; + while((len = in.read(buf)) >= 0) { + sink.received(buf, 0, len); + } + sink.closed(); + } catch (IOException ioe) { + _log.error("Fwdcvr", ioe); + } + } + } catch (IOException ioe) { + _log.error("Fwdcvr", ioe); + } + } + } + + private static class Pinger extends I2PAppThread { + private final OutputStream _out; + + public Pinger(OutputStream out) { + super("SAM Sink Pinger"); + setDaemon(true); + _out = out; + } + + public void run() { + while (true) { + try { + Thread.sleep(127*1000); + synchronized(_out) { + _out.write(DataHelper.getASCII("PING " + System.currentTimeMillis() + '\n')); + _out.flush(); + } + } catch (InterruptedException ie) { + break; + } catch (IOException ioe) { + break; + } } } } private class SinkEventHandler extends SAMEventHandler { - public SinkEventHandler(I2PAppContext ctx) { super(ctx); } + protected final OutputStream _out; + + public SinkEventHandler(I2PAppContext ctx, OutputStream out) { + super(ctx); + _out = out; + } @Override - public void streamClosedReceived(String result, int id, String message) { - Sink sink = null; + public void streamClosedReceived(String result, String id, String message) { + Sink sink; synchronized (_remotePeers) { - sink = _remotePeers.remove(Integer.valueOf(id)); + sink = _remotePeers.remove(id); } if (sink != null) { sink.closed(); if (_log.shouldLog(Log.DEBUG)) _log.debug("Connection " + sink.getConnectionId() + " closed to " + sink.getDestination()); } else { - _log.error("wtf, not connected to " + id + " but we were just closed?"); + _log.error("not connected to " + id + " but we were just closed?"); } } @Override - public void streamDataReceived(int id, byte data[], int offset, int length) { - Sink sink = null; + public void streamDataReceived(String id, byte data[], int offset, int length) { + Sink sink; synchronized (_remotePeers) { - sink = _remotePeers.get(Integer.valueOf(id)); + sink = _remotePeers.get(id); } if (sink != null) { sink.received(data, offset, length); } else { - _log.error("wtf, not connected to " + id + " but we received " + length + "?"); + _log.error("not connected to " + id + " but we received " + length + "?"); } } @Override - public void streamConnectedReceived(String dest, int id) { + public void streamConnectedReceived(String dest, String id) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Connection " + id + " received from " + dest); try { Sink sink = new Sink(id, dest); synchronized (_remotePeers) { - _remotePeers.put(Integer.valueOf(id), sink); + _remotePeers.put(id, sink); } } catch (IOException ioe) { _log.error("Error creating a new sink", ioe); } } + + @Override + public void pingReceived(String data) { + if (_log.shouldInfo()) + _log.info("Got PING " + data + ", sending PONG " + data); + synchronized (_out) { + try { + _out.write(("PONG " + data + '\n').getBytes()); + _out.flush(); + } catch (IOException ioe) { + _log.error("PONG fail", ioe); + } + } + } + + @Override + public void datagramReceived(String dest, byte[] data, int offset, int length, int fromPort, int toPort) { + // just get the first + Sink sink; + synchronized (_remotePeers) { + if (_remotePeers.isEmpty()) { + _log.error("not connected but we received datagram " + length + "?"); + return; + } + sink = _remotePeers.values().iterator().next(); + } + sink.received(data, offset, length); + } + + @Override + public void rawReceived(byte[] data, int offset, int length, int fromPort, int toPort, int protocol) { + // just get the first + Sink sink; + synchronized (_remotePeers) { + if (_remotePeers.isEmpty()) { + _log.error("not connected but we received raw " + length + "?"); + return; + } + sink = _remotePeers.values().iterator().next(); + } + sink.received(data, offset, length); + } + } + + private class SinkEventHandler2 extends SinkEventHandler { + + private final InputStream _in; + + public SinkEventHandler2(I2PAppContext ctx, InputStream in, OutputStream out) { + super(ctx, out); + _in = in; + } + + @Override + public void streamStatusReceived(String result, String id, String message) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("got STREAM STATUS, result=" + result); + super.streamStatusReceived(result, id, message); + Sink sink = null; + try { + String dest = "TODO_if_not_silent"; + sink = new Sink(_v3ID, dest); + synchronized (_remotePeers) { + _remotePeers.put(_v3ID, sink); + } + } catch (IOException ioe) { + _log.error("Error creating a new sink", ioe); + try { _in.close(); } catch (IOException ioe2) {} + if (sink != null) + sink.closed(); + return; + } + // inline so the reader doesn't grab the data + try { + boolean gotDest = false; + byte[] dest = new byte[1024]; + int dlen = 0; + byte buf[] = new byte[4096]; + int len; + while((len = _in.read(buf)) >= 0) { + if (!gotDest) { + // eat the dest line + for (int i = 0; i < len; i++) { + byte b = buf[i]; + if (b == (byte) '\n') { + gotDest = true; + if (_log.shouldInfo()) { + try { + _log.info("Got incoming accept from: \"" + new String(dest, 0, dlen, "ISO-8859-1") + '"'); + } catch (IOException uee) {} + } + // feed any remaining to the sink + i++; + if (i < len) + sink.received(buf, i, len - i); + break; + } else { + if (dlen < dest.length) { + dest[dlen++] = b; + } else if (dlen == dest.length) { + dlen++; + _log.error("first line overflow on accept"); + } + } + } + } else { + sink.received(buf, 0, len); + } + } + sink.closed(); + } catch (IOException ioe) { + _log.error("Error reading", ioe); + } finally { + try { _in.close(); } catch (IOException ioe) {} + } + } } - private boolean connect() { - try { - _samSocket = new Socket(_samHost, Integer.parseInt(_samPort)); - _samOut = _samSocket.getOutputStream(); - _samIn = _samSocket.getInputStream(); - return true; - } catch (Exception e) { - _log.error("Unable to connect to SAM at " + _samHost + ":" + _samPort, e); - return false; + private Socket connect(boolean isSSL) throws IOException { + int port = Integer.parseInt(_samPort); + if (!isSSL) + return new Socket(_samHost, port); + synchronized(SAMStreamSink.class) { + if (_sslSocketFactory == null) { + try { + _sslSocketFactory = new I2PSSLSocketFactory( + _context, true, "certificates/sam"); + } catch (GeneralSecurityException gse) { + throw new IOException("SSL error", gse); + } + } } + SSLSocket sock = (SSLSocket) _sslSocketFactory.createSocket(_samHost, port); + I2PSSLSocketFactory.verifyHostname(_context, sock, _samHost); + return sock; } - private String handshake() { - synchronized (_samOut) { + /** @return our b64 dest or null */ + private String handshake(OutputStream samOut, String version, boolean isMaster, + SAMEventHandler eventHandler, int mode, String user, String password, + String sopts) { + synchronized (samOut) { try { - _samOut.write("HELLO VERSION MIN=1.0 MAX=1.0\n".getBytes()); - _samOut.flush(); + if (user != null && password != null) + samOut.write(("HELLO VERSION MIN=1.0 MAX=" + version + " USER=" + user + " PASSWORD=" + password + '\n').getBytes()); + else + samOut.write(("HELLO VERSION MIN=1.0 MAX=" + version + '\n').getBytes()); + samOut.flush(); if (_log.shouldLog(Log.DEBUG)) _log.debug("Hello sent"); - boolean ok = _eventHandler.waitForHelloReply(); + String hisVersion = eventHandler.waitForHelloReply(); if (_log.shouldLog(Log.DEBUG)) - _log.debug("Hello reply found: " + ok); - if (!ok) - throw new IOException("wtf, hello failed?"); - String req = "SESSION CREATE STYLE=STREAM DESTINATION=" + _destFile + " " + _conOptions + "\n"; - _samOut.write(req.getBytes()); - _samOut.flush(); + _log.debug("Hello reply found: " + hisVersion); + if (hisVersion == null) + throw new IOException("Hello failed"); + if (!isMaster) { + // only for v3 + //String req = "STREAM ACCEPT SILENT=true ID=" + _v3ID + "\n"; + // TO_PORT not supported until 3.2 but 3.0-3.1 will ignore + String req; + if (mode == STREAM) + req = "STREAM ACCEPT SILENT=false TO_PORT=5678 ID=" + _v3ID + "\n"; + else if (mode == FORWARD) + req = "STREAM FORWARD ID=" + _v3ID + " PORT=" + V3FORWARDPORT + '\n'; + else + throw new IllegalStateException("mode " + mode); + samOut.write(req.getBytes()); + samOut.flush(); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("STREAM ACCEPT/FORWARD sent"); + if (mode == FORWARD) { + // docs were wrong, we do not get a STREAM STATUS if SILENT=true for ACCEPT + boolean ok = eventHandler.waitForStreamStatusReply(); + if (!ok) + throw new IOException("Stream status failed"); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("got STREAM STATUS, awaiting connection"); + } + return "OK"; + } + _isV3 = VersionComparator.comp(hisVersion, "3") >= 0; + String dest; + if (_isV3) { + _isV32 = VersionComparator.comp(hisVersion, "3.2") >= 0; + // we use the filename as the name in sam.keys + // and read it in ourselves + File keys = new File("sam.keys"); + if (keys.exists()) { + Properties opts = new Properties(); + DataHelper.loadProps(opts, keys); + String s = opts.getProperty(_destFile); + if (s != null) { + dest = s; + } else { + dest = "TRANSIENT"; + (new File(_destFile)).delete(); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Requesting new transient destination"); + } + } else { + dest = "TRANSIENT"; + (new File(_destFile)).delete(); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Requesting new transient destination"); + } + if (isMaster) { + byte[] id = new byte[5]; + _context.random().nextBytes(id); + _v3ID = Base32.encode(id); + _conOptions = "ID=" + _v3ID; + } + } else { + // we use the filename as the name in sam.keys + // and give it to the SAM server + dest = _destFile; + } + String style; + if (mode == STREAM || mode == FORWARD) + style = "STREAM"; + else if (mode == V1DG) + style = "DATAGRAM"; + else if (mode == DG) + style = "DATAGRAM PORT=" + V3DGPORT; + else if (mode == V1RAW) + style = "RAW"; + else if (mode == RAW) + style = "RAW PORT=" + V3DGPORT; + else + style = "RAW HEADER=true PORT=" + V3DGPORT; + String req = "SESSION CREATE STYLE=" + style + " DESTINATION=" + dest + ' ' + _conOptions + ' ' + sopts + '\n'; + samOut.write(req.getBytes()); + samOut.flush(); if (_log.shouldLog(Log.DEBUG)) _log.debug("Session create sent"); - ok = _eventHandler.waitForSessionCreateReply(); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Session create reply found: " + ok); - + if (mode == STREAM) { + boolean ok = eventHandler.waitForSessionCreateReply(); + if (!ok) + throw new IOException("Session create failed"); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Session create reply found: " + ok); + } req = "NAMING LOOKUP NAME=ME\n"; - _samOut.write(req.getBytes()); - _samOut.flush(); + samOut.write(req.getBytes()); + samOut.flush(); if (_log.shouldLog(Log.DEBUG)) _log.debug("Naming lookup sent"); - String destination = _eventHandler.waitForNamingReply("ME"); + String destination = eventHandler.waitForNamingReply("ME"); if (_log.shouldLog(Log.DEBUG)) _log.debug("Naming lookup reply found: " + destination); if (destination == null) { _log.error("No naming lookup reply found!"); return null; - } else { + } + if (_log.shouldInfo()) _log.info(_destFile + " is located at " + destination); + if (mode == V1DG || mode == V1RAW) { + // fake it so the sink starts + eventHandler.streamConnectedReceived(destination, "FAKE"); } return destination; - } catch (Exception e) { + } catch (IOException e) { _log.error("Error handshaking", e); return null; } @@ -186,11 +638,21 @@ public class SAMStreamSink { } private boolean writeDest(String dest) { + File f = new File(_destFile); +/* + if (f.exists()) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Destination file exists, not overwriting: " + _destFile); + return false; + } +*/ FileOutputStream fos = null; try { - fos = new FileOutputStream(_destFile); + fos = new FileOutputStream(f); fos.write(dest.getBytes()); - } catch (Exception e) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("My destination written to " + _destFile); + } catch (IOException e) { _log.error("Error writing to " + _destFile, e); return false; } finally { @@ -200,14 +662,14 @@ public class SAMStreamSink { } private class Sink { - private int _connectionId; - private String _remoteDestination; - private boolean _closed; - private long _started; + private final String _connectionId; + private final String _remoteDestination; + private volatile boolean _closed; + private final long _started; private long _lastReceivedOn; - private OutputStream _out; + private final OutputStream _out; - public Sink(int conId, String remDest) throws IOException { + public Sink(String conId, String remDest) throws IOException { _connectionId = conId; _remoteDestination = remDest; _closed = false; @@ -221,10 +683,13 @@ public class SAMStreamSink { sinkDir.mkdirs(); File out = File.createTempFile("sink", ".dat", sinkDir); + if (_log.shouldWarn()) + _log.warn("outputting to " + out); _out = new FileOutputStream(out); + _started = _context.clock().now(); } - public int getConnectionId() { return _connectionId; } + public String getConnectionId() { return _connectionId; } public String getDestination() { return _remoteDestination; } public void closed() { @@ -235,7 +700,7 @@ public class SAMStreamSink { try { _out.close(); } catch (IOException ioe) { - _log.error("Error closing", ioe); + _log.info("Error closing", ioe); } } public void received(byte data[], int offset, int len) { diff --git a/apps/streaming/java/src/net/i2p/client/streaming/impl/ConnectionHandler.java b/apps/streaming/java/src/net/i2p/client/streaming/impl/ConnectionHandler.java index b92648d0b86a6dbddd0e34db0e1de2432dcc852e..8af6de367e5663ea3cecce6887e714745ecc14d3 100644 --- a/apps/streaming/java/src/net/i2p/client/streaming/impl/ConnectionHandler.java +++ b/apps/streaming/java/src/net/i2p/client/streaming/impl/ConnectionHandler.java @@ -294,9 +294,12 @@ class ConnectionHandler { private static class PoisonPacket extends Packet { public static final int POISON_MAX_DELAY_REQUEST = Packet.MAX_DELAY_REQUEST + 1; - public PoisonPacket() { - super(null); - setOptionalDelay(POISON_MAX_DELAY_REQUEST); + @Override + public int getOptionalDelay() { return POISON_MAX_DELAY_REQUEST; } + + @Override + public String toString() { + return "POISON"; } } } diff --git a/build.properties b/build.properties index 9e219c33359db5a3fd9f83a27177963bb41e0adc..3bd2548bf8403d248485699441ce08b0c8afd2f6 100644 --- a/build.properties +++ b/build.properties @@ -46,8 +46,11 @@ javac.version=1.6 #javac.classpath=/PATH/TO/pack200.jar # Optional compiler args +# This one is for subsystems requiring Java 6 # This one keeps gcj a lot quieter #javac.compilerargs=-warn:-unchecked,raw,unused,serial +# This one is for subsystems requiring Java 7 +#javac.compilerargs7= # # Note to packagers, embedders, distributors: diff --git a/core/java/src/net/i2p/util/PasswordManager.java b/core/java/src/net/i2p/util/PasswordManager.java index 5e51dcb7dfb9bc8d95747f9525bc56d54cae7ca4..5ca812b4e77bf07efd458d8213dc2e311ecef681 100644 --- a/core/java/src/net/i2p/util/PasswordManager.java +++ b/core/java/src/net/i2p/util/PasswordManager.java @@ -99,6 +99,18 @@ public class PasswordManager { String shash = _context.getProperty(pfx + PROP_SHASH); if (shash == null) return false; + return checkHash(shash, pw); + } + + /** + * Check pw against b64 salt+hash, as generated by createHash() + * + * @param shash b64 string + * @param pw plain text non-null, already trimmed + * @return if pw verified + * @since 0.9.24 + */ + public boolean checkHash(String shash, String pw) { byte[] shashBytes = Base64.decode(shash); if (shashBytes == null || shashBytes.length != SHASH_LENGTH) return false; @@ -110,6 +122,23 @@ public class PasswordManager { return DataHelper.eq(hash, pwHash); } + /** + * Create a salt+hash, to be saved and verified later by verifyHash(). + * + * @param pw plain text non-null, already trimmed + * @return salted+hash b64 string + * @since 0.9.24 + */ + public String createHash(String pw) { + byte[] salt = new byte[SALT_LENGTH]; + _context.random().nextBytes(salt); + byte[] pwHash = _context.keyGenerator().generateSessionKey(salt, DataHelper.getUTF8(pw)).getData(); + byte[] shashBytes = new byte[SHASH_LENGTH]; + System.arraycopy(salt, 0, shashBytes, 0, SALT_LENGTH); + System.arraycopy(pwHash, 0, shashBytes, SALT_LENGTH, SessionKey.KEYSIZE_BYTES); + return Base64.encode(shashBytes); + } + /** * Either plain or b64 * diff --git a/installer/install.xml b/installer/install.xml index ba8a195c48b965d94663532dbc69a536cca4649a..3a54086185f35c9912430760cfb57a7369ebc650 100644 --- a/installer/install.xml +++ b/installer/install.xml @@ -9,7 +9,7 @@ <author name="I2P" email="https://geti2p.net/"/> </authors> <url>https://geti2p.net/</url> - <javaversion>1.6</javaversion> + <javaversion>1.7</javaversion> <!-- use pack200 compression, saves about 33% see http://java.sun.com/j2se/1.5.0/docs/guide/deployment/deployment-guide/pack200.html diff --git a/router/java/src/net/i2p/router/util/RouterPasswordManager.java b/router/java/src/net/i2p/router/util/RouterPasswordManager.java index 97ef2468dbd815a7c70346e31f7e06d3f29406a7..3c2fb9b7d687cb5ff81c0f64d929fe7555147bbd 100644 --- a/router/java/src/net/i2p/router/util/RouterPasswordManager.java +++ b/router/java/src/net/i2p/router/util/RouterPasswordManager.java @@ -158,13 +158,7 @@ public class RouterPasswordManager extends PasswordManager { String pfx = realm; if (user != null && user.length() > 0) pfx += '.' + user; - byte[] salt = new byte[SALT_LENGTH]; - _context.random().nextBytes(salt); - byte[] pwHash = _context.keyGenerator().generateSessionKey(salt, DataHelper.getUTF8(pw)).getData(); - byte[] shashBytes = new byte[SHASH_LENGTH]; - System.arraycopy(salt, 0, shashBytes, 0, SALT_LENGTH); - System.arraycopy(pwHash, 0, shashBytes, SALT_LENGTH, SessionKey.KEYSIZE_BYTES); - String shash = Base64.encode(shashBytes); + String shash = createHash(pw); Map<String, String> toAdd = Collections.singletonMap(pfx + PROP_SHASH, shash); List<String> toDel = new ArrayList<String>(4); toDel.add(pfx + PROP_PW);