You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

HttpConnector.java 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2018 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. package org.sonarqube.ws.client;
  21. import java.io.IOException;
  22. import java.net.Proxy;
  23. import java.util.Map;
  24. import java.util.concurrent.TimeUnit;
  25. import javax.annotation.Nullable;
  26. import javax.net.ssl.SSLSocketFactory;
  27. import javax.net.ssl.X509TrustManager;
  28. import okhttp3.Call;
  29. import okhttp3.Credentials;
  30. import okhttp3.FormBody;
  31. import okhttp3.HttpUrl;
  32. import okhttp3.MediaType;
  33. import okhttp3.MultipartBody;
  34. import okhttp3.OkHttpClient;
  35. import okhttp3.Request;
  36. import okhttp3.RequestBody;
  37. import okhttp3.Response;
  38. import static com.google.common.base.Preconditions.checkArgument;
  39. import static com.google.common.base.Strings.isNullOrEmpty;
  40. import static com.google.common.base.Strings.nullToEmpty;
  41. import static java.lang.String.format;
  42. import static java.net.HttpURLConnection.HTTP_MOVED_PERM;
  43. import static java.net.HttpURLConnection.HTTP_MOVED_TEMP;
  44. import static java.nio.charset.StandardCharsets.UTF_8;
  45. import static okhttp3.internal.http.StatusLine.HTTP_PERM_REDIRECT;
  46. import static okhttp3.internal.http.StatusLine.HTTP_TEMP_REDIRECT;
  47. /**
  48. * Connect to any SonarQube server available through HTTP or HTTPS.
  49. * <p>TLS 1.0, 1.1 and 1.2 are supported on both Java 7 and 8. SSLv3 is not supported.</p>
  50. * <p>The JVM system proxies are used.</p>
  51. */
  52. public class HttpConnector implements WsConnector {
  53. public static final int DEFAULT_CONNECT_TIMEOUT_MILLISECONDS = 30_000;
  54. public static final int DEFAULT_READ_TIMEOUT_MILLISECONDS = 60_000;
  55. /**
  56. * Base URL with trailing slash, for instance "https://localhost/sonarqube/".
  57. * It is required for further usage of {@link HttpUrl#resolve(String)}.
  58. */
  59. private final HttpUrl baseUrl;
  60. private final String systemPassCode;
  61. private final OkHttpClient okHttpClient;
  62. private final OkHttpClient noRedirectOkHttpClient;
  63. private HttpConnector(Builder builder) {
  64. this.baseUrl = HttpUrl.parse(builder.url.endsWith("/") ? builder.url : format("%s/", builder.url));
  65. checkArgument(this.baseUrl != null, "Malformed URL: '%s'", builder.url);
  66. OkHttpClientBuilder okHttpClientBuilder = new OkHttpClientBuilder();
  67. okHttpClientBuilder.setUserAgent(builder.userAgent);
  68. if (!isNullOrEmpty(builder.login)) {
  69. // password is null when login represents an access token. In this case
  70. // the Basic credentials consider an empty password.
  71. okHttpClientBuilder.setCredentials(Credentials.basic(builder.login, nullToEmpty(builder.password), UTF_8));
  72. }
  73. this.systemPassCode = builder.systemPassCode;
  74. okHttpClientBuilder.setProxy(builder.proxy);
  75. okHttpClientBuilder.setProxyLogin(builder.proxyLogin);
  76. okHttpClientBuilder.setProxyPassword(builder.proxyPassword);
  77. okHttpClientBuilder.setConnectTimeoutMs(builder.connectTimeoutMs);
  78. okHttpClientBuilder.setReadTimeoutMs(builder.readTimeoutMs);
  79. okHttpClientBuilder.setSSLSocketFactory(builder.sslSocketFactory);
  80. okHttpClientBuilder.setTrustManager(builder.sslTrustManager);
  81. this.okHttpClient = okHttpClientBuilder.build();
  82. this.noRedirectOkHttpClient = newClientWithoutRedirect(this.okHttpClient);
  83. }
  84. private static OkHttpClient newClientWithoutRedirect(OkHttpClient client) {
  85. return client.newBuilder()
  86. .followRedirects(false)
  87. .followSslRedirects(false)
  88. .build();
  89. }
  90. @Override
  91. public String baseUrl() {
  92. return baseUrl.url().toExternalForm();
  93. }
  94. public OkHttpClient okHttpClient() {
  95. return okHttpClient;
  96. }
  97. @Override
  98. public WsResponse call(WsRequest httpRequest) {
  99. if (httpRequest instanceof GetRequest) {
  100. return get((GetRequest) httpRequest);
  101. }
  102. if (httpRequest instanceof PostRequest) {
  103. return post((PostRequest) httpRequest);
  104. }
  105. throw new IllegalArgumentException(format("Unsupported implementation: %s", httpRequest.getClass()));
  106. }
  107. private WsResponse get(GetRequest getRequest) {
  108. HttpUrl.Builder urlBuilder = prepareUrlBuilder(getRequest);
  109. completeUrlQueryParameters(getRequest, urlBuilder);
  110. Request.Builder okRequestBuilder = prepareOkRequestBuilder(getRequest, urlBuilder).get();
  111. return new OkHttpResponse(doCall(prepareOkHttpClient(okHttpClient, getRequest), okRequestBuilder.build()));
  112. }
  113. private WsResponse post(PostRequest postRequest) {
  114. HttpUrl.Builder urlBuilder = prepareUrlBuilder(postRequest);
  115. RequestBody body;
  116. Map<String, PostRequest.Part> parts = postRequest.getParts();
  117. if (parts.isEmpty()) {
  118. // parameters are defined in the body (application/x-www-form-urlencoded)
  119. FormBody.Builder formBody = new FormBody.Builder();
  120. postRequest.getParameters().getKeys()
  121. .forEach(key -> postRequest.getParameters().getValues(key)
  122. .forEach(value -> formBody.add(key, value)));
  123. body = formBody.build();
  124. } else {
  125. // parameters are defined in the URL (as GET)
  126. completeUrlQueryParameters(postRequest, urlBuilder);
  127. MultipartBody.Builder bodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM);
  128. parts.entrySet().forEach(param -> {
  129. PostRequest.Part part = param.getValue();
  130. bodyBuilder.addFormDataPart(
  131. param.getKey(),
  132. part.getFile().getName(),
  133. RequestBody.create(MediaType.parse(part.getMediaType()), part.getFile()));
  134. });
  135. body = bodyBuilder.build();
  136. }
  137. Request.Builder okRequestBuilder = prepareOkRequestBuilder(postRequest, urlBuilder).post(body);
  138. Response response = doCall(prepareOkHttpClient(noRedirectOkHttpClient, postRequest), okRequestBuilder.build());
  139. response = checkRedirect(response, postRequest);
  140. return new OkHttpResponse(response);
  141. }
  142. private HttpUrl.Builder prepareUrlBuilder(WsRequest wsRequest) {
  143. String path = wsRequest.getPath();
  144. return baseUrl
  145. .resolve(path.startsWith("/") ? path.replaceAll("^(/)+", "") : path)
  146. .newBuilder();
  147. }
  148. private static OkHttpClient prepareOkHttpClient(OkHttpClient okHttpClient, WsRequest wsRequest) {
  149. if (!wsRequest.getTimeOutInMs().isPresent()) {
  150. return okHttpClient;
  151. }
  152. return okHttpClient.newBuilder()
  153. .readTimeout(wsRequest.getTimeOutInMs().getAsInt(), TimeUnit.MILLISECONDS)
  154. .build();
  155. }
  156. private static void completeUrlQueryParameters(BaseRequest<?> request, HttpUrl.Builder urlBuilder) {
  157. request.getParameters().getKeys()
  158. .forEach(key -> request.getParameters().getValues(key)
  159. .forEach(value -> urlBuilder.addQueryParameter(key, value)));
  160. }
  161. private Request.Builder prepareOkRequestBuilder(WsRequest getRequest, HttpUrl.Builder urlBuilder) {
  162. Request.Builder okHttpRequestBuilder = new Request.Builder()
  163. .url(urlBuilder.build())
  164. .header("Accept", getRequest.getMediaType())
  165. .header("Accept-Charset", "UTF-8");
  166. if (systemPassCode != null) {
  167. okHttpRequestBuilder.header("X-Sonar-Passcode", systemPassCode);
  168. }
  169. getRequest.getHeaders().getNames().forEach(name -> okHttpRequestBuilder.header(name, getRequest.getHeaders().getValue(name).get()));
  170. return okHttpRequestBuilder;
  171. }
  172. private static Response doCall(OkHttpClient client, Request okRequest) {
  173. Call call = client.newCall(okRequest);
  174. try {
  175. return call.execute();
  176. } catch (IOException e) {
  177. throw new IllegalStateException("Fail to request " + okRequest.url(), e);
  178. }
  179. }
  180. private Response checkRedirect(Response response, PostRequest postRequest) {
  181. switch (response.code()) {
  182. case HTTP_MOVED_PERM:
  183. case HTTP_MOVED_TEMP:
  184. case HTTP_TEMP_REDIRECT:
  185. case HTTP_PERM_REDIRECT:
  186. // OkHttpClient does not follow the redirect with the same HTTP method. A POST is
  187. // redirected to a GET. Because of that the redirect must be manually implemented.
  188. // See:
  189. // https://github.com/square/okhttp/blob/07309c1c7d9e296014268ebd155ebf7ef8679f6c/okhttp/src/main/java/okhttp3/internal/http/RetryAndFollowUpInterceptor.java#L316
  190. // https://github.com/square/okhttp/issues/936#issuecomment-266430151
  191. return followPostRedirect(response, postRequest);
  192. default:
  193. return response;
  194. }
  195. }
  196. private Response followPostRedirect(Response response, PostRequest postRequest) {
  197. String location = response.header("Location");
  198. if (location == null) {
  199. throw new IllegalStateException(format("Missing HTTP header 'Location' in redirect of %s", response.request().url()));
  200. }
  201. HttpUrl url = response.request().url().resolve(location);
  202. // Don't follow redirects to unsupported protocols.
  203. if (url == null) {
  204. throw new IllegalStateException(format("Unsupported protocol in redirect of %s to %s", response.request().url(), location));
  205. }
  206. Request.Builder redirectRequest = response.request().newBuilder();
  207. redirectRequest.post(response.request().body());
  208. response.body().close();
  209. return doCall(prepareOkHttpClient(noRedirectOkHttpClient, postRequest), redirectRequest.url(url).build());
  210. }
  211. /**
  212. * @since 5.5
  213. */
  214. public static Builder newBuilder() {
  215. return new Builder();
  216. }
  217. public static class Builder {
  218. private String url;
  219. private String userAgent;
  220. private String login;
  221. private String password;
  222. private Proxy proxy;
  223. private String proxyLogin;
  224. private String proxyPassword;
  225. private String systemPassCode;
  226. private int connectTimeoutMs = DEFAULT_CONNECT_TIMEOUT_MILLISECONDS;
  227. private int readTimeoutMs = DEFAULT_READ_TIMEOUT_MILLISECONDS;
  228. private SSLSocketFactory sslSocketFactory = null;
  229. private X509TrustManager sslTrustManager = null;
  230. /**
  231. * Private since 5.5.
  232. *
  233. * @see HttpConnector#newBuilder()
  234. */
  235. private Builder() {
  236. }
  237. /**
  238. * Optional User Agent
  239. */
  240. public Builder userAgent(@Nullable String userAgent) {
  241. this.userAgent = userAgent;
  242. return this;
  243. }
  244. /**
  245. * Mandatory HTTP server URL, eg "http://localhost:9000"
  246. */
  247. public Builder url(String url) {
  248. this.url = url;
  249. return this;
  250. }
  251. /**
  252. * Optional login/password, for example "admin"
  253. */
  254. public Builder credentials(@Nullable String login, @Nullable String password) {
  255. this.login = login;
  256. this.password = password;
  257. return this;
  258. }
  259. /**
  260. * Optional access token, for example {@code "ABCDE"}. Alternative to {@link #credentials(String, String)}
  261. */
  262. public Builder token(@Nullable String token) {
  263. this.login = token;
  264. this.password = null;
  265. return this;
  266. }
  267. /**
  268. * Sets a specified timeout value, in milliseconds, to be used when opening HTTP connection.
  269. * A timeout of zero is interpreted as an infinite timeout. Default value is {@link #DEFAULT_CONNECT_TIMEOUT_MILLISECONDS}
  270. */
  271. public Builder connectTimeoutMilliseconds(int i) {
  272. this.connectTimeoutMs = i;
  273. return this;
  274. }
  275. /**
  276. * Optional SSL socket factory with which SSL sockets will be created to establish SSL connections.
  277. * If not set, a default SSL socket factory will be used, base d on the JVM's default key store.
  278. */
  279. public Builder setSSLSocketFactory(@Nullable SSLSocketFactory sslSocketFactory) {
  280. this.sslSocketFactory = sslSocketFactory;
  281. return this;
  282. }
  283. /**
  284. * Optional SSL trust manager used to validate certificates.
  285. * If not set, a default system trust manager will be used, based on the JVM's default truststore.
  286. */
  287. public Builder setTrustManager(@Nullable X509TrustManager sslTrustManager) {
  288. this.sslTrustManager = sslTrustManager;
  289. return this;
  290. }
  291. /**
  292. * Sets the read timeout to a specified timeout, in milliseconds.
  293. * A timeout of zero is interpreted as an infinite timeout. Default value is {@link #DEFAULT_READ_TIMEOUT_MILLISECONDS}
  294. */
  295. public Builder readTimeoutMilliseconds(int i) {
  296. this.readTimeoutMs = i;
  297. return this;
  298. }
  299. public Builder proxy(@Nullable Proxy proxy) {
  300. this.proxy = proxy;
  301. return this;
  302. }
  303. public Builder proxyCredentials(@Nullable String proxyLogin, @Nullable String proxyPassword) {
  304. this.proxyLogin = proxyLogin;
  305. this.proxyPassword = proxyPassword;
  306. return this;
  307. }
  308. public Builder systemPassCode(@Nullable String systemPassCode) {
  309. this.systemPassCode = systemPassCode;
  310. return this;
  311. }
  312. public HttpConnector build() {
  313. checkArgument(!isNullOrEmpty(url), "Server URL is not defined");
  314. return new HttpConnector(this);
  315. }
  316. }
  317. }