View Javadoc
1   /*
2    * Copyright (C) 2010, Google Inc.
3    * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
4    *
5    * This program and the accompanying materials are made available under the
6    * terms of the Eclipse Distribution License v. 1.0 which is available at
7    * https://www.eclipse.org/org/documents/edl-v10.php.
8    *
9    * SPDX-License-Identifier: BSD-3-Clause
10   */
11  
12  package org.eclipse.jgit.util;
13  
14  import static java.nio.charset.StandardCharsets.UTF_8;
15  
16  import java.io.IOException;
17  import java.io.UnsupportedEncodingException;
18  import java.net.ConnectException;
19  import java.net.Proxy;
20  import java.net.ProxySelector;
21  import java.net.URI;
22  import java.net.URISyntaxException;
23  import java.net.URL;
24  import java.net.URLEncoder;
25  import java.security.KeyManagementException;
26  import java.security.NoSuchAlgorithmException;
27  import java.security.cert.X509Certificate;
28  import java.text.MessageFormat;
29  import java.util.Arrays;
30  import java.util.Collections;
31  import java.util.LinkedHashSet;
32  import java.util.Set;
33  
34  import javax.net.ssl.HostnameVerifier;
35  import javax.net.ssl.SSLSession;
36  import javax.net.ssl.SSLSocket;
37  import javax.net.ssl.TrustManager;
38  import javax.net.ssl.X509TrustManager;
39  
40  import org.eclipse.jgit.internal.JGitText;
41  import org.eclipse.jgit.transport.http.HttpConnection;
42  import org.slf4j.Logger;
43  import org.slf4j.LoggerFactory;
44  
45  /**
46   * Extra utilities to support usage of HTTP.
47   */
48  public class HttpSupport {
49  	private final static Logger LOG = LoggerFactory
50  			.getLogger(HttpSupport.class);
51  
52  	/** The {@code GET} HTTP method. */
53  	public static final String METHOD_GET = "GET"; //$NON-NLS-1$
54  
55  	/** The {@code HEAD} HTTP method.
56  	 * @since 4.3 */
57  	public static final String METHOD_HEAD = "HEAD"; //$NON-NLS-1$
58  
59  	/** The {@code POST} HTTP method.
60  	 * @since 4.3 */
61  	public static final String METHOD_PUT = "PUT"; //$NON-NLS-1$
62  
63  	/** The {@code POST} HTTP method. */
64  	public static final String METHOD_POST = "POST"; //$NON-NLS-1$
65  
66  	/** The {@code Cache-Control} header. */
67  	public static final String HDR_CACHE_CONTROL = "Cache-Control"; //$NON-NLS-1$
68  
69  	/** The {@code Pragma} header. */
70  	public static final String HDR_PRAGMA = "Pragma"; //$NON-NLS-1$
71  
72  	/** The {@code User-Agent} header. */
73  	public static final String HDR_USER_AGENT = "User-Agent"; //$NON-NLS-1$
74  
75  	/**
76  	 * The {@code Server} header.
77  	 * @since 4.0
78  	 */
79  	public static final String HDR_SERVER = "Server"; //$NON-NLS-1$
80  
81  	/** The {@code Date} header. */
82  	public static final String HDR_DATE = "Date"; //$NON-NLS-1$
83  
84  	/** The {@code Expires} header. */
85  	public static final String HDR_EXPIRES = "Expires"; //$NON-NLS-1$
86  
87  	/** The {@code ETag} header. */
88  	public static final String HDR_ETAG = "ETag"; //$NON-NLS-1$
89  
90  	/** The {@code If-None-Match} header. */
91  	public static final String HDR_IF_NONE_MATCH = "If-None-Match"; //$NON-NLS-1$
92  
93  	/** The {@code Last-Modified} header. */
94  	public static final String HDR_LAST_MODIFIED = "Last-Modified"; //$NON-NLS-1$
95  
96  	/** The {@code If-Modified-Since} header. */
97  	public static final String HDR_IF_MODIFIED_SINCE = "If-Modified-Since"; //$NON-NLS-1$
98  
99  	/** The {@code Accept} header. */
100 	public static final String HDR_ACCEPT = "Accept"; //$NON-NLS-1$
101 
102 	/** The {@code Content-Type} header. */
103 	public static final String HDR_CONTENT_TYPE = "Content-Type"; //$NON-NLS-1$
104 
105 	/** The {@code Content-Length} header. */
106 	public static final String HDR_CONTENT_LENGTH = "Content-Length"; //$NON-NLS-1$
107 
108 	/** The {@code Content-Encoding} header. */
109 	public static final String HDR_CONTENT_ENCODING = "Content-Encoding"; //$NON-NLS-1$
110 
111 	/** The {@code Content-Range} header. */
112 	public static final String HDR_CONTENT_RANGE = "Content-Range"; //$NON-NLS-1$
113 
114 	/** The {@code Accept-Ranges} header. */
115 	public static final String HDR_ACCEPT_RANGES = "Accept-Ranges"; //$NON-NLS-1$
116 
117 	/** The {@code If-Range} header. */
118 	public static final String HDR_IF_RANGE = "If-Range"; //$NON-NLS-1$
119 
120 	/** The {@code Range} header. */
121 	public static final String HDR_RANGE = "Range"; //$NON-NLS-1$
122 
123 	/** The {@code Accept-Encoding} header. */
124 	public static final String HDR_ACCEPT_ENCODING = "Accept-Encoding"; //$NON-NLS-1$
125 
126 	/**
127 	 * The {@code Location} header.
128 	 * @since 4.7
129 	 */
130 	public static final String HDR_LOCATION = "Location"; //$NON-NLS-1$
131 
132 	/** The {@code gzip} encoding value for {@link #HDR_ACCEPT_ENCODING}. */
133 	public static final String ENCODING_GZIP = "gzip"; //$NON-NLS-1$
134 
135 	/**
136 	 * The {@code x-gzip} encoding value for {@link #HDR_ACCEPT_ENCODING}.
137 	 * @since 4.6
138 	 */
139 	public static final String ENCODING_X_GZIP = "x-gzip"; //$NON-NLS-1$
140 
141 	/** The standard {@code text/plain} MIME type. */
142 	public static final String TEXT_PLAIN = "text/plain"; //$NON-NLS-1$
143 
144 	/** The {@code Authorization} header. */
145 	public static final String HDR_AUTHORIZATION = "Authorization"; //$NON-NLS-1$
146 
147 	/** The {@code WWW-Authenticate} header. */
148 	public static final String HDR_WWW_AUTHENTICATE = "WWW-Authenticate"; //$NON-NLS-1$
149 
150 	/**
151 	 * The {@code Cookie} header.
152 	 *
153 	 * @since 5.4
154 	 */
155 	public static final String HDR_COOKIE = "Cookie"; //$NON-NLS-1$
156 
157 	/**
158 	 * The {@code Set-Cookie} header.
159 	 *
160 	 * @since 5.4
161 	 */
162 	public static final String HDR_SET_COOKIE = "Set-Cookie"; //$NON-NLS-1$
163 
164 	/**
165 	 * The {@code Set-Cookie2} header.
166 	 *
167 	 * @since 5.4
168 	 */
169 	public static final String HDR_SET_COOKIE2 = "Set-Cookie2"; //$NON-NLS-1$
170 
171 	private static Set<String> configuredHttpsProtocols;
172 
173 	/**
174 	 * URL encode a value string into an output buffer.
175 	 *
176 	 * @param urlstr
177 	 *            the output buffer.
178 	 * @param key
179 	 *            value which must be encoded to protected special characters.
180 	 */
181 	public static void encode(StringBuilder urlstr, String key) {
182 		if (key == null || key.length() == 0)
183 			return;
184 		try {
185 			urlstr.append(URLEncoder.encode(key, UTF_8.name()));
186 		} catch (UnsupportedEncodingException e) {
187 			throw new RuntimeException(JGitText.get().couldNotURLEncodeToUTF8, e);
188 		}
189 	}
190 
191 	/**
192 	 * Get the HTTP response code from the request.
193 	 * <p>
194 	 * Roughly the same as <code>c.getResponseCode()</code> but the
195 	 * ConnectException is translated to be more understandable.
196 	 *
197 	 * @param c
198 	 *            connection the code should be obtained from.
199 	 * @return r HTTP status code, usually 200 to indicate success. See
200 	 *         {@link org.eclipse.jgit.transport.http.HttpConnection} for other
201 	 *         defined constants.
202 	 * @throws java.io.IOException
203 	 *             communications error prevented obtaining the response code.
204 	 * @since 3.3
205 	 */
206 	public static int response(HttpConnection c) throws IOException {
207 		try {
208 			return c.getResponseCode();
209 		} catch (ConnectException ce) {
210 			final URL url = c.getURL();
211 			final String host = (url == null) ? "<null>" : url.getHost(); //$NON-NLS-1$
212 			// The standard J2SE error message is not very useful.
213 			//
214 			if ("Connection timed out: connect".equals(ce.getMessage())) //$NON-NLS-1$
215 				throw new ConnectException(MessageFormat.format(JGitText.get().connectionTimeOut, host));
216 			throw new ConnectException(ce.getMessage() + " " + host); //$NON-NLS-1$
217 		}
218 	}
219 
220 	/**
221 	 * Get the HTTP response code from the request.
222 	 * <p>
223 	 * Roughly the same as <code>c.getResponseCode()</code> but the
224 	 * ConnectException is translated to be more understandable.
225 	 *
226 	 * @param c
227 	 *            connection the code should be obtained from.
228 	 * @return r HTTP status code, usually 200 to indicate success. See
229 	 *         {@link org.eclipse.jgit.transport.http.HttpConnection} for other
230 	 *         defined constants.
231 	 * @throws java.io.IOException
232 	 *             communications error prevented obtaining the response code.
233 	 */
234 	public static int response(java.net.HttpURLConnection c)
235 			throws IOException {
236 		try {
237 			return c.getResponseCode();
238 		} catch (ConnectException ce) {
239 			final URL url = c.getURL();
240 			final String host = (url == null) ? "<null>" : url.getHost(); //$NON-NLS-1$
241 			// The standard J2SE error message is not very useful.
242 			//
243 			if ("Connection timed out: connect".equals(ce.getMessage())) //$NON-NLS-1$
244 				throw new ConnectException(MessageFormat.format(
245 						JGitText.get().connectionTimeOut, host));
246 			throw new ConnectException(ce.getMessage() + " " + host); //$NON-NLS-1$
247 		}
248 	}
249 
250 	/**
251 	 * Extract a HTTP header from the response.
252 	 *
253 	 * @param c
254 	 *            connection the header should be obtained from.
255 	 * @param headerName
256 	 *            the header name
257 	 * @return the header value
258 	 * @throws java.io.IOException
259 	 *             communications error prevented obtaining the header.
260 	 * @since 4.7
261 	 */
262 	public static String responseHeader(final HttpConnection c,
263 			final String headerName) throws IOException {
264 		return c.getHeaderField(headerName);
265 	}
266 
267 	/**
268 	 * Determine the proxy server (if any) needed to obtain a URL.
269 	 *
270 	 * @param proxySelector
271 	 *            proxy support for the caller.
272 	 * @param u
273 	 *            location of the server caller wants to talk to.
274 	 * @return proxy to communicate with the supplied URL.
275 	 * @throws java.net.ConnectException
276 	 *             the proxy could not be computed as the supplied URL could not
277 	 *             be read. This failure should never occur.
278 	 */
279 	public static Proxy proxyFor(ProxySelector proxySelector, URL u)
280 			throws ConnectException {
281 		try {
282 			URI uri = new URI(u.getProtocol(), null, u.getHost(), u.getPort(),
283 					null, null, null);
284 			return proxySelector.select(uri).get(0);
285 		} catch (URISyntaxException e) {
286 			final ConnectException err;
287 			err = new ConnectException(MessageFormat.format(JGitText.get().cannotDetermineProxyFor, u));
288 			err.initCause(e);
289 			throw err;
290 		}
291 	}
292 
293 	/**
294 	 * Disable SSL and hostname verification for given HTTP connection
295 	 *
296 	 * @param conn
297 	 *            a {@link org.eclipse.jgit.transport.http.HttpConnection}
298 	 *            object.
299 	 * @throws java.io.IOException
300 	 * @since 4.3
301 	 */
302 	public static void disableSslVerify(HttpConnection conn)
303 			throws IOException {
304 		final TrustManager[] trustAllCerts = new TrustManager[] {
305 				new DummyX509TrustManager() };
306 		try {
307 			conn.configure(null, trustAllCerts, null);
308 			conn.setHostnameVerifier(new DummyHostnameVerifier());
309 		} catch (KeyManagementException | NoSuchAlgorithmException e) {
310 			throw new IOException(e.getMessage());
311 		}
312 	}
313 
314 	private static class DummyX509TrustManager implements X509TrustManager {
315 		@Override
316 		public X509Certificate[] getAcceptedIssuers() {
317 			return null;
318 		}
319 
320 		@Override
321 		public void checkClientTrusted(X509Certificate[] certs,
322 				String authType) {
323 			// no check
324 		}
325 
326 		@Override
327 		public void checkServerTrusted(X509Certificate[] certs,
328 				String authType) {
329 			// no check
330 		}
331 	}
332 
333 	private static class DummyHostnameVerifier implements HostnameVerifier {
334 		@Override
335 		public boolean verify(String hostname, SSLSession session) {
336 			// always accept
337 			return true;
338 		}
339 	}
340 
341 	/**
342 	 * Enables all supported TLS protocol versions on the socket given. If
343 	 * system property "https.protocols" is set, only protocols specified there
344 	 * are enabled.
345 	 * <p>
346 	 * This is primarily a mechanism to deal with using TLS on IBM JDK. IBM JDK
347 	 * returns sockets that support all TLS protocol versions but have only the
348 	 * one specified in the context enabled. Oracle or OpenJDK return sockets
349 	 * that have all available protocols enabled already, up to the one
350 	 * specified.
351 	 * <p>
352 	 * <table>
353 	 * <tr>
354 	 * <td>SSLContext.getInstance()</td>
355 	 * <td>OpenJDK</td>
356 	 * <td>IDM JDK</td>
357 	 * </tr>
358 	 * <tr>
359 	 * <td>"TLS"</td>
360 	 * <td>Supported: TLSv1, TLSV1.1, TLSv1.2 (+ TLSv1.3)<br />
361 	 * Enabled: TLSv1, TLSV1.1, TLSv1.2 (+ TLSv1.3)</td>
362 	 * <td>Supported: TLSv1, TLSV1.1, TLSv1.2<br />
363 	 * Enabled: TLSv1</td>
364 	 * </tr>
365 	 * <tr>
366 	 * <td>"TLSv1.2"</td>
367 	 * <td>Supported: TLSv1, TLSV1.1, TLSv1.2<br />
368 	 * Enabled: TLSv1, TLSV1.1, TLSv1.2</td>
369 	 * <td>Supported: TLSv1, TLSV1.1, TLSv1.2<br />
370 	 * Enabled: TLSv1.2</td>
371 	 * </tr>
372 	 * </table>
373 	 *
374 	 * @param socket
375 	 *            to configure
376 	 * @see <a href=
377 	 *      "https://www.ibm.com/support/knowledgecenter/en/SSYKE2_8.0.0/com.ibm.java.security.component.80.doc/security-component/jsse2Docs/matchsslcontext_tls.html">Behavior
378 	 *      of SSLContext.getInstance("TLS") on IBM JDK</a>
379 	 * @see <a href=
380 	 *      "https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html#InstallationAndCustomization">Customizing
381 	 *      JSSE about https.protocols</a>
382 	 * @since 5.7
383 	 */
384 	public static void configureTLS(SSLSocket socket) {
385 		// 1. Enable all available TLS protocol versions
386 		Set<String> enabled = new LinkedHashSet<>(
387 				Arrays.asList(socket.getEnabledProtocols()));
388 		for (String s : socket.getSupportedProtocols()) {
389 			if (s.startsWith("TLS")) { //$NON-NLS-1$
390 				enabled.add(s);
391 			}
392 		}
393 		// 2. Respect the https.protocols system property
394 		Set<String> configured = getConfiguredProtocols();
395 		if (!configured.isEmpty()) {
396 			enabled.retainAll(configured);
397 		}
398 		if (!enabled.isEmpty()) {
399 			socket.setEnabledProtocols(enabled.toArray(new String[0]));
400 		}
401 	}
402 
403 	private static Set<String> getConfiguredProtocols() {
404 		Set<String> result = configuredHttpsProtocols;
405 		if (result == null) {
406 			String configured = getProperty("https.protocols"); //$NON-NLS-1$
407 			if (StringUtils.isEmptyOrNull(configured)) {
408 				result = Collections.emptySet();
409 			} else {
410 				result = new LinkedHashSet<>(
411 						Arrays.asList(configured.split("\\s*,\\s*"))); //$NON-NLS-1$
412 			}
413 			configuredHttpsProtocols = result;
414 		}
415 		return result;
416 	}
417 
418 	private static String getProperty(String property) {
419 		try {
420 			return SystemReader.getInstance().getProperty(property);
421 		} catch (SecurityException e) {
422 			LOG.warn(JGitText.get().failedReadHttpsProtocols, e);
423 			return null;
424 		}
425 	}
426 
427 	private HttpSupport() {
428 		// Utility class only.
429 	}
430 }