]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-8351 share OkHttpClient utilities
authorSimon Brandhof <simon.brandhof@sonarsource.com>
Tue, 8 Nov 2016 14:34:18 +0000 (15:34 +0100)
committerSimon Brandhof <simon.brandhof@sonarsource.com>
Wed, 9 Nov 2016 19:48:20 +0000 (20:48 +0100)
sonar-ws correctly configures OkHttpClient for the
support of HTTPS, proxy (incl. authentication), timeouts and user agent.
This code should be reused by web server and compute engine, for
example when sending webhook payloads.

The new class OkHttpClientProvider allows web server/CE
to instantiate and configure a single instance of OkHttpClient.

server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java
server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel1.java
server/sonar-server/src/main/java/org/sonar/server/util/OkHttpClientProvider.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/util/OkHttpClientProviderTest.java [new file with mode: 0644]
sonar-scanner-engine/src/test/java/org/sonar/scanner/bootstrap/BatchWsClientProviderTest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/HttpConnector.java
sonar-ws/src/main/java/org/sonarqube/ws/client/OkHttpClientBuilder.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/OkHttpClientBuilderTest.java [new file with mode: 0644]

index 6b2d0fdf938f52552f9d2ae9588c517091d7b598..4bcdb4b567f3482b3b9b4feb793fa91b195ecd0b 100644 (file)
@@ -137,6 +137,7 @@ import org.sonar.server.user.DefaultUserFinder;
 import org.sonar.server.user.DeprecatedUserFinder;
 import org.sonar.server.user.index.UserIndex;
 import org.sonar.server.user.index.UserIndexer;
+import org.sonar.server.util.OkHttpClientProvider;
 import org.sonar.server.view.index.ViewIndex;
 import org.sonar.server.view.index.ViewIndexer;
 import org.sonarqube.ws.Rules;
@@ -241,6 +242,8 @@ public class ComputeEngineContainerImpl implements ComputeEngineContainer {
 
       // issues
       IssueIndex.class,
+
+      new OkHttpClientProvider()
     };
   }
 
index 4f323c24de009c309561731715ce1c0309394400..b9b59c7dc48235b94b1579a35507e7a88c25ae62 100644 (file)
@@ -105,7 +105,7 @@ public class ComputeEngineContainerImplTest {
     );
     assertThat(picoContainer.getParent().getParent().getParent().getComponentAdapters()).hasSize(
       COMPONENTS_IN_LEVEL_1_AT_CONSTRUCTION
-        + 24 // level 1
+        + 25 // level 1
         + 46 // content of DaoModule
         + 2 // content of EsSearchModule
         + 62 // content of CorePropertyDefinitions
index 036f37de56251eb95a2d37d310226cc87032b666..2bbe67023bb7b980589265e78f44862ed90873bb 100644 (file)
@@ -55,6 +55,7 @@ import org.sonar.server.rule.index.RuleIndex;
 import org.sonar.server.search.EsSearchModule;
 import org.sonar.server.setting.ThreadLocalSettings;
 import org.sonar.server.user.ThreadLocalUserSession;
+import org.sonar.server.util.OkHttpClientProvider;
 
 public class PlatformLevel1 extends PlatformLevel {
   private final Platform platform;
@@ -118,6 +119,7 @@ public class PlatformLevel1 extends PlatformLevel {
       // issues
       IssueIndex.class,
 
+      new OkHttpClientProvider(),
       // Classes kept for backward compatibility of plugins/libs (like sonar-license) that are directly calling classes from the core
       org.sonar.core.properties.PropertiesDao.class);
     addAll(CorePropertyDefinitions.all());
diff --git a/server/sonar-server/src/main/java/org/sonar/server/util/OkHttpClientProvider.java b/server/sonar-server/src/main/java/org/sonar/server/util/OkHttpClientProvider.java
new file mode 100644 (file)
index 0000000..da1e407
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.util;
+
+import okhttp3.OkHttpClient;
+import org.picocontainer.injectors.ProviderAdapter;
+import org.sonar.api.SonarRuntime;
+import org.sonar.api.ce.ComputeEngineSide;
+import org.sonar.api.config.Settings;
+import org.sonar.api.server.ServerSide;
+import org.sonar.process.ProcessProperties;
+import org.sonarqube.ws.client.OkHttpClientBuilder;
+
+import static java.lang.String.format;
+
+/**
+ * Provide a unique instance of {@link OkHttpClient} which configuration:
+ * <ul>
+ *   <li>supports HTTPS</li>
+ *   <li>supports proxy, including authentication, as defined by the properties like "http.proxyHost" in
+ *   conf/sonar.properties</li>
+ *   <li>has connect and read timeouts of 10 seconds each</li>
+ *   <li>sends automatically the HTTP header "User-Agent" with value "SonarQube/{version}", for instance "SonarQube/6.2"</li>
+ * </ul>
+ */
+@ServerSide
+@ComputeEngineSide
+public class OkHttpClientProvider extends ProviderAdapter {
+
+  private static final int DEFAULT_CONNECT_TIMEOUT_IN_MS = 10_000;
+  private static final int DEFAULT_READ_TIMEOUT_IN_MS = 10_000;
+
+  private okhttp3.OkHttpClient okHttpClient;
+
+  /**
+   * @return a {@link OkHttpClient} singleton
+   */
+  public OkHttpClient provide(Settings settings, SonarRuntime runtime) {
+    if (okHttpClient == null) {
+      OkHttpClientBuilder builder = new OkHttpClientBuilder();
+      builder.setConnectTimeoutMs(DEFAULT_CONNECT_TIMEOUT_IN_MS);
+      builder.setReadTimeoutMs(DEFAULT_READ_TIMEOUT_IN_MS);
+      // no need to define proxy URL as system-wide proxy is used and properly
+      // configured by bootstrap process.
+      builder.setProxyLogin(settings.getString(ProcessProperties.HTTP_PROXY_USER));
+      builder.setProxyPassword(settings.getString(ProcessProperties.HTTP_PROXY_PASSWORD));
+      builder.setUserAgent(format("SonarQube/%s", runtime.getApiVersion().toString()));
+      okHttpClient = builder.build();
+    }
+    return okHttpClient;
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/util/OkHttpClientProviderTest.java b/server/sonar-server/src/test/java/org/sonar/server/util/OkHttpClientProviderTest.java
new file mode 100644 (file)
index 0000000..e3f3f4b
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.util;
+
+import java.io.IOException;
+import java.util.Base64;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.SonarQubeSide;
+import org.sonar.api.SonarRuntime;
+import org.sonar.api.config.MapSettings;
+import org.sonar.api.config.Settings;
+import org.sonar.api.internal.SonarRuntimeImpl;
+import org.sonar.api.utils.Version;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class OkHttpClientProviderTest {
+
+  private Settings settings = new MapSettings();
+  private SonarRuntime runtime = SonarRuntimeImpl.forSonarQube(Version.parse("6.2"), SonarQubeSide.SERVER);
+  private final OkHttpClientProvider underTest = new OkHttpClientProvider();
+
+  @Rule
+  public MockWebServer server = new MockWebServer();
+
+  @Test
+  public void get_returns_a_OkHttpClient_with_default_configuration() throws Exception {
+    OkHttpClient client = underTest.provide(settings, runtime);
+
+    assertThat(client.connectTimeoutMillis()).isEqualTo(10_000);
+    assertThat(client.readTimeoutMillis()).isEqualTo(10_000);
+    assertThat(client.proxy()).isNull();
+
+    RecordedRequest recordedRequest = call(client);
+    assertThat(recordedRequest.getHeader("User-Agent")).isEqualTo("SonarQube/6.2");
+    assertThat(recordedRequest.getHeader("Proxy-Authorization")).isNull();
+  }
+
+  @Test
+  public void get_returns_a_OkHttpClient_with_proxy_authentication() throws Exception {
+    settings.setProperty("http.proxyUser", "the-login");
+    settings.setProperty("http.proxyPassword", "the-password");
+
+    OkHttpClient client = underTest.provide(settings, runtime);
+    RecordedRequest recordedRequest = call(client);
+
+    assertThat(recordedRequest.getHeader("Proxy-Authorization")).isEqualTo("Basic " + Base64.getEncoder().encodeToString("the-login:the-password".getBytes()));
+  }
+
+  @Test
+  public void get_returns_a_singleton() {
+    OkHttpClient client1 = underTest.provide(settings, runtime);
+    OkHttpClient client2 = underTest.provide(settings, runtime);
+    assertThat(client2).isNotNull().isSameAs(client1);
+  }
+
+  private RecordedRequest call(OkHttpClient client) throws IOException, InterruptedException {
+    server.enqueue(new MockResponse().setBody("pong"));
+    client.newCall(new Request.Builder().url(server.url("/ping")).build()).execute();
+
+    return server.takeRequest();
+  }
+}
index 8fb927c62059982ad9f2575c053efa0f4d1cf5a5..5719c8b65a7d2fe81f8e04b54a76b01c3508ca32 100644 (file)
@@ -23,21 +23,18 @@ import java.util.HashMap;
 import java.util.Map;
 import org.junit.Test;
 import org.sonar.batch.bootstrapper.EnvironmentInformation;
-import org.sonar.scanner.bootstrap.BatchWsClient;
-import org.sonar.scanner.bootstrap.BatchWsClientProvider;
-import org.sonar.scanner.bootstrap.GlobalProperties;
 import org.sonarqube.ws.client.HttpConnector;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
 public class BatchWsClientProviderTest {
 
-  BatchWsClientProvider underTest = new BatchWsClientProvider();
-  EnvironmentInformation env = new EnvironmentInformation("Maven Plugin", "2.3");
+  private BatchWsClientProvider underTest = new BatchWsClientProvider();
+  private EnvironmentInformation env = new EnvironmentInformation("Maven Plugin", "2.3");
 
   @Test
   public void provide_client_with_default_settings() {
-    GlobalProperties settings = new GlobalProperties(new HashMap<String, String>());
+    GlobalProperties settings = new GlobalProperties(new HashMap<>());
 
     BatchWsClient client = underTest.provide(settings, env);
 
@@ -48,7 +45,6 @@ public class BatchWsClientProviderTest {
     assertThat(httpConnector.okHttpClient().proxy()).isNull();
     assertThat(httpConnector.okHttpClient().connectTimeoutMillis()).isEqualTo(5_000);
     assertThat(httpConnector.okHttpClient().readTimeoutMillis()).isEqualTo(60_000);
-    assertThat(httpConnector.userAgent()).isEqualTo("Maven Plugin/2.3");
   }
 
   @Test
@@ -66,12 +62,11 @@ public class BatchWsClientProviderTest {
     HttpConnector httpConnector = (HttpConnector) client.wsConnector();
     assertThat(httpConnector.baseUrl()).isEqualTo("https://here/sonarqube/");
     assertThat(httpConnector.okHttpClient().proxy()).isNull();
-    assertThat(httpConnector.userAgent()).isEqualTo("Maven Plugin/2.3");
   }
 
   @Test
   public void build_singleton() {
-    GlobalProperties settings = new GlobalProperties(new HashMap<String, String>());
+    GlobalProperties settings = new GlobalProperties(new HashMap<>());
     BatchWsClient first = underTest.provide(settings, env);
     BatchWsClient second = underTest.provide(settings, env);
     assertThat(first).isSameAs(second);
index 3091c40faf869e4417d1ced4d19d4daa54ba432a..47e756defc3c1ad878338f15f3d065f6f928ffdd 100644 (file)
  */
 package org.sonarqube.ws.client;
 
-import java.io.FileInputStream;
 import java.io.IOException;
 import java.net.Proxy;
-import java.security.GeneralSecurityException;
-import java.security.KeyStore;
-import java.util.Arrays;
 import java.util.Map;
-import java.util.concurrent.TimeUnit;
-import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
-import javax.net.ssl.KeyManager;
-import javax.net.ssl.KeyManagerFactory;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLSocketFactory;
-import javax.net.ssl.TrustManager;
-import javax.net.ssl.TrustManagerFactory;
-import javax.net.ssl.X509TrustManager;
 import okhttp3.Call;
-import okhttp3.ConnectionSpec;
 import okhttp3.Credentials;
 import okhttp3.Headers;
 import okhttp3.HttpUrl;
@@ -52,7 +38,6 @@ 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.
@@ -63,23 +48,21 @@ 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;
-  private static final String NONE = "NONE";
-  private static final String P11KEYSTORE = "PKCS11";
 
   /**
    * Base URL with trailing slash, for instance "https://localhost/sonarqube/".
    * It is required for further usage of {@link HttpUrl#resolve(String)}.
    */
   private final HttpUrl baseUrl;
-  private final String userAgent;
   private final String credentials;
-  private final String proxyCredentials;
   private final OkHttpClient okHttpClient;
 
   private HttpConnector(Builder builder) {
     this.baseUrl = HttpUrl.parse(builder.url.endsWith("/") ? builder.url : format("%s/", builder.url));
     checkArgument(this.baseUrl != null, "Malformed URL: '%s'", builder.url);
-    this.userAgent = builder.userAgent;
+
+    OkHttpClientBuilder okHttpClientBuilder = new OkHttpClientBuilder();
+    okHttpClientBuilder.setUserAgent(builder.userAgent);
 
     if (isNullOrEmpty(builder.login)) {
       // no login nor access token
@@ -89,129 +72,12 @@ public class HttpConnector implements WsConnector {
       // the Basic credentials consider an empty password.
       this.credentials = Credentials.basic(builder.login, nullToEmpty(builder.password));
     }
-    // proxy credentials can be used on system-wide proxies, so even if builder.proxy is null
-    if (isNullOrEmpty(builder.proxyLogin)) {
-      this.proxyCredentials = null;
-    } else {
-      this.proxyCredentials = Credentials.basic(builder.proxyLogin, nullToEmpty(builder.proxyPassword));
-    }
-    this.okHttpClient = buildClient(builder);
-  }
-
-  private static OkHttpClient buildClient(Builder builder) {
-    OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder();
-    if (builder.proxy != null) {
-      okHttpClientBuilder.proxy(builder.proxy);
-    }
-
-    okHttpClientBuilder.connectTimeout(builder.connectTimeoutMs, TimeUnit.MILLISECONDS);
-    okHttpClientBuilder.readTimeout(builder.readTimeoutMs, TimeUnit.MILLISECONDS);
-
-    ConnectionSpec tls = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
-      .allEnabledTlsVersions()
-      .allEnabledCipherSuites()
-      .supportsTlsExtensions(true)
-      .build();
-    okHttpClientBuilder.connectionSpecs(asList(tls, ConnectionSpec.CLEARTEXT));
-    X509TrustManager systemDefaultTrustManager = systemDefaultTrustManager();
-    okHttpClientBuilder.sslSocketFactory(systemDefaultSslSocketFactory(systemDefaultTrustManager), systemDefaultTrustManager);
-
-    return okHttpClientBuilder.build();
-  }
-
-  private static X509TrustManager systemDefaultTrustManager() {
-    try {
-      TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
-      trustManagerFactory.init((KeyStore) null);
-      TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
-      if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
-        throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers));
-      }
-      return (X509TrustManager) trustManagers[0];
-    } catch (GeneralSecurityException e) {
-      // The system has no TLS. Just give up.
-      throw new AssertionError(e);
-    }
-  }
-
-  private static SSLSocketFactory systemDefaultSslSocketFactory(X509TrustManager trustManager) {
-    KeyManager[] defaultKeyManager;
-    try {
-      defaultKeyManager = getDefaultKeyManager();
-    } catch (Exception e) {
-      throw new IllegalStateException("Unable to get default key manager", e);
-    }
-    try {
-      SSLContext sslContext = SSLContext.getInstance("TLS");
-      sslContext.init(defaultKeyManager, new TrustManager[] {trustManager}, null);
-      return sslContext.getSocketFactory();
-    } catch (GeneralSecurityException e) {
-      // The system has no TLS. Just give up.
-      throw new AssertionError(e);
-    }
-  }
-
-  private static void logDebug(String msg) {
-    boolean debugEnabled = "all".equals(System.getProperty("javax.net.debug"));
-    if (debugEnabled) {
-      System.out.println(msg);
-    }
-  }
-
-  /**
-   * Inspired from sun.security.ssl.SSLContextImpl#getDefaultKeyManager()
-   */
-  private static synchronized KeyManager[] getDefaultKeyManager() throws Exception {
-
-    final String defaultKeyStore = System.getProperty("javax.net.ssl.keyStore", "");
-    String defaultKeyStoreType = System.getProperty("javax.net.ssl.keyStoreType", KeyStore.getDefaultType());
-    String defaultKeyStoreProvider = System.getProperty("javax.net.ssl.keyStoreProvider", "");
-
-    logDebug("keyStore is : " + defaultKeyStore);
-    logDebug("keyStore type is : " + defaultKeyStoreType);
-    logDebug("keyStore provider is : " + defaultKeyStoreProvider);
-
-    if (P11KEYSTORE.equals(defaultKeyStoreType) && !NONE.equals(defaultKeyStore)) {
-      throw new IllegalArgumentException("if keyStoreType is " + P11KEYSTORE + ", then keyStore must be " + NONE);
-    }
-
-    KeyStore ks = null;
-    String defaultKeyStorePassword = System.getProperty("javax.net.ssl.keyStorePassword", "");
-    char[] passwd = defaultKeyStorePassword.isEmpty() ? null : defaultKeyStorePassword.toCharArray();
-
-    /**
-     * Try to initialize key store.
-     */
-    if (!defaultKeyStoreType.isEmpty()) {
-      logDebug("init keystore");
-      if (defaultKeyStoreProvider.isEmpty()) {
-        ks = KeyStore.getInstance(defaultKeyStoreType);
-      } else {
-        ks = KeyStore.getInstance(defaultKeyStoreType, defaultKeyStoreProvider);
-      }
-      if (!defaultKeyStore.isEmpty() && !NONE.equals(defaultKeyStore)) {
-        try (FileInputStream fs = new FileInputStream(defaultKeyStore)) {
-          ks.load(fs, passwd);
-        }
-      } else {
-        ks.load(null, passwd);
-      }
-    }
-
-    /*
-     * Try to initialize key manager.
-     */
-    logDebug("init keymanager of type " + KeyManagerFactory.getDefaultAlgorithm());
-    KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
-
-    if (P11KEYSTORE.equals(defaultKeyStoreType)) {
-      // do not pass key passwd if using token
-      kmf.init(ks, null);
-    } else {
-      kmf.init(ks, passwd);
-    }
-
-    return kmf.getKeyManagers();
+    okHttpClientBuilder.setProxy(builder.proxy);
+    okHttpClientBuilder.setProxyLogin(builder.proxyLogin);
+    okHttpClientBuilder.setProxyPassword(builder.proxyPassword);
+    okHttpClientBuilder.setConnectTimeoutMs(builder.connectTimeoutMs);
+    okHttpClientBuilder.setReadTimeoutMs(builder.readTimeoutMs);
+    this.okHttpClient = okHttpClientBuilder.build();
   }
 
   @Override
@@ -219,11 +85,6 @@ public class HttpConnector implements WsConnector {
     return baseUrl.url().toExternalForm();
   }
 
-  @CheckForNull
-  public String userAgent() {
-    return userAgent;
-  }
-
   public OkHttpClient okHttpClient() {
     return okHttpClient;
   }
@@ -287,12 +148,6 @@ public class HttpConnector implements WsConnector {
     if (credentials != null) {
       okHttpRequestBuilder.header("Authorization", credentials);
     }
-    if (proxyCredentials != null) {
-      okHttpRequestBuilder.header("Proxy-Authorization", proxyCredentials);
-    }
-    if (userAgent != null) {
-      okHttpRequestBuilder.addHeader("User-Agent", userAgent);
-    }
     return okHttpRequestBuilder;
   }
 
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/OkHttpClientBuilder.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/OkHttpClientBuilder.java
new file mode 100644 (file)
index 0000000..961d9e0
--- /dev/null
@@ -0,0 +1,248 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.FileInputStream;
+import java.io.IOException;
+import java.net.Proxy;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
+import javax.annotation.Nullable;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
+import okhttp3.ConnectionSpec;
+import okhttp3.Credentials;
+import okhttp3.Interceptor;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+
+import static com.google.common.base.Strings.nullToEmpty;
+import static java.util.Arrays.asList;
+
+/**
+ * Helper to build an instance of {@link okhttp3.OkHttpClient} that
+ * correctly supports HTTPS and proxy authentication. It also handles
+ * sending of User-Agent header.
+ */
+public class OkHttpClientBuilder {
+
+  private static final String NONE = "NONE";
+  private static final String P11KEYSTORE = "PKCS11";
+
+  private String userAgent;
+  private Proxy proxy;
+  private String proxyLogin;
+  private String proxyPassword;
+  private long connectTimeoutMs = -1;
+  private long readTimeoutMs = -1;
+
+  /**
+   * Optional User-Agent. If set, then all the requests sent by the
+   * {@link OkHttpClient} will include the header "User-Agent".
+   */
+  public OkHttpClientBuilder setUserAgent(@Nullable String s) {
+    this.userAgent = s;
+    return this;
+  }
+
+  /**
+   * Optional proxy. If set, then all the requests sent by the
+   * {@link OkHttpClient} will reach the proxy. If not set,
+   * then the system-wide proxy is used.
+   */
+  public OkHttpClientBuilder setProxy(@Nullable Proxy proxy) {
+    this.proxy = proxy;
+    return this;
+  }
+
+  /**
+   * Login required for proxy authentication.
+   */
+  public OkHttpClientBuilder setProxyLogin(@Nullable String s) {
+    this.proxyLogin = s;
+    return this;
+  }
+
+  /**
+   * Password used for proxy authentication. It is ignored if
+   * proxy login is not defined (see {@link #setProxyLogin(String)}).
+   * It can be null or empty when login is defined.
+   */
+  public OkHttpClientBuilder setProxyPassword(@Nullable String s) {
+    this.proxyPassword = s;
+    return this;
+  }
+
+  /**
+   * Sets the default connect timeout for new connections. A value of 0 means no timeout.
+   * Default is defined by OkHttp (10 seconds in OkHttp 3.3).
+   */
+  public OkHttpClientBuilder setConnectTimeoutMs(long l) {
+    if (l < 0) {
+      throw new IllegalArgumentException("Connect timeout must be positive. Got " + l);
+    }
+    this.connectTimeoutMs = l;
+    return this;
+  }
+
+  /**
+   * Sets the default read timeout for new connections. A value of 0 means no timeout.
+   * Default is defined by OkHttp (10 seconds in OkHttp 3.3).
+   */
+  public OkHttpClientBuilder setReadTimeoutMs(long l) {
+    if (l < 0) {
+      throw new IllegalArgumentException("Read timeout must be positive. Got " + l);
+    }
+    this.readTimeoutMs = l;
+    return this;
+  }
+
+  public OkHttpClient build() {
+    OkHttpClient.Builder builder = new OkHttpClient.Builder();
+    builder.proxy(proxy);
+    if (connectTimeoutMs >= 0) {
+      builder.connectTimeout(connectTimeoutMs, TimeUnit.MILLISECONDS);
+    }
+    if (readTimeoutMs >= 0) {
+      builder.readTimeout(readTimeoutMs, TimeUnit.MILLISECONDS);
+    }
+    builder.addInterceptor(this::completeHeaders);
+
+    ConnectionSpec tls = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
+      .allEnabledTlsVersions()
+      .allEnabledCipherSuites()
+      .supportsTlsExtensions(true)
+      .build();
+    builder.connectionSpecs(asList(tls, ConnectionSpec.CLEARTEXT));
+    X509TrustManager systemDefaultTrustManager = systemDefaultTrustManager();
+    builder.sslSocketFactory(systemDefaultSslSocketFactory(systemDefaultTrustManager), systemDefaultTrustManager);
+
+    return builder.build();
+  }
+
+  private Response completeHeaders(Interceptor.Chain chain) throws IOException {
+    Request.Builder newRequest = chain.request().newBuilder();
+    if (userAgent != null) {
+      newRequest.header("User-Agent", userAgent);
+    }
+    if (proxyLogin != null) {
+      newRequest.header("Proxy-Authorization", Credentials.basic(proxyLogin, nullToEmpty(proxyPassword)));
+    }
+    return chain.proceed(newRequest.build());
+  }
+
+  private static X509TrustManager systemDefaultTrustManager() {
+    try {
+      TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+      trustManagerFactory.init((KeyStore) null);
+      TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
+      if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
+        throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers));
+      }
+      return (X509TrustManager) trustManagers[0];
+    } catch (GeneralSecurityException e) {
+      // The system has no TLS. Just give up.
+      throw new AssertionError(e);
+    }
+  }
+
+  private static SSLSocketFactory systemDefaultSslSocketFactory(X509TrustManager trustManager) {
+    KeyManager[] defaultKeyManager;
+    try {
+      defaultKeyManager = getDefaultKeyManager();
+    } catch (Exception e) {
+      throw new IllegalStateException("Unable to get default key manager", e);
+    }
+    try {
+      SSLContext sslContext = SSLContext.getInstance("TLS");
+      sslContext.init(defaultKeyManager, new TrustManager[] {trustManager}, null);
+      return sslContext.getSocketFactory();
+    } catch (GeneralSecurityException e) {
+      // The system has no TLS. Just give up.
+      throw new AssertionError(e);
+    }
+  }
+
+  private static void logDebug(String msg) {
+    boolean debugEnabled = "all".equals(System.getProperty("javax.net.debug"));
+    if (debugEnabled) {
+      System.out.println(msg);
+    }
+  }
+
+  /**
+   * Inspired from sun.security.ssl.SSLContextImpl#getDefaultKeyManager()
+   */
+  private static synchronized KeyManager[] getDefaultKeyManager() throws Exception {
+    final String defaultKeyStore = System.getProperty("javax.net.ssl.keyStore", "");
+    String defaultKeyStoreType = System.getProperty("javax.net.ssl.keyStoreType", KeyStore.getDefaultType());
+    String defaultKeyStoreProvider = System.getProperty("javax.net.ssl.keyStoreProvider", "");
+
+    logDebug("keyStore is : " + defaultKeyStore);
+    logDebug("keyStore type is : " + defaultKeyStoreType);
+    logDebug("keyStore provider is : " + defaultKeyStoreProvider);
+
+    if (P11KEYSTORE.equals(defaultKeyStoreType) && !NONE.equals(defaultKeyStore)) {
+      throw new IllegalArgumentException("if keyStoreType is " + P11KEYSTORE + ", then keyStore must be " + NONE);
+    }
+
+    KeyStore ks = null;
+    String defaultKeyStorePassword = System.getProperty("javax.net.ssl.keyStorePassword", "");
+    char[] passwd = defaultKeyStorePassword.isEmpty() ? null : defaultKeyStorePassword.toCharArray();
+
+    // Try to initialize key store.
+    if (!defaultKeyStoreType.isEmpty()) {
+      logDebug("init keystore");
+      if (defaultKeyStoreProvider.isEmpty()) {
+        ks = KeyStore.getInstance(defaultKeyStoreType);
+      } else {
+        ks = KeyStore.getInstance(defaultKeyStoreType, defaultKeyStoreProvider);
+      }
+      if (!defaultKeyStore.isEmpty() && !NONE.equals(defaultKeyStore)) {
+        try (FileInputStream fs = new FileInputStream(defaultKeyStore)) {
+          ks.load(fs, passwd);
+        }
+      } else {
+        ks.load(null, passwd);
+      }
+    }
+
+    // Try to initialize key manager.
+    logDebug("init keymanager of type " + KeyManagerFactory.getDefaultAlgorithm());
+    KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+
+    if (P11KEYSTORE.equals(defaultKeyStoreType)) {
+      // do not pass key passwd if using token
+      kmf.init(ks, null);
+    } else {
+      kmf.init(ks, passwd);
+    }
+
+    return kmf.getKeyManagers();
+  }
+}
index 527fcc5bf2749ef19cc2c6e8a8db046a6224f30b..7cc937b62339a19155b1211637dc95f7bdf2dd9c 100644 (file)
@@ -49,10 +49,9 @@ public class HttpConnectorTest {
   @Rule
   public ExpectedException expectedException = ExpectedException.none();
 
-  MockWebServer server;
-  String serverUrl;
-
-  HttpConnector underTest;
+  private MockWebServer server;
+  private String serverUrl;
+  private HttpConnector underTest;
 
   @Before
   public void setUp() throws Exception {
diff --git a/sonar-ws/src/test/java/org/sonarqube/ws/client/OkHttpClientBuilderTest.java b/sonar-ws/src/test/java/org/sonarqube/ws/client/OkHttpClientBuilderTest.java
new file mode 100644 (file)
index 0000000..a96e7a3
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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 okhttp3.OkHttpClient;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class OkHttpClientBuilderTest {
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  private OkHttpClientBuilder underTest = new OkHttpClientBuilder();
+
+  @Test
+  public void build_default_instance_of_OkHttpClient() {
+    OkHttpClient okHttpClient = underTest.build();
+
+    assertThat(okHttpClient.proxy()).isNull();
+    assertThat(okHttpClient.interceptors()).hasSize(1);
+    assertThat(okHttpClient.sslSocketFactory()).isNotNull();
+  }
+
+  @Test
+  public void build_throws_IAE_if_connect_timeout_is_negative() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Connect timeout must be positive. Got -10");
+
+    underTest.setConnectTimeoutMs(-10);
+  }
+
+  @Test
+  public void build_throws_IAE_if_read_timeout_is_negative() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Read timeout must be positive. Got -10");
+
+    underTest.setReadTimeoutMs(-10);
+  }
+}