/* * MIT License * * Copyright (c) 2020-present Cloudogu GmbH and Contributors * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package sonia.scm.server; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.ForwardedRequestCustomizer; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.SecureRequestCustomizer; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.SslConnectionFactory; import org.eclipse.jetty.server.handler.HandlerCollection; import org.eclipse.jetty.util.resource.ResourceCollection; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.webapp.WebAppContext; import java.net.URL; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; public final class ServerConfiguration { @SuppressWarnings("java:S1075") // not a real uri private static final String DEFAULT_CONTEXT_PATH = "/scm"; private final ServerConfigYaml configYaml; private Path testTempDir; public ServerConfiguration() { this.configYaml = new ServerConfigParser().parse(); } // Visible for testing public ServerConfiguration(URL configFile, Path tempDir) { this.configYaml = new ServerConfigParser().parse(configFile); this.testTempDir = tempDir; } public void configureServer(Server server) { try { configureHttp(server); configureHandler(server); if (configYaml.getHttps().getKeyStorePath() != null && !configYaml.getHttps().getKeyStorePath().isEmpty()) { configureSSL(server); } } catch (Exception ex) { throw new ScmServerException("error during server configuration", ex); } } private void configureSSL(Server server) { SslContextFactory.Server sslServer = new SslContextFactory.Server(); ServerConfigYaml.SSLConfig https = configYaml.getHttps(); sslServer.setKeyStorePath(https.getKeyStorePath()); sslServer.setKeyStorePassword(https.getKeyStorePassword()); sslServer.setKeyStoreType(https.getKeyStoreType()); sslServer.setIncludeProtocols("TLSv1.2", "TLSv1.3"); sslServer.setIncludeCipherSuites( "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256"); sslServer.setUseCipherSuitesOrder(false); HttpConfiguration sslHttpConfig = new HttpConfiguration(createCustomHttpConfig()); sslHttpConfig.addCustomizer(new SecureRequestCustomizer( false, true, -1, false ) ); ServerConnector sslConnector = new ServerConnector(server, ( new SslConnectionFactory(sslServer, "http/1.1")), new HttpConnectionFactory(sslHttpConfig)); sslConnector.setHost(configYaml.getAddressBinding()); sslConnector.setPort(https.getSslPort()); server.addConnector(sslConnector); } private void configureHttp(Server server) { HttpConfiguration httpConfig = createCustomHttpConfig(); redirectHttpToHttps(httpConfig); ServerConnector connector = new ServerConnector(server, new HttpConnectionFactory(httpConfig)); System.out.println("Set http address binding to " + configYaml.getAddressBinding()); connector.setHost(configYaml.getAddressBinding()); System.out.println("Set http port to " + configYaml.getPort()); connector.setPort(configYaml.getPort()); server.addConnector(connector); } private void redirectHttpToHttps(HttpConfiguration httpConfig) { ServerConfigYaml.SSLConfig https = configYaml.getHttps(); if (configYaml.getHttps().isRedirectHttpToHttps()) { httpConfig.setSecurePort(https.getSslPort()); httpConfig.setSecureScheme("https"); } } private void configureHandler(Server server) { HandlerCollection handlerCollection = new HandlerCollection(); handlerCollection.setHandlers(new Handler[]{createWebAppContext(), createDocRoot()}); server.setHandler(handlerCollection); } private WebAppContext createWebAppContext() { WebAppContext webApp = new WebAppContext(); webApp.setContextPath(configYaml.getContextPath()); // disable directory listings webApp.setInitParameter("org.eclipse.jetty.servlet.Default.dirAllowed", "false"); String baseDir = resolveBaseDir(); webApp.setWar(baseDir + "/var/webapp/scm-webapp.war"); String tempDir = configYaml.getTempDir(); webApp.setTempDirectory(tempDir.startsWith("/") ? Paths.get(tempDir, "webapp").toFile() : Paths.get(baseDir, tempDir).toFile()); return webApp; } private WebAppContext createDocRoot() { WebAppContext docRoot = new WebAppContext(); docRoot.setContextPath("/"); String baseDir = resolveBaseDir(); docRoot.setBaseResource(new ResourceCollection(new String[]{baseDir + "/var/webapp/docroot"})); String tempDir = configYaml.getTempDir(); docRoot.setTempDirectory(tempDir.startsWith("/") ? Paths.get(tempDir, "work/docroot").toFile() : Paths.get(baseDir, tempDir).toFile()); return docRoot; } private HttpConfiguration createCustomHttpConfig() { HttpConfiguration httpConfig = new HttpConfiguration(); System.out.println("Set http request header size to " + configYaml.getHttpHeaderSize()); httpConfig.setRequestHeaderSize(configYaml.getHttpHeaderSize()); System.out.println("Set http response header size to " + configYaml.getHttpHeaderSize()); httpConfig.setResponseHeaderSize(configYaml.getHttpHeaderSize()); httpConfig.setSendServerVersion(false); System.out.println("Set forward request customizer: " + configYaml.isForwardHeadersEnabled()); if (configYaml.isForwardHeadersEnabled()) { httpConfig.addCustomizer(new ForwardedRequestCustomizer()); } return httpConfig; } public List getListeners() { List listeners = new ArrayList<>(); Server server = new Server(); configureServer(server); String contextPath = findContextPath(server.getHandlers()); if (contextPath == null) { contextPath = DEFAULT_CONTEXT_PATH; } for (Connector connector : server.getConnectors()) { if (connector instanceof ServerConnector) { ServerConnector serverConnector = (ServerConnector) connector; String scheme = "http"; String protocol = serverConnector.getDefaultProtocol(); if ("SSL".equalsIgnoreCase(protocol) || "TLS".equalsIgnoreCase(protocol)) { scheme = "https"; } listeners.add(new Listener(scheme, serverConnector.getPort(), contextPath)); } } return listeners; } private String resolveBaseDir() { return testTempDir != null ? testTempDir.toString() : System.getProperty("baseDir", "."); } private String findContextPath(Handler[] handlers) { for (Handler handler : handlers) { if (handler instanceof WebAppContext) { return ((WebAppContext) handler).getContextPath(); } else if (handler instanceof HandlerCollection) { String contextPath = findContextPath(((HandlerCollection) handler).getHandlers()); if (contextPath != null) { return contextPath; } } } return null; } }