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