]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-7126 Scanner on Java 7 must support TLS 1.2 when connecting to SonarQube HTTPS...
authorSimon Brandhof <simon.brandhof@sonarsource.com>
Wed, 9 Dec 2015 14:32:04 +0000 (15:32 +0100)
committerSimon Brandhof <simon.brandhof@sonarsource.com>
Wed, 16 Dec 2015 15:46:52 +0000 (16:46 +0100)
sonar-ws/src/main/java/org/sonarqube/ws/client/HttpConnector.java
sonar-ws/src/main/java/org/sonarqube/ws/client/Tls12Java7SocketFactory.java [new file with mode: 0644]
sonar-ws/src/test/java/org/sonarqube/ws/client/HttpConnectorTest.java
sonar-ws/src/test/java/org/sonarqube/ws/client/Tls12Java7SocketFactoryTest.java [new file with mode: 0644]

index c76672924c52b34777c43089e4be734757874096..52e5c805c1bc545a02953adc1105bccb076af19e 100644 (file)
@@ -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.
+ * <p>TLS 1.0, 1.1 and 1.2 are supported on both Java 7 and 8. SSLv3 is not supported.</p>
+ * <p>The JVM system proxies are used.</p>
+ */
 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 (file)
index 0000000..02b27bf
--- /dev/null
@@ -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;
+  }
+}
index 27f68d64bc7b51ef57e9695c99bde66cf191b9fa..8d3aa0eb23bb63dfc2441416c9f341607590ed13 100644 (file)
  */
 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<ConnectionSpec> 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 (file)
index 0000000..6ed764c
--- /dev/null
@@ -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);
+  }
+}