001    /*
002     * SonarQube, open source software quality management tool.
003     * Copyright (C) 2008-2013 SonarSource
004     * mailto:contact AT sonarsource DOT com
005     *
006     * SonarQube is free software; you can redistribute it and/or
007     * modify it under the terms of the GNU Lesser General Public
008     * License as published by the Free Software Foundation; either
009     * version 3 of the License, or (at your option) any later version.
010     *
011     * SonarQube is distributed in the hope that it will be useful,
012     * but WITHOUT ANY WARRANTY; without even the implied warranty of
013     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014     * Lesser General Public License for more details.
015     *
016     * You should have received a copy of the GNU Lesser General Public License
017     * along with this program; if not, write to the Free Software Foundation,
018     * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
019     */
020    package org.sonar.api.utils;
021    
022    import com.google.common.annotations.VisibleForTesting;
023    import com.google.common.base.Joiner;
024    import com.google.common.base.Strings;
025    import com.google.common.collect.ImmutableList;
026    import com.google.common.collect.Lists;
027    import com.google.common.io.ByteStreams;
028    import com.google.common.io.CharStreams;
029    import com.google.common.io.Files;
030    import com.google.common.io.InputSupplier;
031    import org.apache.commons.codec.binary.Base64;
032    import org.apache.commons.io.FileUtils;
033    import org.apache.commons.io.IOUtils;
034    import org.slf4j.LoggerFactory;
035    import org.sonar.api.BatchComponent;
036    import org.sonar.api.ServerComponent;
037    import org.sonar.api.config.Settings;
038    import org.sonar.api.platform.Server;
039    
040    import javax.annotation.Nullable;
041    
042    import java.io.File;
043    import java.io.IOException;
044    import java.io.InputStream;
045    import java.net.Authenticator;
046    import java.net.HttpURLConnection;
047    import java.net.PasswordAuthentication;
048    import java.net.Proxy;
049    import java.net.ProxySelector;
050    import java.net.URI;
051    import java.nio.charset.Charset;
052    import java.util.List;
053    import java.util.Map;
054    
055    /**
056     * This component downloads HTTP files
057     *
058     * @since 2.2
059     */
060    public class HttpDownloader extends UriReader.SchemeProcessor implements BatchComponent, ServerComponent {
061      public static final int TIMEOUT_MILLISECONDS = 20 * 1000;
062    
063      private final BaseHttpDownloader downloader;
064      private final Integer readTimeout;
065    
066      public HttpDownloader(Server server, Settings settings) {
067        this(server, settings, null);
068      }
069    
070      public HttpDownloader(Server server, Settings settings, @Nullable Integer readTimeout) {
071        this.readTimeout = readTimeout;
072        downloader = new BaseHttpDownloader(settings.getProperties(), server.getVersion());
073      }
074    
075      public HttpDownloader(Settings settings) {
076        this(settings, null);
077      }
078    
079      public HttpDownloader(Settings settings, @Nullable Integer readTimeout) {
080        this.readTimeout = readTimeout;
081        downloader = new BaseHttpDownloader(settings.getProperties(), null);
082      }
083    
084      @Override
085      String description(URI uri) {
086        return String.format("%s (%s)", uri.toString(), getProxySynthesis(uri));
087      }
088    
089      @Override
090      String[] getSupportedSchemes() {
091        return new String[] {"http", "https"};
092      }
093    
094      @Override
095      byte[] readBytes(URI uri) {
096        return download(uri);
097      }
098    
099      @Override
100      String readString(URI uri, Charset charset) {
101        try {
102          return CharStreams.toString(CharStreams.newReaderSupplier(downloader.newInputSupplier(uri, this.readTimeout), charset));
103        } catch (IOException e) {
104          throw failToDownload(uri, e);
105        }
106      }
107    
108      public String downloadPlainText(URI uri, String encoding) {
109        return readString(uri, Charset.forName(encoding));
110      }
111    
112      public byte[] download(URI uri) {
113        try {
114          return ByteStreams.toByteArray(downloader.newInputSupplier(uri, this.readTimeout));
115        } catch (IOException e) {
116          throw failToDownload(uri, e);
117        }
118      }
119    
120      public String getProxySynthesis(URI uri) {
121        return downloader.getProxySynthesis(uri);
122      }
123    
124      public InputStream openStream(URI uri) {
125        try {
126          return downloader.newInputSupplier(uri, this.readTimeout).getInput();
127        } catch (IOException e) {
128          throw failToDownload(uri, e);
129        }
130      }
131    
132      public void download(URI uri, File toFile) {
133        try {
134          Files.copy(downloader.newInputSupplier(uri, this.readTimeout), toFile);
135        } catch (IOException e) {
136          FileUtils.deleteQuietly(toFile);
137          throw failToDownload(uri, e);
138        }
139      }
140    
141      private SonarException failToDownload(URI uri, IOException e) {
142        throw new SonarException(String.format("Fail to download: %s (%s)", uri, getProxySynthesis(uri)), e);
143      }
144    
145      public static class BaseHttpDownloader {
146        private static final List<String> PROXY_SETTINGS = ImmutableList.of(
147            "http.proxyHost", "http.proxyPort", "http.nonProxyHosts",
148            "http.auth.ntlm.domain", "socksProxyHost", "socksProxyPort");
149    
150        private String userAgent;
151    
152        public BaseHttpDownloader(Map<String, String> settings, String userAgent) {
153          initProxy(settings);
154          initUserAgent(userAgent);
155        }
156    
157        private void initProxy(Map<String, String> settings) {
158          propagateProxySystemProperties(settings);
159          if (requiresProxyAuthentication(settings)) {
160            registerProxyCredentials(settings);
161          }
162        }
163    
164        private void initUserAgent(String sonarVersion) {
165          userAgent = (sonarVersion == null ? "Sonar" : String.format("Sonar %s", sonarVersion));
166          System.setProperty("http.agent", userAgent);
167        }
168    
169        private String getProxySynthesis(URI uri) {
170          return getProxySynthesis(uri, ProxySelector.getDefault());
171        }
172    
173        @VisibleForTesting
174        static String getProxySynthesis(URI uri, ProxySelector proxySelector) {
175          List<Proxy> proxies = proxySelector.select(uri);
176          if (proxies.size() == 1 && proxies.get(0).type().equals(Proxy.Type.DIRECT)) {
177            return "no proxy";
178          }
179    
180          List<String> descriptions = Lists.newArrayList();
181          for (Proxy proxy : proxies) {
182            if (proxy.type() != Proxy.Type.DIRECT) {
183              descriptions.add("proxy: " + proxy.address());
184            }
185          }
186    
187          return Joiner.on(", ").join(descriptions);
188        }
189    
190        private void registerProxyCredentials(Map<String, String> settings) {
191          Authenticator.setDefault(new ProxyAuthenticator(
192              settings.get("http.proxyUser"),
193              settings.get("http.proxyPassword")));
194        }
195    
196        private boolean requiresProxyAuthentication(Map<String, String> settings) {
197          return settings.containsKey("http.proxyUser");
198        }
199    
200        private void propagateProxySystemProperties(Map<String, String> settings) {
201          for (String key : PROXY_SETTINGS) {
202            if (settings.containsKey(key)) {
203              System.setProperty(key, settings.get(key));
204            }
205          }
206        }
207    
208        public InputSupplier<InputStream> newInputSupplier(URI uri) {
209          return new HttpInputSupplier(uri, userAgent, null, null, TIMEOUT_MILLISECONDS);
210        }
211    
212        public InputSupplier<InputStream> newInputSupplier(URI uri, @Nullable Integer readTimeoutMillis) {
213          if (readTimeoutMillis != null) {
214            return new HttpInputSupplier(uri, userAgent, null, null, readTimeoutMillis);
215          }
216          return new HttpInputSupplier(uri, userAgent, null, null, TIMEOUT_MILLISECONDS);
217        }
218    
219        public InputSupplier<InputStream> newInputSupplier(URI uri, String login, String password) {
220          return new HttpInputSupplier(uri, userAgent, login, password, TIMEOUT_MILLISECONDS);
221        }
222    
223        public InputSupplier<InputStream> newInputSupplier(URI uri, String login, String password, @Nullable Integer readTimeoutMillis) {
224          if (readTimeoutMillis != null) {
225            return new HttpInputSupplier(uri, userAgent, login, password, readTimeoutMillis);
226          }
227          return new HttpInputSupplier(uri, userAgent, login, password, TIMEOUT_MILLISECONDS);
228        }
229    
230        private static class HttpInputSupplier implements InputSupplier<InputStream> {
231          private final String login;
232          private final String password;
233          private final URI uri;
234          private final String userAgent;
235          private final int readTimeoutMillis;
236    
237          HttpInputSupplier(URI uri, String userAgent, String login, String password, int readTimeoutMillis) {
238            this.uri = uri;
239            this.userAgent = userAgent;
240            this.login = login;
241            this.password = password;
242            this.readTimeoutMillis = readTimeoutMillis;
243          }
244    
245          public InputStream getInput() throws IOException {
246            LoggerFactory.getLogger(getClass()).debug("Download: " + uri + " (" + getProxySynthesis(uri, ProxySelector.getDefault()) + ")");
247    
248            HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection();
249            if (!Strings.isNullOrEmpty(login)) {
250              String encoded = new String(Base64.encodeBase64((login + ":" + password).getBytes()));
251              connection.setRequestProperty("Authorization", "Basic " + encoded);
252            }
253            connection.setConnectTimeout(TIMEOUT_MILLISECONDS);
254            connection.setReadTimeout(readTimeoutMillis);
255            connection.setUseCaches(true);
256            connection.setInstanceFollowRedirects(true);
257            connection.setRequestProperty("User-Agent", userAgent);
258    
259            int responseCode = connection.getResponseCode();
260            if (responseCode >= 400) {
261              InputStream errorResponse = null;
262              try {
263                errorResponse = connection.getErrorStream();
264                if (errorResponse != null) {
265                  String errorResponseContent = IOUtils.toString(errorResponse);
266                  throw new HttpException(uri, responseCode, errorResponseContent);
267                }
268                else {
269                  throw new HttpException(uri, responseCode);
270                }
271              } finally {
272                IOUtils.closeQuietly(errorResponse);
273              }
274            }
275    
276            return connection.getInputStream();
277          }
278        }
279    
280        private static class ProxyAuthenticator extends Authenticator {
281          private final PasswordAuthentication auth;
282    
283          ProxyAuthenticator(String user, String password) {
284            auth = new PasswordAuthentication(user, password == null ? new char[0] : password.toCharArray());
285          }
286    
287          @Override
288          protected PasswordAuthentication getPasswordAuthentication() {
289            return auth;
290          }
291        }
292      }
293    
294      public static class HttpException extends RuntimeException {
295        private final URI uri;
296        private final int responseCode;
297        private final String responseContent;
298    
299        public HttpException(URI uri, int responseContent) {
300          this(uri, responseContent, null);
301        }
302    
303        public HttpException(URI uri, int responseCode, String responseContent) {
304          super("Fail to download [" + uri + "]. Response code: " + responseCode);
305          this.uri = uri;
306          this.responseCode = responseCode;
307          this.responseContent = responseContent;
308        }
309    
310        public int getResponseCode() {
311          return responseCode;
312        }
313    
314        public URI getUri() {
315          return uri;
316        }
317    
318        public String getResponseContent() {
319          return responseContent;
320        }
321      }
322    }