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  
35  import static java.lang.String.format;
36  
37  /**
38   * A utility to download files from the Internet.
39   *
40   * @author Jeremy Long
41   */
42  public final class Downloader {
43  
44      /**
45       * The logger.
46       */
47      private static final Logger LOGGER = LoggerFactory.getLogger(Downloader.class);
48      /**
49       * The maximum number of redirects that will be followed when attempting to 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 downloading the file
75       */
76      public static void fetchFile(URL url, File outputPath) throws DownloadFailedException {
77          fetchFile(url, outputPath, true);
78      }
79  
80      /**
81       * Retrieves a file from a given URL and saves it to the outputPath.
82       *
83       * @param url the URL of the file to download
84       * @param outputPath the path to the save the file to
85       * @param useProxy whether to use the configured proxy when downloading files
86       * @throws DownloadFailedException is thrown if there is an error downloading the file
87       */
88      public static void fetchFile(URL url, File outputPath, boolean useProxy) throws DownloadFailedException {
89          if ("file".equalsIgnoreCase(url.getProtocol())) {
90              File file;
91              try {
92                  file = new File(url.toURI());
93              } catch (URISyntaxException ex) {
94                  final String msg = format("Download failed, unable to locate '%s'", url.toString());
95                  throw new DownloadFailedException(msg);
96              }
97              if (file.exists()) {
98                  try {
99                      org.apache.commons.io.FileUtils.copyFile(file, outputPath);
100                 } catch (IOException ex) {
101                     final String msg = format("Download failed, unable to copy '%s' to '%s'", url.toString(), outputPath.getAbsolutePath());
102                     throw new DownloadFailedException(msg);
103                 }
104             } else {
105                 final String msg = format("Download failed, file ('%s') does not exist", url.toString());
106                 throw new DownloadFailedException(msg);
107             }
108         } else {
109             HttpURLConnection conn = null;
110             try {
111                 LOGGER.debug("Attempting download of {}", url.toString());
112                 conn = URLConnectionFactory.createHttpURLConnection(url, useProxy);
113                 conn.setRequestProperty("Accept-Encoding", "gzip, deflate");
114                 conn.connect();
115                 int status = conn.getResponseCode();
116                 int redirectCount = 0;
117                 while ((status == HttpURLConnection.HTTP_MOVED_TEMP
118                         || status == HttpURLConnection.HTTP_MOVED_PERM
119                         || status == HttpURLConnection.HTTP_SEE_OTHER)
120                         && MAX_REDIRECT_ATTEMPTS > redirectCount++) {
121                     final String location = conn.getHeaderField("Location");
122                     try {
123                         conn.disconnect();
124                     } finally {
125                         conn = null;
126                     }
127                     LOGGER.debug("Download is being redirected from {} to {}", url.toString(), location);
128                     conn = URLConnectionFactory.createHttpURLConnection(new URL(location), useProxy);
129                     conn.setRequestProperty("Accept-Encoding", "gzip, deflate");
130                     conn.connect();
131                     status = conn.getResponseCode();
132                 }
133                 if (status != 200) {
134                     try {
135                         conn.disconnect();
136                     } finally {
137                         conn = null;
138                     }
139                     final String msg = format("Error downloading file %s; received response code %s.", url.toString(), status);
140                     throw new DownloadFailedException(msg);
141 
142                 }
143             } catch (IOException ex) {
144                 try {
145                     if (conn != null) {
146                         conn.disconnect();
147                     }
148                 } finally {
149                     conn = null;
150                 }
151                 final String msg = format("Error downloading file %s; unable to connect.", url.toString());
152                 throw new DownloadFailedException(msg, ex);
153             }
154 
155             final String encoding = conn.getContentEncoding();
156             BufferedOutputStream writer = null;
157             InputStream reader = null;
158             try {
159                 if (encoding != null && "gzip".equalsIgnoreCase(encoding)) {
160                     reader = new GZIPInputStream(conn.getInputStream());
161                 } else if (encoding != null && "deflate".equalsIgnoreCase(encoding)) {
162                     reader = new InflaterInputStream(conn.getInputStream());
163                 } else {
164                     reader = conn.getInputStream();
165                 }
166 
167                 writer = new BufferedOutputStream(new FileOutputStream(outputPath));
168                 final byte[] buffer = new byte[4096];
169                 int bytesRead;
170                 while ((bytesRead = reader.read(buffer)) > 0) {
171                     writer.write(buffer, 0, bytesRead);
172                 }
173                 LOGGER.debug("Download of {} complete", url.toString());
174             } catch (IOException ex) {
175                 analyzeException(ex);
176                 final String msg = format("Error saving '%s' to file '%s'%nConnection Timeout: %d%nEncoding: %s%n",
177                         url.toString(), outputPath.getAbsolutePath(), conn.getConnectTimeout(), encoding);
178                 throw new DownloadFailedException(msg, ex);
179             } catch (Throwable ex) {
180                 final String msg = format("Unexpected exception saving '%s' to file '%s'%nConnection Timeout: %d%nEncoding: %s%n",
181                         url.toString(), outputPath.getAbsolutePath(), conn.getConnectTimeout(), encoding);
182                 throw new DownloadFailedException(msg, ex);
183             } finally {
184                 if (writer != null) {
185                     try {
186                         writer.close();
187                     } catch (IOException ex) {
188                         LOGGER.trace("Error closing the writer in Downloader.", ex);
189                     }
190                 }
191                 if (reader != null) {
192                     try {
193                         reader.close();
194                     } catch (IOException ex) {
195                         LOGGER.trace("Error closing the reader in Downloader.", ex);
196                     }
197                 }
198                 try {
199                     conn.disconnect();
200                 } finally {
201                     conn = null;
202                 }
203             }
204         }
205     }
206 
207     /**
208      * Makes an HTTP Head request to retrieve the last modified date of the given URL. If the file:// protocol is specified, then
209      * the lastTimestamp of the file is returned.
210      *
211      * @param url the URL to retrieve the timestamp from
212      * @return an epoch timestamp
213      * @throws DownloadFailedException is thrown if an exception occurs making the HTTP request
214      */
215     public static long getLastModified(URL url) throws DownloadFailedException {
216         long timestamp = 0;
217         //TODO add the FTP protocol?
218         if ("file".equalsIgnoreCase(url.getProtocol())) {
219             File lastModifiedFile;
220             try {
221                 lastModifiedFile = new File(url.toURI());
222             } catch (URISyntaxException ex) {
223                 final String msg = format("Unable to locate '%s'", url.toString());
224                 throw new DownloadFailedException(msg);
225             }
226             timestamp = lastModifiedFile.lastModified();
227         } else {
228             final String httpMethod = determineHttpMethod();
229             HttpURLConnection conn = null;
230             try {
231                 conn = URLConnectionFactory.createHttpURLConnection(url);
232                 conn.setRequestMethod(httpMethod);
233                 conn.connect();
234                 final int t = conn.getResponseCode();
235                 if (t >= 200 && t < 300) {
236                     timestamp = conn.getLastModified();
237                 } else {
238                     throw new DownloadFailedException(format("%s request returned a non-200 status code", httpMethod));
239                 }
240             } catch (URLConnectionFailureException ex) {
241                 throw new DownloadFailedException(format("Error creating URL Connection for HTTP %s request.", httpMethod), ex);
242             } catch (IOException ex) {
243                 analyzeException(ex);
244                 try {
245                     //retry
246                     if (!Settings.getBoolean(Settings.KEYS.DOWNLOADER_QUICK_QUERY_TIMESTAMP)) {
247                         Settings.setBoolean(Settings.KEYS.DOWNLOADER_QUICK_QUERY_TIMESTAMP, true);
248                         return getLastModified(url);
249                     }
250                 } catch (InvalidSettingException ex1) {
251                     LOGGER.debug("invalid setting?", ex);
252                 }
253 
254                 throw new DownloadFailedException(format("Error making HTTP %s request.", httpMethod), ex);
255             } finally {
256                 if (conn != null) {
257                     try {
258                         conn.disconnect();
259                     } finally {
260                         conn = null;
261                     }
262                 }
263             }
264         }
265         return timestamp;
266     }
267 
268     /**
269      * Analyzes the IOException, logs the appropriate information for debugging purposes, and then throws a
270      * DownloadFailedException that wraps the IO Exception.
271      *
272      * @param ex the original exception
273      * @throws DownloadFailedException a wrapper exception that contains the original exception as the cause
274      */
275     protected static void analyzeException(IOException ex) throws DownloadFailedException {
276         Throwable cause = ex;
277         while (cause != null) {
278             if (cause instanceof InvalidAlgorithmParameterException) {
279                 final String keystore = System.getProperty("javax.net.ssl.keyStore");
280                 final String version = System.getProperty("java.version");
281                 final String vendor = System.getProperty("java.vendor");
282                 LOGGER.info("Error making HTTPS request - InvalidAlgorithmParameterException");
283                 LOGGER.info("There appears to be an issue with the installation of Java and the cacerts."
284                         + "See closed issue #177 here: https://github.com/jeremylong/DependencyCheck/issues/177");
285                 LOGGER.info("Java Info:\njavax.net.ssl.keyStore='{}'\njava.version='{}'\njava.vendor='{}'",
286                         keystore, version, vendor);
287                 throw new DownloadFailedException("Error making HTTPS request. Please see the log for more details.");
288             }
289             cause = cause.getCause();
290         }
291     }
292 
293     /**
294      * Returns the HEAD or GET HTTP method. HEAD is the default.
295      *
296      * @return the HTTP method to use
297      */
298     private static String determineHttpMethod() {
299         return isQuickQuery() ? HEAD : GET;
300     }
301 
302     /**
303      * Determines if the HTTP method GET or HEAD should be used to check the timestamp on external resources.
304      *
305      * @return true if configured to use HEAD requests
306      */
307     private static boolean isQuickQuery() {
308         boolean quickQuery;
309 
310         try {
311             quickQuery = Settings.getBoolean(Settings.KEYS.DOWNLOADER_QUICK_QUERY_TIMESTAMP, true);
312         } catch (InvalidSettingException e) {
313             quickQuery = true;
314         }
315         return quickQuery;
316     }
317 }