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}