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