org.loklak.LoklakInstallation.java Source code

Java tutorial

Introduction

Here is the source code for org.loklak.LoklakInstallation.java

Source

/**
 *  LoklakInstallation
 *  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.LoklakServer.readConfig;

public class LoklakInstallation {

    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 ? 9000 : 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);

        LoklakInstallation.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");
                    LoklakInstallation.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 **

        LoklakInstallation.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);
        LoklakInstallation.server = new Server(pool);
        LoklakInstallation.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(LoklakInstallation.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
            LoklakInstallation.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(LoklakServer.server, ssl, alpn, http2, http1);
            ServerConnector sslConnector = new ServerConnector(LoklakInstallation.server, ssl, http1);
            sslConnector.setPort(httpsPort);
            sslConnector.setName("httpd:" + httpsPort);
            sslConnector.setIdleTimeout(20000); // timout in ms when no bytes send / received
            LoklakInstallation.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)
                LoklakInstallation.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();
        LoklakInstallation.server.setSessionIdManager(idmanager);
        SessionHandler sessions = new SessionHandler(new HashSessionManager());
        sessions.setHandler(gzipHandler);
        securityHandler.setHandler(sessions);
        ipaccess.setHandler(securityHandler);

        LoklakInstallation.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 loklak 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 loklak 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);
    }
}