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 */
020package org.sonar.api.utils;
021
022import com.google.common.annotations.VisibleForTesting;
023
024import com.google.common.base.Joiner;
025import com.google.common.collect.ImmutableList;
026import com.google.common.collect.Lists;
027import com.google.common.io.ByteStreams;
028import com.google.common.io.CharStreams;
029import com.google.common.io.Files;
030import com.google.common.io.InputSupplier;
031import org.apache.commons.io.FileUtils;
032import org.slf4j.LoggerFactory;
033import org.sonar.api.BatchComponent;
034import org.sonar.api.ServerComponent;
035import org.sonar.api.config.Settings;
036import org.sonar.api.platform.Server;
037
038import java.io.File;
039import java.io.IOException;
040import java.io.InputStream;
041import java.net.Authenticator;
042import java.net.HttpURLConnection;
043import java.net.PasswordAuthentication;
044import java.net.Proxy;
045import java.net.ProxySelector;
046import java.net.URI;
047import java.nio.charset.Charset;
048import java.util.List;
049
050/**
051 * This component downloads HTTP files
052 *
053 * @since 2.2
054 */
055public 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
205class 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}