diff --git a/rexster-console/src/main/java/com/tinkerpop/rexster/console/ConsoleSettings.java b/rexster-console/src/main/java/com/tinkerpop/rexster/console/ConsoleSettings.java index 68cae61f..01aaa34d 100644 --- a/rexster-console/src/main/java/com/tinkerpop/rexster/console/ConsoleSettings.java +++ b/rexster-console/src/main/java/com/tinkerpop/rexster/console/ConsoleSettings.java @@ -27,6 +27,8 @@ public class ConsoleSettings { private final String username; private final String password; private final String fileToExecute; + private final String sslConfig; + private boolean sslEnabled; private String language; public ConsoleSettings(final String [] commandLineArgs) throws Exception { @@ -52,6 +54,9 @@ public ConsoleSettings(final String [] commandLineArgs) throws Exception { this.username = line.getOptionValue("user", ""); this.password = line.getOptionValue("pass", ""); this.fileToExecute = line.getOptionValue("execute", null); + + this.sslEnabled = Boolean.valueOf(line.getOptionValue("enablessl","false")); + this.sslConfig = line.getOptionValue("sslconfig", null); } public String getHost() { @@ -90,6 +95,10 @@ public boolean isExecuteMode() { return fileToExecute != null; } + public boolean isSslEnabled() { return sslEnabled; } + + public String getSslConfig() { return sslConfig; } + public String getHostPort() { return "[" + this.host + ":" + this.port + "]"; } @@ -148,6 +157,18 @@ private static Options getCliOptions() { .withLongOpt("pass") .create("p"); + final Option sslEnable = OptionBuilder.withArgName("enable-ssl") + .hasArg() + .withDescription("Enables ssl if true.") + .withLongOpt("enablessl") + .create("s"); + + final Option sslConfig = OptionBuilder.withArgName("ssl-config") + .hasArg() + .withDescription("SSL configuration to use.") + .withLongOpt("sslconfig") + .create("sc"); + final Options options = new Options(); options.addOption(help); options.addOption(hostName); @@ -157,6 +178,8 @@ private static Options getCliOptions() { options.addOption(scriptFile); options.addOption(username); options.addOption(password); + options.addOption(sslEnable); + options.addOption(sslConfig); return options; } diff --git a/rexster-console/src/main/java/com/tinkerpop/rexster/console/RexsterConsole.java b/rexster-console/src/main/java/com/tinkerpop/rexster/console/RexsterConsole.java index f426b8f9..c60c88e4 100644 --- a/rexster-console/src/main/java/com/tinkerpop/rexster/console/RexsterConsole.java +++ b/rexster-console/src/main/java/com/tinkerpop/rexster/console/RexsterConsole.java @@ -87,8 +87,13 @@ private void oneTimeExecuteScript(final String script) { } private void initAndOpenSessionFromSettings() { - this.session = new RemoteRexsterSession(this.settings.getHost(), this.settings.getPort(), - this.settings.getTimeout(), this.settings.getUsername(), this.settings.getPassword()); + if(!settings.isSslEnabled()) { + this.session = new RemoteRexsterSession( + this.settings.getHost(), this.settings.getPort(), this.settings.getTimeout(), this.settings.getUsername(), this.settings.getPassword()); + }else{ + this.session = new RemoteRexsterSession( + this.settings.getHost(), this.settings.getPort(), this.settings.getTimeout(), this.settings.getUsername(), this.settings.getPassword(),this.settings.getSslConfig()); + } this.session.open(); } diff --git a/rexster-core/src/main/java/com/tinkerpop/rexster/util/RexsterSslHelper.java b/rexster-core/src/main/java/com/tinkerpop/rexster/util/RexsterSslHelper.java new file mode 100644 index 00000000..32a326c1 --- /dev/null +++ b/rexster-core/src/main/java/com/tinkerpop/rexster/util/RexsterSslHelper.java @@ -0,0 +1,305 @@ +package com.tinkerpop.rexster.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLException; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; + +import org.apache.commons.configuration.CompositeConfiguration; +import org.apache.commons.configuration.Configuration; +import org.apache.commons.configuration.XMLConfiguration; +import org.apache.log4j.Logger; + +/** + * Assists in various parts of SSL setup for Rexster, particularly creating the SSLContext from a configuration. The + * default configuration is for clients relying on the default JVM TrustStore, it is expected that servers will specify + * at least some SSL properties. + */ +public class RexsterSslHelper { + public static final String KEY_HTTP_SSL_ENABLED = "http.enable-ssl"; + public static final String KEY_REXPRO_SSL_ENABLED = "rexpro.enable-ssl"; + public static final String KEY_SSL_PROTOCOL = "ssl.protocol"; + public static final String KEY_SSL_TRUST_STORE = "ssl.trust-store"; + public static final String KEY_SSL_TRUST_STORE_PASSWORD = "ssl.trust-store-password"; + public static final String KEY_SSL_TRUST_STORE_PROVIDER = "ssl.trust-store-provider"; + public static final String KEY_SSL_TRUST_MANAGER_FACTORY_ALGORITHM = "ssl.trust-manager-factory.algorithm"; + public static final String KEY_SSL_KEY_STORE = "ssl.key-store"; + public static final String KEY_SSL_KEY_STORE_PASSWORD = "ssl.key-store-password"; + public static final String KEY_SSL_KEY_STORE_PROVIDER = "ssl.key-store-provider"; + public static final String KEY_SSL_KEY_MANAGER_FACTORY_ALGORITHM = "ssl.key-manager-factory.algorithm"; + public static final String KEY_SSL_NEED_CLIENT_AUTH = "ssl.need-client-auth"; + public static final String KEY_SSL_WANT_CLIENT_AUTH = "ssl.want-client-auth"; + + private static final Logger logger = Logger.getLogger(RexsterSslHelper.class); + private static final String DEFAULT_STORE_PROVIDER = "JKS"; + private static final String DEFAULT_SSL_PROTOCOL = "TLS"; + + /** + * An empty String. Not specifying a keystore results in no keystore being used. Not having a keystore is + * appropriate for clients when client-auth is disabled, in which case only a truststore is needed. + */ + private static final String DEFAULT_KEY_STORE_PATH = ""; + + /** + * No default keystore password. + */ + private static final char[] DEFAULT_KEY_STORE_PASSWORD = null; + + /** + * Use the default JVM truststore if omit. + */ + private static final String DEFAULT_TRUST_STORE_PATH = System.getenv("JAVA_HOME") + "/jre/lib/security/cacerts"; + + /** + * Standard JVM default truststore password. + */ + private static final String DEFAULT_TRUST_STORE_PASSWORD = "changeit"; + + /** + * Configuration this class utilizes for it's SSL functions. + */ + private final Configuration configuration; + + /** + * Constructor. + * + * @param configuration Configuration which includes SSL properties. + */ + public RexsterSslHelper(Configuration configuration) { + this.configuration = configuration; + } + + /** + * Merges a configuration from a file source (if it exists) with a passed in configuration. The passed in + * configuration will override properties on the configuration loaded from a file. + * + * @param fileLocation Location of the file to load properties from. + * @param sslConfiguration Existing configuration whose properties take priority. + */ + public RexsterSslHelper(String fileLocation, Configuration sslConfiguration) { + XMLConfiguration xmlConfiguration = new XMLConfiguration(); + + if (fileLocation != null) { + File rexsterClientConfig = new File(fileLocation); + + if (rexsterClientConfig.exists()) { + logger.info( + String.format( + "Attempting to get base SSL configuration from file: %s", + rexsterClientConfig.getName())); + try { + xmlConfiguration.load( + new FileReader(rexsterClientConfig)); + logger.info( + String.format( + "Using [%s] as base SSL configuration source.", + rexsterClientConfig.getAbsolutePath())); + } catch (Exception e) { + final String msg = String.format( + "Could not load configuration from [%s]", rexsterClientConfig.getAbsolutePath()); + logger.warn(msg); + throw new RuntimeException(msg); + } + } else { + final String msg = String.format( + "No configuration found for client SSL at: [%s], using default SSL configuration as base", + rexsterClientConfig.getAbsolutePath()); + logger.info(msg); + } + } else { + final String msg = "No configuration specified for client SSL, using default SSL configuration as base."; + logger.info(msg); + } + + final CompositeConfiguration jointSslConfig = new CompositeConfiguration(sslConfiguration); + jointSslConfig.addConfiguration(xmlConfiguration); + this.configuration = jointSslConfig; + logger.info("Existing SSL configuration successfully merged into base configuration."); + } + + /** + * Logs a message and throws an SSLException with the same message. + * + * @param msg The message to log and pass into the exception. + * @param e The exception to throw with the message. + * @throws SSLException Rethrown exception indicating the cause exception was SSL-related. + */ + private static void logAndRethrow(String msg, Exception e) throws SSLException { + logger.error(msg, e); + throw new SSLException(msg, e); + } + + /** + * Creates an {@link javax.net.ssl.SSLContext} object using {@code configuration}. + * + * @return An SSLContext object that can be used for securing Rexster servers with SSL. + * @throws SSLException If a variety of SSL related exceptions occur. + */ + public SSLContext createRexsterSslContext() throws SSLException { + String rexsterHome = System.getenv("REXSTER_HOME"); + rexsterHome = rexsterHome == null ? "" : rexsterHome; + + logger.info("Creating SSLContext."); + final char[] secretServerPassword = getKeyStorePassword(); + + TrustManagerFactory tmf = null; + KeyManagerFactory kmf = null; + + try { + tmf = initTrustManagerFactory(rexsterHome); + // Keystore only used if it has been specified. + if (!getKeyStore().isEmpty()) { + kmf = initKeyManagerFactory(rexsterHome, secretServerPassword); + } + } catch (final IOException e) { + logAndRethrow("Problem loading KeyStore files!", e); + } catch (final CertificateException e) { + logAndRethrow("Problem with certificate while loading KeyStore!", e); + } catch (final NoSuchAlgorithmException e) { + logAndRethrow("Invalid KeyStore algorithm!", e); + } catch (final UnrecoverableKeyException e) { + logAndRethrow("Problem initializing KeyManagerFactory!", e); + } catch (final KeyStoreException e) { + logAndRethrow("Unable to load Keystore!", e); + } + + return initSslContext(tmf, kmf); + } + + public final String getSslProtocol() { + return configuration.getString(KEY_SSL_PROTOCOL, DEFAULT_SSL_PROTOCOL); + } + + public final String getTrustStore() { + return configuration.getString(KEY_SSL_TRUST_STORE, DEFAULT_TRUST_STORE_PATH); + } + + public final char[] getTrustStorePassword() { + return configuration.getString(KEY_SSL_TRUST_STORE_PASSWORD, DEFAULT_TRUST_STORE_PASSWORD).toCharArray(); + } + + public final String getTrustStoreProvider() { + return configuration.getString(KEY_SSL_TRUST_STORE_PROVIDER, DEFAULT_STORE_PROVIDER); + } + + public final String getKeyStore() { + return configuration.getString(KEY_SSL_KEY_STORE, DEFAULT_KEY_STORE_PATH); + } + + public final String getKeyStoreProvider() { + return configuration.getString(KEY_SSL_KEY_STORE_PROVIDER, DEFAULT_STORE_PROVIDER); + } + + public final boolean getNeedClientAuth() { + return configuration.getBoolean(KEY_SSL_NEED_CLIENT_AUTH, false); + } + + public final boolean getWantClientAuth() { + return configuration.getBoolean(KEY_SSL_WANT_CLIENT_AUTH, false); + } + + private char[] getKeyStorePassword() { + final String keyStorePassword = configuration.getString(KEY_SSL_KEY_STORE_PASSWORD, null); + if (keyStorePassword == null) { + return DEFAULT_KEY_STORE_PASSWORD; + } + return keyStorePassword.toCharArray(); + } + + /** + * Initializes the SSL Context based on {@code configuration}; + * + * @param trustManagerFactory Provides trust managers to use with the produced SSLContext, if any. + * @param keyManagerFactory Provides key managers to use with the produced SSLContext, if any. + * @return an SSLContext based on this class' configuration. + * @throws SSLException If a variety of SSL related errors occur. + */ + private SSLContext initSslContext(TrustManagerFactory trustManagerFactory, KeyManagerFactory keyManagerFactory) + throws SSLException { + SSLContext sslContext = null; + final String sslProtocol = getSslProtocol(); + + KeyManager[] keyManagers = null; + + if (keyManagerFactory != null) { + keyManagers = keyManagerFactory.getKeyManagers(); + } + final TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); + try { + sslContext = SSLContext.getInstance(sslProtocol); + sslContext.init(keyManagers, trustManagers, null); + } catch (final NoSuchAlgorithmException e) { + logAndRethrow(String.format("Invalid SSL Protocol '%s'", sslProtocol), e); + } catch (final KeyManagementException e) { + logAndRethrow("Unable to initialize SSLContext.", e); + } + return sslContext; + } + + private TrustManagerFactory initTrustManagerFactory(String rexsterHome) + throws IOException, KeyStoreException, NoSuchAlgorithmException, CertificateException { + final String tmfAlgorithm = this.configuration.getString( + KEY_SSL_TRUST_MANAGER_FACTORY_ALGORITHM, TrustManagerFactory.getDefaultAlgorithm()); + final TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm); + + final KeyStore trustStore = KeyStore.getInstance(getTrustStoreProvider()); + InputStream trustStoreInputStream = null; + + final String trustStorePath = getTrustStore(); + + // If truststore is intentionally blank, do not use a truststore. + if (!trustStorePath.isEmpty()) { + try { + trustStoreInputStream = new FileInputStream(rexsterHome + trustStorePath); + trustStore.load(trustStoreInputStream, getTrustStorePassword()); + } finally { + if (trustStoreInputStream != null) { + trustStoreInputStream.close(); + } + } + } + tmf.init(trustStore); + + return tmf; + } + + private KeyManagerFactory initKeyManagerFactory( + String rexsterHome, char[] secretServerPassword) + throws NoSuchAlgorithmException, IOException, CertificateException, UnrecoverableKeyException, + KeyStoreException { + final String kmfAlgorithm = this.configuration.getString( + KEY_SSL_KEY_MANAGER_FACTORY_ALGORITHM, KeyManagerFactory.getDefaultAlgorithm()); + + final KeyManagerFactory kmf = KeyManagerFactory.getInstance(kmfAlgorithm); + + final KeyStore keyStore = KeyStore.getInstance(getKeyStoreProvider()); + final String keyStorePath = getKeyStore(); + InputStream keyStoreInputStream = null; + + try { + keyStoreInputStream = new FileInputStream(rexsterHome + keyStorePath); + keyStore.load(keyStoreInputStream, secretServerPassword); + kmf.init(keyStore, secretServerPassword); + } finally { + if (keyStoreInputStream != null) { + keyStoreInputStream.close(); + } + } + + return kmf; + } +} diff --git a/rexster-protocol/src/main/java/com/tinkerpop/rexster/client/RemoteRexsterSession.java b/rexster-protocol/src/main/java/com/tinkerpop/rexster/client/RemoteRexsterSession.java index 9959a47e..b3b95b35 100644 --- a/rexster-protocol/src/main/java/com/tinkerpop/rexster/client/RemoteRexsterSession.java +++ b/rexster-protocol/src/main/java/com/tinkerpop/rexster/client/RemoteRexsterSession.java @@ -23,6 +23,7 @@ public class RemoteRexsterSession { private String rexProHost = "localhost"; private String username = ""; private String password = ""; + private String sslConfig = ""; private RexProClientConnection rexProConnection; @@ -43,6 +44,16 @@ public RemoteRexsterSession(String rexProHost, int rexProPort, int timeout, Stri this.rexProConnection = new RexProClientConnection(rexProHost, rexProPort); } + public RemoteRexsterSession(String rexProHost, int rexProPort, int timeout, String username, String password,String sslConfigFile) { + this.rexProHost = rexProHost; + this.rexProPort = rexProPort; + this.timeout = timeout; + this.username = username; + this.password = password; + this.sslConfig = sslConfigFile; + this.rexProConnection = new RexProClientConnection(rexProHost, rexProPort, sslConfigFile); + } + public void open() { if (sessionKey == RexProMessage.EMPTY_SESSION) { SessionRequestMessage sessionRequestMessageToSend = new SessionRequestMessage(); diff --git a/rexster-protocol/src/main/java/com/tinkerpop/rexster/client/RexProClientConnection.java b/rexster-protocol/src/main/java/com/tinkerpop/rexster/client/RexProClientConnection.java index 5ca60695..1f0c8fbb 100644 --- a/rexster-protocol/src/main/java/com/tinkerpop/rexster/client/RexProClientConnection.java +++ b/rexster-protocol/src/main/java/com/tinkerpop/rexster/client/RexProClientConnection.java @@ -1,5 +1,6 @@ package com.tinkerpop.rexster.client; +import com.tinkerpop.rexster.protocol.filter.RexsterClientSslFilterHelper; import com.tinkerpop.rexster.protocol.msg.RexProMessage; import org.glassfish.grizzly.Connection; import org.glassfish.grizzly.filterchain.BaseFilter; @@ -23,19 +24,21 @@ public final class RexProClientConnection { public static final int DEFAULT_TIMEOUT_SECONDS = 100; - private final Connection connection; + private Connection connection; private final BlockingQueue responseQueue = new SynchronousQueue(true); - private final TCPNIOTransport transport = getTransport(responseQueue); + private TCPNIOTransport transport; + private final String sslConfigFile; public RexProClientConnection(String rexProHost, int rexProPort) { - try { - transport.start(); + sslConfigFile = null; + transport = getTransport(responseQueue); + startTransport(rexProHost, rexProPort, transport); + } - connection = transport.connect(rexProHost, rexProPort).get(10, TimeUnit.SECONDS); - connection.configureBlocking(true); - } catch (Exception e) { - throw new AssertionError(e); - } + public RexProClientConnection(String rexProHost, int rexProPort, String sslConfig) { + sslConfigFile = sslConfig; + transport = getSecureTransport(responseQueue, sslConfigFile); + startTransport(rexProHost, rexProPort, transport); } public RexProMessage sendMessage(RexProMessage messageToSend) throws IOException { @@ -84,4 +87,41 @@ public NextAction handleRead(final FilterChainContext ctx) throws IOException { return transport; } + + public static TCPNIOTransport getSecureTransport(final BlockingQueue responseQueue, String sslConfigPath) { + // Create a FilterChain using FilterChainBuilder + final FilterChainBuilder filterChainBuilder = FilterChainBuilder.stateless(); + + // Add TransportFilter, which is responsible + // for reading and writing data to the connection + filterChainBuilder.add(new TransportFilter()); + filterChainBuilder.add(new RexsterClientSslFilterHelper(sslConfigPath).getSslFilter()); + filterChainBuilder.add(new RexProClientFilter()); + filterChainBuilder.add(new BaseFilter() { + @Override + public NextAction handleRead(final FilterChainContext ctx) throws IOException { + try { + responseQueue.put((RexProMessage) ctx.getMessage()); + } catch (InterruptedException ignored) { + } + return ctx.getStopAction(); + } + }); + + // Create TCP NIO transport + final TCPNIOTransport transport = TCPNIOTransportBuilder.newInstance().build(); + transport.setProcessor(filterChainBuilder.build()); + + return transport; + } + + private void startTransport(String rexProHost, int rexProPort, TCPNIOTransport transport) { + try { + transport.start(); + connection = transport.connect(rexProHost, rexProPort).get(10, TimeUnit.SECONDS); + connection.configureBlocking(true); + } catch (Exception e) { + throw new AssertionError(e); + } + } } diff --git a/rexster-protocol/src/main/java/com/tinkerpop/rexster/client/RexsterClientFactory.java b/rexster-protocol/src/main/java/com/tinkerpop/rexster/client/RexsterClientFactory.java index 5690b523..65dff029 100644 --- a/rexster-protocol/src/main/java/com/tinkerpop/rexster/client/RexsterClientFactory.java +++ b/rexster-protocol/src/main/java/com/tinkerpop/rexster/client/RexsterClientFactory.java @@ -1,12 +1,16 @@ package com.tinkerpop.rexster.client; +import com.tinkerpop.rexster.protocol.filter.RexsterClientSslFilterHelper; import com.tinkerpop.rexster.protocol.serializer.msgpack.MsgPackSerializer; +import com.tinkerpop.rexster.util.RexsterSslHelper; + import org.apache.commons.configuration.BaseConfiguration; import org.apache.commons.configuration.CompositeConfiguration; import org.apache.commons.configuration.Configuration; import org.apache.commons.configuration.ConfigurationUtils; import org.apache.commons.configuration.MapConfiguration; import org.apache.log4j.Logger; +import org.glassfish.grizzly.Processor; import org.glassfish.grizzly.filterchain.FilterChainBuilder; import org.glassfish.grizzly.filterchain.TransportFilter; import org.glassfish.grizzly.nio.transport.TCPNIOTransport; @@ -39,13 +43,20 @@ public class RexsterClientFactory { addProperty(RexsterClientTokens.CONFIG_GRAPH_NAME, null); addProperty(RexsterClientTokens.CONFIG_TRANSACTION, true); addProperty(RexsterClientTokens.CONFIG_SERIALIZER, MsgPackSerializer.SERIALIZER_ID); + addProperty(RexsterClientTokens.CONFIG_ENABLE_CLIENT_SSL, false); + addProperty(RexsterClientTokens.CONFIG_CLIENT_SSL_CONFIGURATION, null); }}; /** - * The transport used by all instantiated RexsterClient objects. + * The transport used by all instantiated RexsterClient objects not using SSL. */ private static TCPNIOTransport transport; + /** + * The transport used by all instantiated RexsterClientObjects that use SSL. + */ + private static TCPNIOTransport secureTransport; + /** * Creates a RexsterClient instance with default settings for the factory using localhost and 8184 for the port. */ @@ -74,6 +85,33 @@ public static RexsterClient open(final String host, final int port) throws Excep return open(specificConfiguration); } + /** + * Creates a RexsterClient instance allowing the override of host and port and secured using + * SSL properties found at the default location. + */ + public static RexsterClient openSecure(final String host, final int port) throws Exception{ + final BaseConfiguration specificConfiguration = new BaseConfiguration(); + specificConfiguration.addProperty(RexsterClientTokens.CONFIG_HOSTNAME, host); + specificConfiguration.addProperty(RexsterClientTokens.CONFIG_PORT, port); + specificConfiguration.addProperty(RexsterClientTokens.CONFIG_ENABLE_CLIENT_SSL, true); + + return open(specificConfiguration); + } + + /** + * Creates a RexsterClient instance allowing the override of host and port and secured using + * SSL properties found at the passed sslConfigFile location. + */ + public static RexsterClient openSecure(final String host, final int port, String sslConfigFile) throws Exception{ + final BaseConfiguration specificConfiguration = new BaseConfiguration(); + specificConfiguration.addProperty(RexsterClientTokens.CONFIG_HOSTNAME, host); + specificConfiguration.addProperty(RexsterClientTokens.CONFIG_PORT, port); + specificConfiguration.addProperty(RexsterClientTokens.CONFIG_ENABLE_CLIENT_SSL, true); + specificConfiguration.addProperty(RexsterClientTokens.CONFIG_CLIENT_SSL_CONFIGURATION, sslConfigFile); + + return open(specificConfiguration); + } + /** * Creates a RexsterClient instance using 8184 for the port and allowing explicit specification of the * name of the graph to connect to. Passing a value other than null will automatically establish a binding @@ -116,7 +154,10 @@ public static synchronized RexsterClient open(final Configuration specificConfig final CompositeConfiguration jointConfig = new CompositeConfiguration(defaultConfiguration); jointConfig.addConfiguration(specificConfiguration); - final RexsterClient client = new RexsterClient(jointConfig, getTransport()); + final TCPNIOTransport tcpnioTransport = + jointConfig.getBoolean(RexsterClientTokens.CONFIG_ENABLE_CLIENT_SSL) ? getSecureTransport(jointConfig) : + getTransport(); + final RexsterClient client = new RexsterClient(jointConfig, tcpnioTransport); logger.info(String.format("Create RexsterClient instance: [%s]", ConfigurationUtils.toString(jointConfig))); @@ -125,12 +166,6 @@ public static synchronized RexsterClient open(final Configuration specificConfig private synchronized static TCPNIOTransport getTransport() throws Exception { if (transport == null) { - final RexsterClientHandler handler = new RexsterClientHandler(); - final FilterChainBuilder filterChainBuilder = FilterChainBuilder.stateless(); - filterChainBuilder.add(new TransportFilter()); - filterChainBuilder.add(new RexProClientFilter()); - filterChainBuilder.add(handler); - transport = TCPNIOTransportBuilder.newInstance().build(); transport.setIOStrategy(LeaderFollowerNIOStrategy.getInstance()); final ThreadPoolConfig workerThreadPoolConfig = ThreadPoolConfig.defaultConfig() @@ -141,10 +176,44 @@ private synchronized static TCPNIOTransport getTransport() throws Exception { .setCorePoolSize(4) .setMaxPoolSize(12); transport.setKernelThreadPoolConfig(kernalThreadPoolConfig); - transport.setProcessor(filterChainBuilder.build()); + transport.setProcessor(getRexsterProcessor(false, null)); transport.start(); } return transport; } + + private synchronized static TCPNIOTransport getSecureTransport(Configuration sslConfiguration) throws Exception { + //Recreating the transport is necessary as SSL settings may have changed + if (secureTransport != null) { + secureTransport.stop(); + } + + secureTransport = TCPNIOTransportBuilder.newInstance().build(); + secureTransport.setIOStrategy(LeaderFollowerNIOStrategy.getInstance()); + final ThreadPoolConfig workerThreadPoolConfig = ThreadPoolConfig.defaultConfig() + .setCorePoolSize(4) + .setMaxPoolSize(12); + secureTransport.setWorkerThreadPoolConfig(workerThreadPoolConfig); + final ThreadPoolConfig kernalThreadPoolConfig = ThreadPoolConfig.defaultConfig() + .setCorePoolSize(4) + .setMaxPoolSize(12); + secureTransport.setKernelThreadPoolConfig(kernalThreadPoolConfig); + secureTransport.setProcessor(getRexsterProcessor(true, sslConfiguration)); + secureTransport.start(); + + return secureTransport; + } + + private static Processor getRexsterProcessor(boolean useSsl, Configuration sslConfiguration) { + final RexsterClientHandler handler = new RexsterClientHandler(); + final FilterChainBuilder filterChainBuilder = FilterChainBuilder.stateless(); + filterChainBuilder.add(new TransportFilter()); + if (useSsl) { + filterChainBuilder.add(new RexsterClientSslFilterHelper(sslConfiguration).getSslFilter()); + } + filterChainBuilder.add(new RexProClientFilter()); + filterChainBuilder.add(handler); + return filterChainBuilder.build(); + } } diff --git a/rexster-protocol/src/main/java/com/tinkerpop/rexster/client/RexsterClientTokens.java b/rexster-protocol/src/main/java/com/tinkerpop/rexster/client/RexsterClientTokens.java index 9362b88d..18e2f8fa 100644 --- a/rexster-protocol/src/main/java/com/tinkerpop/rexster/client/RexsterClientTokens.java +++ b/rexster-protocol/src/main/java/com/tinkerpop/rexster/client/RexsterClientTokens.java @@ -18,4 +18,6 @@ public class RexsterClientTokens { public static final String CONFIG_MAX_ASYNC_WRITE_QUEUE_BYTES = "max-async-write-queue-size"; public static final String CONFIG_LANGUAGE = "language"; public static final String CONFIG_SERIALIZER = "serializer"; + public static final String CONFIG_ENABLE_CLIENT_SSL = "use-ssl"; + public static final String CONFIG_CLIENT_SSL_CONFIGURATION = "ssl-config-path"; } diff --git a/rexster-protocol/src/main/java/com/tinkerpop/rexster/protocol/filter/RexsterClientSslFilterHelper.java b/rexster-protocol/src/main/java/com/tinkerpop/rexster/protocol/filter/RexsterClientSslFilterHelper.java new file mode 100644 index 00000000..fde1363a --- /dev/null +++ b/rexster-protocol/src/main/java/com/tinkerpop/rexster/protocol/filter/RexsterClientSslFilterHelper.java @@ -0,0 +1,76 @@ +package com.tinkerpop.rexster.protocol.filter; + +import javax.net.ssl.SSLContext; + +import org.apache.commons.configuration.BaseConfiguration; +import org.apache.commons.configuration.Configuration; +import org.apache.log4j.Logger; +import org.glassfish.grizzly.ssl.SSLEngineConfigurator; +import org.glassfish.grizzly.ssl.SSLFilter; + +import com.tinkerpop.rexster.client.RexsterClientTokens; +import com.tinkerpop.rexster.util.RexsterSslHelper; + +/** + * Helps set up SSL for Rexpro clients by providing a {@link org.glassfish.grizzly.ssl.SSLFilter} that can be configured + * to work with RexPro servers. The {@link com.tinkerpop.rexster.util.RexsterSslHelper} is used to build the {@link + * javax.net.ssl.SSLContext}. + */ +public class RexsterClientSslFilterHelper { + private static final Logger logger = Logger.getLogger(RexsterClientSslFilterHelper.class); + + /** + * Configuration to pass to the {@link com.tinkerpop.rexster.util.RexsterSslHelper} when creating the {@link + * javax.net.ssl.SSLContext}. + */ + private final Configuration sslConfiguration; + + /** + * Construct a new RexsterClientSslFilterHelper with a desired SSL configuration. + * + * @param sslConfiguration The SSL configuration to use. + */ + public RexsterClientSslFilterHelper(Configuration sslConfiguration) { + this.sslConfiguration = sslConfiguration; + } + + /** + * Construct a new RexsterClientSslFilterHelper with the location of a file containing the desired SSL + * configuration. + * + * @param sslConfigurationFile The path of an XML formatted SSL configuration file to get SSL configuration + * from. + */ + public RexsterClientSslFilterHelper(String sslConfigurationFile) { + final Configuration config = new BaseConfiguration(); + config.setProperty(RexsterClientTokens.CONFIG_CLIENT_SSL_CONFIGURATION, sslConfigurationFile); + this.sslConfiguration = config; + } + + /** + * Get an {@link org.glassfish.grizzly.ssl.SSLFilter} based on the configuration this RexsterClientSslFilterHelper + * was constructed with. + * + * @return An SSL filter that can be used to secure a RexPro client with SSL. + */ + public SSLFilter getSslFilter() { + final RexsterSslHelper clientSslHelper = new RexsterSslHelper( + sslConfiguration.getString(RexsterClientTokens.CONFIG_CLIENT_SSL_CONFIGURATION), sslConfiguration); + logger.info("Attempting to configure SSLFilter for RexPro client."); + SSLContext sslContext = null; + try { + sslContext = clientSslHelper.createRexsterSslContext(); + } catch (final Exception e) { + final String msg = "Failed to initialize SSLContext for RexPro client."; + logger.error(msg, e); + throw new RuntimeException(msg, e); + } + final SSLEngineConfigurator server = + new SSLEngineConfigurator(sslContext).setNeedClientAuth(clientSslHelper.getNeedClientAuth()) + .setWantClientAuth(clientSslHelper.getWantClientAuth()).setClientMode(false); + final SSLEngineConfigurator client = new SSLEngineConfigurator(sslContext).setClientMode(true); + + logger.info("SSLFilter configured for RexPro client!"); + return new SSLFilter(server, client); + } +} \ No newline at end of file diff --git a/rexster-server/config/rexster.xml b/rexster-server/config/rexster.xml index a065321c..0f46e3f9 100644 --- a/rexster-server/config/rexster.xml +++ b/rexster-server/config/rexster.xml @@ -7,6 +7,7 @@ public UTF-8 false + false true 2097152 8192 @@ -30,6 +31,7 @@ 3000000 65536 false + false 8 @@ -67,6 +69,23 @@ + + TLS + JKS + JKS + + config/ssl/serverKeyStore.jks + + + + SunX509 + + + SunX509 + + false + false + jmx diff --git a/rexster-server/src/integration/java/com/tinkerpop/rexster/AbstractResourceIntegrationTest.java b/rexster-server/src/integration/java/com/tinkerpop/rexster/AbstractResourceIntegrationTest.java index a621b3f6..fce8a2f4 100644 --- a/rexster-server/src/integration/java/com/tinkerpop/rexster/AbstractResourceIntegrationTest.java +++ b/rexster-server/src/integration/java/com/tinkerpop/rexster/AbstractResourceIntegrationTest.java @@ -26,6 +26,7 @@ public abstract class AbstractResourceIntegrationTest { protected RexsterServer rexsterServer; protected final ClientConfig clientConfiguration = new DefaultClientConfig(); protected Client client; + private RexsterApplication application; static { EngineController.configure(-1, null); @@ -39,7 +40,7 @@ public void setUp() throws Exception { rexsterServer = new HttpRexsterServer(properties); final List graphConfigs = properties.configurationsAt(Tokens.REXSTER_GRAPH_PATH); - final RexsterApplication application = new XmlRexsterApplication(graphConfigs); + application = new XmlRexsterApplication(graphConfigs); rexsterServer.start(application); client = Client.create(clientConfiguration); @@ -47,6 +48,7 @@ public void setUp() throws Exception { public void tearDown() throws Exception { rexsterServer.stop(); + application.stop(); } protected URI createUri(String path) { @@ -206,7 +208,7 @@ private static void clean() { removeDirectory(new File("/tmp/rexster-integration-tests")); } - private static boolean removeDirectory(final File directory) { + public static boolean removeDirectory(final File directory) { if (directory == null) return false; if (!directory.exists()) diff --git a/rexster-server/src/integration/java/com/tinkerpop/rexster/rexpro/AbstractRexProIntegrationTest.java b/rexster-server/src/integration/java/com/tinkerpop/rexster/rexpro/AbstractRexProIntegrationTest.java index 46f6d8ca..d1d3cdb8 100644 --- a/rexster-server/src/integration/java/com/tinkerpop/rexster/rexpro/AbstractRexProIntegrationTest.java +++ b/rexster-server/src/integration/java/com/tinkerpop/rexster/rexpro/AbstractRexProIntegrationTest.java @@ -27,6 +27,7 @@ public abstract class AbstractRexProIntegrationTest { protected RexsterServer rexsterServer; + private RexsterApplication application; static { EngineController.configure(-1, null); @@ -43,13 +44,14 @@ public void setUp() throws Exception { rexsterServer = new RexProRexsterServer(properties); final List graphConfigs = properties.configurationsAt(Tokens.REXSTER_GRAPH_PATH); - final RexsterApplication application = new XmlRexsterApplication(graphConfigs); + application = new XmlRexsterApplication(graphConfigs); EngineController.configure(-1, null); rexsterServer.start(application); } @After public void tearDown() throws Exception { + application.stop(); rexsterServer.stop(); } diff --git a/rexster-server/src/integration/java/com/tinkerpop/rexster/rexpro/AbstractRexsterClientIntegrationTest.java b/rexster-server/src/integration/java/com/tinkerpop/rexster/rexpro/AbstractRexsterClientIntegrationTest.java index f5f4ef3b..fb05f809 100644 --- a/rexster-server/src/integration/java/com/tinkerpop/rexster/rexpro/AbstractRexsterClientIntegrationTest.java +++ b/rexster-server/src/integration/java/com/tinkerpop/rexster/rexpro/AbstractRexsterClientIntegrationTest.java @@ -385,6 +385,7 @@ public void executeForProperties() throws Exception { assertEquals("age", k.get(0)); assertEquals("name", k.get(1)); + client.close(); } @Test @@ -398,6 +399,7 @@ public void executeForTextWithBreaks() throws Exception { assertEquals(1, text.size()); assertEquals("test1\r\ntest2\r\ntest3", text.get(0)); + client.close(); } /* this test fails on neo4j given inconsistencies in its blueprints implementation. a failing test diff --git a/rexster-server/src/integration/java/com/tinkerpop/rexster/rexpro/AbstractScriptRequestIntegrationTest.java b/rexster-server/src/integration/java/com/tinkerpop/rexster/rexpro/AbstractScriptRequestIntegrationTest.java index e3bae3c2..45833df2 100644 --- a/rexster-server/src/integration/java/com/tinkerpop/rexster/rexpro/AbstractScriptRequestIntegrationTest.java +++ b/rexster-server/src/integration/java/com/tinkerpop/rexster/rexpro/AbstractScriptRequestIntegrationTest.java @@ -40,6 +40,8 @@ public void testGraphObjMetaOnSessionlessRequest() throws Exception { Assert.assertTrue(inMsg instanceof ScriptResponseMessage); Assert.assertTrue(((ScriptResponseMessage) inMsg).Results.get() != null); } + + client.close(); } /** @@ -88,6 +90,8 @@ public void testBindingsDontStickAroundAfterRequests() throws Exception { Assert.assertTrue(inMsg instanceof ErrorResponseMessage); Assert.assertEquals(((ErrorResponseMessage) inMsg).metaGetFlag(), ErrorResponseMessage.SCRIPT_FAILURE_ERROR); } + + client.close(); } @Test @@ -132,6 +136,8 @@ public void testGraphObjMetaOnSessionedRequest() throws Exception { Assert.assertTrue(inMsg instanceof ErrorResponseMessage); Assert.assertEquals(((ErrorResponseMessage) inMsg).metaGetFlag(), ErrorResponseMessage.SCRIPT_FAILURE_ERROR); } + + client.close(); } @Test @@ -164,6 +170,8 @@ public void testGraphObjMetaOnSessionWithExistingGraphObjFails() throws Exceptio Assert.assertTrue(inMsg instanceof ErrorResponseMessage); Assert.assertEquals(ErrorResponseMessage.GRAPH_CONFIG_ERROR, ((ErrorResponseMessage) inMsg).metaGetFlag()); } + + client.close(); } @Test @@ -182,6 +190,8 @@ public void testDefiningNonExistentGraphNameFails() throws Exception { RexProMessage inMsg = client.execute(scriptMessage); Assert.assertTrue(inMsg instanceof ErrorResponseMessage); Assert.assertEquals(((ErrorResponseMessage) inMsg).metaGetFlag(), ErrorResponseMessage.GRAPH_CONFIG_ERROR); + + client.close(); } /** @@ -226,6 +236,7 @@ public void testQueryIsolation() throws Exception { Assert.assertTrue(inMsg instanceof ErrorResponseMessage); Assert.assertEquals(((ErrorResponseMessage) inMsg).metaGetFlag(), ErrorResponseMessage.SCRIPT_FAILURE_ERROR); + client.close(); } @Test @@ -266,6 +277,8 @@ public void testDisabledQueryIsolation() throws Exception { inMsg = client.execute(scriptMessage2); Assert.assertTrue(inMsg instanceof ScriptResponseMessage); Assert.assertTrue(((ScriptResponseMessage) inMsg).Results.get() != null); + + client.close(); } @Test @@ -308,6 +321,8 @@ public void testDisabledQueryIsolationInSession() throws Exception { inMsg = client.execute(scriptMessage2); Assert.assertTrue(inMsg instanceof ScriptResponseMessage); Assert.assertTrue(((ScriptResponseMessage) inMsg).Results.get() != null); + + client.close(); } @Test @@ -338,6 +353,7 @@ public void testTransactionMetaFlagWithoutSession() throws Exception { Assert.assertTrue(inMsg instanceof ScriptResponseMessage); Assert.assertTrue(((ScriptResponseMessage) inMsg).Results.get() != null); + client.close(); } @Test @@ -359,6 +375,8 @@ public void testTransactionMetaFlagWithSession() throws Exception { Assert.assertTrue(inMsg instanceof ScriptResponseMessage); Assert.assertTrue(((ScriptResponseMessage) inMsg).Results.get() != null); } + + client.close(); } } diff --git a/rexster-server/src/integration/java/com/tinkerpop/rexster/rexpro/AbstractSessionRequestMessageTest.java b/rexster-server/src/integration/java/com/tinkerpop/rexster/rexpro/AbstractSessionRequestMessageTest.java index f877cb87..87063126 100644 --- a/rexster-server/src/integration/java/com/tinkerpop/rexster/rexpro/AbstractSessionRequestMessageTest.java +++ b/rexster-server/src/integration/java/com/tinkerpop/rexster/rexpro/AbstractSessionRequestMessageTest.java @@ -59,6 +59,8 @@ public void testSessionRequestAndResponse() throws Exception { inMsg = client.execute(scriptMessage); Assert.assertTrue(inMsg instanceof ErrorResponseMessage); Assert.assertEquals(((ErrorResponseMessage) inMsg).metaGetFlag(), ErrorResponseMessage.INVALID_SESSION_ERROR); + + client.close(); } /** @@ -93,6 +95,8 @@ public void testSessionGraphDefinition() throws Exception { Assert.assertTrue(inMsg instanceof ScriptResponseMessage); Assert.assertTrue(((ScriptResponseMessage) inMsg).Results.get() != null); } + + client.close(); } /** @@ -113,6 +117,7 @@ public void testDefiningNonExistentGraphNameFails() throws Exception { Assert.assertTrue(inMsg instanceof ErrorResponseMessage); Assert.assertEquals(((ErrorResponseMessage) inMsg).metaGetFlag(), ErrorResponseMessage.GRAPH_CONFIG_ERROR); + client.close(); } } diff --git a/rexster-server/src/integration/java/com/tinkerpop/rexster/ssl/RexsterHttpSslTest.java b/rexster-server/src/integration/java/com/tinkerpop/rexster/ssl/RexsterHttpSslTest.java new file mode 100644 index 00000000..a952540e --- /dev/null +++ b/rexster-server/src/integration/java/com/tinkerpop/rexster/ssl/RexsterHttpSslTest.java @@ -0,0 +1,330 @@ +package com.tinkerpop.rexster.ssl; + +import static org.junit.Assert.fail; +import static javax.ws.rs.HttpMethod.GET; + +import com.sun.jersey.api.client.Client; +import com.sun.jersey.api.client.ClientHandlerException; +import com.sun.jersey.api.client.ClientRequest; +import com.sun.jersey.api.client.config.ClientConfig; +import com.sun.jersey.api.client.config.DefaultClientConfig; +import com.sun.jersey.client.urlconnection.HTTPSProperties; +import com.tinkerpop.rexster.AbstractResourceIntegrationTest; +import com.tinkerpop.rexster.Application; +import com.tinkerpop.rexster.Tokens; +import com.tinkerpop.rexster.server.HttpRexsterServer; +import com.tinkerpop.rexster.server.RexsterApplication; +import com.tinkerpop.rexster.server.RexsterServer; +import com.tinkerpop.rexster.server.XmlRexsterApplication; + +import org.apache.commons.configuration.HierarchicalConfiguration; +import org.apache.commons.configuration.XMLConfiguration; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.SocketException; +import java.net.URI; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.util.List; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.TrustManagerFactory; + +/** + * Tests Rexster HTTP SSL support. Note that the same keystore is used as both keystore and truststore for brevity, this + * shouldn't be done in production. See {@code rexster-integration-test-ssl.xml} in the test resources dir for the SSL + * configuration being used by Rexster. + */ +public class RexsterHttpSslTest { + private static final Logger logger = LoggerFactory.getLogger(RexsterHttpSslTest.class); + private static final String REXSTER_INTEGRATION_TEST_DIR = "/tmp/rexster-integration-tests"; + private static final String CLIENT_KEYSTORE_PATH = "clientSslKeys.jks"; + private static final String UNTRUSTED_KEYSTORE = CLIENT_KEYSTORE_PATH + "_untrusted"; + private static final String SERVER_KEYSTORE_PATH = "serverSslKeys.jks"; + private static final String CLIENT_CERT = "client.cert"; + private static final String SERVER_CERT = "server.cert"; + private static final String BASE_URI = "https://127.0.0.1:8182"; + private static final URI GRAPHS_URI = URI.create(BASE_URI + "/graphs"); + private static final String REXSTER_SSL_BASE_CONFIGURATION = "rexster-integration-test-ssl.xml"; + private static final String PASSWORD = "password"; + + private RexsterServer rexsterServer; + private RexsterApplication application; + + private final ClientConfig clientConfiguration = new DefaultClientConfig(); + private Client client; + + @Before + public void setUp() throws Exception { + clean(); + + new File(REXSTER_INTEGRATION_TEST_DIR).mkdirs(); + buildSslKeys(); + + final XMLConfiguration properties = new XMLConfiguration(); + properties.load(Application.class.getResourceAsStream(REXSTER_SSL_BASE_CONFIGURATION)); + + rexsterServer = new HttpRexsterServer(properties); + final List graphConfigs = properties.configurationsAt(Tokens.REXSTER_GRAPH_PATH); + application = new XmlRexsterApplication(graphConfigs); + rexsterServer.start(application); + + client = Client.create(clientConfiguration); + } + + @After + public void tearDown() throws Exception { + rexsterServer.stop(); + application.stop(); + } + + @Test + public void testSslExceptionOccursIfSslIsEnabledForServerAndClientDoesntTrustServer() + throws NoSuchAlgorithmException { + final HTTPSProperties httpsProperties = new HTTPSProperties(DONT_VERIFY_HOSTNAME, SSLContext.getDefault()); + client.getProperties().put(HTTPSProperties.PROPERTY_HTTPS_PROPERTIES, httpsProperties); + + // should fail because the client doesn't trust the server: + final ClientRequest graphRequest = ClientRequest.create().build(GRAPHS_URI, GET); + try { + this.client.handle(graphRequest); + fail("Expected exception did not occur."); + } catch (ClientHandlerException e) { + if (!e.getCause().getClass().equals(SSLHandshakeException.class)) { + fail("Unexpected exception."); + } + } + } + + @Test + @SuppressWarnings({"JUnitTestMethodWithNoAssertions"}) + public void testOneWaySslWorksWhenClientTrustsServer() throws Exception { + final SSLContext sslContext = SSLContext.getInstance("TLS"); + final TrustManagerFactory trustManagerFactory = initAndGetClientTrustManagerFactory(CLIENT_KEYSTORE_PATH); + sslContext.init(null, trustManagerFactory.getTrustManagers(), null); + + final HTTPSProperties httpsProperties = new HTTPSProperties(DONT_VERIFY_HOSTNAME, sslContext); + client.getProperties().put(HTTPSProperties.PROPERTY_HTTPS_PROPERTIES, httpsProperties); + + // client trusts server, this should succeed: + final ClientRequest graphRequest = ClientRequest.create().build(GRAPHS_URI, GET); + this.client.handle(graphRequest); + } + + @Test + public void testRequireClientAuthRequestThrowsSocketExceptionIfServerDoesntTrustClient() throws Exception { + reinitializeRexsterWithRequireClientAuth(); + + final SSLContext sslContext = SSLContext.getInstance("TLS"); + final TrustManagerFactory trustManagerFactory = initAndGetClientTrustManagerFactory(UNTRUSTED_KEYSTORE); + final KeyManagerFactory keyManagerFactory = initAndGetClientKeyManagerFactory(UNTRUSTED_KEYSTORE); + sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null); + + final HTTPSProperties httpsProperties = new HTTPSProperties(DONT_VERIFY_HOSTNAME, sslContext); + client.getProperties().put(HTTPSProperties.PROPERTY_HTTPS_PROPERTIES, httpsProperties); + + final ClientRequest graphRequest = ClientRequest.create().build(GRAPHS_URI, GET); + + // server should throw an SSLHandshakeException internally and reset the connection: + try { + this.client.handle(graphRequest); + fail("Expected exception did not occur."); + } catch (ClientHandlerException e) { + if (!e.getCause().getClass().equals(SocketException.class) && !e.getCause().getClass() + .equals(SSLHandshakeException.class)) { + final String errMsg = "Unexpected exception."; + logger.error(errMsg, e); + fail(errMsg); + } + } + } + + @Test + @SuppressWarnings({"JUnitTestMethodWithNoAssertions"}) + public void testRequireClientAuthWorksWhenServerTrustsClient() throws Exception { + reinitializeRexsterWithRequireClientAuth(); + + final SSLContext sslContext = SSLContext.getInstance("TLS"); + final TrustManagerFactory trustManagerFactory = initAndGetClientTrustManagerFactory(CLIENT_KEYSTORE_PATH); + final KeyManagerFactory keyManagerFactory = initAndGetClientKeyManagerFactory(CLIENT_KEYSTORE_PATH); + sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null); + + final HTTPSProperties httpsProperties = new HTTPSProperties(DONT_VERIFY_HOSTNAME, sslContext); + client.getProperties().put(HTTPSProperties.PROPERTY_HTTPS_PROPERTIES, httpsProperties); + + final ClientRequest graphRequest = ClientRequest.create().build(GRAPHS_URI, GET); + + // client and server both trust each other, this should succeed: + this.client.handle(graphRequest); + } + + /** + * Restarts the Rexster server and configures its SSL to require client authentication. + */ + private void reinitializeRexsterWithRequireClientAuth() throws Exception { + rexsterServer.stop(); + final XMLConfiguration properties = new XMLConfiguration(); + properties.load(Application.class.getResourceAsStream(REXSTER_SSL_BASE_CONFIGURATION)); + properties.setProperty("ssl.need-client-auth", "true"); + properties.setProperty("ssl.want-client-auth", "true"); + + rexsterServer = new HttpRexsterServer(properties); + + final List graphConfigs = properties.configurationsAt(Tokens.REXSTER_GRAPH_PATH); + final RexsterApplication application = new XmlRexsterApplication(graphConfigs); + rexsterServer.start(application); + } + + /** + * Generates a {@code TrustManagerFactory} that provides trust for the Rexster server. + * + * @return a {@code TrustManagerFactory} that can be used for SSL operations and trusts the Rexster server + */ + private static TrustManagerFactory initAndGetClientTrustManagerFactory(String pathToStore) + throws NoSuchAlgorithmException, KeyStoreException, IOException, CertificateException { + final TrustManagerFactory trustManagerFactory = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + + final KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + + FileInputStream trustStoreInputStream = null; + try { + trustStoreInputStream = new FileInputStream(REXSTER_INTEGRATION_TEST_DIR + '/' + pathToStore); + trustStore.load(trustStoreInputStream, PASSWORD.toCharArray()); + } finally { + if (trustStoreInputStream != null) { + trustStoreInputStream.close(); + } + } + trustManagerFactory.init(trustStore); + + return trustManagerFactory; + } + + /** + * Generates a {@code KeyManagerFactory} loaded with the key store at the given path. + * + * @param keyStorePath path to the keystore with which to load the returned {@code KeyManagerFactory} + * @return a {@code KeyManagerFactory} that can be used for SSL operations + */ + private static KeyManagerFactory initAndGetClientKeyManagerFactory(final String keyStorePath) throws Exception { + final KeyManagerFactory keyManagerFactory = + KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + + final KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + FileInputStream keyStoreInputStream = null; + try { + keyStoreInputStream = new FileInputStream(REXSTER_INTEGRATION_TEST_DIR + '/' + keyStorePath); + keyStore.load(keyStoreInputStream, PASSWORD.toCharArray()); + } finally { + if (keyStoreInputStream != null) { + keyStoreInputStream.close(); + } + } + keyManagerFactory.init(keyStore, PASSWORD.toCharArray()); + + return keyManagerFactory; + } + + /** + * Generates SSL keys for the client and server. Imports the client cert into the server keystore and the server + * cert into the client keystore. Also generates a client keystore that imports the sever cert but doesnt have its + * cert imported into the server keystore - the 'untrusted client'. Note that a single keystore serves as both + * keystore and trust store in these tests + */ + public static void buildSslKeys() throws IOException, InterruptedException { + final String[] generateClientKeyStore = + {"keytool", "-genkey", "-v", "-alias", "client", "-keypass", PASSWORD, "-keystore", + CLIENT_KEYSTORE_PATH, "-storepass", PASSWORD, "-storetype", "jks", "-dname", + "CN=client, O=client, C=US", "-keyalg", "RSA"}; + + final String[] exportClientCertificate = + {"keytool", "-export", "-v", "-alias", "client", "-file", CLIENT_CERT, "-rfc", "-keystore", + CLIENT_KEYSTORE_PATH, "-storepass", PASSWORD, "-storetype", "jks"}; + + final String[] generateServerKeyStore = + {"keytool", "-genkey", "-v", "-alias", "server", "-keypass", PASSWORD, "-keystore", + SERVER_KEYSTORE_PATH, "-storepass", PASSWORD, "-storetype", "jks", "-dname", + "CN=server, O=server, C=US", "-keyalg", "RSA"}; + + final String[] exportServerCertificate = + {"keytool", "-export", "-v", "-alias", "server", "-file", SERVER_CERT, "-rfc", "-keystore", + SERVER_KEYSTORE_PATH, "-storepass", PASSWORD, "-storetype", "jks"}; + + final String[] importServerCertToClient = + {"keytool", "-import", "-v", "-alias", "server", "-noprompt", "-file", SERVER_CERT, "-keystore", + CLIENT_KEYSTORE_PATH, "-storepass", PASSWORD, "-storetype", "jks"}; + + final String[] importClientCertToServer = + {"keytool", "-import", "-v", "-alias", "client", "-noprompt", "-file", CLIENT_CERT, "-keystore", + SERVER_KEYSTORE_PATH, "-storepass", PASSWORD, "-storetype", "jks"}; + + final String[] generateUntrustedClientKeys = + {"keytool", "-genkey", "-v", "-alias", "untrusted", "-keypass", PASSWORD, "-keystore", + UNTRUSTED_KEYSTORE, "-storepass", PASSWORD, "-storetype", "jks", "-dname", + "CN=untrusted, O=client, C=US", "-keyalg", "RSA"}; + + final String[] importServerCertToUntrustedClientKeys = + {"keytool", "-import", "-v", "-alias", "server", "-noprompt", "-file", SERVER_CERT, "-keystore", + UNTRUSTED_KEYSTORE, "-storepass", PASSWORD, "-storetype", "jks"}; + + final String[][] keystoreGenerationCommands = + {generateClientKeyStore, exportClientCertificate, generateServerKeyStore, exportServerCertificate, + importServerCertToClient, importClientCertToServer, generateUntrustedClientKeys, + importServerCertToUntrustedClientKeys}; + + for (final String[] command : keystoreGenerationCommands) { + final ProcessBuilder pb = new ProcessBuilder(); + pb.command(command); + pb.directory(new File(REXSTER_INTEGRATION_TEST_DIR)); + pb.redirectErrorStream(); + + final Process process = pb.start(); + final InputStream sterr = process.getErrorStream(); + + final BufferedReader reader = new BufferedReader(new InputStreamReader(sterr)); + try { + String line; + while ((line = reader.readLine()) != null) { + logger.debug(line); + } + } finally { + reader.close(); + } + process.waitFor(); + } + } + + /** + * Deletes the test directory where we store the SSL keys used for testing. + */ + public static void clean() { + AbstractResourceIntegrationTest.removeDirectory(new File(REXSTER_INTEGRATION_TEST_DIR)); + } + + /** + * Ignoring host name verification by using this {@code HostnameVerifier}. + */ + private static final HostnameVerifier DONT_VERIFY_HOSTNAME = new HostnameVerifier() { + @Override + public boolean verify(String s, SSLSession sslSession) { + return true; + } + }; +} diff --git a/rexster-server/src/integration/java/com/tinkerpop/rexster/ssl/RexsterRexProSslTest.java b/rexster-server/src/integration/java/com/tinkerpop/rexster/ssl/RexsterRexProSslTest.java new file mode 100644 index 00000000..fd1332dd --- /dev/null +++ b/rexster-server/src/integration/java/com/tinkerpop/rexster/ssl/RexsterRexProSslTest.java @@ -0,0 +1,165 @@ +package com.tinkerpop.rexster.ssl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import com.tinkerpop.rexster.Application; +import com.tinkerpop.rexster.Tokens; +import com.tinkerpop.rexster.client.RexsterClient; +import com.tinkerpop.rexster.client.RexsterClientFactory; +import com.tinkerpop.rexster.client.RexsterClientTokens; +import com.tinkerpop.rexster.rexpro.AbstractRexProIntegrationTest; +import com.tinkerpop.rexster.server.RexProRexsterServer; +import com.tinkerpop.rexster.server.RexsterApplication; +import com.tinkerpop.rexster.server.RexsterServer; +import com.tinkerpop.rexster.server.XmlRexsterApplication; +import com.tinkerpop.rexster.util.RexsterSslHelper; + +import org.apache.commons.configuration.BaseConfiguration; +import org.apache.commons.configuration.HierarchicalConfiguration; +import org.apache.commons.configuration.XMLConfiguration; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.List; + +public class RexsterRexProSslTest { + private static final Logger logger = LoggerFactory.getLogger(RexsterRexProSslTest.class); + private static final String REXSTER_INTEGRATION_TEST_DIR = "/tmp/rexster-integration-tests"; + private static final String CLIENT_KEYSTORE_PATH = "clientSslKeys.jks"; + private static final String UNTRUSTED_KEYSTORE = CLIENT_KEYSTORE_PATH + "_untrusted"; + private static final String REXSTER_SSL_BASE_CONFIGURATION = "rexster-integration-test-ssl.xml"; + private static final String PASSWORD = "password"; + + private RexsterServer rexsterServer; + private RexsterApplication application; + + @Before + public void setUp() throws Exception { + RexsterHttpSslTest.clean(); + + new File(REXSTER_INTEGRATION_TEST_DIR).mkdirs(); + RexsterHttpSslTest.buildSslKeys(); + + final XMLConfiguration properties = new XMLConfiguration(); + properties.load(Application.class.getResourceAsStream(REXSTER_SSL_BASE_CONFIGURATION)); + + rexsterServer = new RexProRexsterServer(properties); + final List graphConfigs = properties.configurationsAt(Tokens.REXSTER_GRAPH_PATH); + application = new XmlRexsterApplication(graphConfigs); + rexsterServer.start(application); + } + + @After + public void tearDown() throws Exception { + rexsterServer.stop(); + application.stop(); + } + + @Test + @SuppressWarnings({"JUnitTestMethodWithNoAssertions"}) + public void testSslExceptionOccursIfSslIsEnabledForServerAndClientDoesntTrustServer() throws Exception { + final BaseConfiguration config = getRexproClientBaseConfig(); + + // remove all keystores from client so client does not trust server + config.setProperty(RexsterSslHelper.KEY_SSL_KEY_STORE, ""); + config.setProperty(RexsterSslHelper.KEY_SSL_TRUST_STORE, ""); + + final RexsterClient client = RexsterClientFactory.open(config); + + try { + AbstractRexProIntegrationTest.getAvailableGraphs(client).toString(); + fail("Expected exception did not occur!"); + } catch (final Exception ex) { + assertEquals("Could not send message.", ex.getCause().getMessage()); + } + + client.close(); + } + + @Test + @SuppressWarnings({"JUnitTestMethodWithNoAssertions"}) + public void testOneWaySslWorksWhenClientTrustsServer() throws Exception { + final BaseConfiguration config = getRexproClientBaseConfig(); + + // remove the keystore to show it isnt necessary here (without client auth) + config.setProperty(RexsterSslHelper.KEY_SSL_KEY_STORE, ""); + + final RexsterClient client = RexsterClientFactory.open(config); + logger.debug(AbstractRexProIntegrationTest.getAvailableGraphs(client).toString()); + client.close(); + } + + @Test + public void testRequireClientAuthRequestThrowsExceptionIfServerDoesntTrustClient() throws Exception { + reinitializeRexsterWithRequireClientAuth(); + + final BaseConfiguration config = getRexproClientBaseConfig(); + + // use 'untrusted' client certs so server should reject client + config.setProperty(RexsterSslHelper.KEY_SSL_KEY_STORE, REXSTER_INTEGRATION_TEST_DIR + '/' + UNTRUSTED_KEYSTORE); + config.setProperty( + RexsterSslHelper.KEY_SSL_TRUST_STORE, REXSTER_INTEGRATION_TEST_DIR + '/' + UNTRUSTED_KEYSTORE); + + final RexsterClient client = RexsterClientFactory.open(config); + + try { + AbstractRexProIntegrationTest.getAvailableGraphs(client).toString(); + fail("Expected exception did not occur!"); + } catch (final Exception ex) { + assertEquals("Could not send message.", ex.getCause().getMessage()); + } + + client.close(); + } + + @Test + @SuppressWarnings({"JUnitTestMethodWithNoAssertions"}) + public void testRequireClientAuthWorksWhenServerTrustsClient() throws Exception { + reinitializeRexsterWithRequireClientAuth(); + + // use the 'base' configuration in which client and server trust each other + final RexsterClient client = RexsterClientFactory.open(getRexproClientBaseConfig()); + logger.debug(AbstractRexProIntegrationTest.getAvailableGraphs(client).toString()); + client.close(); + } + + /** + * Gets configuration for the Rexpro client in which the client trusts the server and server will trust the client. + + * @return configuration for Rexpro client + */ + private static BaseConfiguration getRexproClientBaseConfig() { + final BaseConfiguration conf = new BaseConfiguration(); + conf.setProperty(RexsterClientTokens.CONFIG_ENABLE_CLIENT_SSL, true); + conf.setProperty(RexsterSslHelper.KEY_SSL_KEY_STORE, REXSTER_INTEGRATION_TEST_DIR + '/' + CLIENT_KEYSTORE_PATH); + conf.setProperty( + RexsterSslHelper.KEY_SSL_TRUST_STORE, REXSTER_INTEGRATION_TEST_DIR + '/' + CLIENT_KEYSTORE_PATH); + conf.setProperty(RexsterSslHelper.KEY_SSL_KEY_STORE_PASSWORD, PASSWORD); + conf.setProperty(RexsterSslHelper.KEY_SSL_TRUST_STORE_PASSWORD, PASSWORD); + conf.setProperty(RexsterClientTokens.CONFIG_MESSAGE_RETRY_COUNT, 1); + + return conf; + } + + /** + * Restarts the Rexster server and configures its SSL to require client authentication. + */ + private void reinitializeRexsterWithRequireClientAuth() throws Exception { + rexsterServer.stop(); + final XMLConfiguration properties = new XMLConfiguration(); + properties.load(Application.class.getResourceAsStream(REXSTER_SSL_BASE_CONFIGURATION)); + properties.setProperty("ssl.need-client-auth", "true"); + properties.setProperty("ssl.want-client-auth", "true"); + + rexsterServer = new RexProRexsterServer(properties); + + final List graphConfigs = properties.configurationsAt(Tokens.REXSTER_GRAPH_PATH); + final RexsterApplication application = new XmlRexsterApplication(graphConfigs); + rexsterServer.start(application); + } +} diff --git a/rexster-server/src/integration/resources/com/tinkerpop/rexster/rexster-integration-test-ssl.xml b/rexster-server/src/integration/resources/com/tinkerpop/rexster/rexster-integration-test-ssl.xml new file mode 100644 index 00000000..631a5840 --- /dev/null +++ b/rexster-server/src/integration/resources/com/tinkerpop/rexster/rexster-integration-test-ssl.xml @@ -0,0 +1,133 @@ + + + + TLS + JKS + JKS + + /tmp/rexster-integration-tests/serverSslKeys.jks + /tmp/rexster-integration-tests/serverSslKeys.jks + password + password + + SunX509 + + + SunX509 + + false + false + + + 8182 + 0.0.0.0 + + https://localhost:8182 + public + UTF-8 + false + true + true + 2097152 + 8192 + 30000 + + + 8 + 8 + + + 4 + 4 + + + leader-follower + + + 8184 + 0.0.0.0 + 1790000 + 3000000 + false + true + + + 8 + 8 + + + 4 + 4 + + + leader-follower + + 8183 + 127.0.0.1 + 10000 + + + gremlin-groovy + -1 + config/init.groovy + com.tinkerpop.rexster.client.* + java.lang.Math.PI + + + + + none + + + + rexster + rexster + + + + + + + + jmx + + + http + + + console + + SECONDS + SECONDS + 10 + MINUTES + http.rest.* + http.rest.*.delete + + + + + + true + emptygraph + tinkergraph + true + + + tp:gremlin + + + + + false + sparkseesample + sparkseegraph + /tmp/rexster-integration-tests/graph.sparksee + + + tp:gremlin + + + + + diff --git a/rexster-server/src/integration/resources/com/tinkerpop/rexster/rexster-integration-test.xml b/rexster-server/src/integration/resources/com/tinkerpop/rexster/rexster-integration-test.xml index 123d946d..a2b515d1 100644 --- a/rexster-server/src/integration/resources/com/tinkerpop/rexster/rexster-integration-test.xml +++ b/rexster-server/src/integration/resources/com/tinkerpop/rexster/rexster-integration-test.xml @@ -97,65 +97,6 @@ - - true - neo4jsample - neo4jgraph - /tmp/rexster-integration-tests/neo4j-graph - - - - 285M - 285M - 100M - 100M - 10M - 10M - 10M - - - - - YES - - - 0.77 - - - 1.15 - - - 1.1 - - - 3000 - - - 0 - - - 0 - - - 1500 - - - 3500 - - - - tp:gremlin - - - false sparkseesample diff --git a/rexster-server/src/main/java/com/tinkerpop/rexster/server/HttpRexsterServer.java b/rexster-server/src/main/java/com/tinkerpop/rexster/server/HttpRexsterServer.java index e589ad78..9f633d3c 100644 --- a/rexster-server/src/main/java/com/tinkerpop/rexster/server/HttpRexsterServer.java +++ b/rexster-server/src/main/java/com/tinkerpop/rexster/server/HttpRexsterServer.java @@ -25,6 +25,8 @@ import com.tinkerpop.rexster.servlet.DogHouseServlet; import com.tinkerpop.rexster.servlet.EvaluatorServlet; import com.tinkerpop.rexster.servlet.RexsterStaticHttpHandler; +import com.tinkerpop.rexster.util.RexsterSslHelper; + import org.apache.commons.configuration.HierarchicalConfiguration; import org.apache.commons.configuration.XMLConfiguration; import org.apache.log4j.Level; @@ -37,13 +39,21 @@ import org.glassfish.grizzly.http.server.ServerConfiguration; import org.glassfish.grizzly.servlet.ServletRegistration; import org.glassfish.grizzly.servlet.WebappContext; +import org.glassfish.grizzly.ssl.SSLEngineConfigurator; import org.glassfish.grizzly.threadpool.GrizzlyExecutorService; import org.glassfish.grizzly.threadpool.ThreadPoolConfig; import javax.management.MalformedObjectNameException; import javax.management.ObjectName; +import javax.net.ssl.SSLException; import javax.ws.rs.core.Context; import java.io.File; +import java.io.IOException; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; /** * Initializes the HTTP server for Rexster serving REST and Dog House. @@ -482,6 +492,11 @@ private void configureNetworkListener() throws Exception { NetworkListener listener = this.httpServer.getListener("grizzly"); if (listener == null) { listener = new NetworkListener("grizzly", rexsterServerHost, rexsterServerPort); + + if (properties.getConfiguration().getBoolean(RexsterSslHelper.KEY_HTTP_SSL_ENABLED, false)) { + secureWithSsl(listener); + } + this.httpServer.addListener(listener); allowPortChange = false; } @@ -528,4 +543,18 @@ private void configureNetworkListener() throws Exception { logger.info(String.format("Using %s IOStrategy for HTTP/REST.", strategy.getClass().getName())); } } + + private void secureWithSsl(NetworkListener listener) throws SSLException { + logger.info("Attempting to secure HttpRexsterServer with SSL..."); + RexsterSslHelper rexsterSslHelper = new RexsterSslHelper(properties.getConfiguration()); + final SSLEngineConfigurator configurator = + new SSLEngineConfigurator(rexsterSslHelper.createRexsterSslContext()); + + configurator.setNeedClientAuth(rexsterSslHelper.getNeedClientAuth()).setWantClientAuth( + rexsterSslHelper.getWantClientAuth()).setClientMode(false); + + listener.setSecure(true); + listener.setSSLEngineConfig(configurator); + logger.info("HttpRexsterServer successfully secured with SSL!"); + } } diff --git a/rexster-server/src/main/java/com/tinkerpop/rexster/server/RexProRexsterServer.java b/rexster-server/src/main/java/com/tinkerpop/rexster/server/RexProRexsterServer.java index 5229969d..c61a9ed5 100644 --- a/rexster-server/src/main/java/com/tinkerpop/rexster/server/RexProRexsterServer.java +++ b/rexster-server/src/main/java/com/tinkerpop/rexster/server/RexProRexsterServer.java @@ -7,6 +7,8 @@ import com.tinkerpop.rexster.filter.DefaultSecurityFilter; import com.tinkerpop.rexster.protocol.session.RexProSessionMonitor; import com.tinkerpop.rexster.protocol.filter.*; +import com.tinkerpop.rexster.util.RexsterSslHelper; + import org.apache.commons.configuration.HierarchicalConfiguration; import org.apache.commons.configuration.XMLConfiguration; import org.apache.log4j.Logger; @@ -18,6 +20,8 @@ import org.glassfish.grizzly.monitoring.jmx.JmxObject; import org.glassfish.grizzly.nio.transport.TCPNIOTransport; import org.glassfish.grizzly.nio.transport.TCPNIOTransportBuilder; +import org.glassfish.grizzly.ssl.SSLEngineConfigurator; +import org.glassfish.grizzly.ssl.SSLFilter; import org.glassfish.grizzly.threadpool.GrizzlyExecutorService; import org.glassfish.grizzly.threadpool.ThreadPoolConfig; import org.glassfish.grizzly.utils.DelayedExecutor; @@ -25,6 +29,9 @@ import javax.management.MalformedObjectNameException; import javax.management.ObjectName; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLException; + import java.util.concurrent.TimeUnit; /** @@ -201,6 +208,20 @@ private FilterChain constructFilterChain(final RexsterApplication application) t final FilterChainBuilder filterChainBuilder = FilterChainBuilder.stateless(); filterChainBuilder.add(new TransportFilter()); + if (properties.getConfiguration().getBoolean(RexsterSslHelper.KEY_REXPRO_SSL_ENABLED, false)) { + logger.info("Attempting to secure RexProRexsterServer with SSL..."); + + try { + filterChainBuilder.add(createSslFilter()); + } catch (SSLException e) { + final String msg = "A problem occurred while initializing the SSL context."; + logger.error(msg,e); + throw new RuntimeException(msg,e); + } + + logger.info("RexproRexsterServer successfully secured with SSL!"); + } + final DelayedExecutor idleDelayedExecutor = IdleTimeoutFilter.createDefaultIdleDelayedExecutor( this.sessionCheckInterval, TimeUnit.MILLISECONDS); idleDelayedExecutor.start(); @@ -327,4 +348,15 @@ private void configureTransport() throws Exception { logger.info(String.format("RexPro Server bound to [%s:%s]", rexproServerHost, rexproServerPort)); } + + private SSLFilter createSslFilter() throws SSLException { + RexsterSslHelper sslHelper = new RexsterSslHelper(properties.getConfiguration()); + SSLContext sslContext = sslHelper.createRexsterSslContext(); + SSLEngineConfigurator server = + new SSLEngineConfigurator(sslContext).setNeedClientAuth(sslHelper.getNeedClientAuth()) + .setWantClientAuth(sslHelper.getWantClientAuth()).setClientMode(false); + + SSLEngineConfigurator client = new SSLEngineConfigurator(sslContext).setClientMode(true); + return new SSLFilter(server, client); + } }