From b6cb074c043a4b883eb1f1a169a8e8dcb880b929 Mon Sep 17 00:00:00 2001
From: zzz <zzz@mail.i2p>
Date: Fri, 26 Jun 2015 15:40:20 +0000
Subject: [PATCH] Add sam.config file support and -c file option Add partial
 SSL support (will require Java 7 due to SocketChannel changes) won't compile,
 SSLServerSocketChannel and SSLSocketChannel not checked in, pending decisions
 on implementation Bump version to 3.2

---
 apps/sam/java/src/net/i2p/sam/SAMBridge.java  | 115 ++++++++---
 .../src/net/i2p/sam/SAMHandlerFactory.java    |   5 +-
 apps/sam/java/src/net/i2p/sam/SSLUtil.java    | 188 ++++++++++++++++++
 3 files changed, 284 insertions(+), 24 deletions(-)
 create mode 100644 apps/sam/java/src/net/i2p/sam/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 5f03436c2b..77ec2cffea 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,16 +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.
@@ -50,6 +57,7 @@ public class SAMBridge implements Runnable, ClientApp {
     private final String _listenHost;
     private final int _listenPort;
     private final Properties i2cpProps;
+    private final boolean _useSSL;
     private volatile Thread _runner;
 
     /** 
@@ -72,6 +80,9 @@ public class SAMBridge implements Runnable, ClientApp {
     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 = "127.0.0.1";
@@ -99,6 +110,9 @@ public class SAMBridge implements Runnable, ClientApp {
         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;
         nameToPrivKeys = new HashMap<String,String>(8);
         _handlers = new HashSet<Handler>(8);
@@ -123,11 +137,15 @@ 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) {
         _log = I2PAppContext.getGlobalContext().logManager().getLog(SAMBridge.class);
         _mgr = null;
         _listenHost = listenHost;
         _listenPort = listenPort;
+        _useSSL = isSSL;
+        if (_useSSL && !SystemVersion.isJava7())
+            throw new IllegalArgumentException("SSL requires Java 7 or higher");
+        this.i2cpProps = i2cpProps;
         persistFilename = persistFile;
         nameToPrivKeys = new HashMap<String,String>(8);
         _handlers = new HashSet<Handler>(8);
@@ -141,7 +159,6 @@ public class SAMBridge implements Runnable, ClientApp {
                            + ":" + listenPort, e);
             throw new RuntimeException(e);
         }
-        this.i2cpProps = i2cpProps;
         _state = INITIALIZED;
     }
 
@@ -149,17 +166,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);
+            }
         }
     }
 
@@ -420,7 +448,7 @@ 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);
             bridge.startThread();
         } catch (RuntimeException e) {
             e.printStackTrace();
@@ -477,15 +505,18 @@ 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;
+        String keyfile = null;
+        int port = -1;
+        String host = null;
         boolean isSSL = false;
-        Properties opts = null;
-        Getopt g = new Getopt("SAM", args, "hs");
+        String cfile = null;
+        Getopt g = new Getopt("SAM", args, "hsc:");
         int c;
         while ((c = g.getopt()) != -1) {
           switch (c) {
@@ -493,6 +524,10 @@ public class SAMBridge implements Runnable, ClientApp {
                 isSSL = true;
                 break;
 
+            case 'c':
+                cfile = g.getOptarg();
+                break;
+
             case 'h':
             case '?':
             case ':':
@@ -539,19 +574,52 @@ public class SAMBridge implements Runnable, ClientApp {
                 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) {
-       		opts = parseOptions(args, startOpts);
+       		parseOptions(args, startOpts, opts);
         }
         return new Options(host, port, isSSL, opts, keyfile);
     }
 
     /**
      *  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 Properties parseOptions(String args[], int startArgs) throws HelpRequestedException {
-        Properties props = new Properties();
+    private static void parseOptions(String args[], int startArgs, Properties props) throws HelpRequestedException {
         for (int i = startArgs; i < args.length; i++) {
             int eq = args[i].indexOf('=');
             if (eq <= 0)
@@ -567,13 +635,14 @@ public class SAMBridge implements Runnable, ClientApp {
             else
                 throw new HelpRequestedException();
         }
-        return props;
     }
     
     private static void usage() {
-        System.err.println("Usage: SAMBridge [keyfile [listenHost] listenPortNum[ name=val]*]\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" +
diff --git a/apps/sam/java/src/net/i2p/sam/SAMHandlerFactory.java b/apps/sam/java/src/net/i2p/sam/SAMHandlerFactory.java
index 582854d870..dc4c8f24f6 100644
--- a/apps/sam/java/src/net/i2p/sam/SAMHandlerFactory.java
+++ b/apps/sam/java/src/net/i2p/sam/SAMHandlerFactory.java
@@ -25,7 +25,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;
 
@@ -131,6 +131,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/SSLUtil.java b/apps/sam/java/src/net/i2p/sam/SSLUtil.java
new file mode 100644
index 0000000000..bc0331417e
--- /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.22 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);
+    }
+}
-- 
GitLab