Java tutorial
/** * SusiInstallation * Copyright 04.08.2016 by Robert Mader, @treba123 * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program in the file lgpl21.txt * If not, see <http://www.gnu.org/licenses/>. */ package org.loklak; import org.apache.logging.log4j.LogManager; import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; import org.bouncycastle.openssl.PEMParser; import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; import org.eclipse.jetty.rewrite.handler.RewriteHandler; import org.eclipse.jetty.rewrite.handler.RewriteRegexRule; import org.eclipse.jetty.security.ConstraintMapping; import org.eclipse.jetty.security.ConstraintSecurityHandler; import org.eclipse.jetty.security.authentication.BasicAuthenticator; import org.eclipse.jetty.server.*; import org.eclipse.jetty.server.handler.DefaultHandler; import org.eclipse.jetty.server.handler.ErrorHandler; import org.eclipse.jetty.server.handler.HandlerList; import org.eclipse.jetty.server.handler.IPAccessHandler; import org.eclipse.jetty.server.handler.gzip.GzipHandler; import org.eclipse.jetty.server.session.HashSessionIdManager; import org.eclipse.jetty.server.session.HashSessionManager; import org.eclipse.jetty.server.session.SessionHandler; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.util.ConcurrentHashSet; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.security.Constraint; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.eclipse.jetty.webapp.WebAppContext; import org.loklak.api.cms.InstallationPageService; import org.loklak.data.DAO; import org.loklak.http.RemoteAccess; import org.loklak.server.FileHandler; import org.loklak.server.HttpsMode; import org.loklak.tools.Browser; import java.io.*; import java.net.ServerSocket; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.security.KeyStore; import java.security.PrivateKey; import java.security.Security; import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.util.Map; import java.util.Random; import java.util.Set; import static org.loklak.SusiServer.readConfig; public class SusiInstallation { public final static Set<String> blacklistedHosts = new ConcurrentHashSet<>(); public static Server server = null; private static HttpsMode httpsMode = HttpsMode.OFF; public static void main(String[] args) throws Exception { System.setProperty("java.awt.headless", "true"); // no awt used here so we can switch off that stuff // init config, log and elasticsearch Path data = FileSystems.getDefault().getPath("data"); File dataFile = data.toFile(); if (!dataFile.exists()) dataFile.mkdirs(); // should already be there since the start.sh script creates it Log.getLog().info("Starting loklak-installation initialization"); // prepare shutdown signal File pid = new File(dataFile, "loklak.pid"); if (pid.exists()) pid.deleteOnExit(); // thats a signal for the stop.sh script that loklak has terminated // prepare signal for startup script File startup = new File(dataFile, "startup.tmp"); if (startup.exists()) { startup.deleteOnExit(); FileWriter writer = new FileWriter(startup); writer.write("startup"); writer.close(); } // load the config file(s); Map<String, String> config = readConfig(data); // set localhost pattern String server_localhost = config.get("server.localhost"); if (server_localhost != null && server_localhost.length() > 0) { for (String h : server_localhost.split(",")) RemoteAccess.addLocalhost(h); } // check for https modus switch (config.get("https.mode")) { case "on": httpsMode = HttpsMode.ON; break; case "redirect": httpsMode = HttpsMode.REDIRECT; break; case "only": httpsMode = HttpsMode.ONLY; break; default: httpsMode = HttpsMode.OFF; break; } // get server ports Map<String, String> env = System.getenv(); String httpPortS = config.get("port.http"); int httpPort = httpPortS == null ? 4000 : Integer.parseInt(httpPortS); if (env.containsKey("PORT")) { httpPort = Integer.parseInt(env.get("PORT")); } String httpsPortS = config.get("port.https"); int httpsPort = httpsPortS == null ? 9443 : Integer.parseInt(httpsPortS); if (env.containsKey("PORTSSL")) { httpsPort = Integer.parseInt(env.get("PORTSSL")); } // check if a loklak service is already running on configured port try { checkServerPorts(httpPort, httpsPort); } catch (IOException e) { Log.getLog().warn(e.getMessage()); System.exit(-1); } // initialize all data try { DAO.init(config, data); } catch (Exception e) { Log.getLog().warn(e.getMessage()); Log.getLog().warn("Could not initialize DAO. Exiting."); System.exit(-1); } // init the http server try { setupHttpServer(httpPort, httpsPort); } catch (Exception e) { Log.getLog().warn(e.getMessage()); System.exit(-1); } setServerHandler(dataFile); SusiInstallation.server.start(); // if this is not headless, we can open a browser automatically Browser.openBrowser("http://127.0.0.1:" + httpPort + "/"); Log.getLog().info("finished startup!"); // signal to startup script if (startup.exists()) { FileWriter writer = new FileWriter(startup); writer.write("done"); writer.close(); } // ** services are now running ** // start a shutdown hook Runtime.getRuntime().addShutdownHook(new Thread() { public void run() { try { Log.getLog().info("catched main termination signal"); SusiInstallation.server.stop(); DAO.close(); Log.getLog().info("main terminated, goodby."); Log.getLog().info("Shutting down log4j2"); LogManager.shutdown(); } catch (Exception e) { } } }); // ** wait for shutdown signal, do this with a kill HUP (default level 1, 'kill -1') signal ** SusiInstallation.server.join(); Log.getLog().info("server terminated"); // After this, the jvm processes all shutdown hooks and terminates then. // The main termination line is therefore inside the shutdown hook. } //initiate http server private static void setupHttpServer(int httpPort, int httpsPort) throws Exception { QueuedThreadPool pool = new QueuedThreadPool(); pool.setMaxThreads(500); SusiInstallation.server = new Server(pool); SusiInstallation.server.setStopAtShutdown(true); //http if (!httpsMode.equals(HttpsMode.ONLY)) { HttpConfiguration http_config = new HttpConfiguration(); if (httpsMode.equals(HttpsMode.REDIRECT)) { //redirect http_config.addCustomizer(new SecureRequestCustomizer()); http_config.setSecureScheme("https"); http_config.setSecurePort(httpsPort); } ServerConnector connector = new ServerConnector(SusiInstallation.server); connector.addConnectionFactory(new HttpConnectionFactory(http_config)); connector.setPort(httpPort); connector.setName("httpd:" + httpPort); connector.setIdleTimeout(20000); // timout in ms when no bytes send / received SusiInstallation.server.addConnector(connector); } //https //uncommented lines for http2 (jetty 9.3 / java 8) if (httpsMode.isGreaterOrEqualTo(HttpsMode.ON)) { Log.getLog().info("HTTPS activated"); String keySource = DAO.getConfig("https.keysource", "keystore"); KeyStore keyStore; String keystoreManagerPass; //check for key source. Can be a java keystore or in pem format (gets converted automatically) if ("keystore".equals(keySource)) { Log.getLog().info("Loading keystore from disk"); //use native keystore format File keystoreFile = new File(DAO.conf_dir, DAO.getConfig("keystore.name", "keystore.jks")); if (!keystoreFile.exists() || !keystoreFile.isFile() || !keystoreFile.canRead()) { throw new Exception("Could not find keystore"); } keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); keyStore.load(new FileInputStream(keystoreFile.getAbsolutePath()), DAO.getConfig("keystore.password", "").toCharArray()); keystoreManagerPass = DAO.getConfig("keystore.password", ""); } else if ("key-cert".equals(keySource)) { Log.getLog().info("Importing keystore from key/cert files"); //use more common pem format as used by openssl //generate random password char[] chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toCharArray(); StringBuilder sb = new StringBuilder(); Random random = new Random(); for (int i = 0; i < 20; i++) { char c = chars[random.nextInt(chars.length)]; sb.append(c); } String password = keystoreManagerPass = sb.toString(); //get key and cert File keyFile = new File(DAO.getConfig("https.key", "")); if (!keyFile.exists() || !keyFile.isFile() || !keyFile.canRead()) { throw new Exception("Could not find key file"); } File certFile = new File(DAO.getConfig("https.cert", "")); if (!certFile.exists() || !certFile.isFile() || !certFile.canRead()) { throw new Exception("Could not find cert file"); } Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); byte[] keyBytes = Files.readAllBytes(keyFile.toPath()); byte[] certBytes = Files.readAllBytes(certFile.toPath()); PEMParser parser = new PEMParser(new InputStreamReader(new ByteArrayInputStream(certBytes))); X509Certificate cert = new JcaX509CertificateConverter().setProvider("BC") .getCertificate((X509CertificateHolder) parser.readObject()); parser = new PEMParser(new InputStreamReader(new ByteArrayInputStream(keyBytes))); PrivateKey key = new JcaPEMKeyConverter().setProvider("BC") .getPrivateKey((PrivateKeyInfo) parser.readObject()); keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); keyStore.load(null, null); keyStore.setCertificateEntry(cert.getSubjectX500Principal().getName(), cert); keyStore.setKeyEntry("defaultKey", key, password.toCharArray(), new Certificate[] { cert }); Log.getLog().info("Successfully imported keystore from key/cert files"); } else { throw new Exception("Invalid option for https.keysource"); } HttpConfiguration https_config = new HttpConfiguration(); https_config.addCustomizer(new SecureRequestCustomizer()); HttpConnectionFactory http1 = new HttpConnectionFactory(https_config); //HTTP2ServerConnectionFactory http2 = new HTTP2ServerConnectionFactory(https_config); //NegotiatingServerConnectionFactory.checkProtocolNegotiationAvailable(); //ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory(); //alpn.setDefaultProtocol(http1.getProtocol()); SslContextFactory sslContextFactory = new SslContextFactory(); sslContextFactory.setKeyStore(keyStore); sslContextFactory.setKeyManagerPassword(keystoreManagerPass); //sslContextFactory.setCipherComparator(HTTP2Cipher.COMPARATOR); //sslContextFactory.setUseCipherSuitesOrder(true); //SslConnectionFactory ssl = new SslConnectionFactory(sslContextFactory, alpn.getProtocol()); SslConnectionFactory ssl = new SslConnectionFactory(sslContextFactory, "http/1.1"); ServerConnector sslConnector = new ServerConnector(SusiInstallation.server, ssl, http1); sslConnector.setPort(httpsPort); sslConnector.setName("httpd:" + httpsPort); sslConnector.setIdleTimeout(20000); // timout in ms when no bytes send / received SusiInstallation.server.addConnector(sslConnector); } } private static void setServerHandler(File dataFile) { // create security handler for http auth and http-to-https redirects ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler(); boolean redirect = httpsMode.equals(HttpsMode.REDIRECT); boolean auth = "true".equals(DAO.getConfig("http.auth", "false")); if (redirect || auth) { org.eclipse.jetty.security.LoginService loginService = new org.eclipse.jetty.security.HashLoginService( "LoklakRealm", DAO.conf_dir.getAbsolutePath() + "/http_auth"); if (auth) SusiInstallation.server.addBean(loginService); Constraint constraint = new Constraint(); if (redirect) constraint.setDataConstraint(Constraint.DC_CONFIDENTIAL); if (auth) { constraint.setAuthenticate(true); constraint.setRoles(new String[] { "user", "admin" }); } //makes the constraint apply to all uri paths ConstraintMapping mapping = new ConstraintMapping(); mapping.setPathSpec("/*"); mapping.setConstraint(constraint); securityHandler.addConstraintMapping(mapping); if (auth) { securityHandler.setAuthenticator(new BasicAuthenticator()); securityHandler.setLoginService(loginService); } if (redirect) Log.getLog().info("Activated http-to-https redirect"); if (auth) Log.getLog().info("Activated basic http auth"); } // Setup IPAccessHandler for blacklists IPAccessHandler ipaccess = new IPAccessHandler(); String blacklist = DAO.getConfig("server.blacklist", ""); if (blacklist != null && blacklist.length() > 0) try { ipaccess = new IPAccessHandler(); String[] bx = blacklist.split(","); ipaccess.setBlack(bx); for (String b : bx) { int p = b.indexOf('|'); blacklistedHosts.add(p < 0 ? b : b.substring(0, p)); } } catch (IllegalArgumentException e) { Log.getLog().warn("bad blacklist:" + blacklist, e); } WebAppContext htrootContext = new WebAppContext(); htrootContext.setContextPath("/"); ServletContextHandler servletHandler = new ServletContextHandler(); // add services try { servletHandler.addServlet(InstallationPageService.class, (InstallationPageService.class.newInstance()).getAPIPath()); } catch (InstantiationException | IllegalAccessException e) { e.printStackTrace(); } servletHandler.setMaxFormContentSize(10 * 1024 * 1024); // 10 MB ErrorHandler errorHandler = new ErrorHandler(); errorHandler.setShowStacks(true); servletHandler.setErrorHandler(errorHandler); FileHandler fileHandler = new FileHandler(0); fileHandler.setDirectoriesListed(true); fileHandler.setWelcomeFiles(new String[] { "index.html" }); fileHandler.setResourceBase("installation"); RewriteHandler rewriteHandler = new RewriteHandler(); rewriteHandler.setRewriteRequestURI(true); rewriteHandler.setRewritePathInfo(false); rewriteHandler.setOriginalPathAttribute("originalPath"); // the attribute name where the original request is stored RewriteRegexRule rssSearchRule = new RewriteRegexRule(); rssSearchRule.setRegex("/rss/(.*)"); rssSearchRule.setReplacement("/search.rss?q=$1"); rewriteHandler.addRule(rssSearchRule); rewriteHandler.setHandler(servletHandler); HandlerList handlerlist2 = new HandlerList(); handlerlist2.setHandlers(new Handler[] { fileHandler, rewriteHandler, new DefaultHandler() }); GzipHandler gzipHandler = new GzipHandler(); gzipHandler.setIncludedMimeTypes( "text/html,text/plain,text/xml,text/css,application/javascript,text/javascript,application/json"); gzipHandler.setHandler(handlerlist2); HashSessionIdManager idmanager = new HashSessionIdManager(); SusiInstallation.server.setSessionIdManager(idmanager); SessionHandler sessions = new SessionHandler(new HashSessionManager()); sessions.setHandler(gzipHandler); securityHandler.setHandler(sessions); ipaccess.setHandler(securityHandler); SusiInstallation.server.setHandler(ipaccess); } private static void checkServerPorts(int httpPort, int httpsPort) throws IOException { // check http port if (!httpsMode.equals(HttpsMode.ONLY)) { ServerSocket ss = null; try { ss = new ServerSocket(httpPort); ss.setReuseAddress(true); ss.setReceiveBufferSize(65536); } catch (IOException e) { // the socket is already occupied by another service throw new IOException("port " + httpPort + " is already occupied by another service, maybe another susi is running on this port already. exit."); } finally { // close the socket again if (ss != null) ss.close(); } } // check https port if (httpsMode.isGreaterOrEqualTo(HttpsMode.ON)) { ServerSocket sss = null; try { sss = new ServerSocket(httpsPort); sss.setReuseAddress(true); sss.setReceiveBufferSize(65536); } catch (IOException e) { // the socket is already occupied by another service throw new IOException("port " + httpsPort + " is already occupied by another service, maybe another susi is running on this port already. exit."); } finally { // close the socket again if (sss != null) sss.close(); } } } public static void shutdown(int exitcode) { Log.getLog().info("Shutting down installation now"); server.setStopTimeout(0); System.exit(exitcode); } }