001    /*
002     * Sonar, open source software quality management tool.
003     * Copyright (C) 2008-2012 SonarSource
004     * mailto:contact AT sonarsource DOT com
005     *
006     * Sonar 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     * Sonar 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
017     * License along with Sonar; if not, write to the Free Software
018     * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
019     */
020    package org.sonar.api.utils;
021    
022    import com.google.common.annotations.VisibleForTesting;
023    
024    import com.google.common.base.Joiner;
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.io.FileUtils;
032    import org.slf4j.LoggerFactory;
033    import org.sonar.api.BatchComponent;
034    import org.sonar.api.ServerComponent;
035    import org.sonar.api.config.Settings;
036    import org.sonar.api.platform.Server;
037    
038    import java.io.File;
039    import java.io.IOException;
040    import java.io.InputStream;
041    import java.net.Authenticator;
042    import java.net.HttpURLConnection;
043    import java.net.PasswordAuthentication;
044    import java.net.Proxy;
045    import java.net.ProxySelector;
046    import java.net.URI;
047    import java.nio.charset.Charset;
048    import java.util.List;
049    
050    /**
051     * This component downloads HTTP files
052     *
053     * @since 2.2
054     */
055    public class HttpDownloader extends UriReader.SchemeProcessor implements BatchComponent, ServerComponent {
056    
057      public static final int TIMEOUT_MILLISECONDS = 20 * 1000;
058      private static final List<String> PROXY_SETTINGS = ImmutableList.of(
059          "http.proxyHost", "http.proxyPort", "http.nonProxyHosts",
060          "http.auth.ntlm.domain", "socksProxyHost", "socksProxyPort");
061    
062      private String userAgent;
063    
064      public HttpDownloader(Server server, Settings settings) {
065        this(settings, server.getVersion());
066      }
067    
068      public HttpDownloader(Settings settings) {
069        this(settings, null);
070      }
071    
072      private HttpDownloader(Settings settings, String userAgent) {
073        initProxy(settings);
074        initUserAgent(userAgent);
075      }
076    
077      private void initProxy(Settings settings) {
078        propagateProxySystemProperties(settings);
079        if (requiresProxyAuthentication(settings)) {
080          registerProxyCredentials(settings);
081        }
082      }
083    
084      private void initUserAgent(String sonarVersion) {
085        userAgent = (sonarVersion == null ? "Sonar" : String.format("Sonar %s", sonarVersion));
086        System.setProperty("http.agent", userAgent);
087      }
088    
089      public String getProxySynthesis(URI uri) {
090        return getProxySynthesis(uri, ProxySelector.getDefault());
091      }
092    
093      @VisibleForTesting
094      static String getProxySynthesis(URI uri, ProxySelector proxySelector) {
095        List<Proxy> proxies = proxySelector.select(uri);
096        if (proxies.size() == 1 && proxies.get(0).type().equals(Proxy.Type.DIRECT)) {
097          return "no proxy";
098        }
099    
100        List<String> descriptions = Lists.newArrayList();
101        for (Proxy proxy : proxies) {
102          if (proxy.type() != Proxy.Type.DIRECT) {
103            descriptions.add("proxy: " + proxy.address());
104          }
105        }
106    
107        return Joiner.on(", ").join(descriptions);
108      }
109    
110      private void registerProxyCredentials(Settings settings) {
111        Authenticator.setDefault(new ProxyAuthenticator(settings.getString("http.proxyUser"), settings
112            .getString("http.proxyPassword")));
113      }
114    
115      private boolean requiresProxyAuthentication(Settings settings) {
116        return settings.getString("http.proxyUser") != null;
117      }
118    
119      private void propagateProxySystemProperties(Settings settings) {
120        for (String key : PROXY_SETTINGS) {
121          if (settings.getString(key) != null) {
122            System.setProperty(key, settings.getString(key));
123          }
124        }
125      }
126    
127      @Override
128      String description(URI uri) {
129        return String.format("%s (%s)", uri.toString(), getProxySynthesis(uri));
130      }
131    
132      @Override
133      String[] getSupportedSchemes() {
134        return new String[] {"http", "https"};
135      }
136    
137      @Override
138      byte[] readBytes(URI uri) {
139        try {
140          return ByteStreams.toByteArray(new HttpInputSupplier(uri));
141        } catch (IOException e) {
142          throw failToDownload(uri, e);
143        }
144      }
145    
146      @Override
147      String readString(URI uri, Charset charset) {
148        try {
149          return CharStreams.toString(CharStreams.newReaderSupplier(new HttpInputSupplier(uri), charset));
150        } catch (IOException e) {
151          throw failToDownload(uri, e);
152        }
153      }
154    
155      public byte[] download(URI uri) {
156        return readBytes(uri);
157      }
158    
159      public String downloadPlainText(URI uri, String encoding) {
160        return readString(uri, Charset.forName(encoding));
161      }
162    
163      public InputStream openStream(URI uri) {
164        try {
165          return new HttpInputSupplier(uri).getInput();
166        } catch (Exception e) {
167          throw new SonarException("Fail to download the file: " + uri + " (" + getProxySynthesis(uri) + ")", e);
168        }
169      }
170    
171      public void download(URI uri, File toFile) {
172        try {
173          Files.copy(new HttpInputSupplier(uri), toFile);
174        } catch (IOException e) {
175          FileUtils.deleteQuietly(toFile);
176          throw failToDownload(uri, e);
177        }
178      }
179    
180      private SonarException failToDownload(URI uri, IOException e) {
181        return new SonarException(String.format("Fail to download the file: %s (%s)", uri, getProxySynthesis(uri)), e);
182      }
183    
184      class HttpInputSupplier implements InputSupplier<InputStream> {
185        private final URI uri;
186    
187        HttpInputSupplier(URI uri) {
188          this.uri = uri;
189        }
190    
191        public InputStream getInput() throws IOException {
192          LoggerFactory.getLogger(getClass()).debug("Download: " + uri + " (" + getProxySynthesis(uri) + ")");
193    
194          HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection();
195          connection.setConnectTimeout(TIMEOUT_MILLISECONDS);
196          connection.setReadTimeout(TIMEOUT_MILLISECONDS);
197          connection.setUseCaches(true);
198          connection.setInstanceFollowRedirects(true);
199          connection.setRequestProperty("User-Agent", userAgent);
200          return connection.getInputStream();
201        }
202      }
203    }
204    
205    class ProxyAuthenticator extends Authenticator {
206      private PasswordAuthentication auth;
207    
208      ProxyAuthenticator(String user, String password) {
209        auth = new PasswordAuthentication(user, password == null ? new char[0] : password.toCharArray());
210      }
211    
212      @Override
213      protected PasswordAuthentication getPasswordAuthentication() {
214        return auth;
215      }
216    }