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}