001package co.codewizards.cloudstore.server; 002 003import java.io.File; 004import java.io.FileInputStream; 005import java.io.FileOutputStream; 006import java.io.IOException; 007import java.math.BigInteger; 008import java.security.InvalidKeyException; 009import java.security.KeyPair; 010import java.security.KeyPairGenerator; 011import java.security.KeyStore; 012import java.security.KeyStore.PrivateKeyEntry; 013import java.security.KeyStoreException; 014import java.security.NoSuchAlgorithmException; 015import java.security.NoSuchProviderException; 016import java.security.SecureRandom; 017import java.security.SignatureException; 018import java.security.UnrecoverableEntryException; 019import java.security.cert.Certificate; 020import java.security.cert.CertificateException; 021import java.security.cert.X509Certificate; 022import java.util.Date; 023import java.util.concurrent.atomic.AtomicBoolean; 024 025import org.bouncycastle.jce.X509Principal; 026import org.bouncycastle.x509.X509V3CertificateGenerator; 027import org.eclipse.jetty.server.HttpConfiguration; 028import org.eclipse.jetty.server.HttpConnectionFactory; 029import org.eclipse.jetty.server.SecureRequestCustomizer; 030import org.eclipse.jetty.server.Server; 031import org.eclipse.jetty.server.ServerConnector; 032import org.eclipse.jetty.server.SslConnectionFactory; 033import org.eclipse.jetty.servlet.ServletContextHandler; 034import org.eclipse.jetty.servlet.ServletHolder; 035import org.eclipse.jetty.util.ssl.SslContextFactory; 036import org.eclipse.jetty.util.thread.QueuedThreadPool; 037import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler; 038import org.glassfish.jersey.servlet.ServletContainer; 039import org.slf4j.Logger; 040import org.slf4j.LoggerFactory; 041 042import ch.qos.logback.classic.LoggerContext; 043import ch.qos.logback.classic.joran.JoranConfigurator; 044import ch.qos.logback.core.joran.spi.JoranException; 045import ch.qos.logback.core.util.StatusPrinter; 046import co.codewizards.cloudstore.core.auth.BouncyCastleRegistrationUtil; 047import co.codewizards.cloudstore.core.config.Config; 048import co.codewizards.cloudstore.core.config.ConfigDir; 049import co.codewizards.cloudstore.core.util.DerbyUtil; 050import co.codewizards.cloudstore.core.util.HashUtil; 051import co.codewizards.cloudstore.core.util.IOUtil; 052import co.codewizards.cloudstore.core.util.MainArgsUtil; 053import co.codewizards.cloudstore.rest.server.CloudStoreREST; 054 055public class CloudStoreServer implements Runnable { 056 public static final String CONFIG_KEY_SECURE_PORT = "server.securePort"; 057 058 private static final Logger logger = LoggerFactory.getLogger(CloudStoreServer.class); 059 060 private static final int DEFAULT_SECURE_PORT = 8443; 061 062 private static final String CERTIFICATE_ALIAS = "CloudStoreServer"; 063 private static final String CERTIFICATE_COMMON_NAME = CERTIFICATE_ALIAS; 064 065 // TODO the passwords are necessary. we get exceptions without them. so maybe we should somehow make this secure, later. 066 private static final String KEY_STORE_PASSWORD_STRING = "CloudStore-key-store"; 067 private static final char[] KEY_STORE_PASSWORD_CHAR_ARRAY = KEY_STORE_PASSWORD_STRING.toCharArray(); 068 private static final String KEY_PASSWORD_STRING = "CloudStore-private-key"; 069 private static final char[] KEY_PASSWORD_CHAR_ARRAY = KEY_PASSWORD_STRING.toCharArray(); 070 071 private File keyStoreFile; 072 private SecureRandom random = new SecureRandom(); 073 private int securePort; 074 private final AtomicBoolean running = new AtomicBoolean(); 075 private Server server; 076 077 public static void main(String[] args) throws Exception { 078 initLogging(); 079 try { 080 args = MainArgsUtil.extractAndApplySystemPropertiesReturnOthers(args); 081 new CloudStoreServer().run(); 082 } catch (Throwable x) { 083 logger.error(x.toString(), x); 084 System.exit(999); 085 } 086 } 087 088 public CloudStoreServer() { 089 BouncyCastleRegistrationUtil.registerBouncyCastleIfNeeded(); 090 } 091 092 @Override 093 public void run() { 094 if (!running.compareAndSet(false, true)) 095 throw new IllegalStateException("Server is already running!"); 096 097 try { 098 initKeyStore(); 099 synchronized (this) { 100 server = createServer(); 101 server.start(); 102 } 103 104 server.join(); 105 106 synchronized (this) { 107 server = null; 108 } 109 } catch (RuntimeException x) { 110 throw x; 111 } catch (Exception x) { 112 throw new RuntimeException(x); 113 } finally { 114 running.set(false); 115 } 116 } 117 118 public synchronized void stop() { 119 if (server != null) { 120 try { 121 server.stop(); 122 } catch (Exception e) { 123 throw new RuntimeException(); 124 } 125 } 126 } 127 128 public synchronized File getKeyStoreFile() { 129 if (keyStoreFile == null) { 130 File sslServer = new File(ConfigDir.getInstance().getFile(), "ssl.server"); 131 132 if (!sslServer.isDirectory()) 133 sslServer.mkdirs(); 134 135 if (!sslServer.isDirectory()) 136 throw new IllegalStateException("Could not create directory: " + sslServer); 137 138 keyStoreFile = new File(sslServer, "keystore"); 139 } 140 return keyStoreFile; 141 } 142 143 public synchronized void setKeyStoreFile(File keyStoreFile) { 144 assertNotRunning(); 145 this.keyStoreFile = keyStoreFile; 146 } 147 148 public synchronized int getSecurePort() { 149 if (securePort <= 0) { 150 securePort = Config.getInstance().getPropertyAsInt(CONFIG_KEY_SECURE_PORT, DEFAULT_SECURE_PORT); 151 if (securePort < 1 || securePort > 65535) { 152 logger.warn("Config key '{}' is set to the value '{}' which is out of range for a port number. Falling back to default port {}.", 153 CONFIG_KEY_SECURE_PORT, securePort, DEFAULT_SECURE_PORT); 154 securePort = DEFAULT_SECURE_PORT; 155 } 156 } 157 return securePort; 158 } 159 160 public synchronized void setSecurePort(int securePort) { 161 assertNotRunning(); 162 this.securePort = securePort; 163 } 164 165 private void assertNotRunning() { 166 if (running.get()) 167 throw new IllegalStateException("Server is already running."); 168 } 169 170 private void initKeyStore() throws IOException, KeyStoreException, NoSuchAlgorithmException, CertificateException, InvalidKeyException, SecurityException, SignatureException, NoSuchProviderException, UnrecoverableEntryException { 171 if (!getKeyStoreFile().exists()) { 172 logger.info("initKeyStore: keyStoreFile='{}' does not exist!", getKeyStoreFile()); 173 logger.info("initKeyStore: Creating RSA key pair (this might take a while)..."); 174 System.out.println("**********************************************************************"); 175 System.out.println("There is no key, yet. Creating a new RSA key pair, now. This might"); 176 System.out.println("take a while (a few seconds up to a few minutes). Please be patient!"); 177 System.out.println("**********************************************************************"); 178 long keyGenStartTimestamp = System.currentTimeMillis(); 179 KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); 180 ks.load(null, KEY_STORE_PASSWORD_CHAR_ARRAY); 181 182 KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); 183 keyGen.initialize(4096, random); // TODO make configurable 184 KeyPair pair = keyGen.generateKeyPair(); 185 186 X509V3CertificateGenerator v3CertGen = new X509V3CertificateGenerator(); 187 188 long serial = new SecureRandom().nextLong(); 189 190 v3CertGen.setSerialNumber(BigInteger.valueOf(serial).abs()); 191 v3CertGen.setIssuerDN(new X509Principal("CN=" + CERTIFICATE_COMMON_NAME + ", OU=None, O=None, C=None")); 192 v3CertGen.setNotBefore(new Date(System.currentTimeMillis() - (1000L * 60 * 60 * 24 * 3))); 193 v3CertGen.setNotAfter(new Date(System.currentTimeMillis() + (1000L * 60 * 60 * 24 * 365 * 10))); 194 v3CertGen.setSubjectDN(new X509Principal("CN=" + CERTIFICATE_COMMON_NAME + ", OU=None, O=None, C=None")); 195 196 v3CertGen.setPublicKey(pair.getPublic()); 197 v3CertGen.setSignatureAlgorithm("SHA1WithRSAEncryption"); 198 199 X509Certificate pkCertificate = v3CertGen.generateX509Certificate(pair.getPrivate()); 200 201 PrivateKeyEntry entry = new PrivateKeyEntry(pair.getPrivate(), new Certificate[]{ pkCertificate }); 202 ks.setEntry(CERTIFICATE_ALIAS, entry, new KeyStore.PasswordProtection(KEY_PASSWORD_CHAR_ARRAY)); 203 204 FileOutputStream fos = new FileOutputStream(getKeyStoreFile()); 205 try { 206 ks.store(fos, KEY_STORE_PASSWORD_CHAR_ARRAY); 207 } finally { 208 fos.close(); 209 } 210 211 long keyGenDuration = System.currentTimeMillis() - keyGenStartTimestamp; 212 logger.info("initKeyStore: Creating RSA key pair took {} ms.", keyGenDuration); 213 System.out.println(String.format("Generating a new RSA key pair took %s ms.", keyGenDuration)); 214 } 215 216 KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); 217 FileInputStream fis = new FileInputStream(getKeyStoreFile()); 218 try { 219 ks.load(fis, KEY_STORE_PASSWORD_CHAR_ARRAY); 220 } finally { 221 fis.close(); 222 } 223 X509Certificate certificate = (X509Certificate) ks.getCertificate(CERTIFICATE_ALIAS); 224 String certificateSha1 = HashUtil.sha1ForHuman(certificate.getEncoded()); 225 System.out.println("**********************************************************************"); 226 System.out.println("Server certificate fingerprint (SHA1):"); 227 System.out.println(); 228 System.out.println(" " + certificateSha1); 229 System.out.println(); 230 System.out.println("Use this fingerprint to verify on the client-side, whether you're"); 231 System.out.println("really talking to this server. If the client shows you a different"); 232 System.out.println("value, someone is tampering with your connection!"); 233 System.out.println(); 234 System.out.println("Please keep this fingerprint at a safe place. You'll need it whenever"); 235 System.out.println("one of your clients connects to this server for the first time."); 236 System.out.println("**********************************************************************"); 237 logger.info("initKeyStore: RSA fingerprint (SHA1): {}", certificateSha1); 238 } 239 240 private Server createServer() { 241 QueuedThreadPool threadPool = new QueuedThreadPool(); 242 threadPool.setMaxThreads(500); 243 244 Server server = new Server(threadPool); 245 server.addBean(new ScheduledExecutorScheduler()); 246 247 HttpConfiguration http_config = createHttpConfigurationForHTTP(); 248 249// ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(http_config)); 250// http.setPort(8080); 251// http.setIdleTimeout(30000); 252// server.addConnector(http); 253 254 server.setHandler(createServletContextHandler()); 255 server.setDumpAfterStart(false); 256 server.setDumpBeforeStop(false); 257 server.setStopAtShutdown(true); 258 259 HttpConfiguration https_config = createHttpConfigurationForHTTPS(http_config); 260 server.addConnector(createServerConnectorForHTTPS(server, https_config)); 261 262 return server; 263 } 264 265 private HttpConfiguration createHttpConfigurationForHTTP() { 266 HttpConfiguration http_config = new HttpConfiguration(); 267 http_config.setSecureScheme("https"); 268 http_config.setSecurePort(getSecurePort()); 269 http_config.setOutputBufferSize(32768); 270 http_config.setRequestHeaderSize(8192); 271 http_config.setResponseHeaderSize(8192); 272 http_config.setSendServerVersion(true); 273 http_config.setSendDateHeader(false); 274 return http_config; 275 } 276 277 private HttpConfiguration createHttpConfigurationForHTTPS(HttpConfiguration httpConfigurationForHTTP) { 278 HttpConfiguration https_config = new HttpConfiguration(httpConfigurationForHTTP); 279 https_config.addCustomizer(new SecureRequestCustomizer()); 280 return https_config; 281 } 282 283 private ServletContextHandler createServletContextHandler() { 284 ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); 285 context.setContextPath("/"); 286 ServletContainer servletContainer = new ServletContainer(new CloudStoreREST()); 287 context.addServlet(new ServletHolder(servletContainer), "/*"); 288// context.addFilter(GzipFilter.class, "/*", EnumSet.allOf(DispatcherType.class)); // Does not work :-( Using GZip...Interceptor instead ;-) 289 return context; 290 } 291 292 private ServerConnector createServerConnectorForHTTPS(Server server, HttpConfiguration httpConfigurationForHTTPS) { 293 SslContextFactory sslContextFactory = new SslContextFactory(); 294 sslContextFactory.setKeyStorePath(getKeyStoreFile().getPath()); 295 sslContextFactory.setKeyStorePassword(KEY_STORE_PASSWORD_STRING); 296 sslContextFactory.setKeyManagerPassword(KEY_PASSWORD_STRING); 297 sslContextFactory.setTrustStorePath(getKeyStoreFile().getPath()); 298 sslContextFactory.setTrustStorePassword(KEY_STORE_PASSWORD_STRING); 299 sslContextFactory.setExcludeCipherSuites( 300// "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", 301// "SSL_RSA_EXPORT_WITH_DES40_CBC_SHA", "SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA", "SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA", 302// Using wildcards instead. This should be much safer: 303 ".*RC4.*", 304 ".*DES.*"); 305 // sslContextFactory.setCertAlias(CERTIFICATE_ALIAS); // Jetty uses our certificate. We put only one single cert into the key store. Hence, we don't need this. 306 307 ServerConnector sslConnector = new ServerConnector(server, new SslConnectionFactory(sslContextFactory, "http/1.1"), new HttpConnectionFactory(httpConfigurationForHTTPS)); 308 sslConnector.setPort(getSecurePort()); 309 return sslConnector; 310 } 311 312 private static void initLogging() throws IOException, JoranException { 313 File logDir = ConfigDir.getInstance().getLogDir(); 314 DerbyUtil.setLogFile(new File(logDir, "derby.log")); 315 316 String logbackXmlName = "logback.server.xml"; 317 File logbackXmlFile = new File(ConfigDir.getInstance().getFile(), logbackXmlName); 318 if (!logbackXmlFile.exists()) 319 IOUtil.copyResource(CloudStoreServer.class, logbackXmlName, logbackXmlFile); 320 321 LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); 322 try { 323 JoranConfigurator configurator = new JoranConfigurator(); 324 configurator.setContext(context); 325 // Call context.reset() to clear any previous configuration, e.g. default 326 // configuration. For multi-step configuration, omit calling context.reset(). 327 context.reset(); 328 configurator.doConfigure(logbackXmlFile); 329 } catch (JoranException je) { 330 // StatusPrinter will handle this 331 doNothing(); 332 } 333 StatusPrinter.printInCaseOfErrorsOrWarnings(context); 334 } 335 336 private static void doNothing() { } 337}