/**
* 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);
}
}