aboutsummaryrefslogtreecommitdiffstats
path: root/sonar-ws/src/main/java/org/sonarqube/ws/client/OkHttpClientBuilder.java
blob: ede86381037999bf15913a9f9adb1eca1c430e14 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
/*
 * SonarQube
 * Copyright (C) 2009-2021 SonarSource SA
 * mailto:info 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.HttpURLConnection;
import java.net.Proxy;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
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 java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Arrays.asList;
import static org.sonarqube.ws.WsUtils.nullToEmpty;

/**
 * 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 static final String PROXY_AUTHORIZATION = "Proxy-Authorization";

  private String userAgent;
  private Proxy proxy;
  private String credentials;
  private String proxyLogin;
  private String proxyPassword;
  private long connectTimeoutMs = -1;
  private long readTimeoutMs = -1;
  private SSLSocketFactory sslSocketFactory = null;
  private X509TrustManager sslTrustManager = null;

  /**
   * 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 SSL socket factory with which SSL sockets will be created to establish SSL connections.
   * If not set, a default SSL socket factory will be used, base d on the JVM's default key store.
   */
  public OkHttpClientBuilder setSSLSocketFactory(@Nullable SSLSocketFactory sslSocketFactory) {
    this.sslSocketFactory = sslSocketFactory;
    return this;
  }

  /**
   * Optional SSL trust manager used to validate certificates.
   * If not set, a default system trust manager will be used, based on the JVM's default truststore.
   */
  public OkHttpClientBuilder setTrustManager(@Nullable X509TrustManager sslTrustManager) {
    this.sslTrustManager = sslTrustManager;
    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;
  }

  /**
   * Set credentials that will be passed on every request
   */
  public OkHttpClientBuilder setCredentials(String credentials) {
    this.credentials = credentials;
    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.addNetworkInterceptor(this::addHeaders);
    if (proxyLogin != null) {
      builder.proxyAuthenticator((route, response) -> {
        if (response.request().header(PROXY_AUTHORIZATION) != null) {
          // Give up, we've already attempted to authenticate.
          return null;
        }
        if (HttpURLConnection.HTTP_PROXY_AUTH == response.code()) {
          String credential = Credentials.basic(proxyLogin, nullToEmpty(proxyPassword), UTF_8);
          return response.request().newBuilder().header(PROXY_AUTHORIZATION, credential).build();
        }
        return null;
      });
    }

    ConnectionSpec tls = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
      .allEnabledTlsVersions()
      .allEnabledCipherSuites()
      .supportsTlsExtensions(true)
      .build();
    builder.connectionSpecs(asList(tls, ConnectionSpec.CLEARTEXT));

    X509TrustManager trustManager = sslTrustManager != null ? sslTrustManager : systemDefaultTrustManager();
    SSLSocketFactory sslFactory = sslSocketFactory != null ? sslSocketFactory : systemDefaultSslSocketFactory(trustManager);
    builder.sslSocketFactory(sslFactory, trustManager);

    return builder.build();
  }

  private Response addHeaders(Interceptor.Chain chain) throws IOException {
    Request.Builder newRequest = chain.request().newBuilder();
    if (userAgent != null) {
      newRequest.header("User-Agent", userAgent);
    }
    if (credentials != null) {
      newRequest.header("Authorization", credentials);
    }
    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 KeyStoreException, NoSuchProviderException,
    IOException, CertificateException, NoSuchAlgorithmException, UnrecoverableKeyException {
    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();
  }

}