]> source.dussan.org Git - sonarqube.git/blob
5ab475e6e461fa8b60db45200df53e563846f3e7
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2023 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.sonar.alm.client.github;
21
22 import java.io.IOException;
23 import java.net.MalformedURLException;
24 import java.net.URL;
25 import java.util.Optional;
26 import java.util.function.Function;
27 import java.util.regex.Matcher;
28 import java.util.regex.Pattern;
29 import javax.annotation.CheckForNull;
30 import javax.annotation.Nullable;
31 import okhttp3.FormBody;
32 import okhttp3.MediaType;
33 import okhttp3.OkHttpClient;
34 import okhttp3.Request;
35 import okhttp3.RequestBody;
36 import okhttp3.ResponseBody;
37 import org.apache.commons.lang.StringUtils;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
40 import org.sonar.alm.client.TimeoutConfiguration;
41 import org.sonar.alm.client.github.security.AccessToken;
42 import org.sonarqube.ws.client.OkHttpClientBuilder;
43
44 import static com.google.common.base.Preconditions.checkArgument;
45 import static java.net.HttpURLConnection.HTTP_ACCEPTED;
46 import static java.net.HttpURLConnection.HTTP_CREATED;
47 import static java.net.HttpURLConnection.HTTP_NO_CONTENT;
48 import static java.net.HttpURLConnection.HTTP_OK;
49 import static java.util.Optional.empty;
50 import static java.util.Optional.of;
51 import static java.util.Optional.ofNullable;
52
53 public class GithubApplicationHttpClientImpl implements GithubApplicationHttpClient {
54
55   private static final Logger LOG = LoggerFactory.getLogger(GithubApplicationHttpClientImpl.class);
56   private static final Pattern NEXT_LINK_PATTERN = Pattern.compile("<([^<]+)>; rel=\"next\"");
57   private static final String GH_API_VERSION_HEADER = "X-GitHub-Api-Version";
58   private static final String GH_API_VERSION = "2022-11-28";
59
60   private static final String GH_RATE_LIMIT_REMAINING_HEADER = "x-ratelimit-remaining";
61   private static final String GH_RATE_LIMIT_LIMIT_HEADER = "x-ratelimit-limit";
62   private static final String GH_RATE_LIMIT_RESET_HEADER = "x-ratelimit-reset";
63
64   private final OkHttpClient client;
65
66   public GithubApplicationHttpClientImpl(TimeoutConfiguration timeoutConfiguration) {
67     client = new OkHttpClientBuilder()
68       .setConnectTimeoutMs(timeoutConfiguration.getConnectTimeout())
69       .setReadTimeoutMs(timeoutConfiguration.getReadTimeout())
70       .setFollowRedirects(false)
71       .build();
72   }
73
74   @Override
75   public GetResponse get(String appUrl, AccessToken token, String endPoint) throws IOException {
76     return get(appUrl, token, endPoint, true);
77   }
78
79   @Override
80   public GetResponse getSilent(String appUrl, AccessToken token, String endPoint) throws IOException {
81     return get(appUrl, token, endPoint, false);
82   }
83
84   private GetResponse get(String appUrl, AccessToken token, String endPoint, boolean withLog) throws IOException {
85     validateEndPoint(endPoint);
86     try (okhttp3.Response response = client.newCall(newGetRequest(appUrl, token, endPoint)).execute()) {
87       int responseCode = response.code();
88       RateLimit rateLimit = readRateLimit(response);
89       if (responseCode != HTTP_OK) {
90         String content = StringUtils.trimToNull(attemptReadContent(response));
91         if (withLog) {
92           LOG.warn("GET response did not have expected HTTP code (was {}): {}", responseCode, content);
93         }
94         return new GetResponseImpl(responseCode, content, null, rateLimit);
95       }
96       return new GetResponseImpl(responseCode, readContent(response.body()).orElse(null), readNextEndPoint(response), rateLimit);
97     }
98   }
99
100   private static void validateEndPoint(String endPoint) {
101     checkArgument(endPoint.startsWith("/") || endPoint.startsWith("http") || endPoint.isEmpty(),
102       "endpoint must start with '/' or 'http'");
103   }
104
105   private static Request newGetRequest(String appUrl, AccessToken token, String endPoint) {
106     return newRequestBuilder(appUrl, token, endPoint).get().build();
107   }
108
109   @Override
110   public Response post(String appUrl, AccessToken token, String endPoint) throws IOException {
111     return doPost(appUrl, token, endPoint, new FormBody.Builder().build());
112   }
113
114   @Override
115   public Response post(String appUrl, AccessToken token, String endPoint, String json) throws IOException {
116     RequestBody body = RequestBody.create(json, MediaType.parse("application/json; charset=utf-8"));
117     return doPost(appUrl, token, endPoint, body);
118   }
119
120   @Override
121   public Response patch(String appUrl, AccessToken token, String endPoint, String json) throws IOException {
122     RequestBody body = RequestBody.create(json, MediaType.parse("application/json; charset=utf-8"));
123     return doPatch(appUrl, token, endPoint, body);
124   }
125
126   @Override
127   public Response delete(String appUrl, AccessToken token, String endPoint) throws IOException {
128     validateEndPoint(endPoint);
129
130     try (okhttp3.Response response = client.newCall(newDeleteRequest(appUrl, token, endPoint)).execute()) {
131       int responseCode = response.code();
132       RateLimit rateLimit = readRateLimit(response);
133       if (responseCode != HTTP_NO_CONTENT) {
134         String content = attemptReadContent(response);
135         LOG.warn("DELETE response did not have expected HTTP code (was {}): {}", responseCode, content);
136         return new ResponseImpl(responseCode, content, rateLimit);
137       }
138       return new ResponseImpl(responseCode, null, rateLimit);
139     }
140   }
141
142   private static Request newDeleteRequest(String appUrl, AccessToken token, String endPoint) {
143     return newRequestBuilder(appUrl, token, endPoint).delete().build();
144   }
145
146   private Response doPost(String appUrl, @Nullable AccessToken token, String endPoint, RequestBody body) throws IOException {
147     validateEndPoint(endPoint);
148
149     try (okhttp3.Response response = client.newCall(newPostRequest(appUrl, token, endPoint, body)).execute()) {
150       int responseCode = response.code();
151       RateLimit rateLimit = readRateLimit(response);
152       if (responseCode == HTTP_OK || responseCode == HTTP_CREATED || responseCode == HTTP_ACCEPTED) {
153         return new ResponseImpl(responseCode, readContent(response.body()).orElse(null), rateLimit);
154       } else if (responseCode == HTTP_NO_CONTENT) {
155         return new ResponseImpl(responseCode, null, rateLimit);
156       }
157       String content = attemptReadContent(response);
158       LOG.warn("POST response did not have expected HTTP code (was {}): {}", responseCode, content);
159       return new ResponseImpl(responseCode, content, rateLimit);
160     }
161   }
162
163   private Response doPatch(String appUrl, AccessToken token, String endPoint, RequestBody body) throws IOException {
164     validateEndPoint(endPoint);
165
166     try (okhttp3.Response response = client.newCall(newPatchRequest(token, appUrl, endPoint, body)).execute()) {
167       int responseCode = response.code();
168       RateLimit rateLimit = readRateLimit(response);
169       if (responseCode == HTTP_OK) {
170         return new ResponseImpl(responseCode, readContent(response.body()).orElse(null), rateLimit);
171       } else if (responseCode == HTTP_NO_CONTENT) {
172         return new ResponseImpl(responseCode, null, rateLimit);
173       }
174       String content = attemptReadContent(response);
175       LOG.warn("PATCH response did not have expected HTTP code (was {}): {}", responseCode, content);
176       return new ResponseImpl(responseCode, content, rateLimit);
177     }
178   }
179
180   private static Request newPostRequest(String appUrl, @Nullable AccessToken token, String endPoint, RequestBody body) {
181     return newRequestBuilder(appUrl, token, endPoint).post(body).build();
182   }
183
184   private static Request newPatchRequest(AccessToken token, String appUrl, String endPoint, RequestBody body) {
185     return newRequestBuilder(appUrl, token, endPoint).patch(body).build();
186   }
187
188   private static Request.Builder newRequestBuilder(String appUrl, @Nullable AccessToken token, String endPoint) {
189     Request.Builder url = new Request.Builder().url(toAbsoluteEndPoint(appUrl, endPoint));
190     if (token != null) {
191       url.addHeader("Authorization", token.getAuthorizationHeaderPrefix() + " " + token);
192       url.addHeader(GH_API_VERSION_HEADER, GH_API_VERSION);
193     }
194     return url;
195   }
196
197   private static String toAbsoluteEndPoint(String host, String endPoint) {
198     if (endPoint.startsWith("http")) {
199       return endPoint;
200     }
201     try {
202       return new URL(host + endPoint).toExternalForm();
203     } catch (MalformedURLException e) {
204       throw new IllegalArgumentException(String.format("%s is not a valid url", host + endPoint));
205     }
206   }
207
208   private static String attemptReadContent(okhttp3.Response response) {
209     try {
210       return readContent(response.body()).orElse(null);
211     } catch (IOException e) {
212       return null;
213     }
214   }
215
216   private static Optional<String> readContent(@Nullable ResponseBody body) throws IOException {
217     if (body == null) {
218       return empty();
219     }
220     try {
221       return of(body.string());
222     } finally {
223       body.close();
224     }
225   }
226
227   @CheckForNull
228   private static String readNextEndPoint(okhttp3.Response response) {
229     String links = response.headers().get("link");
230     if (links == null || links.isEmpty() || !links.contains("rel=\"next\"")) {
231       return null;
232     }
233
234     Matcher nextLinkMatcher = NEXT_LINK_PATTERN.matcher(links);
235     if (!nextLinkMatcher.find()) {
236       return null;
237     }
238
239     return nextLinkMatcher.group(1);
240   }
241
242   @CheckForNull
243   private static RateLimit readRateLimit(okhttp3.Response response) {
244     Integer remaining = headerValueOrNull(response, GH_RATE_LIMIT_REMAINING_HEADER, Integer::valueOf);
245     Integer limit = headerValueOrNull(response, GH_RATE_LIMIT_LIMIT_HEADER, Integer::valueOf);
246     Long reset = headerValueOrNull(response, GH_RATE_LIMIT_RESET_HEADER, Long::valueOf);
247     if (remaining == null || limit == null || reset == null) {
248       return null;
249     }
250     return new RateLimit(remaining, limit, reset);
251   }
252
253   @CheckForNull
254   private static <T> T headerValueOrNull(okhttp3.Response response, String header, Function<String, T> mapper) {
255     return ofNullable(response.header(header)).map(mapper::apply).orElse(null);
256   }
257
258   private static class ResponseImpl implements Response {
259     private final int code;
260     private final String content;
261
262     private final RateLimit rateLimit;
263
264     private ResponseImpl(int code, @Nullable String content, @Nullable RateLimit rateLimit) {
265       this.code = code;
266       this.content = content;
267       this.rateLimit = rateLimit;
268     }
269
270     @Override
271     public int getCode() {
272       return code;
273     }
274
275     @Override
276     public Optional<String> getContent() {
277       return ofNullable(content);
278     }
279
280     @Override
281     @CheckForNull
282     public RateLimit getRateLimit() {
283       return rateLimit;
284     }
285
286   }
287
288   private static final class GetResponseImpl extends ResponseImpl implements GetResponse {
289     private final String nextEndPoint;
290
291     private GetResponseImpl(int code, @Nullable String content, @Nullable String nextEndPoint, @Nullable RateLimit rateLimit) {
292       super(code, content, rateLimit);
293       this.nextEndPoint = nextEndPoint;
294     }
295
296     @Override
297     public Optional<String> getNextEndPoint() {
298       return ofNullable(nextEndPoint);
299     }
300   }
301 }