summaryrefslogtreecommitdiff
path: root/plugins/github/src/org/jetbrains/plugins/github/api/GithubConnection.java
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/github/src/org/jetbrains/plugins/github/api/GithubConnection.java')
-rw-r--r--plugins/github/src/org/jetbrains/plugins/github/api/GithubConnection.java491
1 files changed, 491 insertions, 0 deletions
diff --git a/plugins/github/src/org/jetbrains/plugins/github/api/GithubConnection.java b/plugins/github/src/org/jetbrains/plugins/github/api/GithubConnection.java
new file mode 100644
index 000000000000..6358b79e38ee
--- /dev/null
+++ b/plugins/github/src/org/jetbrains/plugins/github/api/GithubConnection.java
@@ -0,0 +1,491 @@
+package org.jetbrains.plugins.github.api;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonParser;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.util.net.HttpConfigurable;
+import com.intellij.util.net.ssl.CertificateManager;
+import org.apache.http.*;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.Credentials;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.CredentialsProvider;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.*;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.config.ConnectionConfig;
+import org.apache.http.conn.ssl.X509HostnameVerifier;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.auth.BasicScheme;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.message.BasicHeader;
+import org.apache.http.protocol.HttpContext;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.annotations.TestOnly;
+import org.jetbrains.plugins.github.exceptions.*;
+import org.jetbrains.plugins.github.util.GithubAuthData;
+import org.jetbrains.plugins.github.util.GithubSettings;
+import org.jetbrains.plugins.github.util.GithubUrlUtil;
+import org.jetbrains.plugins.github.util.GithubUtil;
+import sun.security.validator.ValidatorException;
+
+import javax.net.ssl.SSLHandshakeException;
+import java.awt.*;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.util.*;
+import java.util.List;
+
+import static org.jetbrains.plugins.github.api.GithubApiUtil.createDataFromRaw;
+import static org.jetbrains.plugins.github.api.GithubApiUtil.fromJson;
+
+public class GithubConnection {
+ private static final Logger LOG = GithubUtil.LOG;
+
+ private static final HttpRequestInterceptor PREEMPTIVE_BASIC_AUTH = new PreemptiveBasicAuthInterceptor();
+
+ @NotNull private final String myHost;
+ @NotNull private final CloseableHttpClient myClient;
+ private final boolean myReusable;
+
+ private volatile HttpUriRequest myRequest;
+ private volatile boolean myAborted;
+
+ @TestOnly
+ public GithubConnection(@NotNull GithubAuthData auth) {
+ this(auth, false);
+ }
+
+ public GithubConnection(@NotNull GithubAuthData auth, boolean reusable) {
+ myHost = auth.getHost();
+ myClient = createClient(auth);
+ myReusable = reusable;
+ }
+
+ private enum HttpVerb {
+ GET, POST, DELETE, HEAD, PATCH
+ }
+
+ @Nullable
+ public JsonElement getRequest(@NotNull String path,
+ @NotNull Header... headers) throws IOException {
+ return request(path, null, Arrays.asList(headers), HttpVerb.GET).getJsonElement();
+ }
+
+ @Nullable
+ public JsonElement postRequest(@NotNull String path,
+ @Nullable String requestBody,
+ @NotNull Header... headers) throws IOException {
+ return request(path, requestBody, Arrays.asList(headers), HttpVerb.POST).getJsonElement();
+ }
+
+ @Nullable
+ public JsonElement patchRequest(@NotNull String path,
+ @Nullable String requestBody,
+ @NotNull Header... headers) throws IOException {
+ return request(path, requestBody, Arrays.asList(headers), HttpVerb.PATCH).getJsonElement();
+ }
+
+ @Nullable
+ public JsonElement deleteRequest(@NotNull String path,
+ @NotNull Header... headers) throws IOException {
+ return request(path, null, Arrays.asList(headers), HttpVerb.DELETE).getJsonElement();
+ }
+
+ @NotNull
+ public Header[] headRequest(@NotNull String path,
+ @NotNull Header... headers) throws IOException {
+ return request(path, null, Arrays.asList(headers), HttpVerb.HEAD).getHeaders();
+ }
+
+ public void abort() {
+ if (myAborted) return;
+ myAborted = true;
+
+ HttpUriRequest request = myRequest;
+ if (request != null) request.abort();
+ }
+
+ public void close() throws IOException {
+ myClient.close();
+ }
+
+ @NotNull
+ private static CloseableHttpClient createClient(@NotNull GithubAuthData auth) {
+ HttpClientBuilder builder = HttpClients.custom();
+
+ return builder
+ .setDefaultRequestConfig(createRequestConfig(auth))
+ .setDefaultConnectionConfig(createConnectionConfig(auth))
+ .setDefaultCredentialsProvider(createCredentialsProvider(auth))
+ .setDefaultHeaders(createHeaders(auth))
+ .addInterceptorFirst(PREEMPTIVE_BASIC_AUTH)
+ .setSslcontext(CertificateManager.getInstance().getSslContext())
+ .setHostnameVerifier((X509HostnameVerifier)CertificateManager.HOSTNAME_VERIFIER)
+ .build();
+ }
+
+ @NotNull
+ private static RequestConfig createRequestConfig(@NotNull GithubAuthData auth) {
+ RequestConfig.Builder builder = RequestConfig.custom();
+
+ int timeout = GithubSettings.getInstance().getConnectionTimeout();
+ builder
+ .setConnectTimeout(timeout)
+ .setSocketTimeout(timeout);
+
+ final HttpConfigurable proxySettings = HttpConfigurable.getInstance();
+ if (auth.isUseProxy() && proxySettings.USE_HTTP_PROXY && !StringUtil.isEmptyOrSpaces(proxySettings.PROXY_HOST)) {
+ builder
+ .setProxy(new HttpHost(proxySettings.PROXY_HOST, proxySettings.PROXY_PORT));
+ }
+
+ return builder.build();
+ }
+
+ @NotNull
+ private static ConnectionConfig createConnectionConfig(@NotNull GithubAuthData auth) {
+ return ConnectionConfig.custom()
+ .setCharset(Consts.UTF_8)
+ .build();
+ }
+
+
+ @NotNull
+ private static CredentialsProvider createCredentialsProvider(@NotNull GithubAuthData auth) {
+ CredentialsProvider provider = new BasicCredentialsProvider();
+ // Basic authentication
+ GithubAuthData.BasicAuth basicAuth = auth.getBasicAuth();
+ if (basicAuth != null) {
+ provider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(basicAuth.getLogin(), basicAuth.getPassword()));
+ }
+
+ final HttpConfigurable proxySettings = HttpConfigurable.getInstance();
+ //proxySettings.USE_HTTP_PROXY
+ if (auth.isUseProxy() && proxySettings.USE_HTTP_PROXY && !StringUtil.isEmptyOrSpaces(proxySettings.PROXY_HOST)) {
+ if (proxySettings.PROXY_AUTHENTICATION) {
+ provider.setCredentials(new AuthScope(proxySettings.PROXY_HOST, proxySettings.PROXY_PORT),
+ new UsernamePasswordCredentials(proxySettings.PROXY_LOGIN, proxySettings.getPlainProxyPassword()));
+ }
+ }
+ return provider;
+ }
+
+ @NotNull
+ private static Collection<? extends Header> createHeaders(@NotNull GithubAuthData auth) {
+ List<Header> headers = new ArrayList<Header>();
+ GithubAuthData.TokenAuth tokenAuth = auth.getTokenAuth();
+ if (tokenAuth != null) {
+ headers.add(new BasicHeader("Authorization", "token " + tokenAuth.getToken()));
+ }
+ GithubAuthData.BasicAuth basicAuth = auth.getBasicAuth();
+ if (basicAuth != null && basicAuth.getCode() != null) {
+ headers.add(new BasicHeader("X-GitHub-OTP", basicAuth.getCode()));
+ }
+ return headers;
+ }
+
+ @NotNull
+ private ResponsePage request(@NotNull String path,
+ @Nullable String requestBody,
+ @NotNull Collection<Header> headers,
+ @NotNull HttpVerb verb) throws IOException {
+ if (myAborted) throw new GithubOperationCanceledException();
+
+ if (EventQueue.isDispatchThread() && !ApplicationManager.getApplication().isUnitTestMode()) {
+ LOG.warn("Network operation in EDT"); // TODO: fix
+ }
+
+ CloseableHttpResponse response = null;
+ try {
+ String uri = GithubUrlUtil.getApiUrl(myHost) + path;
+ response = doREST(uri, requestBody, headers, verb);
+
+ if (myAborted) throw new GithubOperationCanceledException();
+
+ checkStatusCode(response, requestBody);
+
+ HttpEntity entity = response.getEntity();
+ if (entity == null) {
+ return createResponse(response);
+ }
+
+ JsonElement ret = parseResponse(entity.getContent());
+ if (ret.isJsonNull()) {
+ return createResponse(response);
+ }
+
+ String newPath = null;
+ Header pageHeader = response.getFirstHeader("Link");
+ if (pageHeader != null) {
+ for (HeaderElement element : pageHeader.getElements()) {
+ NameValuePair rel = element.getParameterByName("rel");
+ if (rel != null && "next".equals(rel.getValue())) {
+ String urlString = element.toString();
+ int begin = urlString.indexOf('<');
+ int end = urlString.lastIndexOf('>');
+ if (begin == -1 || end == -1) {
+ LOG.error("Invalid 'Link' header", "{" + pageHeader.toString() + "}");
+ break;
+ }
+
+ String url = urlString.substring(begin + 1, end);
+ String newUrl = GithubUrlUtil.removeProtocolPrefix(url);
+ int index = newUrl.indexOf('/');
+ newPath = newUrl.substring(index);
+ break;
+ }
+ }
+ }
+
+ return createResponse(ret, newPath, response);
+ }
+ catch (SSLHandshakeException e) { // User canceled operation from CertificateManager
+ if (e.getCause() instanceof ValidatorException) {
+ LOG.info("Host SSL certificate is not trusted", e);
+ throw new GithubOperationCanceledException("Host SSL certificate is not trusted", e);
+ }
+ throw e;
+ }
+ catch (IOException e) {
+ if (myAborted) throw new GithubOperationCanceledException("Operation canceled", e);
+ throw e;
+ }
+ finally {
+ myRequest = null;
+ if (response != null) {
+ response.close();
+ }
+ if (!myReusable) {
+ myClient.close();
+ }
+ }
+ }
+
+ @NotNull
+ private CloseableHttpResponse doREST(@NotNull final String uri,
+ @Nullable final String requestBody,
+ @NotNull final Collection<Header> headers,
+ @NotNull final HttpVerb verb) throws IOException {
+ HttpRequestBase request;
+ switch (verb) {
+ case POST:
+ request = new HttpPost(uri);
+ if (requestBody != null) {
+ ((HttpPost)request).setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON));
+ }
+ break;
+ case PATCH:
+ request = new HttpPatch(uri);
+ if (requestBody != null) {
+ ((HttpPatch)request).setEntity(new StringEntity(requestBody, ContentType.APPLICATION_JSON));
+ }
+ break;
+ case GET:
+ request = new HttpGet(uri);
+ break;
+ case DELETE:
+ request = new HttpDelete(uri);
+ break;
+ case HEAD:
+ request = new HttpHead(uri);
+ break;
+ default:
+ throw new IllegalStateException("Unknown HttpVerb: " + verb.toString());
+ }
+
+ for (Header header : headers) {
+ request.addHeader(header);
+ }
+
+ myRequest = request;
+ return myClient.execute(request);
+ }
+
+ private static void checkStatusCode(@NotNull CloseableHttpResponse response, @Nullable String body) throws IOException {
+ int code = response.getStatusLine().getStatusCode();
+ switch (code) {
+ case HttpStatus.SC_OK:
+ case HttpStatus.SC_CREATED:
+ case HttpStatus.SC_ACCEPTED:
+ case HttpStatus.SC_NO_CONTENT:
+ return;
+ case HttpStatus.SC_UNAUTHORIZED:
+ case HttpStatus.SC_PAYMENT_REQUIRED:
+ case HttpStatus.SC_FORBIDDEN:
+ String message = getErrorMessage(response);
+
+ Header headerOTP = response.getFirstHeader("X-GitHub-OTP");
+ if (headerOTP != null) {
+ for (HeaderElement element : headerOTP.getElements()) {
+ if ("required".equals(element.getName())) {
+ throw new GithubTwoFactorAuthenticationException(message);
+ }
+ }
+ }
+
+ if (message.contains("API rate limit exceeded")) {
+ throw new GithubRateLimitExceededException(message);
+ }
+
+ throw new GithubAuthenticationException("Request response: " + message);
+ case HttpStatus.SC_BAD_REQUEST:
+ case HttpStatus.SC_UNPROCESSABLE_ENTITY:
+ if (body != null) {
+ LOG.info(body);
+ }
+ throw new GithubStatusCodeException(code + ": " + getErrorMessage(response), code);
+ default:
+ throw new GithubStatusCodeException(code + ": " + getErrorMessage(response), code);
+ }
+ }
+
+ @NotNull
+ private static String getErrorMessage(@NotNull CloseableHttpResponse response) {
+ try {
+ HttpEntity entity = response.getEntity();
+ if (entity != null) {
+ GithubErrorMessageRaw error = fromJson(parseResponse(entity.getContent()), GithubErrorMessageRaw.class);
+ return response.getStatusLine().getReasonPhrase() + " - " + error.getMessage();
+ }
+ }
+ catch (IOException e) {
+ LOG.info(e);
+ }
+ return response.getStatusLine().getReasonPhrase();
+ }
+
+ @NotNull
+ private static JsonElement parseResponse(@NotNull InputStream githubResponse) throws IOException {
+ Reader reader = new InputStreamReader(githubResponse, "UTF-8");
+ try {
+ return new JsonParser().parse(reader);
+ }
+ catch (JsonParseException jse) {
+ throw new GithubJsonException("Couldn't parse GitHub response", jse);
+ }
+ finally {
+ reader.close();
+ }
+ }
+
+ public static class PagedRequest<T> {
+ @Nullable private String myNextPage;
+ @NotNull private final Collection<Header> myHeaders;
+ @NotNull private final Class<T> myResult;
+ @NotNull private final Class<? extends DataConstructor[]> myRawArray;
+
+ @SuppressWarnings("NullableProblems")
+ public PagedRequest(@NotNull String path,
+ @NotNull Class<T> result,
+ @NotNull Class<? extends DataConstructor[]> rawArray,
+ @NotNull Header... headers) {
+ myNextPage = path;
+ myResult = result;
+ myRawArray = rawArray;
+ myHeaders = Arrays.asList(headers);
+ }
+
+ @NotNull
+ public List<T> next(@NotNull GithubConnection connection) throws IOException {
+ if (myNextPage == null) {
+ throw new NoSuchElementException();
+ }
+
+ String page = myNextPage;
+ myNextPage = null;
+
+ ResponsePage response = connection.request(page, null, myHeaders, HttpVerb.GET);
+
+ if (response.getJsonElement() == null) {
+ throw new GithubConfusingException("Empty response");
+ }
+
+ if (!response.getJsonElement().isJsonArray()) {
+ throw new GithubJsonException("Wrong json type: expected JsonArray", new Exception(response.getJsonElement().toString()));
+ }
+
+ myNextPage = response.getNextPage();
+
+ List<T> result = new ArrayList<T>();
+ for (DataConstructor raw : fromJson(response.getJsonElement().getAsJsonArray(), myRawArray)) {
+ result.add(createDataFromRaw(raw, myResult));
+ }
+ return result;
+ }
+
+ public boolean hasNext() {
+ return myNextPage != null;
+ }
+
+ @NotNull
+ public List<T> getAll(@NotNull GithubConnection connection) throws IOException {
+ List<T> result = new ArrayList<T>();
+ while (hasNext()) {
+ result.addAll(next(connection));
+ }
+ return result;
+ }
+ }
+
+ private ResponsePage createResponse(@NotNull CloseableHttpResponse response) throws GithubOperationCanceledException {
+ if (myAborted) throw new GithubOperationCanceledException();
+
+ return new ResponsePage(null, null, response.getAllHeaders());
+ }
+
+ private ResponsePage createResponse(@NotNull JsonElement ret, @Nullable String path, @NotNull CloseableHttpResponse response)
+ throws GithubOperationCanceledException {
+ if (myAborted) throw new GithubOperationCanceledException();
+
+ return new ResponsePage(ret, path, response.getAllHeaders());
+ }
+
+ private static class ResponsePage {
+ @Nullable private final JsonElement myResponse;
+ @Nullable private final String myNextPage;
+ @NotNull private final Header[] myHeaders;
+
+ public ResponsePage(@Nullable JsonElement response, @Nullable String next, @NotNull Header[] headers) {
+ myResponse = response;
+ myNextPage = next;
+ myHeaders = headers;
+ }
+
+ @Nullable
+ public JsonElement getJsonElement() {
+ return myResponse;
+ }
+
+ @Nullable
+ public String getNextPage() {
+ return myNextPage;
+ }
+
+ @NotNull
+ public Header[] getHeaders() {
+ return myHeaders;
+ }
+ }
+
+ private static class PreemptiveBasicAuthInterceptor implements HttpRequestInterceptor {
+ @Override
+ public void process(HttpRequest request, HttpContext context) throws HttpException, IOException {
+ CredentialsProvider provider = (CredentialsProvider)context.getAttribute(HttpClientContext.CREDS_PROVIDER);
+ Credentials credentials = provider.getCredentials(AuthScope.ANY);
+ if (credentials != null) {
+ request.addHeader(new BasicScheme(Consts.UTF_8).authenticate(credentials, request, context));
+ }
+ }
+ }
+}