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 14KB

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