From 53170bebb544567dc9fb3fad2860a671032c7efb Mon Sep 17 00:00:00 2001 From: Simon Brandhof Date: Wed, 9 Dec 2015 15:32:04 +0100 Subject: [PATCH] SONAR-7126 Scanner on Java 7 must support TLS 1.2 when connecting to SonarQube HTTPS server --- .../sonarqube/ws/client/HttpConnector.java | 44 ++++++++- .../ws/client/Tls12Java7SocketFactory.java | 94 +++++++++++++++++++ .../ws/client/HttpConnectorTest.java | 37 ++++++++ .../client/Tls12Java7SocketFactoryTest.java | 61 ++++++++++++ 4 files changed, 234 insertions(+), 2 deletions(-) create mode 100644 sonar-ws/src/main/java/org/sonarqube/ws/client/Tls12Java7SocketFactory.java create mode 100644 sonar-ws/src/test/java/org/sonarqube/ws/client/Tls12Java7SocketFactoryTest.java diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/HttpConnector.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/HttpConnector.java index c76672924c5..52e5c805c1b 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/HttpConnector.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/HttpConnector.java @@ -19,7 +19,9 @@ */ package org.sonarqube.ws.client; +import com.google.common.annotations.VisibleForTesting; import com.squareup.okhttp.Call; +import com.squareup.okhttp.ConnectionSpec; import com.squareup.okhttp.Credentials; import com.squareup.okhttp.Headers; import com.squareup.okhttp.HttpUrl; @@ -35,16 +37,24 @@ import java.util.Map; import java.util.concurrent.TimeUnit; import javax.annotation.CheckForNull; import javax.annotation.Nullable; +import javax.net.ssl.SSLSocketFactory; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Strings.isNullOrEmpty; import static com.google.common.base.Strings.nullToEmpty; import static java.lang.String.format; +import static java.util.Arrays.asList; +/** + * Connect to any SonarQube server available through HTTP or HTTPS. + *

TLS 1.0, 1.1 and 1.2 are supported on both Java 7 and 8. SSLv3 is not supported.

+ *

The JVM system proxies are used.

+ */ public class HttpConnector implements WsConnector { public static final int DEFAULT_CONNECT_TIMEOUT_MILLISECONDS = 30_000; public static final int DEFAULT_READ_TIMEOUT_MILLISECONDS = 60_000; + public static final String[] TLS_PROTOCOLS = new String[] {"TLSv1", "TLSv1.1", "TLSv1.2"}; /** * Base URL with trailing slash, for instance "https://localhost/sonarqube/". @@ -56,7 +66,7 @@ public class HttpConnector implements WsConnector { private final String proxyCredentials; private final OkHttpClient okHttpClient = new OkHttpClient(); - private HttpConnector(Builder builder) { + private HttpConnector(Builder builder, JavaVersion javaVersion) { this.baseUrl = HttpUrl.parse(builder.url.endsWith("/") ? builder.url : format("%s/", builder.url)); this.userAgent = builder.userAgent; @@ -81,6 +91,26 @@ public class HttpConnector implements WsConnector { this.okHttpClient.setConnectTimeout(builder.connectTimeoutMs, TimeUnit.MILLISECONDS); this.okHttpClient.setReadTimeout(builder.readTimeoutMs, TimeUnit.MILLISECONDS); + + ConnectionSpec tls = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) + .allEnabledTlsVersions() + .allEnabledCipherSuites() + .supportsTlsExtensions(true) + .build(); + this.okHttpClient.setConnectionSpecs(asList(tls, ConnectionSpec.CLEARTEXT)); + if (javaVersion.isJava7()) { + // OkHttp executes SSLContext.getInstance("TLS") by default (see + // https://github.com/square/okhttp/blob/c358656/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java#L616) + // As only TLS 1.0 is enabled by default in Java 7, the SSLContextFactory must be changed + // in order to support all versions from 1.0 to 1.2. + // Note that this is not overridden for Java 8 as TLS 1.2 is enabled by default. + // Keeping getInstance("TLS") allows to support potential future versions of TLS on Java 8. + try { + this.okHttpClient.setSslSocketFactory(new Tls12Java7SocketFactory((SSLSocketFactory) SSLSocketFactory.getDefault())); + } catch (Exception e) { + throw new IllegalStateException("Fail to init TLS context", e); + } + } } @Override @@ -248,9 +278,19 @@ public class HttpConnector implements WsConnector { } public HttpConnector build() { + return build(new JavaVersion()); + } + + @VisibleForTesting + HttpConnector build(JavaVersion javaVersion) { checkArgument(!isNullOrEmpty(url), "Server URL is not defined"); - return new HttpConnector(this); + return new HttpConnector(this, javaVersion); } + } + static class JavaVersion { + boolean isJava7() { + return System.getProperty("java.version").startsWith("1.7."); + } } } diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/Tls12Java7SocketFactory.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/Tls12Java7SocketFactory.java new file mode 100644 index 00000000000..02b27bf549c --- /dev/null +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/Tls12Java7SocketFactory.java @@ -0,0 +1,94 @@ +/* + * 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.sonarqube.ws.client; + +import com.google.common.annotations.VisibleForTesting; +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +/** + * {@link SSLSocketFactory} which enables all the versions of TLS. This is required + * to support TLSv1.2 on Java 7. Note that Java 8 supports TLSv1.2 natively, without + * any configuration + */ +public class Tls12Java7SocketFactory extends SSLSocketFactory { + + @VisibleForTesting + static final String[] TLS_PROTOCOLS = new String[] {"TLSv1", "TLSv1.1", "TLSv1.2"}; + + private final SSLSocketFactory delegate; + + public Tls12Java7SocketFactory(SSLSocketFactory delegate) { + this.delegate = delegate; + } + + @Override + public String[] getDefaultCipherSuites() { + return delegate.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return delegate.getSupportedCipherSuites(); + } + + @Override + public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException { + Socket underlyingSocket = delegate.createSocket(socket, host, port, autoClose); + return overrideProtocol(underlyingSocket); + } + + @Override + public Socket createSocket(String host, int port) throws IOException { + Socket underlyingSocket = delegate.createSocket(host, port); + return overrideProtocol(underlyingSocket); + } + + @Override + public Socket createSocket(String host, int port, InetAddress localAddress, int localPort) throws IOException { + Socket underlyingSocket = delegate.createSocket(host, port, localAddress, localPort); + return overrideProtocol(underlyingSocket); + } + + @Override + public Socket createSocket(InetAddress host, int port) throws IOException { + Socket underlyingSocket = delegate.createSocket(host, port); + return overrideProtocol(underlyingSocket); + } + + @Override + public Socket createSocket(InetAddress host, int port, InetAddress localAddress, int localPort) throws IOException { + Socket underlyingSocket = delegate.createSocket(host, port, localAddress, localPort); + return overrideProtocol(underlyingSocket); + } + + /** + * Enables TLS v1.0, 1.1 and 1.2 on the socket + */ + private static Socket overrideProtocol(Socket socket) { + if (socket instanceof SSLSocket) { + ((SSLSocket) socket).setEnabledProtocols(TLS_PROTOCOLS); + } + return socket; + } +} diff --git a/sonar-ws/src/test/java/org/sonarqube/ws/client/HttpConnectorTest.java b/sonar-ws/src/test/java/org/sonarqube/ws/client/HttpConnectorTest.java index 27f68d64bc7..8d3aa0eb23b 100644 --- a/sonar-ws/src/test/java/org/sonarqube/ws/client/HttpConnectorTest.java +++ b/sonar-ws/src/test/java/org/sonarqube/ws/client/HttpConnectorTest.java @@ -19,10 +19,12 @@ */ package org.sonarqube.ws.client; +import com.squareup.okhttp.ConnectionSpec; import com.squareup.okhttp.mockwebserver.MockResponse; import com.squareup.okhttp.mockwebserver.MockWebServer; import com.squareup.okhttp.mockwebserver.RecordedRequest; import java.io.File; +import java.util.List; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; @@ -36,12 +38,14 @@ import static com.squareup.okhttp.Credentials.basic; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class HttpConnectorTest { @Rule public TemporaryFolder temp = new TemporaryFolder(); + HttpConnector.JavaVersion javaVersion = mock(HttpConnector.JavaVersion.class); MockWebServer server; String serverUrl; @@ -261,6 +265,39 @@ public class HttpConnectorTest { assertThat(underTest.call(request).requestUrl()).isEqualTo(serverUrl + "sonar/api/issues/search"); } + @Test + public void support_tls_1_2_on_java7() { + when(javaVersion.isJava7()).thenReturn(true); + HttpConnector underTest = new HttpConnector.Builder().url(serverUrl).build(javaVersion); + + assertTlsAndClearTextSpecifications(underTest); + // enable TLS 1.0, 1.1 and 1.2 + assertThat(underTest.okHttpClient().getSslSocketFactory()).isNotNull().isInstanceOf(Tls12Java7SocketFactory.class); + } + + @Test + public void support_tls_versions_of_java8() { + when(javaVersion.isJava7()).thenReturn(false); + HttpConnector underTest = new HttpConnector.Builder().url(serverUrl).build(javaVersion); + + assertTlsAndClearTextSpecifications(underTest); + // do not override the default TLS context provided by java 8 + assertThat(underTest.okHttpClient().getSslSocketFactory()).isNull(); + } + + private void assertTlsAndClearTextSpecifications(HttpConnector underTest) { + List connectionSpecs = underTest.okHttpClient().getConnectionSpecs(); + assertThat(connectionSpecs).hasSize(2); + + // TLS. tlsVersions()==null means all TLS versions + assertThat(connectionSpecs.get(0).tlsVersions()).isNull(); + assertThat(connectionSpecs.get(0).isTls()).isTrue(); + + // HTTP + assertThat(connectionSpecs.get(1).tlsVersions()).isNull(); + assertThat(connectionSpecs.get(1).isTls()).isFalse(); + } + private void answerHelloWorld() { server.enqueue(new MockResponse().setBody("hello, world!")); } diff --git a/sonar-ws/src/test/java/org/sonarqube/ws/client/Tls12Java7SocketFactoryTest.java b/sonar-ws/src/test/java/org/sonarqube/ws/client/Tls12Java7SocketFactoryTest.java new file mode 100644 index 00000000000..6ed764c0faa --- /dev/null +++ b/sonar-ws/src/test/java/org/sonarqube/ws/client/Tls12Java7SocketFactoryTest.java @@ -0,0 +1,61 @@ +/* + * 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.sonarqube.ws.client; + +import java.io.IOException; +import java.net.InetAddress; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import org.junit.Test; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class Tls12Java7SocketFactoryTest { + + SSLSocketFactory delegate = mock(SSLSocketFactory.class); + SSLSocket socket = mock(SSLSocket.class); + Tls12Java7SocketFactory underTest = new Tls12Java7SocketFactory(delegate); + + @Test + public void createSocket_1() throws IOException { + InetAddress address = mock(InetAddress.class); + when(delegate.createSocket(address, 80)).thenReturn(socket); + SSLSocket socket = (SSLSocket) underTest.createSocket(address, 80); + verify(socket).setEnabledProtocols(Tls12Java7SocketFactory.TLS_PROTOCOLS); + } + + @Test + public void createSocket_2() throws IOException { + InetAddress address = mock(InetAddress.class); + InetAddress address2 = mock(InetAddress.class); + when(delegate.createSocket(address, 80, address2, 443)).thenReturn(socket); + SSLSocket socket = (SSLSocket) underTest.createSocket(address, 80, address2, 443); + verify(socket).setEnabledProtocols(Tls12Java7SocketFactory.TLS_PROTOCOLS); + } + + @Test + public void createSocket_3() throws IOException { + when(delegate.createSocket("", 80)).thenReturn(socket); + SSLSocket socket = (SSLSocket) underTest.createSocket("", 80); + verify(socket).setEnabledProtocols(Tls12Java7SocketFactory.TLS_PROTOCOLS); + } +} -- 2.39.5