forked from I2P_Developers/i2p.i2p
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
This commit is contained in:
@ -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" +
|
||||
|
@ -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 &&
|
||||
|
188
apps/sam/java/src/net/i2p/sam/SSLUtil.java
Normal file
188
apps/sam/java/src/net/i2p/sam/SSLUtil.java
Normal file
@ -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);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user