@@ -21,7 +21,6 @@ package org.sonar.server.app; | |||
import org.apache.catalina.connector.Connector; | |||
import org.apache.catalina.startup.Tomcat; | |||
import org.slf4j.LoggerFactory; | |||
import org.sonar.process.Props; | |||
import javax.annotation.Nullable; | |||
@@ -33,6 +32,9 @@ import java.util.HashSet; | |||
import java.util.List; | |||
import java.util.Set; | |||
/** | |||
* Configuration of Tomcat connectors | |||
*/ | |||
class Connectors { | |||
private static final int DISABLED_PORT = -1; | |||
@@ -77,7 +79,6 @@ class Connectors { | |||
if (port > DISABLED_PORT) { | |||
connector = newConnector(props, HTTP_PROTOCOL, "http"); | |||
connector.setPort(port); | |||
info("HTTP connector is enabled on port " + port); | |||
} | |||
return connector; | |||
} | |||
@@ -89,7 +90,6 @@ class Connectors { | |||
if (port > DISABLED_PORT) { | |||
connector = newConnector(props, AJP_PROTOCOL, "http"); | |||
connector.setPort(port); | |||
info("AJP connector is enabled on port " + port); | |||
} | |||
return connector; | |||
} | |||
@@ -115,12 +115,12 @@ class Connectors { | |||
setConnectorAttribute(connector, "truststoreType", props.value("sonar.web.https.truststoreType", "JKS")); | |||
setConnectorAttribute(connector, "truststoreProvider", props.value("sonar.web.https.truststoreProvider")); | |||
setConnectorAttribute(connector, "clientAuth", props.value("sonar.web.https.clientAuth", "false")); | |||
setConnectorAttribute(connector, "ciphers", props.value("sonar.web.https.ciphers")); | |||
// SSLv3 must not be enable because of Poodle vulnerability | |||
// See https://jira.codehaus.org/browse/SONAR-5860 | |||
setConnectorAttribute(connector, "sslEnabledProtocols", "TLSv1,TLSv1.1,TLSv1.2"); | |||
setConnectorAttribute(connector, "sslProtocol", "TLS"); | |||
setConnectorAttribute(connector, "SSLEnabled", true); | |||
info("HTTPS connector is enabled on port " + port); | |||
} | |||
return connector; | |||
} | |||
@@ -153,8 +153,4 @@ class Connectors { | |||
c.setAttribute(key, value); | |||
} | |||
} | |||
private static void info(String message) { | |||
LoggerFactory.getLogger(Connectors.class).info(message); | |||
} | |||
} |
@@ -60,12 +60,16 @@ class EmbeddedTomcat { | |||
webappContext = Webapp.configure(tomcat, props); | |||
try { | |||
tomcat.start(); | |||
new StartupLogs(LoggerFactory.getLogger(getClass())).log(tomcat); | |||
} catch (LifecycleException e) { | |||
Throwables.propagate(e); | |||
} | |||
} | |||
boolean isReady() { | |||
if (webappContext == null) { | |||
return false; | |||
} | |||
switch (webappContext.getState()) { | |||
case NEW: | |||
case INITIALIZING: |
@@ -0,0 +1,74 @@ | |||
/* | |||
* SonarQube, open source software quality management tool. | |||
* Copyright (C) 2008-2014 SonarSource | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* SonarQube 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 3 of the License, or (at your option) any later version. | |||
* | |||
* SonarQube 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; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.app; | |||
import org.apache.catalina.connector.Connector; | |||
import org.apache.catalina.startup.Tomcat; | |||
import org.apache.commons.lang.StringUtils; | |||
import org.apache.coyote.ProtocolHandler; | |||
import org.apache.coyote.http11.AbstractHttp11JsseProtocol; | |||
import org.slf4j.Logger; | |||
class StartupLogs { | |||
private final Logger log; | |||
StartupLogs(Logger log) { | |||
this.log = log; | |||
} | |||
void log(Tomcat tomcat) { | |||
Connector[] connectors = tomcat.getService().findConnectors(); | |||
for (Connector connector : connectors) { | |||
if (StringUtils.containsIgnoreCase(connector.getProtocol(), "AJP")) { | |||
logAjp(connector); | |||
} else if (StringUtils.equalsIgnoreCase(connector.getScheme(), "https")) { | |||
logHttps(connector); | |||
} else if (StringUtils.equalsIgnoreCase(connector.getScheme(), "http")) { | |||
logHttp(connector); | |||
} else { | |||
throw new IllegalArgumentException("Unsupported connector: " + connector); | |||
} | |||
} | |||
} | |||
private void logAjp(Connector connector) { | |||
log.info(String.format("%s connector enabled on port %d", connector.getProtocol(), connector.getPort())); | |||
} | |||
private void logHttp(Connector connector) { | |||
log.info(String.format("HTTP connector enabled on port %d", connector.getPort())); | |||
} | |||
private void logHttps(Connector connector) { | |||
StringBuilder additional = new StringBuilder(); | |||
ProtocolHandler protocol = connector.getProtocolHandler(); | |||
if (protocol instanceof AbstractHttp11JsseProtocol) { | |||
additional.append("| ciphers="); | |||
String ciphers = ((AbstractHttp11JsseProtocol) protocol).getCiphers(); | |||
if (StringUtils.isBlank(ciphers)) { | |||
additional.append("JVM defaults"); | |||
} else { | |||
additional.append(ciphers); | |||
} | |||
} | |||
log.info(String.format("HTTPS connector enabled on port %d %s", connector.getPort(), additional)); | |||
} | |||
} |
@@ -30,6 +30,9 @@ import org.sonar.process.Props; | |||
import java.io.File; | |||
import java.util.Map; | |||
/** | |||
* Configures webapp into Tomcat | |||
*/ | |||
class Webapp { | |||
private static final String JRUBY_MAX_RUNTIMES = "jruby.max.runtimes"; |
@@ -0,0 +1,78 @@ | |||
/* | |||
* SonarQube, open source software quality management tool. | |||
* Copyright (C) 2008-2014 SonarSource | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* SonarQube 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 3 of the License, or (at your option) any later version. | |||
* | |||
* SonarQube 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; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.app; | |||
import org.apache.commons.io.FileUtils; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.junit.rules.TemporaryFolder; | |||
import org.sonar.process.NetworkUtils; | |||
import org.sonar.process.Props; | |||
import java.io.File; | |||
import java.net.ConnectException; | |||
import java.net.Inet4Address; | |||
import java.net.URL; | |||
import java.util.Properties; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.junit.Assert.fail; | |||
public class EmbeddedTomcatTest { | |||
@Rule | |||
public TemporaryFolder temp = new TemporaryFolder(); | |||
@Test | |||
public void start() throws Exception { | |||
Props props = new Props(new Properties()); | |||
// prepare file system | |||
File home = temp.newFolder(); | |||
File webDir = new File(home, "web"); | |||
FileUtils.write(new File(home, "web/WEB-INF/web.xml"), "<web-app/>"); | |||
props.set("sonar.path.home", home.getAbsolutePath()); | |||
props.set("sonar.path.web", webDir.getAbsolutePath()); | |||
// start server on a random port | |||
int httpPort = NetworkUtils.freePort(); | |||
int ajpPort = NetworkUtils.freePort(); | |||
props.set("sonar.web.port", String.valueOf(httpPort)); | |||
props.set("sonar.ajp.port", String.valueOf(ajpPort)); | |||
EmbeddedTomcat tomcat = new EmbeddedTomcat(props); | |||
assertThat(tomcat.isReady()).isFalse(); | |||
tomcat.start(); | |||
assertThat(tomcat.isReady()).isTrue(); | |||
// check that http connector accepts requests | |||
URL url = new URL("http://" + Inet4Address.getLocalHost().getHostAddress() + ":" + httpPort); | |||
url.openConnection().connect(); | |||
// stop server | |||
tomcat.terminate(); | |||
// tomcat.isReady() must not be called. It is used to wait for server startup, not shutdown. | |||
try { | |||
url.openConnection().connect(); | |||
fail(); | |||
} catch (ConnectException e) { | |||
// expected | |||
} | |||
} | |||
} |
@@ -0,0 +1,104 @@ | |||
/* | |||
* SonarQube, open source software quality management tool. | |||
* Copyright (C) 2008-2014 SonarSource | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* SonarQube 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 3 of the License, or (at your option) any later version. | |||
* | |||
* SonarQube 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; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.app; | |||
import org.apache.catalina.connector.Connector; | |||
import org.apache.catalina.startup.Tomcat; | |||
import org.apache.coyote.http11.Http11Protocol; | |||
import org.junit.Test; | |||
import org.mockito.Mockito; | |||
import org.slf4j.Logger; | |||
import static org.junit.Assert.fail; | |||
import static org.mockito.Mockito.*; | |||
public class StartupLogsTest { | |||
Tomcat tomcat = mock(Tomcat.class, Mockito.RETURNS_DEEP_STUBS); | |||
Logger logger = mock(Logger.class); | |||
StartupLogs sut = new StartupLogs(logger); | |||
@Test | |||
public void logAjp() throws Exception { | |||
Connector connector = newConnector("AJP/1.3", "http"); | |||
when(tomcat.getService().findConnectors()).thenReturn(new Connector[] {connector}); | |||
sut.log(tomcat); | |||
verify(logger).info("AJP/1.3 connector enabled on port 1234"); | |||
verifyNoMoreInteractions(logger); | |||
} | |||
@Test | |||
public void logHttp() throws Exception { | |||
Connector connector = newConnector("HTTP/1.1", "http"); | |||
when(tomcat.getService().findConnectors()).thenReturn(new Connector[] {connector}); | |||
sut.log(tomcat); | |||
verify(logger).info("HTTP connector enabled on port 1234"); | |||
verifyNoMoreInteractions(logger); | |||
} | |||
@Test | |||
public void logHttps_default_ciphers() throws Exception { | |||
Connector connector = newConnector("HTTP/1.1", "https"); | |||
when(tomcat.getService().findConnectors()).thenReturn(new Connector[] {connector}); | |||
sut.log(tomcat); | |||
verify(logger).info("HTTPS connector enabled on port 1234 | ciphers=JVM defaults"); | |||
verifyNoMoreInteractions(logger); | |||
} | |||
@Test | |||
public void logHttps_overridden_ciphers() throws Exception { | |||
Connector connector = newConnector("HTTP/1.1", "https"); | |||
connector.setProtocolHandlerClassName("org.apache.coyote.http11.Http11Protocol"); | |||
((Http11Protocol) connector.getProtocolHandler()).setCiphers("SSL_RSA,TLS_RSA_WITH_RC4"); | |||
when(tomcat.getService().findConnectors()).thenReturn(new Connector[] {connector}); | |||
sut.log(tomcat); | |||
verify(logger).info("HTTPS connector enabled on port 1234 | ciphers=SSL_RSA,TLS_RSA_WITH_RC4"); | |||
verifyNoMoreInteractions(logger); | |||
} | |||
@Test | |||
public void unsupported_connector() throws Exception { | |||
Connector connector = mock(Connector.class, Mockito.RETURNS_DEEP_STUBS); | |||
when(connector.getProtocol()).thenReturn("SPDY/1.1"); | |||
when(connector.getScheme()).thenReturn("spdy"); | |||
when(tomcat.getService().findConnectors()).thenReturn(new Connector[] {connector}); | |||
try { | |||
sut.log(tomcat); | |||
fail(); | |||
} catch (IllegalArgumentException e) { | |||
// expected | |||
} | |||
} | |||
private Connector newConnector(String protocol, String schema) { | |||
Connector httpConnector = new Connector(protocol); | |||
httpConnector.setScheme(schema); | |||
httpConnector.setPort(1234); | |||
return httpConnector; | |||
} | |||
} |
@@ -154,6 +154,15 @@ | |||
# and 'true' (certificates are required). | |||
#sonar.web.https.clientAuth=false | |||
# HTTPS - comma separated list of encryption ciphers to support for HTTPS connections. | |||
# If specified, only the ciphers that are listed and supported by the SSL implementation will be used. | |||
# By default, the default ciphers for the JVM will be used. Note that this usually means that the weak | |||
# export grade ciphers will be included in the list of available ciphers. | |||
# The ciphers are specified using the JSSE cipher naming convention (see | |||
# https://www.openssl.org/docs/apps/ciphers.html) | |||
# Example: sonar.web.https.ciphers=TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 | |||
#sonar.web.https.ciphers= | |||
# The maximum number of connections that the server will accept and process at any given time. | |||
# When this number has been reached, the server will not accept any more connections until | |||
# the number of connections falls below this value. The operating system may still accept connections |