View Javadoc
1   /*
2    * This file is part of dependency-check-core.
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   * Copyright (c) 2012 Jeremy Long. All Rights Reserved.
17   */
18  package org.owasp.dependencycheck.utils;
19  
20  import java.io.BufferedOutputStream;
21  import org.slf4j.Logger;
22  import org.slf4j.LoggerFactory;
23  
24  import java.io.File;
25  import java.io.FileOutputStream;
26  import java.io.IOException;
27  import java.io.InputStream;
28  import java.net.HttpURLConnection;
29  import java.net.URISyntaxException;
30  import java.net.URL;
31  import java.security.InvalidAlgorithmParameterException;
32  import java.util.zip.GZIPInputStream;
33  import java.util.zip.InflaterInputStream;
34  import static java.lang.String.format;
35  
36  /**
37   * A utility to download files from the Internet.
38   *
39   * @author Jeremy Long
40   */
41  public final class Downloader {
42  
43      /**
44       * The logger.
45       */
46      private static final Logger LOGGER = LoggerFactory.getLogger(Downloader.class);
47      /**
48       * The maximum number of redirects that will be followed when attempting to
49       * download a file.
50       */
51      private static final int MAX_REDIRECT_ATTEMPTS = 5;
52  
53      /**
54       * The default HTTP request method for query timestamp
55       */
56      private static final String HEAD = "HEAD";
57  
58      /**
59       * The HTTP request method which can be used by query timestamp
60       */
61      private static final String GET = "GET";
62  
63      /**
64       * Private constructor for utility class.
65       */
66      private Downloader() {
67      }
68  
69      /**
70       * Retrieves a file from a given URL and saves it to the outputPath.
71       *
72       * @param url the URL of the file to download
73       * @param outputPath the path to the save the file to
74       * @throws DownloadFailedException is thrown if there is an error
75       * downloading the file
76       */
77      public static void fetchFile(URL url, File outputPath) throws DownloadFailedException {
78          fetchFile(url, outputPath, true);
79      }
80  
81      /**
82       * Retrieves a file from a given URL and saves it to the outputPath.
83       *
84       * @param url the URL of the file to download
85       * @param outputPath the path to the save the file to
86       * @param useProxy whether to use the configured proxy when downloading
87       * files
88       * @throws DownloadFailedException is thrown if there is an error
89       * downloading the file
90       */
91      public static void fetchFile(URL url, File outputPath, boolean useProxy) throws DownloadFailedException {
92          if ("file".equalsIgnoreCase(url.getProtocol())) {
93              File file;
94              try {
95                  file = new File(url.toURI());
96              } catch (URISyntaxException ex) {
97                  final String msg = format("Download failed, unable to locate '%s'", url.toString());
98                  throw new DownloadFailedException(msg);
99              }
100             if (file.exists()) {
101                 try {
102                     org.apache.commons.io.FileUtils.copyFile(file, outputPath);
103                 } catch (IOException ex) {
104                     final String msg = format("Download failed, unable to copy '%s' to '%s'", url.toString(), outputPath.getAbsolutePath());
105                     throw new DownloadFailedException(msg, ex);
106                 }
107             } else {
108                 final String msg = format("Download failed, file ('%s') does not exist", url.toString());
109                 throw new DownloadFailedException(msg);
110             }
111         } else {
112             HttpURLConnection conn = null;
113             try {
114                 LOGGER.debug("Attempting download of {}", url.toString());
115                 conn = URLConnectionFactory.createHttpURLConnection(url, useProxy);
116                 conn.setRequestProperty("Accept-Encoding", "gzip, deflate");
117                 conn.connect();
118                 int status = conn.getResponseCode();
119                 int redirectCount = 0;
120                 while ((status == HttpURLConnection.HTTP_MOVED_TEMP
121                         || status == HttpURLConnection.HTTP_MOVED_PERM
122                         || status == HttpURLConnection.HTTP_SEE_OTHER)
123                         && MAX_REDIRECT_ATTEMPTS > redirectCount++) {
124                     final String location = conn.getHeaderField("Location");
125                     try {
126                         conn.disconnect();
127                     } finally {
128                         conn = null;
129                     }
130                     LOGGER.debug("Download is being redirected from {} to {}", url.toString(), location);
131                     conn = URLConnectionFactory.createHttpURLConnection(new URL(location), useProxy);
132                     conn.setRequestProperty("Accept-Encoding", "gzip, deflate");
133                     conn.connect();
134                     status = conn.getResponseCode();
135                 }
136                 if (status != 200) {
137                     try {
138                         conn.disconnect();
139                     } finally {
140                         conn = null;
141                     }
142                     final String msg = format("Error downloading file %s; received response code %s.", url.toString(), status);
143                     throw new DownloadFailedException(msg);
144 
145                 }
146             } catch (IOException ex) {
147                 try {
148                     if (conn != null) {
149                         conn.disconnect();
150                     }
151                 } finally {
152                     conn = null;
153                 }
154                 if ("Connection reset".equalsIgnoreCase(ex.getMessage())) {
155                     final String msg = format("TLS Connection Reset%nPlease see "
156                             + "http://jeremylong.github.io/DependencyCheck/general/tlsfailures.html "
157                             + "for more information regarding how to resolve the issue.");
158                     LOGGER.error(msg);
159                     throw new DownloadFailedException(msg, ex);
160                 }
161                 final String msg = format("Error downloading file %s; unable to connect.", url.toString());
162                 throw new DownloadFailedException(msg, ex);
163             }
164 
165             final String encoding = conn.getContentEncoding();
166             BufferedOutputStream writer = null;
167             InputStream reader = null;
168             try {
169                 if (encoding != null && "gzip".equalsIgnoreCase(encoding)) {
170                     reader = new GZIPInputStream(conn.getInputStream());
171                 } else if (encoding != null && "deflate".equalsIgnoreCase(encoding)) {
172                     reader = new InflaterInputStream(conn.getInputStream());
173                 } else {
174                     reader = conn.getInputStream();
175                 }
176 
177                 writer = new BufferedOutputStream(new FileOutputStream(outputPath));
178                 final byte[] buffer = new byte[4096];
179                 int bytesRead;
180                 while ((bytesRead = reader.read(buffer)) > 0) {
181                     writer.write(buffer, 0, bytesRead);
182                 }
183                 LOGGER.debug("Download of {} complete", url.toString());
184             } catch (IOException ex) {
185                 checkForCommonExceptionTypes(ex);
186                 final String msg = format("Error saving '%s' to file '%s'%nConnection Timeout: %d%nEncoding: %s%n",
187                         url.toString(), outputPath.getAbsolutePath(), conn.getConnectTimeout(), encoding);
188                 throw new DownloadFailedException(msg, ex);
189             } catch (Exception ex) {
190                 final String msg = format("Unexpected exception saving '%s' to file '%s'%nConnection Timeout: %d%nEncoding: %s%n",
191                         url.toString(), outputPath.getAbsolutePath(), conn.getConnectTimeout(), encoding);
192                 throw new DownloadFailedException(msg, ex);
193             } finally {
194                 if (writer != null) {
195                     try {
196                         writer.close();
197                     } catch (IOException ex) {
198                         LOGGER.trace("Error closing the writer in Downloader.", ex);
199                     }
200                 }
201                 if (reader != null) {
202                     try {
203                         reader.close();
204                     } catch (IOException ex) {
205                         LOGGER.trace("Error closing the reader in Downloader.", ex);
206                     }
207                 }
208                 try {
209                     conn.disconnect();
210                 } finally {
211                     conn = null;
212                 }
213             }
214         }
215     }
216 
217     /**
218      * Makes an HTTP Head request to retrieve the last modified date of the
219      * given URL. If the file:// protocol is specified, then the lastTimestamp
220      * of the file is returned.
221      *
222      * @param url the URL to retrieve the timestamp from
223      * @return an epoch timestamp
224      * @throws DownloadFailedException is thrown if an exception occurs making
225      * the HTTP request
226      */
227     public static long getLastModified(URL url) throws DownloadFailedException {
228         return getLastModified(url, false);
229     }
230 
231     /**
232      * Makes an HTTP Head request to retrieve the last modified date of the
233      * given URL. If the file:// protocol is specified, then the lastTimestamp
234      * of the file is returned.
235      *
236      * @param url the URL to retrieve the timestamp from
237      * @param isRetry indicates if this is a retry - to prevent endless loop and
238      * stack overflow
239      * @return an epoch timestamp
240      * @throws DownloadFailedException is thrown if an exception occurs making
241      * the HTTP request
242      */
243     private static long getLastModified(URL url, boolean isRetry) throws DownloadFailedException {
244         long timestamp = 0;
245         //TODO add the FTP protocol?
246         if ("file".equalsIgnoreCase(url.getProtocol())) {
247             File lastModifiedFile;
248             try {
249                 lastModifiedFile = new File(url.toURI());
250             } catch (URISyntaxException ex) {
251                 final String msg = format("Unable to locate '%s'", url.toString());
252                 throw new DownloadFailedException(msg, ex);
253             }
254             timestamp = lastModifiedFile.lastModified();
255         } else {
256             final String httpMethod = determineHttpMethod();
257             HttpURLConnection conn = null;
258             try {
259                 conn = URLConnectionFactory.createHttpURLConnection(url);
260                 conn.setRequestMethod(httpMethod);
261                 conn.connect();
262                 final int t = conn.getResponseCode();
263                 if (t >= 200 && t < 300) {
264                     timestamp = conn.getLastModified();
265                 } else {
266                     throw new DownloadFailedException(format("%s request returned a non-200 status code", httpMethod));
267                 }
268             } catch (URLConnectionFailureException ex) {
269                 throw new DownloadFailedException(format("Error creating URL Connection for HTTP %s request.", httpMethod), ex);
270             } catch (IOException ex) {
271                 checkForCommonExceptionTypes(ex);
272                 LOGGER.error("IO Exception: " + ex.getMessage());
273                 LOGGER.debug("Exception details", ex);
274                 if (ex.getCause() != null) {
275                     LOGGER.debug("IO Exception cause: " + ex.getCause().getMessage(), ex.getCause());
276                 }
277                 try {
278                     //retry
279                     if (!isRetry && Settings.getBoolean(Settings.KEYS.DOWNLOADER_QUICK_QUERY_TIMESTAMP)) {
280                         Settings.setBoolean(Settings.KEYS.DOWNLOADER_QUICK_QUERY_TIMESTAMP, false);
281                         return getLastModified(url, true);
282                     }
283                 } catch (InvalidSettingException ex1) {
284                     LOGGER.debug("invalid setting?", ex1);
285                 }
286                 throw new DownloadFailedException(format("Error making HTTP %s request.", httpMethod), ex);
287             } finally {
288                 if (conn != null) {
289                     try {
290                         conn.disconnect();
291                     } finally {
292                         conn = null;
293                     }
294                 }
295             }
296         }
297         return timestamp;
298     }
299 
300     /**
301      * Analyzes the IOException, logs the appropriate information for debugging
302      * purposes, and then throws a DownloadFailedException that wraps the IO
303      * Exception for common IO Exceptions. This is to provide additional details
304      * to assist in resolution of the exception.
305      *
306      * @param ex the original exception
307      * @throws DownloadFailedException a wrapper exception that contains the
308      * original exception as the cause
309      */
310     protected static synchronized void checkForCommonExceptionTypes(IOException ex) throws DownloadFailedException {
311         Throwable cause = ex;
312         while (cause != null) {
313             if (cause instanceof java.net.UnknownHostException) {
314                 final String msg = format("Unable to resolve domain '%s'", cause.getMessage());
315                 LOGGER.error(msg);
316                 throw new DownloadFailedException(msg);
317             }
318             if (cause instanceof InvalidAlgorithmParameterException) {
319                 final String keystore = System.getProperty("javax.net.ssl.keyStore");
320                 final String version = System.getProperty("java.version");
321                 final String vendor = System.getProperty("java.vendor");
322                 LOGGER.info("Error making HTTPS request - InvalidAlgorithmParameterException");
323                 LOGGER.info("There appears to be an issue with the installation of Java and the cacerts."
324                         + "See closed issue #177 here: https://github.com/jeremylong/DependencyCheck/issues/177");
325                 LOGGER.info("Java Info:\njavax.net.ssl.keyStore='{}'\njava.version='{}'\njava.vendor='{}'",
326                         keystore, version, vendor);
327                 throw new DownloadFailedException("Error making HTTPS request. Please see the log for more details.");
328             }
329             cause = cause.getCause();
330         }
331     }
332 
333     /**
334      * Returns the HEAD or GET HTTP method. HEAD is the default.
335      *
336      * @return the HTTP method to use
337      */
338     private static String determineHttpMethod() {
339         return isQuickQuery() ? HEAD : GET;
340     }
341 
342     /**
343      * Determines if the HTTP method GET or HEAD should be used to check the
344      * timestamp on external resources.
345      *
346      * @return true if configured to use HEAD requests
347      */
348     private static boolean isQuickQuery() {
349         boolean quickQuery;
350 
351         try {
352             quickQuery = Settings.getBoolean(Settings.KEYS.DOWNLOADER_QUICK_QUERY_TIMESTAMP, true);
353         } catch (InvalidSettingException e) {
354             if (LOGGER.isTraceEnabled()) {
355                 LOGGER.trace("Invalid settings : {}", e.getMessage(), e);
356             }
357             quickQuery = true;
358         }
359         return quickQuery;
360     }
361 }