From 3c8cc162736f63fde3aaa05f02b9332523fb43c5 Mon Sep 17 00:00:00 2001
From: zzz <zzz@mail.i2p>
Date: Mon, 30 Nov 2015 20:20:55 +0000
Subject: [PATCH] SAM: Use the Destination cache Comment out some unused
 methods SAM client: Add SSL forward support Handle header line in forwarded
 stream Name some threads, number some others

---
 apps/sam/java/src/net/i2p/sam/SAMBridge.java  |   3 +-
 .../src/net/i2p/sam/SAMStreamSession.java     |   3 +-
 apps/sam/java/src/net/i2p/sam/SAMUtils.java   |   5 +-
 .../src/net/i2p/sam/SAMv2StreamSession.java   |  15 +-
 .../src/net/i2p/sam/client/SAMReader.java     |   4 +-
 .../src/net/i2p/sam/client/SAMStreamSink.java |  85 ++++++--
 .../java/src/net/i2p/sam/client/SSLUtil.java  | 189 ++++++++++++++++++
 7 files changed, 270 insertions(+), 34 deletions(-)
 create mode 100644 apps/sam/java/src/net/i2p/sam/client/SSLUtil.java

diff --git a/apps/sam/java/src/net/i2p/sam/SAMBridge.java b/apps/sam/java/src/net/i2p/sam/SAMBridge.java
index f62e28187f..d3fffb7c64 100644
--- a/apps/sam/java/src/net/i2p/sam/SAMBridge.java
+++ b/apps/sam/java/src/net/i2p/sam/SAMBridge.java
@@ -207,8 +207,8 @@ public class SAMBridge implements Runnable, ClientApp {
      *
      * @param name name of the destination
      * @return null if the name does not exist, or if it is improperly formatted
-     * @deprecated unused
      */
+/****
     public Destination getDestination(String name) {
         synchronized (nameToPrivKeys) {
             String val = nameToPrivKeys.get(name);
@@ -224,6 +224,7 @@ public class SAMBridge implements Runnable, ClientApp {
             }
         }
     }
+****/
     
     /**
      * Retrieve the I2P private keystream for the given name, formatted
diff --git a/apps/sam/java/src/net/i2p/sam/SAMStreamSession.java b/apps/sam/java/src/net/i2p/sam/SAMStreamSession.java
index 7504072714..14ed92b3ee 100644
--- a/apps/sam/java/src/net/i2p/sam/SAMStreamSession.java
+++ b/apps/sam/java/src/net/i2p/sam/SAMStreamSession.java
@@ -226,8 +226,7 @@ class SAMStreamSession {
             return false;
         }
 
-        Destination d = new Destination();
-        d.fromBase64(dest);
+        Destination d = SAMUtils.getDest(dest);
 
         I2PSocketOptions opts = socketMgr.buildOptions(props);
         if (props.getProperty(I2PSocketOptions.PROP_CONNECT_TIMEOUT) == null)
diff --git a/apps/sam/java/src/net/i2p/sam/SAMUtils.java b/apps/sam/java/src/net/i2p/sam/SAMUtils.java
index 4d37ab2fd8..2fede075db 100644
--- a/apps/sam/java/src/net/i2p/sam/SAMUtils.java
+++ b/apps/sam/java/src/net/i2p/sam/SAMUtils.java
@@ -82,6 +82,7 @@ class SAMUtils {
      *
      * @return True if the destination is valid, false otherwise
      */
+/****
     public static boolean checkDestination(String dest) {
         try {
             Destination d = new Destination();
@@ -92,6 +93,7 @@ class SAMUtils {
             return false;
         }
     }
+****/
 
     /**
      * Check whether a base64-encoded {dest,privkey,signingprivkey} is valid
@@ -105,8 +107,7 @@ class SAMUtils {
             return false;
     	ByteArrayInputStream destKeyStream = new ByteArrayInputStream(b);
     	try {
-    		Destination d = new Destination();
-    		d.readBytes(destKeyStream);
+    		Destination d = Destination.create(destKeyStream);
     		new PrivateKey().readBytes(destKeyStream);
     		SigningPrivateKey spk = new SigningPrivateKey(d.getSigningPublicKey().getType());
     		spk.readBytes(destKeyStream);
diff --git a/apps/sam/java/src/net/i2p/sam/SAMv2StreamSession.java b/apps/sam/java/src/net/i2p/sam/SAMv2StreamSession.java
index b579812c23..dcd8ca4be4 100644
--- a/apps/sam/java/src/net/i2p/sam/SAMv2StreamSession.java
+++ b/apps/sam/java/src/net/i2p/sam/SAMv2StreamSession.java
@@ -105,29 +105,18 @@ class SAMv2StreamSession extends SAMStreamSession
 				return false ;
 			}
 
-			Destination d = new Destination();
-
-			d.fromBase64 ( dest );
-
+			Destination d = SAMUtils.getDest(dest);
 			I2PSocketOptions opts = socketMgr.buildOptions ( props );
-
 			if ( props.getProperty ( I2PSocketOptions.PROP_CONNECT_TIMEOUT ) == null )
 				opts.setConnectTimeout ( 60 * 1000 );
 
 			if (_log.shouldLog(Log.DEBUG))
 				_log.debug ( "Connecting new I2PSocket..." );
 
-
 			// non-blocking connection (SAMv2)
-
-			StreamConnector connector ;
-
-			connector = new StreamConnector ( id, d, opts );
-			
+			StreamConnector connector = new StreamConnector ( id, d, opts );
 			I2PAppThread connectThread = new I2PAppThread ( connector, "StreamConnector" + id ) ;
-
 			connectThread.start() ;
-
 			return true ;
 		}
 
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 22a09c2fbb..0ab88cfcee 100644
--- a/apps/sam/java/src/net/i2p/sam/client/SAMReader.java
+++ b/apps/sam/java/src/net/i2p/sam/client/SAMReader.java
@@ -5,6 +5,7 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.util.Properties;
 import java.util.StringTokenizer;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import net.i2p.I2PAppContext;
 import net.i2p.client.I2PSession;
@@ -22,6 +23,7 @@ public class SAMReader {
     private final SAMClientEventListener _listener;
     private volatile boolean _live;
     private Thread _thread;
+    private static final AtomicInteger _count = new AtomicInteger();
     
     public SAMReader(I2PAppContext context, InputStream samIn, SAMClientEventListener listener) {
         _log = context.logManager().getLog(SAMReader.class);
@@ -33,7 +35,7 @@ public class SAMReader {
         if (_live)
             throw new IllegalStateException();
         _live = true;
-        I2PAppThread t = new I2PAppThread(new Runner(), "SAM reader");
+        I2PAppThread t = new I2PAppThread(new Runner(), "SAM reader " + _count.incrementAndGet());
         t.start();
         _thread = t;
     }
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 d8cd8a3598..56492162e8 100644
--- a/apps/sam/java/src/net/i2p/sam/client/SAMStreamSink.java
+++ b/apps/sam/java/src/net/i2p/sam/client/SAMStreamSink.java
@@ -16,6 +16,7 @@ import java.util.Map;
 import java.util.Properties;
 import javax.net.ssl.SSLSocket;
 import javax.net.ssl.SSLServerSocket;
+import javax.net.ssl.SSLServerSocketFactory;
 
 import gnu.getopt.Getopt;
 
@@ -56,10 +57,13 @@ public class SAMStreamSink {
     private final Map<String, Sink> _remotePeers;
     private static I2PSSLSocketFactory _sslSocketFactory;
     
-    private static final int STREAM=0, DG=1, V1DG=2, RAW=3, V1RAW=4, RAWHDR = 5, FORWARD = 6;
-    private static final String USAGE = "Usage: SAMStreamSink [-s] [-m mode] [-v version] [-b samHost] [-p samPort] [-o opt=val] [-u user] [-w password] myDestFile sinkDir\n" +
-                                        "       modes: stream: 0; datagram: 1; v1datagram: 2; raw: 3; v1raw: 4; raw-with-headers: 5; stream-forward: 6\n" +
-                                        "       -s: use SSL\n" +
+    private static final int STREAM=0, DG=1, V1DG=2, RAW=3, V1RAW=4, RAWHDR = 5, FORWARD = 6, FORWARDSSL=7;
+    private static final String USAGE = "Usage: SAMStreamSink [-s] [-m mode] [-v version] [-b samHost] [-p samPort]\n" +
+                                        "                     [-o opt=val] [-u user] [-w password] myDestFile sinkDir\n" +
+                                        "       modes: stream: 0; datagram: 1; v1datagram: 2;\n" +
+                                        "              raw: 3; v1raw: 4; raw-with-headers: 5;\n" +
+                                        "              stream-forward: 6; stream-forward-ssl: 7\n" +
+                                        "       -s: use SSL to connect to bridge\n" +
                                         "       multiple -o session options are allowed";
     private static final int V3FORWARDPORT=9998;
     private static final int V3DGPORT=9999;
@@ -83,7 +87,7 @@ public class SAMStreamSink {
 
             case 'm':
                 mode = Integer.parseInt(g.getOptarg());
-                if (mode < 0 || mode > FORWARD) {
+                if (mode < 0 || mode > FORWARDSSL) {
                     System.err.println(USAGE);
                     return;
                 }
@@ -174,7 +178,7 @@ public class SAMStreamSink {
                 Thread t = new Pinger(out);
                 t.start();
             }
-            if (_isV3 && (mode == STREAM || mode == FORWARD)) {
+            if (_isV3 && (mode == STREAM || mode == FORWARD || mode == FORWARDSSL)) {
                 // test multiple acceptors, only works in 3.2
                 int acceptors = (_isV32 && mode == STREAM) ? 4 : 1;
                 for (int i = 0; i < acceptors; i++) {
@@ -193,7 +197,18 @@ public class SAMStreamSink {
                 }
                 if (mode == FORWARD) {
                     // set up a listening ServerSocket
-                    (new FwdRcvr(isSSL)).start();
+                    (new FwdRcvr(false, null)).start();
+                } else if (mode == FORWARDSSL) {
+                    // set up a listening ServerSocket
+                    String scfile = SSLUtil.DEFAULT_SAMCLIENT_CONFIGFILE;
+                    File file = new File(scfile);
+                    Properties opts = new Properties();
+                    if (file.exists())
+                        DataHelper.loadProps(opts, file);
+                    boolean shouldSave = SSLUtil.verifyKeyStore(opts);
+                    if (shouldSave)
+                        DataHelper.storeProps(opts, file);
+                    (new FwdRcvr(true, opts)).start();
                 }
             } else if (_isV3 && (mode == DG || mode == RAW || mode == RAWHDR)) {
                 // set up a listening DatagramSocket
@@ -208,7 +223,10 @@ public class SAMStreamSink {
     private class DGRcvr extends I2PAppThread {
         private final int _mode;
 
-        public DGRcvr(int mode) { _mode = mode; }
+        public DGRcvr(int mode) {
+            super("SAM DG Rcvr");
+            _mode = mode;
+        }
 
         public void run() {
             byte[] buf = new byte[32768];
@@ -257,18 +275,23 @@ public class SAMStreamSink {
 
     private class FwdRcvr extends I2PAppThread {
         private final boolean _isSSL;
+        // for SSL only
+        private final Properties _opts;
 
-        public FwdRcvr(boolean isSSL) {
-            if (isSSL)
-                throw new UnsupportedOperationException("TODO");
+        public FwdRcvr(boolean isSSL, Properties opts) {
+            super("SAM Fwd Rcvr");
             _isSSL = isSSL;
+            _opts = opts;
         }
 
         public void run() {
             try {
                 ServerSocket ss;
                 if (_isSSL) {
-                    throw new UnsupportedOperationException("TODO");
+                    SSLServerSocketFactory fact = SSLUtil.initializeFactory(_opts);
+                    SSLServerSocket sock = (SSLServerSocket) fact.createServerSocket(V3FORWARDPORT);
+                    I2PSSLSocketFactory.setProtocolsAndCiphers(sock);
+                    ss = sock;
                 } else {
                     ss = new ServerSocket(V3FORWARDPORT);
                 }
@@ -277,10 +300,40 @@ public class SAMStreamSink {
                     Sink sink = new Sink("FAKE", "FAKEFROM");
                     try {
                         InputStream in = s.getInputStream();
+                        boolean gotDest = false;
+                        byte[] dest = new byte[1024];
+                        int dlen = 0;
                         byte[] buf = new byte[32768];
                         int len;
                         while((len = in.read(buf)) >= 0) {
-                            sink.received(buf, 0, len);
+                            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) {
@@ -534,13 +587,15 @@ public class SAMStreamSink {
                         req = "STREAM ACCEPT SILENT=false TO_PORT=5678 ID=" + _v3ID + "\n";
                     else if (mode == FORWARD)
                         req = "STREAM FORWARD ID=" + _v3ID + " PORT=" + V3FORWARDPORT + '\n';
+                    else if (mode == FORWARDSSL)
+                        req = "STREAM FORWARD ID=" + _v3ID + " PORT=" + V3FORWARDPORT + " SSL=true\n";
                     else
                         throw new IllegalStateException("mode " + mode);
                     samOut.write(req.getBytes("UTF-8"));
                     samOut.flush();
                     if (_log.shouldLog(Log.DEBUG))
                         _log.debug("STREAM ACCEPT/FORWARD sent");
-                    if (mode == FORWARD) {
+                    if (mode == FORWARD || mode == FORWARDSSL) {
                         // docs were wrong, we do not get a STREAM STATUS if SILENT=true for ACCEPT
                         boolean ok = eventHandler.waitForStreamStatusReply();
                         if (!ok) 
@@ -587,7 +642,7 @@ public class SAMStreamSink {
                     dest = _destFile;
                 }
                 String style;
-                if (mode == STREAM || mode == FORWARD)
+                if (mode == STREAM || mode == FORWARD || mode == FORWARDSSL)
                     style = "STREAM";
                 else if (mode == V1DG)
                     style = "DATAGRAM";
diff --git a/apps/sam/java/src/net/i2p/sam/client/SSLUtil.java b/apps/sam/java/src/net/i2p/sam/client/SSLUtil.java
new file mode 100644
index 0000000000..4c73d98830
--- /dev/null
+++ b/apps/sam/java/src/net/i2p/sam/client/SSLUtil.java
@@ -0,0 +1,189 @@
+package net.i2p.sam.client;
+
+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 copied from net.i2p.sam for testing SSL stream forwarding
+ */
+class SSLUtil {
+
+    public static final String DEFAULT_SAMCLIENT_CONFIGFILE = "samclient.config";
+    private static final String PROP_KEYSTORE_PASSWORD = "samclient.keystorePassword";
+    private static final String DEFAULT_KEYSTORE_PASSWORD = "changeit";
+    private static final String PROP_KEY_PASSWORD = "samclient.keyPassword";
+    private static final String PROP_KEY_ALIAS = "samclient.keyAlias";
+    private static final String ASCII_KEYFILE_SUFFIX = ".local.crt";
+    private static final String PROP_KS_NAME = "samclient.keystoreFile";
+    private static final String KS_DIR = "keystore";
+    private static final String PREFIX = "samclient-";
+    private static final String KS_SUFFIX = ".ks";
+    private static final String CERT_DIR = "certificates/samclient";
+
+    /**
+     *  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(), DEFAULT_SAMCLIENT_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(), DEFAULT_SAMCLIENT_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(), DEFAULT_SAMCLIENT_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);
+    }
+}
-- 
GitLab