*/
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;
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/".
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;
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
}
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.");
+ }
}
}
--- /dev/null
+/*
+ * 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;
+ }
+}
*/
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;
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;
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!"));
}
--- /dev/null
+/*
+ * 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);
+ }
+}