819101865ae59c8cdf4a7a45ef8d4625cb065421
[idea/community.git] / platform / platform-api / src / com / intellij / util / io / HttpRequests.java
1 /*
2  * Copyright 2000-2016 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.util.io;
17
18 import com.intellij.Patches;
19 import com.intellij.ide.IdeBundle;
20 import com.intellij.openapi.application.Application;
21 import com.intellij.openapi.application.ApplicationInfo;
22 import com.intellij.openapi.application.ApplicationManager;
23 import com.intellij.openapi.diagnostic.Logger;
24 import com.intellij.openapi.progress.ProgressIndicator;
25 import com.intellij.openapi.util.io.BufferExposingByteArrayOutputStream;
26 import com.intellij.openapi.util.io.FileUtilRt;
27 import com.intellij.openapi.util.io.StreamUtil;
28 import com.intellij.openapi.util.text.StringUtil;
29 import com.intellij.openapi.vfs.CharsetToolkit;
30 import com.intellij.util.ArrayUtil;
31 import com.intellij.util.lang.UrlClassLoader;
32 import com.intellij.util.net.HttpConfigurable;
33 import com.intellij.util.net.NetUtils;
34 import com.intellij.util.net.ssl.CertificateManager;
35 import org.jetbrains.annotations.NotNull;
36 import org.jetbrains.annotations.Nullable;
37
38 import javax.net.ssl.HostnameVerifier;
39 import javax.net.ssl.HttpsURLConnection;
40 import java.io.*;
41 import java.net.*;
42 import java.nio.charset.Charset;
43 import java.util.regex.Matcher;
44 import java.util.regex.Pattern;
45
46 /**
47  * Handy class for reading data from HTTP connections with built-in support for HTTP redirects and gzipped content and automatic cleanup.
48  * Usage: <pre>{@code
49  * int firstByte = HttpRequests.request(url).connect(new HttpRequests.RequestProcessor<Integer>() {
50  *   public Integer process(@NotNull Request request) throws IOException {
51  *     return request.getInputStream().read();
52  *   }
53  * });
54  * }</pre>
55  */
56 public final class HttpRequests {
57   private static final Logger LOG = Logger.getInstance(HttpRequests.class);
58
59   private static final int BLOCK_SIZE = 16 * 1024;
60   private static final Pattern CHARSET_PATTERN = Pattern.compile("charset=([^;]+)");
61
62   private HttpRequests() { }
63
64
65   public interface Request {
66     @NotNull
67     String getURL();
68
69     @NotNull
70     URLConnection getConnection() throws IOException;
71
72     @NotNull
73     InputStream getInputStream() throws IOException;
74
75     @NotNull
76     BufferedReader getReader() throws IOException;
77
78     @NotNull
79     BufferedReader getReader(@Nullable ProgressIndicator indicator) throws IOException;
80
81     /** @deprecated Called automatically on open connection. Use {@link RequestBuilder#tryConnect()} to get response code */
82     boolean isSuccessful() throws IOException;
83
84     @NotNull
85     File saveToFile(@NotNull File file, @Nullable ProgressIndicator indicator) throws IOException;
86
87     @NotNull
88     byte[] readBytes(@Nullable ProgressIndicator indicator) throws IOException;
89
90     @NotNull
91     String readString(@Nullable ProgressIndicator indicator) throws IOException;
92   }
93
94   public interface RequestProcessor<T> {
95     T process(@NotNull Request request) throws IOException;
96   }
97
98   public interface ConnectionTuner {
99     void tune(@NotNull URLConnection connection) throws IOException;
100   }
101
102   public static class HttpStatusException extends IOException {
103     private int myStatusCode;
104     private String myUrl;
105
106     public HttpStatusException(@NotNull String message, int statusCode, @NotNull String url) {
107       super(message);
108       myStatusCode = statusCode;
109       myUrl = url;
110     }
111
112     public int getStatusCode() {
113       return myStatusCode;
114     }
115
116     @NotNull
117     public String getUrl() {
118       return myUrl;
119     }
120
121     @Override
122     public String toString() {
123       return super.toString() + ". Status=" + myStatusCode + ", Url=" + myUrl;
124     }
125   }
126
127
128   @NotNull
129   public static RequestBuilder request(@NotNull String url) {
130     return new RequestBuilderImpl(url);
131   }
132
133   @NotNull
134   public static String createErrorMessage(@NotNull IOException e, @NotNull Request request, boolean includeHeaders) {
135     StringBuilder builder = new StringBuilder();
136
137     builder.append("Cannot download '").append(request.getURL()).append("': ").append(e.getMessage());
138
139     try {
140       URLConnection connection = request.getConnection();
141       if (includeHeaders) {
142         builder.append("\n, headers: ").append(connection.getHeaderFields());
143       }
144       if (connection instanceof HttpURLConnection) {
145         HttpURLConnection httpConnection = (HttpURLConnection)connection;
146         builder.append("\n, response: ").append(httpConnection.getResponseCode()).append(' ').append(httpConnection.getResponseMessage());
147       }
148     }
149     catch (Throwable ignored) { }
150
151     return builder.toString();
152   }
153
154
155   private static class RequestBuilderImpl extends RequestBuilder {
156     private final String myUrl;
157     private int myConnectTimeout = HttpConfigurable.CONNECTION_TIMEOUT;
158     private int myTimeout = HttpConfigurable.READ_TIMEOUT;
159     private int myRedirectLimit = HttpConfigurable.REDIRECT_LIMIT;
160     private boolean myGzip = true;
161     private boolean myForceHttps;
162     private boolean myUseProxy = true;
163     private HostnameVerifier myHostnameVerifier;
164     private String myUserAgent;
165     private String myAccept;
166     private ConnectionTuner myTuner;
167
168     private RequestBuilderImpl(@NotNull String url) {
169       myUrl = url;
170     }
171
172     @Override
173     public RequestBuilder connectTimeout(int value) {
174       myConnectTimeout = value;
175       return this;
176     }
177
178     @Override
179     public RequestBuilder readTimeout(int value) {
180       myTimeout = value;
181       return this;
182     }
183
184     @Override
185     public RequestBuilder redirectLimit(int redirectLimit) {
186       myRedirectLimit = redirectLimit;
187       return this;
188     }
189
190     @Override
191     public RequestBuilder gzip(boolean value) {
192       myGzip = value;
193       return this;
194     }
195
196     @Override
197     public RequestBuilder forceHttps(boolean forceHttps) {
198       myForceHttps = forceHttps;
199       return this;
200     }
201
202     @Override
203     public RequestBuilder useProxy(boolean useProxy) {
204       myUseProxy = useProxy;
205       return this;
206     }
207
208     @Override
209     public RequestBuilder hostNameVerifier(@Nullable HostnameVerifier hostnameVerifier) {
210       myHostnameVerifier = hostnameVerifier;
211       return this;
212     }
213
214     @Override
215     public RequestBuilder userAgent(@Nullable String userAgent) {
216       myUserAgent = userAgent;
217       return this;
218     }
219
220     @Override
221     public RequestBuilder productNameAsUserAgent() {
222       Application app = ApplicationManager.getApplication();
223       if (app != null && !app.isDisposed()) {
224         ApplicationInfo info = ApplicationInfo.getInstance();
225         return userAgent(info.getVersionName() + '/' + info.getBuild().asStringWithoutProductCode());
226       }
227       else {
228         return userAgent("IntelliJ");
229       }
230     }
231
232     @Override
233     public RequestBuilder accept(@Nullable String mimeType) {
234       myAccept = mimeType;
235       return this;
236     }
237
238     @Override
239     public RequestBuilder tuner(@Nullable ConnectionTuner tuner) {
240       myTuner = tuner;
241       return this;
242     }
243
244     @Override
245     public <T> T connect(@NotNull HttpRequests.RequestProcessor<T> processor) throws IOException {
246       return process(this, processor);
247     }
248   }
249
250   private static class RequestImpl implements Request, AutoCloseable {
251     private final RequestBuilderImpl myBuilder;
252     private URLConnection myConnection;
253     private InputStream myInputStream;
254     private BufferedReader myReader;
255
256     private RequestImpl(RequestBuilderImpl builder) {
257       myBuilder = builder;
258     }
259
260     @NotNull
261     @Override
262     public String getURL() {
263       return myBuilder.myUrl;
264     }
265
266     @NotNull
267     @Override
268     public URLConnection getConnection() throws IOException {
269       if (myConnection == null) {
270         myConnection = openConnection(myBuilder);
271       }
272       return myConnection;
273     }
274
275     @NotNull
276     @Override
277     public InputStream getInputStream() throws IOException {
278       if (myInputStream == null) {
279         myInputStream = getConnection().getInputStream();
280         if (myBuilder.myGzip && "gzip".equalsIgnoreCase(getConnection().getContentEncoding())) {
281           //noinspection IOResourceOpenedButNotSafelyClosed
282           myInputStream = CountingGZIPInputStream.create(myInputStream);
283         }
284       }
285       return myInputStream;
286     }
287
288     @NotNull
289     @Override
290     public BufferedReader getReader() throws IOException {
291       return getReader(null);
292     }
293
294     @NotNull
295     @Override
296     public BufferedReader getReader(@Nullable ProgressIndicator indicator) throws IOException {
297       if (myReader == null) {
298         InputStream inputStream = getInputStream();
299         if (indicator != null) {
300           int contentLength = getConnection().getContentLength();
301           if (contentLength > 0) {
302             //noinspection IOResourceOpenedButNotSafelyClosed
303             inputStream = new ProgressMonitorInputStream(indicator, inputStream, contentLength);
304           }
305         }
306         myReader = new BufferedReader(new InputStreamReader(inputStream, getCharset(this)));
307       }
308       return myReader;
309     }
310
311     @Override
312     public boolean isSuccessful() throws IOException {
313       URLConnection connection = getConnection();
314       return !(connection instanceof HttpURLConnection) || ((HttpURLConnection)connection).getResponseCode() == 200;
315     }
316
317     @Override
318     @NotNull
319     public byte[] readBytes(@Nullable ProgressIndicator indicator) throws IOException {
320       int contentLength = getConnection().getContentLength();
321       BufferExposingByteArrayOutputStream out = new BufferExposingByteArrayOutputStream(contentLength > 0 ? contentLength : BLOCK_SIZE);
322       NetUtils.copyStreamContent(indicator, getInputStream(), out, contentLength);
323       return ArrayUtil.realloc(out.getInternalBuffer(), out.size());
324     }
325
326     @NotNull
327     @Override
328     public String readString(@Nullable ProgressIndicator indicator) throws IOException {
329       Charset cs = getCharset(this);
330       byte[] bytes = readBytes(indicator);
331       return new String(bytes, cs);
332     }
333
334     @Override
335     @NotNull
336     public File saveToFile(@NotNull File file, @Nullable ProgressIndicator indicator) throws IOException {
337       FileUtilRt.createParentDirs(file);
338
339       boolean deleteFile = true;
340       try {
341         OutputStream out = new FileOutputStream(file);
342         try {
343           NetUtils.copyStreamContent(indicator, getInputStream(), out, getConnection().getContentLength());
344           deleteFile = false;
345         }
346         catch (IOException e) {
347           throw new IOException(createErrorMessage(e, this, false), e);
348         }
349         finally {
350           out.close();
351         }
352       }
353       finally {
354         if (deleteFile) {
355           FileUtilRt.delete(file);
356         }
357       }
358
359       return file;
360     }
361
362     @Override
363     public void close() {
364       StreamUtil.closeStream(myInputStream);
365       StreamUtil.closeStream(myReader);
366       if (myConnection instanceof HttpURLConnection) {
367         ((HttpURLConnection)myConnection).disconnect();
368       }
369     }
370   }
371
372   private static <T> T process(RequestBuilderImpl builder, RequestProcessor<T> processor) throws IOException {
373     LOG.assertTrue(ApplicationManager.getApplication() == null ||
374                    ApplicationManager.getApplication().isUnitTestMode() ||
375                    !ApplicationManager.getApplication().isReadAccessAllowed(),
376                    "Network shouldn't be accessed in EDT or inside read action");
377
378     ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
379     if (Patches.JDK_BUG_ID_8032832 && !UrlClassLoader.isRegisteredAsParallelCapable(contextLoader)) {
380       // hack-around for class loader lock in sun.net.www.protocol.http.NegotiateAuthentication (IDEA-131621)
381       try (URLClassLoader cl = new URLClassLoader(new URL[0], contextLoader)) {
382         Thread.currentThread().setContextClassLoader(cl);
383         return doProcess(builder, processor);
384       }
385       finally {
386         Thread.currentThread().setContextClassLoader(contextLoader);
387       }
388     }
389     else {
390       return doProcess(builder, processor);
391     }
392   }
393
394   private static <T> T doProcess(RequestBuilderImpl builder, RequestProcessor<T> processor) throws IOException {
395     try (RequestImpl request = new RequestImpl(builder)) {
396       return processor.process(request);
397     }
398   }
399
400   private static Charset getCharset(Request request) throws IOException {
401     String contentType = request.getConnection().getContentType();
402     if (!StringUtil.isEmptyOrSpaces(contentType)) {
403       Matcher m = CHARSET_PATTERN.matcher(contentType);
404       if (m.find()) {
405         try {
406           return Charset.forName(StringUtil.unquoteString(m.group(1)));
407         }
408         catch (IllegalArgumentException e) {
409           throw new IOException("unknown charset (" + contentType + ")", e);
410         }
411       }
412     }
413
414     return CharsetToolkit.UTF8_CHARSET;
415   }
416
417   private static URLConnection openConnection(RequestBuilderImpl builder) throws IOException {
418     String url = builder.myUrl;
419
420     for (int i = 0; i < builder.myRedirectLimit; i++) {
421       if (builder.myForceHttps && StringUtil.startsWith(url, "http:")) {
422         url = "https:" + url.substring(5);
423       }
424
425       if (url.startsWith("https:") && ApplicationManager.getApplication() != null) {
426         CertificateManager.getInstance();
427       }
428
429       URLConnection connection;
430       if (!builder.myUseProxy) {
431         connection = new URL(url).openConnection(Proxy.NO_PROXY);
432       }
433       else if (ApplicationManager.getApplication() == null) {
434         connection = new URL(url).openConnection();
435       }
436       else {
437         connection = HttpConfigurable.getInstance().openConnection(url);
438       }
439
440       connection.setConnectTimeout(builder.myConnectTimeout);
441       connection.setReadTimeout(builder.myTimeout);
442
443       if (builder.myUserAgent != null) {
444         connection.setRequestProperty("User-Agent", builder.myUserAgent);
445       }
446
447       if (builder.myHostnameVerifier != null && connection instanceof HttpsURLConnection) {
448         ((HttpsURLConnection)connection).setHostnameVerifier(builder.myHostnameVerifier);
449       }
450
451       if (builder.myGzip) {
452         connection.setRequestProperty("Accept-Encoding", "gzip");
453       }
454
455       if (builder.myAccept != null) {
456         connection.setRequestProperty("Accept", builder.myAccept);
457       }
458
459       connection.setUseCaches(false);
460
461       if (builder.myTuner != null) {
462         builder.myTuner.tune(connection);
463       }
464
465       if (connection instanceof HttpURLConnection) {
466         int responseCode = ((HttpURLConnection)connection).getResponseCode();
467
468         if (responseCode < 200 || responseCode >= 300 && responseCode != HttpURLConnection.HTTP_NOT_MODIFIED) {
469           ((HttpURLConnection)connection).disconnect();
470
471           if (responseCode == HttpURLConnection.HTTP_MOVED_PERM || responseCode == HttpURLConnection.HTTP_MOVED_TEMP) {
472             url = connection.getHeaderField("Location");
473             if (url != null) {
474               continue;
475             }
476           }
477
478           String message = IdeBundle.message("error.connection.failed.with.http.code.N", responseCode);
479           throw new HttpStatusException(message, responseCode, StringUtil.notNullize(url, "Empty URL"));
480         }
481       }
482
483       return connection;
484     }
485
486     throw new IOException(IdeBundle.message("error.connection.failed.redirects"));
487   }
488 }