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