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

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