package co.codewizards.cloudstore.server; import static co.codewizards.cloudstore.core.io.StreamUtil.*; import static co.codewizards.cloudstore.core.oio.OioFileFactory.*; import static co.codewizards.cloudstore.core.util.AssertUtil.*; import static co.codewizards.cloudstore.core.util.Util.*; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.math.BigInteger; import java.security.InvalidKeyException; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.KeyStore; import java.security.KeyStore.PrivateKeyEntry; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.SecureRandom; import java.security.SignatureException; import java.security.UnrecoverableEntryException; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.Date; import java.util.concurrent.atomic.AtomicBoolean; import org.bouncycastle.jce.X509Principal; import org.bouncycastle.x509.X509V3CertificateGenerator; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.SecureRequestCustomizer; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.SslConnectionFactory; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler; import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.servlet.ServletContainer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.joran.JoranConfigurator; import ch.qos.logback.core.joran.spi.JoranException; import ch.qos.logback.core.util.StatusPrinter; import co.codewizards.cloudstore.core.appid.AppIdRegistry; import co.codewizards.cloudstore.core.auth.BouncyCastleRegistrationUtil; import co.codewizards.cloudstore.core.config.ConfigDir; import co.codewizards.cloudstore.core.config.ConfigImpl; import co.codewizards.cloudstore.core.oio.File; import co.codewizards.cloudstore.core.util.AssertUtil; import co.codewizards.cloudstore.core.util.DerbyUtil; import co.codewizards.cloudstore.core.util.HashUtil; import co.codewizards.cloudstore.core.util.MainArgsUtil; import co.codewizards.cloudstore.ls.server.LocalServer; import co.codewizards.cloudstore.rest.server.CloudStoreRest; public class CloudStoreServer implements Runnable { public static final String CONFIG_KEY_SECURE_PORT = "server.securePort"; private static final Logger logger = LoggerFactory.getLogger(CloudStoreServer.class); private static Class<? extends CloudStoreServer> cloudStoreServerClass = CloudStoreServer.class; private static final int DEFAULT_SECURE_PORT = 8443; private static final String CERTIFICATE_ALIAS = "CloudStoreServer"; private static final String CERTIFICATE_COMMON_NAME = CERTIFICATE_ALIAS; // TODO the passwords are necessary. we get exceptions without them. so maybe we should somehow make this secure, later. private static final String KEY_STORE_PASSWORD_STRING = "CloudStore-key-store"; private static final char[] KEY_STORE_PASSWORD_CHAR_ARRAY = KEY_STORE_PASSWORD_STRING.toCharArray(); private static final String KEY_PASSWORD_STRING = "CloudStore-private-key"; private static final char[] KEY_PASSWORD_CHAR_ARRAY = KEY_PASSWORD_STRING.toCharArray(); private File keyStoreFile; private final SecureRandom random = new SecureRandom(); private int securePort; private final AtomicBoolean running = new AtomicBoolean(); private Server server; private CloudStoreUpdaterTimer updaterTimer; public static void main(String[] args) throws Exception { args = MainArgsUtil.extractAndApplySystemPropertiesReturnOthers(args); initLogging(); try { createCloudStoreServer(args).run(); } catch (final Throwable x) { logger.error(x.toString(), x); System.exit(999); } } public CloudStoreServer(final String... args) { BouncyCastleRegistrationUtil.registerBouncyCastleIfNeeded(); } protected static Constructor<? extends CloudStoreServer> getCloudStoreServerConstructor() throws NoSuchMethodException, SecurityException { final Class<? extends CloudStoreServer> clazz = getCloudStoreServerClass(); final Constructor<? extends CloudStoreServer> constructor = clazz.getConstructor(String[].class); return constructor; } protected static CloudStoreServer createCloudStoreServer(final String[] args) throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { final Constructor<? extends CloudStoreServer> constructor = getCloudStoreServerConstructor(); final CloudStoreServer cloudStoreServer = constructor.newInstance(new Object[] { args }); return cloudStoreServer; } protected static Class<? extends CloudStoreServer> getCloudStoreServerClass() { return cloudStoreServerClass; } protected static void setCloudStoreServerClass(final Class<? extends CloudStoreServer> cloudStoreServerClass) { assertNotNull(cloudStoreServerClass, "cloudStoreServerClass"); CloudStoreServer.cloudStoreServerClass = cloudStoreServerClass; } @Override public void run() { if (!running.compareAndSet(false, true)) throw new IllegalStateException("Server is already running!"); LocalServer localServer = null; try { initKeyStore(); synchronized (this) { localServer = createLocalServer(); if (!localServer.start()) localServer = null; server = createServer(); server.start(); updaterTimer = createUpdaterTimer(); updaterTimer.start(); } server.join(); } catch (final RuntimeException x) { throw x; } catch (final Exception x) { throw new RuntimeException(x); } finally { synchronized (this) { if (localServer != null) { try { localServer.stop(); } catch (Exception x) { logger.warn("localServer.stop() failed: " + x, x); } localServer = null; } server = null; } running.set(false); } } protected CloudStoreUpdaterTimer createUpdaterTimer() { return new CloudStoreUpdaterTimer(); } protected LocalServer createLocalServer() { return new LocalServer(); } public synchronized void stop() { if (updaterTimer != null) updaterTimer.stop(); if (server != null) { try { server.stop(); } catch (final Exception e) { throw new RuntimeException(); } } } public synchronized File getKeyStoreFile() { if (keyStoreFile == null) { final File sslServer = createFile(ConfigDir.getInstance().getFile(), "ssl.server"); if (!sslServer.isDirectory()) sslServer.mkdirs(); if (!sslServer.isDirectory()) throw new IllegalStateException("Could not create directory: " + sslServer); keyStoreFile = createFile(sslServer, "keystore"); } return keyStoreFile; } public synchronized void setKeyStoreFile(final File keyStoreFile) { assertNotRunning(); this.keyStoreFile = keyStoreFile; } public synchronized int getSecurePort() { if (securePort <= 0) { securePort = ConfigImpl.getInstance().getPropertyAsInt(CONFIG_KEY_SECURE_PORT, DEFAULT_SECURE_PORT); if (securePort < 1 || securePort > 65535) { logger.warn("Config key '{}' is set to the value '{}' which is out of range for a port number. Falling back to default port {}.", CONFIG_KEY_SECURE_PORT, securePort, DEFAULT_SECURE_PORT); securePort = DEFAULT_SECURE_PORT; } } return securePort; } public synchronized void setSecurePort(final int securePort) { assertNotRunning(); this.securePort = securePort; } private void assertNotRunning() { if (running.get()) throw new IllegalStateException("Server is already running."); } private void initKeyStore() throws IOException, KeyStoreException, NoSuchAlgorithmException, CertificateException, InvalidKeyException, SecurityException, SignatureException, NoSuchProviderException, UnrecoverableEntryException { if (!getKeyStoreFile().exists()) { logger.info("initKeyStore: keyStoreFile='{}' does not exist!", getKeyStoreFile()); logger.info("initKeyStore: Creating RSA key pair (this might take a while)..."); System.out.println("**********************************************************************"); System.out.println("There is no key, yet. Creating a new RSA key pair, now. This might"); System.out.println("take a while (a few seconds up to a few minutes). Please be patient!"); System.out.println("**********************************************************************"); final long keyGenStartTimestamp = System.currentTimeMillis(); final KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); ks.load(null, KEY_STORE_PASSWORD_CHAR_ARRAY); final KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); keyGen.initialize(4096, random); // TODO make configurable final KeyPair pair = keyGen.generateKeyPair(); final X509V3CertificateGenerator v3CertGen = new X509V3CertificateGenerator(); final long serial = new SecureRandom().nextLong(); v3CertGen.setSerialNumber(BigInteger.valueOf(serial).abs()); v3CertGen.setIssuerDN(new X509Principal("CN=" + CERTIFICATE_COMMON_NAME + ", OU=None, O=None, C=None")); v3CertGen.setNotBefore(new Date(System.currentTimeMillis() - (1000L * 60 * 60 * 24 * 3))); v3CertGen.setNotAfter(new Date(System.currentTimeMillis() + (1000L * 60 * 60 * 24 * 365 * 10))); v3CertGen.setSubjectDN(new X509Principal("CN=" + CERTIFICATE_COMMON_NAME + ", OU=None, O=None, C=None")); v3CertGen.setPublicKey(pair.getPublic()); v3CertGen.setSignatureAlgorithm("SHA1WithRSAEncryption"); final X509Certificate pkCertificate = v3CertGen.generateX509Certificate(pair.getPrivate()); final PrivateKeyEntry entry = new PrivateKeyEntry(pair.getPrivate(), new Certificate[]{ pkCertificate }); ks.setEntry(CERTIFICATE_ALIAS, entry, new KeyStore.PasswordProtection(KEY_PASSWORD_CHAR_ARRAY)); final OutputStream fos = castStream(getKeyStoreFile().createOutputStream()); try { ks.store(fos, KEY_STORE_PASSWORD_CHAR_ARRAY); } finally { fos.close(); } final long keyGenDuration = System.currentTimeMillis() - keyGenStartTimestamp; logger.info("initKeyStore: Creating RSA key pair took {} ms.", keyGenDuration); System.out.println(String.format("Generating a new RSA key pair took %s ms.", keyGenDuration)); } final KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); final InputStream fis = castStream(getKeyStoreFile().createInputStream()); try { ks.load(fis, KEY_STORE_PASSWORD_CHAR_ARRAY); } finally { fis.close(); } final X509Certificate certificate = (X509Certificate) ks.getCertificate(CERTIFICATE_ALIAS); final String certificateSha1 = HashUtil.sha1ForHuman(certificate.getEncoded()); System.out.println("**********************************************************************"); System.out.println("Server certificate fingerprint (SHA1):"); System.out.println(); System.out.println(" " + certificateSha1); System.out.println(); System.out.println("Use this fingerprint to verify on the client-side, whether you're"); System.out.println("really talking to this server. If the client shows you a different"); System.out.println("value, someone is tampering with your connection!"); System.out.println(); System.out.println("Please keep this fingerprint at a safe place. You'll need it whenever"); System.out.println("one of your clients connects to this server for the first time."); System.out.println("**********************************************************************"); logger.info("initKeyStore: RSA fingerprint (SHA1): {}", certificateSha1); } protected Server createServer() { final QueuedThreadPool threadPool = new QueuedThreadPool(); threadPool.setMaxThreads(500); final Server server = new Server(threadPool); server.addBean(new ScheduledExecutorScheduler()); final HttpConfiguration http_config = createHttpConfigurationForHTTP(); // ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(http_config)); // http.setPort(8080); // http.setIdleTimeout(30000); // server.addConnector(http); server.setHandler(createServletContextHandler()); server.setDumpAfterStart(false); server.setDumpBeforeStop(false); server.setStopAtShutdown(true); final HttpConfiguration https_config = createHttpConfigurationForHTTPS(http_config); server.addConnector(createServerConnectorForHTTPS(server, https_config)); return server; } private HttpConfiguration createHttpConfigurationForHTTP() { final HttpConfiguration http_config = new HttpConfiguration(); http_config.setSecureScheme("https"); http_config.setSecurePort(getSecurePort()); http_config.setOutputBufferSize(32768); http_config.setRequestHeaderSize(8192); http_config.setResponseHeaderSize(8192); http_config.setSendServerVersion(true); http_config.setSendDateHeader(false); return http_config; } private HttpConfiguration createHttpConfigurationForHTTPS(final HttpConfiguration httpConfigurationForHTTP) { final HttpConfiguration https_config = new HttpConfiguration(httpConfigurationForHTTP); https_config.addCustomizer(new SecureRequestCustomizer()); return https_config; } private ServletContextHandler createServletContextHandler() { final ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); context.setContextPath("/"); final ServletContainer servletContainer = new ServletContainer(AssertUtil.assertNotNull(createResourceConfig(), "createResourceConfig()")); context.addServlet(new ServletHolder(servletContainer), "/*"); // context.addFilter(GzipFilter.class, "/*", EnumSet.allOf(DispatcherType.class)); // Does not work :-( Using GZip...Interceptor instead ;-) return context; } /** * Creates the actual REST application. * @return the actual REST application. Must not be <code>null</code>. */ protected ResourceConfig createResourceConfig() { return new CloudStoreRest(); } private ServerConnector createServerConnectorForHTTPS(final Server server, final HttpConfiguration httpConfigurationForHTTPS) { final SslContextFactory sslContextFactory = new SslContextFactory(); sslContextFactory.setKeyStorePath(getKeyStoreFile().getPath()); sslContextFactory.setKeyStorePassword(KEY_STORE_PASSWORD_STRING); sslContextFactory.setKeyManagerPassword(KEY_PASSWORD_STRING); sslContextFactory.setTrustStorePath(getKeyStoreFile().getPath()); sslContextFactory.setTrustStorePassword(KEY_STORE_PASSWORD_STRING); sslContextFactory.setExcludeCipherSuites( // TODO make this configurable! // "SSL_RSA_WITH_DES_CBC_SHA", "SSL_DHE_RSA_WITH_DES_CBC_SHA", "SSL_DHE_DSS_WITH_DES_CBC_SHA", "SSL_RSA_EXPORT_WITH_RC4_40_MD5", // "SSL_RSA_EXPORT_WITH_DES40_CBC_SHA", "SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA", "SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA", // Using wildcards instead. This should be much safer: ".*RC4.*", ".*DES.*"); // sslContextFactory.setCertAlias(CERTIFICATE_ALIAS); // Jetty uses our certificate. We put only one single cert into the key store. Hence, we don't need this. final ServerConnector sslConnector = new ServerConnector(server, new SslConnectionFactory(sslContextFactory, "http/1.1"), new HttpConnectionFactory(httpConfigurationForHTTPS)); sslConnector.setPort(getSecurePort()); // sslConnector.setIdleTimeout(300*1000); // sslConnector.setStopTimeout(30*1000); // sslConnector.setSoLingerTime(10); return sslConnector; } private static void initLogging() throws IOException, JoranException { final File logDir = ConfigDir.getInstance().getLogDir(); DerbyUtil.setLogFile(createFile(logDir, "derby.log")); final String logbackXmlName = "logback.server.xml"; final File logbackXmlFile = createFile(ConfigDir.getInstance().getFile(), logbackXmlName); if (!logbackXmlFile.exists()) { AppIdRegistry.getInstance().copyResourceResolvingAppId( CloudStoreServer.class, logbackXmlName, logbackXmlFile); } final LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); try { final JoranConfigurator configurator = new JoranConfigurator(); configurator.setContext(context); // Call context.reset() to clear any previous configuration, e.g. default // configuration. For multi-step configuration, omit calling context.reset(). context.reset(); configurator.doConfigure(logbackXmlFile.getIoFile()); } catch (final JoranException je) { // StatusPrinter will handle this doNothing(); } StatusPrinter.printInCaseOfErrorsOrWarnings(context); } }