001/* 002 * SonarQube 003 * Copyright (C) 2009-2016 SonarSource SA 004 * mailto:contact AT sonarsource DOT com 005 * 006 * This program 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 * This program 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 */ 020package org.sonarqube.ws.client; 021 022import com.google.common.annotations.VisibleForTesting; 023import com.squareup.okhttp.Call; 024import com.squareup.okhttp.ConnectionSpec; 025import com.squareup.okhttp.Credentials; 026import com.squareup.okhttp.Headers; 027import com.squareup.okhttp.HttpUrl; 028import com.squareup.okhttp.MediaType; 029import com.squareup.okhttp.MultipartBuilder; 030import com.squareup.okhttp.OkHttpClient; 031import com.squareup.okhttp.Request; 032import com.squareup.okhttp.RequestBody; 033import com.squareup.okhttp.Response; 034import java.io.IOException; 035import java.net.Proxy; 036import java.util.Map; 037import java.util.concurrent.TimeUnit; 038import javax.annotation.CheckForNull; 039import javax.annotation.Nullable; 040import javax.net.ssl.SSLSocketFactory; 041 042import static com.google.common.base.Preconditions.checkArgument; 043import static com.google.common.base.Strings.isNullOrEmpty; 044import static com.google.common.base.Strings.nullToEmpty; 045import static java.lang.String.format; 046import static java.util.Arrays.asList; 047 048/** 049 * Connect to any SonarQube server available through HTTP or HTTPS. 050 * <p>TLS 1.0, 1.1 and 1.2 are supported on both Java 7 and 8. SSLv3 is not supported.</p> 051 * <p>The JVM system proxies are used.</p> 052 */ 053public class HttpConnector implements WsConnector { 054 055 public static final int DEFAULT_CONNECT_TIMEOUT_MILLISECONDS = 30_000; 056 public static final int DEFAULT_READ_TIMEOUT_MILLISECONDS = 60_000; 057 058 /** 059 * Base URL with trailing slash, for instance "https://localhost/sonarqube/". 060 * It is required for further usage of {@link HttpUrl#resolve(String)}. 061 */ 062 private final HttpUrl baseUrl; 063 private final String userAgent; 064 private final String credentials; 065 private final String proxyCredentials; 066 private final OkHttpClient okHttpClient = new OkHttpClient(); 067 068 private HttpConnector(Builder builder, JavaVersion javaVersion) { 069 this.baseUrl = HttpUrl.parse(builder.url.endsWith("/") ? builder.url : format("%s/", builder.url)); 070 checkArgument(this.baseUrl!=null, "Malformed URL: '%s'", builder.url); 071 this.userAgent = builder.userAgent; 072 073 if (isNullOrEmpty(builder.login)) { 074 // no login nor access token 075 this.credentials = null; 076 } else { 077 // password is null when login represents an access token. In this case 078 // the Basic credentials consider an empty password. 079 this.credentials = Credentials.basic(builder.login, nullToEmpty(builder.password)); 080 } 081 082 if (builder.proxy != null) { 083 this.okHttpClient.setProxy(builder.proxy); 084 } 085 // proxy credentials can be used on system-wide proxies, so even if builder.proxy is null 086 if (isNullOrEmpty(builder.proxyLogin)) { 087 this.proxyCredentials = null; 088 } else { 089 this.proxyCredentials = Credentials.basic(builder.proxyLogin, nullToEmpty(builder.proxyPassword)); 090 } 091 092 this.okHttpClient.setConnectTimeout(builder.connectTimeoutMs, TimeUnit.MILLISECONDS); 093 this.okHttpClient.setReadTimeout(builder.readTimeoutMs, TimeUnit.MILLISECONDS); 094 095 ConnectionSpec tls = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) 096 .allEnabledTlsVersions() 097 .allEnabledCipherSuites() 098 .supportsTlsExtensions(true) 099 .build(); 100 this.okHttpClient.setConnectionSpecs(asList(tls, ConnectionSpec.CLEARTEXT)); 101 this.okHttpClient.setSslSocketFactory(createSslSocketFactory(javaVersion)); 102 } 103 104 private static SSLSocketFactory createSslSocketFactory(JavaVersion javaVersion) { 105 try { 106 SSLSocketFactory sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); 107 return enableTls12InJava7(sslSocketFactory, javaVersion); 108 } catch (Exception e) { 109 throw new IllegalStateException("Fail to init TLS context", e); 110 } 111 } 112 113 private static SSLSocketFactory enableTls12InJava7(SSLSocketFactory sslSocketFactory, JavaVersion javaVersion) { 114 if (javaVersion.isJava7()) { 115 // OkHttp executes SSLContext.getInstance("TLS") by default (see 116 // https://github.com/square/okhttp/blob/c358656/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java#L616) 117 // As only TLS 1.0 is enabled by default in Java 7, the SSLContextFactory must be changed 118 // in order to support all versions from 1.0 to 1.2. 119 // Note that this is not overridden for Java 8 as TLS 1.2 is enabled by default. 120 // Keeping getInstance("TLS") allows to support potential future versions of TLS on Java 8. 121 return new Tls12Java7SocketFactory(sslSocketFactory); 122 } 123 return sslSocketFactory; 124 } 125 126 @Override 127 public String baseUrl() { 128 return baseUrl.url().toExternalForm(); 129 } 130 131 @CheckForNull 132 public String userAgent() { 133 return userAgent; 134 } 135 136 public OkHttpClient okHttpClient() { 137 return okHttpClient; 138 } 139 140 @Override 141 public WsResponse call(WsRequest httpRequest) { 142 if (httpRequest instanceof GetRequest) { 143 return get((GetRequest) httpRequest); 144 } 145 if (httpRequest instanceof PostRequest) { 146 return post((PostRequest) httpRequest); 147 } 148 throw new IllegalArgumentException(format("Unsupported implementation: %s", httpRequest.getClass())); 149 } 150 151 private WsResponse get(GetRequest getRequest) { 152 HttpUrl.Builder urlBuilder = prepareUrlBuilder(getRequest); 153 Request.Builder okRequestBuilder = prepareOkRequestBuilder(getRequest, urlBuilder).get(); 154 return doCall(okRequestBuilder.build()); 155 } 156 157 private WsResponse post(PostRequest postRequest) { 158 HttpUrl.Builder urlBuilder = prepareUrlBuilder(postRequest); 159 Request.Builder okRequestBuilder = prepareOkRequestBuilder(postRequest, urlBuilder); 160 161 Map<String, PostRequest.Part> parts = postRequest.getParts(); 162 if (parts.isEmpty()) { 163 okRequestBuilder.post(RequestBody.create(null, "")); 164 } else { 165 MultipartBuilder body = new MultipartBuilder().type(MultipartBuilder.FORM); 166 for (Map.Entry<String, PostRequest.Part> param : parts.entrySet()) { 167 PostRequest.Part part = param.getValue(); 168 body.addPart( 169 Headers.of("Content-Disposition", format("form-data; name=\"%s\"", param.getKey())), 170 RequestBody.create(MediaType.parse(part.getMediaType()), part.getFile())); 171 } 172 okRequestBuilder.post(body.build()); 173 } 174 175 return doCall(okRequestBuilder.build()); 176 } 177 178 private HttpUrl.Builder prepareUrlBuilder(WsRequest wsRequest) { 179 String path = wsRequest.getPath(); 180 HttpUrl.Builder urlBuilder = baseUrl 181 .resolve(path.startsWith("/") ? path.replaceAll("^(/)+", "") : path) 182 .newBuilder(); 183 for (Map.Entry<String, String> param : wsRequest.getParams().entrySet()) { 184 urlBuilder.addQueryParameter(param.getKey(), param.getValue()); 185 } 186 return urlBuilder; 187 } 188 189 private Request.Builder prepareOkRequestBuilder(WsRequest getRequest, HttpUrl.Builder urlBuilder) { 190 Request.Builder okHttpRequestBuilder = new Request.Builder() 191 .url(urlBuilder.build()) 192 .addHeader("Accept", getRequest.getMediaType()) 193 .addHeader("Accept-Charset", "UTF-8"); 194 if (credentials != null) { 195 okHttpRequestBuilder.header("Authorization", credentials); 196 } 197 if (proxyCredentials != null) { 198 okHttpRequestBuilder.header("Proxy-Authorization", proxyCredentials); 199 } 200 if (userAgent != null) { 201 okHttpRequestBuilder.addHeader("User-Agent", userAgent); 202 } 203 return okHttpRequestBuilder; 204 } 205 206 private OkHttpResponse doCall(Request okRequest) { 207 Call call = okHttpClient.newCall(okRequest); 208 try { 209 Response okResponse = call.execute(); 210 return new OkHttpResponse(okResponse); 211 } catch (IOException e) { 212 throw new IllegalStateException("Fail to request " + okRequest.urlString(), e); 213 } 214 } 215 216 /** 217 * @since 5.5 218 */ 219 public static Builder newBuilder() { 220 return new Builder(); 221 } 222 223 public static class Builder { 224 private String url; 225 private String userAgent; 226 private String login; 227 private String password; 228 private Proxy proxy; 229 private String proxyLogin; 230 private String proxyPassword; 231 private int connectTimeoutMs = DEFAULT_CONNECT_TIMEOUT_MILLISECONDS; 232 private int readTimeoutMs = DEFAULT_READ_TIMEOUT_MILLISECONDS; 233 234 /** 235 * Private since 5.5. 236 * @see HttpConnector#newBuilder() 237 */ 238 private Builder() { 239 } 240 241 /** 242 * Optional User Agent 243 */ 244 public Builder userAgent(@Nullable String userAgent) { 245 this.userAgent = userAgent; 246 return this; 247 } 248 249 /** 250 * Mandatory HTTP server URL, eg "http://localhost:9000" 251 */ 252 public Builder url(String url) { 253 this.url = url; 254 return this; 255 } 256 257 /** 258 * Optional login/password, for example "admin" 259 */ 260 public Builder credentials(@Nullable String login, @Nullable String password) { 261 this.login = login; 262 this.password = password; 263 return this; 264 } 265 266 /** 267 * Optional access token, for example {@code "ABCDE"}. Alternative to {@link #credentials(String, String)} 268 */ 269 public Builder token(@Nullable String token) { 270 this.login = token; 271 this.password = null; 272 return this; 273 } 274 275 /** 276 * Sets a specified timeout value, in milliseconds, to be used when opening HTTP connection. 277 * A timeout of zero is interpreted as an infinite timeout. Default value is {@link #DEFAULT_CONNECT_TIMEOUT_MILLISECONDS} 278 */ 279 public Builder connectTimeoutMilliseconds(int i) { 280 this.connectTimeoutMs = i; 281 return this; 282 } 283 284 /** 285 * Sets the read timeout to a specified timeout, in milliseconds. 286 * A timeout of zero is interpreted as an infinite timeout. Default value is {@link #DEFAULT_READ_TIMEOUT_MILLISECONDS} 287 */ 288 public Builder readTimeoutMilliseconds(int i) { 289 this.readTimeoutMs = i; 290 return this; 291 } 292 293 public Builder proxy(@Nullable Proxy proxy) { 294 this.proxy = proxy; 295 return this; 296 } 297 298 public Builder proxyCredentials(@Nullable String proxyLogin, @Nullable String proxyPassword) { 299 this.proxyLogin = proxyLogin; 300 this.proxyPassword = proxyPassword; 301 return this; 302 } 303 304 public HttpConnector build() { 305 return build(new JavaVersion()); 306 } 307 308 @VisibleForTesting 309 HttpConnector build(JavaVersion javaVersion) { 310 checkArgument(!isNullOrEmpty(url), "Server URL is not defined"); 311 return new HttpConnector(this, javaVersion); 312 } 313 } 314 315 static class JavaVersion { 316 boolean isJava7() { 317 return System.getProperty("java.version").startsWith("1.7."); 318 } 319 } 320}