summaryrefslogtreecommitdiff
path: root/plugins/tasks/tasks-core/src/com/intellij/tasks/impl/httpclient/NewBaseRepositoryImpl.java
blob: 082d49812d5b6b3843ef67798ed0beb028604ccb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
package com.intellij.tasks.impl.httpclient;

import com.intellij.tasks.TaskRepositoryType;
import com.intellij.tasks.config.TaskSettings;
import com.intellij.tasks.impl.BaseRepository;
import com.intellij.tasks.impl.RequestFailedException;
import com.intellij.tasks.impl.TaskUtil;
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.HttpClient;
import org.apache.http.client.config.AuthSchemes;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.protocol.HttpContext;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;

/**
 * This alternative base implementation of {@link com.intellij.tasks.impl.BaseRepository} should be used
 * for new connectors that use httpclient-4.x instead of legacy httpclient-3.1.
 *
 * @author Mikhail Golubev
 */
public abstract class NewBaseRepositoryImpl extends BaseRepository {
  private static final AuthScope BASIC_AUTH_SCOPE =
    new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT, AuthScope.ANY_REALM, AuthSchemes.BASIC);
  // Provides preemptive authentication in HttpClient 4.x
  // see http://stackoverflow.com/questions/2014700/preemptive-basic-authentication-with-apache-httpclient-4
  private static final HttpRequestInterceptor PREEMPTIVE_BASIC_AUTH = new PreemptiveBasicAuthInterceptor();

  /**
   * Serialization constructor
   */
  protected NewBaseRepositoryImpl() {
    // empty
  }

  protected NewBaseRepositoryImpl(TaskRepositoryType type) {
    super(type);
  }

  protected NewBaseRepositoryImpl(BaseRepository other) {
    super(other);
  }

  @NotNull
  protected HttpClient getHttpClient() {
    HttpClientBuilder builder = HttpClients.custom()
      .setDefaultRequestConfig(createRequestConfig())
      .setSslcontext(CertificateManager.getInstance().getSslContext())
        // TODO: use custom one for additional certificate check
        //.setHostnameVerifier(SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER)
      .setHostnameVerifier((X509HostnameVerifier)CertificateManager.HOSTNAME_VERIFIER)
      .setDefaultCredentialsProvider(createCredentialsProvider())
      .addInterceptorFirst(PREEMPTIVE_BASIC_AUTH)
      .addInterceptorLast(createRequestInterceptor());
    return builder.build();
  }

  /**
   * Custom request interceptor can be used for modifying outgoing requests. One possible usage is to
   * add specific header to each request according to authentication scheme used.
   *
   * @return specific request interceptor or null by default
   */
  @Nullable
  protected HttpRequestInterceptor createRequestInterceptor() {
    return null;
  }

  @NotNull
  private CredentialsProvider createCredentialsProvider() {
    CredentialsProvider provider = new BasicCredentialsProvider();
    // Basic authentication
    if (isUseHttpAuthentication()) {
      provider.setCredentials(BASIC_AUTH_SCOPE, new UsernamePasswordCredentials(getUsername(), getPassword()));
    }
    // Proxy authentication
    HttpConfigurable proxySettings = HttpConfigurable.getInstance();
    if (isUseProxy() && proxySettings.PROXY_AUTHENTICATION) {
      provider.setCredentials(new AuthScope(proxySettings.PROXY_HOST, proxySettings.PROXY_PORT),
                              new UsernamePasswordCredentials(proxySettings.PROXY_LOGIN, proxySettings.getPlainProxyPassword()));
    }
    return provider;
  }

  @NotNull
  protected RequestConfig createRequestConfig() {
    TaskSettings tasksSettings = TaskSettings.getInstance();
    HttpConfigurable proxySettings = HttpConfigurable.getInstance();
    RequestConfig.Builder builder = RequestConfig.custom()
      .setConnectTimeout(3000)
      .setSocketTimeout(tasksSettings.CONNECTION_TIMEOUT);
    if (isUseProxy()) {
      builder.setProxy(new HttpHost(proxySettings.PROXY_HOST, proxySettings.PROXY_PORT));
    }
    return builder.build();
  }

  /**
   * Return server's REST API path prefix, e.g. {@code /rest/api/latest} for JIRA or {@code /api/v3/} for Gitlab.
   * This value will be used in {@link #getRestApiUrl(Object...)}
   *
   * @return server's REST API path prefix
   */
  @NotNull
  public String getRestApiPathPrefix() {
    return "";
  }

  /**
   * Build URL using {@link #getUrl()}, {@link #getRestApiPathPrefix()}} and specified path components.
   * <p/>
   * Individual path components will should not contain leading or trailing slashes. Empty or null components
   * will be omitted. Each components is converted to string using its {@link Object#toString()} method and url encoded, so
   * numeric IDs can be used as well. Returned URL doesn't contain trailing '/', because it's not compatible with some services.
   *
   * @return described URL
   */
  @NotNull
  public String getRestApiUrl(@NotNull Object... parts) {
    StringBuilder builder = new StringBuilder(getUrl());
    builder.append(getRestApiPathPrefix());
    if (builder.charAt(builder.length() - 1) == '/') {
      builder.deleteCharAt(builder.length() - 1);
    }
    for (Object part : parts) {
      if (part == null || part.equals("")) {
        continue;
      }
      builder.append('/').append(TaskUtil.encodeUrl(String.valueOf(part)));
    }
    return builder.toString();
  }

  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(BASIC_AUTH_SCOPE);
      if (credentials != null) {
        request.addHeader(new BasicScheme(Consts.UTF_8).authenticate(credentials, request, context));
      }
    }
  }

  public class HttpTestConnection extends CancellableConnection {

    // Request can be changed during test
    protected volatile HttpRequestBase myCurrentRequest;

    public HttpTestConnection(@NotNull HttpRequestBase request) {
      myCurrentRequest = request;
    }

    @Override
    protected void doTest() throws Exception {
      try {
        test();
      }
      catch (IOException e) {
        // Depending on request state AbstractExecutionAwareRequest.abort() can cause either
        // * RequestAbortedException if connection was not yet leased
        // * InterruptedIOException before reading response
        // * SocketException("Socket closed") during reading response
        // However in all cases 'aborted' flag should be properly set
        if (!myCurrentRequest.isAborted()) {
          throw e;
        }
      }
    }

    protected void test() throws Exception {
      HttpResponse response = getHttpClient().execute(myCurrentRequest);
      StatusLine statusLine = response.getStatusLine();
      if (statusLine != null && statusLine.getStatusCode() != HttpStatus.SC_OK) {
        throw RequestFailedException.forStatusCode(statusLine.getStatusCode(), statusLine.getReasonPhrase());
      }
    }

    @Override
    public void cancel() {
      myCurrentRequest.abort();
    }
  }
}