diff --git a/apps/sam/doc/README-test.txt b/apps/sam/doc/README-test.txt
new file mode 100644
index 000000000..c1936274d
--- /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 5d93ae476..9ecc58b8c 100644
--- a/apps/sam/java/build.xml
+++ b/apps/sam/java/build.xml
@@ -21,7 +21,9 @@
-
+
+
+
@@ -30,22 +32,22 @@
-
+
-
+
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 000000000..2af5f19d3
--- /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 383f44fb5..905ae808d 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(8);
_handlers = new HashSet(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(8);
_handlers = new HashSet(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;
}
- return new Options(host, port, opts, keyfile);
+ int numArgs = startOpts - startArgs;
+ switch (numArgs) {
+ case 0:
+ break;
+
+ case 2:
+ keyfile = args[startArgs];
+ try {
+ port = Integer.parseInt(args[startArgs + 1]);
+ } catch (NumberFormatException nfe) {
+ throw new HelpRequestedException();
+ }
+ break;
+
+ case 3:
+ keyfile = args[startArgs];
+ host = args[startArgs + 1];
+ try {
+ port = Integer.parseInt(args[startArgs + 2]);
+ } catch (NumberFormatException nfe) {
+ throw new HelpRequestedException();
+ }
+ break;
+
+ default:
+ throw new HelpRequestedException();
+ }
+
+ String scfile = cfile != null ? cfile : DEFAULT_SAM_CONFIGFILE;
+ File file = new File(scfile);
+ if (!file.isAbsolute())
+ file = new File(I2PAppContext.getGlobalContext().getConfigDir(), scfile);
+
+ Properties opts = new Properties();
+ if (file.exists()) {
+ DataHelper.loadProps(opts, file);
+ } else if (cfile != null) {
+ // only throw if specified on command line
+ throw new IllegalArgumentException("Config file not found: " + file);
+ }
+ // command line trumps config file trumps defaults
+ if (host == null)
+ host = opts.getProperty(PROP_TCP_HOST, DEFAULT_TCP_HOST);
+ if (port < 0) {
+ try {
+ port = Integer.parseInt(opts.getProperty(PROP_TCP_PORT, DEFAULT_TCP_PORT));
+ } catch (NumberFormatException nfe) {
+ throw new HelpRequestedException();
+ }
+ }
+ if (keyfile == null)
+ keyfile = opts.getProperty(PROP_SAM_KEYFILE, DEFAULT_SAM_KEYFILE);
+ if (!isSSL)
+ isSSL = Boolean.parseBoolean(opts.getProperty(PROP_SAM_SSL));
+ if (isSSL) {
+ // must do this before we add command line opts since we may be writing them back out
+ boolean shouldSave = SSLUtil.verifyKeyStore(opts);
+ if (shouldSave)
+ DataHelper.storeProps(opts, file);
+ }
+
+ int remaining = args.length - startOpts;
+ if (remaining > 0) {
+ parseOptions(args, startOpts, opts);
+ }
+ return new Options(host, port, isSSL, opts, keyfile, file);
}
- 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 68971af31..b07ff8fbe 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 ce3c1803c..b25d6ad0b 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 56d68878c..e2bcb84fb 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 582854d87..71a617e25 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 e8e502da8..c95b6a9ab 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 96ebe45d0..564f95c3d 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 e3c421608..ab1cd7632 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 588d5f5e5..5b5947016 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 {
- size = Integer.parseInt(strsize);
+ proto = 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 protocol 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("FROM_PORT");
+ if (s != null) {
+ try {
+ fromPort = Integer.parseInt(s);
+ } catch (NumberFormatException e) {
+ if (_log.shouldLog(Log.WARN))
+ _log.warn("Invalid RAW SEND port specified: " + s);
+ }
+ }
+ 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 000000000..a482318fa
--- /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 a16c92327..ee2782559 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 5e75dcf66..9cc4a500c 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 90eae76f9..b68f3a74a 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 7770d4075..54f58054f 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 000000000..546955cee
--- /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 ServerSocketChannel setOption(SocketOption 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 getOption(SocketOption name) {
+ return null;
+ }
+
+ public Set> 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 000000000..7b956a16e
--- /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 SocketChannel setOption(SocketOption 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 getOption(SocketOption name) {
+ return null;
+ }
+
+ public Set> 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 000000000..bf4ebf1e4
--- /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 5388f5dc2..1b7cdc99a 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 2d1a5b63b..dc90d3fb2 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 _namingReplies = new HashMap();
+ private Boolean _streamStatusOk;
+ private final Object _sessionCreateLock = new Object();
+ private final Object _namingReplyLock = new Object();
+ private final Object _streamStatusLock = new Object();
+ private final Map _namingReplies = new HashMap();
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 f39394901..c82dff721 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 2ce8b3b02..d8f743208 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 samHost samPort peerDestFile dataFile
+ * Usage: SAMStreamSend [options] 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 _remotePeers;
+ private final Map _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;
}
- 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();
+ 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();
+ 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();
+ _remotePeers = new HashMap();
}
- 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 666b7116d..ef865afcc 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 samHost samPort myKeyFile sinkDir
+ * Usage: SAMStreamSink [options] 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 _remotePeers;
+ private final Map _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();
+ _remotePeers = new HashMap();
}
- 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 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 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 String handshake() {
- synchronized (_samOut) {
+ 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;
+ }
+
+ /** @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 b92648d0b..8af6de367 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 9e219c333..3bd2548bf 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 5e51dcb7d..5ca812b4e 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 ba8a195c4..3a5408618 100644
--- a/installer/install.xml
+++ b/installer/install.xml
@@ -9,7 +9,7 @@
https://geti2p.net/
- 1.6
+ 1.7