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.

AzureDevOpsHttpClient.java 7.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2022 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.azure;
  21. import com.google.common.base.Strings;
  22. import com.google.gson.Gson;
  23. import com.google.gson.GsonBuilder;
  24. import com.google.gson.JsonSyntaxException;
  25. import java.io.IOException;
  26. import java.net.HttpURLConnection;
  27. import java.nio.charset.StandardCharsets;
  28. import java.util.function.Function;
  29. import javax.annotation.Nullable;
  30. import okhttp3.OkHttpClient;
  31. import okhttp3.Request;
  32. import okhttp3.RequestBody;
  33. import okhttp3.Response;
  34. import okhttp3.ResponseBody;
  35. import org.apache.commons.codec.binary.Base64;
  36. import org.sonar.alm.client.TimeoutConfiguration;
  37. import org.sonar.api.server.ServerSide;
  38. import org.sonar.api.utils.log.Logger;
  39. import org.sonar.api.utils.log.Loggers;
  40. import org.sonarqube.ws.client.OkHttpClientBuilder;
  41. import static org.sonar.api.internal.apachecommons.lang.StringUtils.isBlank;
  42. import static org.sonar.api.internal.apachecommons.lang.StringUtils.substringBeforeLast;
  43. @ServerSide
  44. public class AzureDevOpsHttpClient {
  45. private static final Logger LOG = Loggers.get(AzureDevOpsHttpClient.class);
  46. public static final String API_VERSION_3 = "api-version=3.0";
  47. protected static final String GET = "GET";
  48. protected static final String UNABLE_TO_CONTACT_AZURE_SERVER = "Unable to contact Azure DevOps server";
  49. protected final OkHttpClient client;
  50. public AzureDevOpsHttpClient(TimeoutConfiguration timeoutConfiguration) {
  51. client = new OkHttpClientBuilder()
  52. .setConnectTimeoutMs(timeoutConfiguration.getConnectTimeout())
  53. .setReadTimeoutMs(timeoutConfiguration.getReadTimeout())
  54. .build();
  55. }
  56. public void checkPAT(String serverUrl, String token) {
  57. String url = String.format("%s/_apis/projects?%s", getTrimmedUrl(serverUrl), API_VERSION_3);
  58. LOG.debug(String.format("check pat : [%s]", url));
  59. doGet(token, url);
  60. }
  61. public GsonAzureProjectList getProjects(String serverUrl, String token) {
  62. String url = String.format("%s/_apis/projects?%s", getTrimmedUrl(serverUrl), API_VERSION_3);
  63. LOG.debug(String.format("get projects : [%s]", url));
  64. return doGet(token, url, r -> buildGson().fromJson(r.body().charStream(), GsonAzureProjectList.class));
  65. }
  66. public GsonAzureProject getProject(String serverUrl, String token, String projectName) {
  67. String url = String.format("%s/_apis/projects/%s?%s", getTrimmedUrl(serverUrl), projectName, API_VERSION_3);
  68. LOG.debug(String.format("get project : [%s]", url));
  69. return doGet(token, url, r -> buildGson().fromJson(r.body().charStream(), GsonAzureProject.class));
  70. }
  71. public GsonAzureRepoList getRepos(String serverUrl, String token, @Nullable String projectName) {
  72. String url;
  73. if (projectName != null && !projectName.isEmpty()) {
  74. url = String.format("%s/%s/_apis/git/repositories?%s", getTrimmedUrl(serverUrl), projectName, API_VERSION_3);
  75. } else {
  76. url = String.format("%s/_apis/git/repositories?%s", getTrimmedUrl(serverUrl), API_VERSION_3);
  77. }
  78. LOG.debug(String.format("get repos : [%s]", url));
  79. return doGet(token, url, r -> buildGson().fromJson(r.body().charStream(), GsonAzureRepoList.class));
  80. }
  81. public GsonAzureRepo getRepo(String serverUrl, String token, String projectName, String repositoryName) {
  82. String url = String.format("%s/%s/_apis/git/repositories/%s?%s", getTrimmedUrl(serverUrl), projectName, repositoryName, API_VERSION_3);
  83. LOG.debug(String.format("get repo : [%s]", url));
  84. return doGet(token, url, r -> buildGson().fromJson(r.body().charStream(), GsonAzureRepo.class));
  85. }
  86. private void doGet(String token, String url) {
  87. Request request = prepareRequestWithToken(token, GET, url, null);
  88. doCall(request);
  89. }
  90. protected void doCall(Request request) {
  91. try (Response response = client.newCall(request).execute()) {
  92. checkResponseIsSuccessful(response);
  93. } catch (IOException e) {
  94. throw new IllegalArgumentException(UNABLE_TO_CONTACT_AZURE_SERVER, e);
  95. }
  96. }
  97. protected <G> G doGet(String token, String url, Function<Response, G> handler) {
  98. Request request = prepareRequestWithToken(token, GET, url, null);
  99. return doCall(request, handler);
  100. }
  101. protected <G> G doCall(Request request, Function<Response, G> handler) {
  102. try (Response response = client.newCall(request).execute()) {
  103. checkResponseIsSuccessful(response);
  104. return handler.apply(response);
  105. } catch (JsonSyntaxException e) {
  106. throw new IllegalArgumentException(UNABLE_TO_CONTACT_AZURE_SERVER + ", got an unexpected response", e);
  107. } catch (IOException e) {
  108. throw new IllegalArgumentException(UNABLE_TO_CONTACT_AZURE_SERVER, e);
  109. }
  110. }
  111. protected static Request prepareRequestWithToken(String token, String method, String url, @Nullable RequestBody body) {
  112. return new Request.Builder()
  113. .method(method, body)
  114. .url(url)
  115. .addHeader("Authorization", encodeToken("accessToken:" + token))
  116. .build();
  117. }
  118. protected static void checkResponseIsSuccessful(Response response) throws IOException {
  119. if (!response.isSuccessful()) {
  120. LOG.debug(UNABLE_TO_CONTACT_AZURE_SERVER + ": {} {}", response.request().url().toString(), response.code());
  121. if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
  122. throw new AzureDevopsServerException(response.code(), "Invalid personal access token");
  123. }
  124. ResponseBody responseBody = response.body();
  125. String body = responseBody == null ? "" : responseBody.string();
  126. String errorMessage = generateErrorMessage(body, UNABLE_TO_CONTACT_AZURE_SERVER);
  127. LOG.info(String.format("Azure API call to [%s] failed with %s http code. Azure response content : [%s]", response.request().url().toString(), response.code(), body));
  128. throw new AzureDevopsServerException(response.code(), errorMessage);
  129. }
  130. }
  131. protected static String generateErrorMessage(String body, String defaultMessage) {
  132. GsonAzureError gsonAzureError = null;
  133. try {
  134. gsonAzureError = buildGson().fromJson(body, GsonAzureError.class);
  135. } catch (JsonSyntaxException e) {
  136. // not a json payload, ignore the error
  137. }
  138. if (gsonAzureError != null && !Strings.isNullOrEmpty(gsonAzureError.message())) {
  139. return defaultMessage + " : " + gsonAzureError.message();
  140. } else {
  141. return defaultMessage;
  142. }
  143. }
  144. protected static String getTrimmedUrl(String rawUrl) {
  145. if (isBlank(rawUrl)) {
  146. return rawUrl;
  147. }
  148. if (rawUrl.endsWith("/")) {
  149. return substringBeforeLast(rawUrl, "/");
  150. }
  151. return rawUrl;
  152. }
  153. protected static String encodeToken(String token) {
  154. return String.format("BASIC %s", Base64.encodeBase64String(token.getBytes(StandardCharsets.UTF_8)));
  155. }
  156. protected static Gson buildGson() {
  157. return new GsonBuilder()
  158. .create();
  159. }
  160. }