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.text.MessageFormat;
28  import java.util.Arrays;
29  import java.util.Collections;
30  import java.util.LinkedHashSet;
31  import java.util.Set;
32  
33  import javax.net.ssl.SSLSocket;
34  import javax.net.ssl.TrustManager;
35  
36  import org.eclipse.jgit.internal.JGitText;
37  import org.eclipse.jgit.transport.http.HttpConnection;
38  import org.eclipse.jgit.transport.http.NoCheckX509TrustManager;
39  import org.slf4j.Logger;
40  import org.slf4j.LoggerFactory;
41  
42  /**
43   * Extra utilities to support usage of HTTP.
44   */
45  public class HttpSupport {
46  	private final static Logger LOG = LoggerFactory
47  			.getLogger(HttpSupport.class);
48  
49  	/** The {@code GET} HTTP method. */
50  	public static final String METHOD_GET = "GET"; //$NON-NLS-1$
51  
52  	/** The {@code HEAD} HTTP method.
53  	 * @since 4.3 */
54  	public static final String METHOD_HEAD = "HEAD"; //$NON-NLS-1$
55  
56  	/** The {@code POST} HTTP method.
57  	 * @since 4.3 */
58  	public static final String METHOD_PUT = "PUT"; //$NON-NLS-1$
59  
60  	/** The {@code POST} HTTP method. */
61  	public static final String METHOD_POST = "POST"; //$NON-NLS-1$
62  
63  	/** The {@code Cache-Control} header. */
64  	public static final String HDR_CACHE_CONTROL = "Cache-Control"; //$NON-NLS-1$
65  
66  	/** The {@code Pragma} header. */
67  	public static final String HDR_PRAGMA = "Pragma"; //$NON-NLS-1$
68  
69  	/** The {@code User-Agent} header. */
70  	public static final String HDR_USER_AGENT = "User-Agent"; //$NON-NLS-1$
71  
72  	/**
73  	 * The {@code Server} header.
74  	 * @since 4.0
75  	 */
76  	public static final String HDR_SERVER = "Server"; //$NON-NLS-1$
77  
78  	/** The {@code Date} header. */
79  	public static final String HDR_DATE = "Date"; //$NON-NLS-1$
80  
81  	/** The {@code Expires} header. */
82  	public static final String HDR_EXPIRES = "Expires"; //$NON-NLS-1$
83  
84  	/** The {@code ETag} header. */
85  	public static final String HDR_ETAG = "ETag"; //$NON-NLS-1$
86  
87  	/** The {@code If-None-Match} header. */
88  	public static final String HDR_IF_NONE_MATCH = "If-None-Match"; //$NON-NLS-1$
89  
90  	/** The {@code Last-Modified} header. */
91  	public static final String HDR_LAST_MODIFIED = "Last-Modified"; //$NON-NLS-1$
92  
93  	/** The {@code If-Modified-Since} header. */
94  	public static final String HDR_IF_MODIFIED_SINCE = "If-Modified-Since"; //$NON-NLS-1$
95  
96  	/** The {@code Accept} header. */
97  	public static final String HDR_ACCEPT = "Accept"; //$NON-NLS-1$
98  
99  	/** The {@code Content-Type} header. */
100 	public static final String HDR_CONTENT_TYPE = "Content-Type"; //$NON-NLS-1$
101 
102 	/** The {@code Content-Length} header. */
103 	public static final String HDR_CONTENT_LENGTH = "Content-Length"; //$NON-NLS-1$
104 
105 	/** The {@code Content-Encoding} header. */
106 	public static final String HDR_CONTENT_ENCODING = "Content-Encoding"; //$NON-NLS-1$
107 
108 	/** The {@code Content-Range} header. */
109 	public static final String HDR_CONTENT_RANGE = "Content-Range"; //$NON-NLS-1$
110 
111 	/** The {@code Accept-Ranges} header. */
112 	public static final String HDR_ACCEPT_RANGES = "Accept-Ranges"; //$NON-NLS-1$
113 
114 	/** The {@code If-Range} header. */
115 	public static final String HDR_IF_RANGE = "If-Range"; //$NON-NLS-1$
116 
117 	/** The {@code Range} header. */
118 	public static final String HDR_RANGE = "Range"; //$NON-NLS-1$
119 
120 	/** The {@code Accept-Encoding} header. */
121 	public static final String HDR_ACCEPT_ENCODING = "Accept-Encoding"; //$NON-NLS-1$
122 
123 	/**
124 	 * The {@code Location} header.
125 	 * @since 4.7
126 	 */
127 	public static final String HDR_LOCATION = "Location"; //$NON-NLS-1$
128 
129 	/** The {@code gzip} encoding value for {@link #HDR_ACCEPT_ENCODING}. */
130 	public static final String ENCODING_GZIP = "gzip"; //$NON-NLS-1$
131 
132 	/**
133 	 * The {@code x-gzip} encoding value for {@link #HDR_ACCEPT_ENCODING}.
134 	 * @since 4.6
135 	 */
136 	public static final String ENCODING_X_GZIP = "x-gzip"; //$NON-NLS-1$
137 
138 	/** The standard {@code text/plain} MIME type. */
139 	public static final String TEXT_PLAIN = "text/plain"; //$NON-NLS-1$
140 
141 	/** The {@code Authorization} header. */
142 	public static final String HDR_AUTHORIZATION = "Authorization"; //$NON-NLS-1$
143 
144 	/** The {@code WWW-Authenticate} header. */
145 	public static final String HDR_WWW_AUTHENTICATE = "WWW-Authenticate"; //$NON-NLS-1$
146 
147 	/**
148 	 * The {@code Cookie} header.
149 	 *
150 	 * @since 5.4
151 	 */
152 	public static final String HDR_COOKIE = "Cookie"; //$NON-NLS-1$
153 
154 	/**
155 	 * The {@code Set-Cookie} header.
156 	 *
157 	 * @since 5.4
158 	 */
159 	public static final String HDR_SET_COOKIE = "Set-Cookie"; //$NON-NLS-1$
160 
161 	/**
162 	 * The {@code Set-Cookie2} header.
163 	 *
164 	 * @since 5.4
165 	 */
166 	public static final String HDR_SET_COOKIE2 = "Set-Cookie2"; //$NON-NLS-1$
167 
168 	private static Set<String> configuredHttpsProtocols;
169 
170 	/**
171 	 * URL encode a value string into an output buffer.
172 	 *
173 	 * @param urlstr
174 	 *            the output buffer.
175 	 * @param key
176 	 *            value which must be encoded to protected special characters.
177 	 */
178 	public static void encode(StringBuilder urlstr, String key) {
179 		if (key == null || key.length() == 0)
180 			return;
181 		try {
182 			urlstr.append(URLEncoder.encode(key, UTF_8.name()));
183 		} catch (UnsupportedEncodingException e) {
184 			throw new RuntimeException(JGitText.get().couldNotURLEncodeToUTF8, e);
185 		}
186 	}
187 
188 	/**
189 	 * Translates the provided URL into application/x-www-form-urlencoded
190 	 * format.
191 	 *
192 	 * @param url
193 	 *            The URL to translate.
194 	 * @param keepPathSlash
195 	 *            Whether or not to keep "/" in the URL (i.e. don't translate
196 	 *            them to "%2F").
197 	 *
198 	 * @return The translated URL.
199 	 * @since 5.13.1
200 	 */
201 	public static String urlEncode(String url, boolean keepPathSlash) {
202 		String encoded;
203 		try {
204 			encoded = URLEncoder.encode(url, UTF_8.name());
205 		} catch (UnsupportedEncodingException e) {
206 			throw new RuntimeException(JGitText.get().couldNotURLEncodeToUTF8,
207 					e);
208 		}
209 		if (keepPathSlash) {
210 			encoded = encoded.replace("%2F", "/"); //$NON-NLS-1$ //$NON-NLS-2$
211 		}
212 		return encoded;
213 	}
214 
215 	/**
216 	 * Get the HTTP response code from the request.
217 	 * <p>
218 	 * Roughly the same as <code>c.getResponseCode()</code> but the
219 	 * ConnectException is translated to be more understandable.
220 	 *
221 	 * @param c
222 	 *            connection the code should be obtained from.
223 	 * @return r HTTP status code, usually 200 to indicate success. See
224 	 *         {@link org.eclipse.jgit.transport.http.HttpConnection} for other
225 	 *         defined constants.
226 	 * @throws java.io.IOException
227 	 *             communications error prevented obtaining the response code.
228 	 * @since 3.3
229 	 */
230 	public static int response(HttpConnection c) throws IOException {
231 		try {
232 			return c.getResponseCode();
233 		} catch (ConnectException ce) {
234 			final URL url = c.getURL();
235 			final String host = (url == null) ? "<null>" : url.getHost(); //$NON-NLS-1$
236 			// The standard J2SE error message is not very useful.
237 			//
238 			if ("Connection timed out: connect".equals(ce.getMessage())) //$NON-NLS-1$
239 				throw new ConnectException(MessageFormat.format(JGitText.get().connectionTimeOut, host));
240 			throw new ConnectException(ce.getMessage() + " " + host); //$NON-NLS-1$
241 		}
242 	}
243 
244 	/**
245 	 * Get the HTTP response code from the request.
246 	 * <p>
247 	 * Roughly the same as <code>c.getResponseCode()</code> but the
248 	 * ConnectException is translated to be more understandable.
249 	 *
250 	 * @param c
251 	 *            connection the code should be obtained from.
252 	 * @return r HTTP status code, usually 200 to indicate success. See
253 	 *         {@link org.eclipse.jgit.transport.http.HttpConnection} for other
254 	 *         defined constants.
255 	 * @throws java.io.IOException
256 	 *             communications error prevented obtaining the response code.
257 	 */
258 	public static int response(java.net.HttpURLConnection c)
259 			throws IOException {
260 		try {
261 			return c.getResponseCode();
262 		} catch (ConnectException ce) {
263 			final URL url = c.getURL();
264 			final String host = (url == null) ? "<null>" : url.getHost(); //$NON-NLS-1$
265 			// The standard J2SE error message is not very useful.
266 			//
267 			if ("Connection timed out: connect".equals(ce.getMessage())) //$NON-NLS-1$
268 				throw new ConnectException(MessageFormat.format(
269 						JGitText.get().connectionTimeOut, host));
270 			throw new ConnectException(ce.getMessage() + " " + host); //$NON-NLS-1$
271 		}
272 	}
273 
274 	/**
275 	 * Extract a HTTP header from the response.
276 	 *
277 	 * @param c
278 	 *            connection the header should be obtained from.
279 	 * @param headerName
280 	 *            the header name
281 	 * @return the header value
282 	 * @throws java.io.IOException
283 	 *             communications error prevented obtaining the header.
284 	 * @since 4.7
285 	 */
286 	public static String responseHeader(final HttpConnection c,
287 			final String headerName) throws IOException {
288 		return c.getHeaderField(headerName);
289 	}
290 
291 	/**
292 	 * Determine the proxy server (if any) needed to obtain a URL.
293 	 *
294 	 * @param proxySelector
295 	 *            proxy support for the caller.
296 	 * @param u
297 	 *            location of the server caller wants to talk to.
298 	 * @return proxy to communicate with the supplied URL.
299 	 * @throws java.net.ConnectException
300 	 *             the proxy could not be computed as the supplied URL could not
301 	 *             be read. This failure should never occur.
302 	 */
303 	public static Proxy proxyFor(ProxySelector proxySelector, URL u)
304 			throws ConnectException {
305 		try {
306 			URI uri = new URI(u.getProtocol(), null, u.getHost(), u.getPort(),
307 					null, null, null);
308 			return proxySelector.select(uri).get(0);
309 		} catch (URISyntaxException e) {
310 			final ConnectException err;
311 			err = new ConnectException(MessageFormat.format(JGitText.get().cannotDetermineProxyFor, u));
312 			err.initCause(e);
313 			throw err;
314 		}
315 	}
316 
317 	/**
318 	 * Disable SSL and hostname verification for given HTTP connection
319 	 *
320 	 * @param conn
321 	 *            a {@link org.eclipse.jgit.transport.http.HttpConnection}
322 	 *            object.
323 	 * @throws java.io.IOException
324 	 * @since 4.3
325 	 */
326 	public static void disableSslVerify(HttpConnection conn)
327 			throws IOException {
328 		TrustManager[] trustAllCerts = {
329 				new NoCheckX509TrustManager() };
330 		try {
331 			conn.configure(null, trustAllCerts, null);
332 			conn.setHostnameVerifier((name, session) -> true);
333 		} catch (KeyManagementException | NoSuchAlgorithmException e) {
334 			throw new IOException(e.getMessage(), e);
335 		}
336 	}
337 
338 	/**
339 	 * Enables all supported TLS protocol versions on the socket given. If
340 	 * system property "https.protocols" is set, only protocols specified there
341 	 * are enabled.
342 	 * <p>
343 	 * This is primarily a mechanism to deal with using TLS on IBM JDK. IBM JDK
344 	 * returns sockets that support all TLS protocol versions but have only the
345 	 * one specified in the context enabled. Oracle or OpenJDK return sockets
346 	 * that have all available protocols enabled already, up to the one
347 	 * specified.
348 	 * <p>
349 	 * <table>
350 	 * <tr>
351 	 * <td>SSLContext.getInstance()</td>
352 	 * <td>OpenJDK</td>
353 	 * <td>IDM JDK</td>
354 	 * </tr>
355 	 * <tr>
356 	 * <td>"TLS"</td>
357 	 * <td>Supported: TLSv1, TLSV1.1, TLSv1.2 (+ TLSv1.3)<br />
358 	 * Enabled: TLSv1, TLSV1.1, TLSv1.2 (+ TLSv1.3)</td>
359 	 * <td>Supported: TLSv1, TLSV1.1, TLSv1.2<br />
360 	 * Enabled: TLSv1</td>
361 	 * </tr>
362 	 * <tr>
363 	 * <td>"TLSv1.2"</td>
364 	 * <td>Supported: TLSv1, TLSV1.1, TLSv1.2<br />
365 	 * Enabled: TLSv1, TLSV1.1, TLSv1.2</td>
366 	 * <td>Supported: TLSv1, TLSV1.1, TLSv1.2<br />
367 	 * Enabled: TLSv1.2</td>
368 	 * </tr>
369 	 * </table>
370 	 *
371 	 * @param socket
372 	 *            to configure
373 	 * @see <a href=
374 	 *      "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
375 	 *      of SSLContext.getInstance("TLS") on IBM JDK</a>
376 	 * @see <a href=
377 	 *      "https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html#InstallationAndCustomization">Customizing
378 	 *      JSSE about https.protocols</a>
379 	 * @since 5.7
380 	 */
381 	public static void configureTLS(SSLSocket socket) {
382 		// 1. Enable all available TLS protocol versions
383 		Set<String> enabled = new LinkedHashSet<>(
384 				Arrays.asList(socket.getEnabledProtocols()));
385 		for (String s : socket.getSupportedProtocols()) {
386 			if (s.startsWith("TLS")) { //$NON-NLS-1$
387 				enabled.add(s);
388 			}
389 		}
390 		// 2. Respect the https.protocols system property
391 		Set<String> configured = getConfiguredProtocols();
392 		if (!configured.isEmpty()) {
393 			enabled.retainAll(configured);
394 		}
395 		if (!enabled.isEmpty()) {
396 			socket.setEnabledProtocols(enabled.toArray(new String[0]));
397 		}
398 	}
399 
400 	private static Set<String> getConfiguredProtocols() {
401 		Set<String> result = configuredHttpsProtocols;
402 		if (result == null) {
403 			String configured = getProperty("https.protocols"); //$NON-NLS-1$
404 			if (StringUtils.isEmptyOrNull(configured)) {
405 				result = Collections.emptySet();
406 			} else {
407 				result = new LinkedHashSet<>(
408 						Arrays.asList(configured.split("\\s*,\\s*"))); //$NON-NLS-1$
409 			}
410 			configuredHttpsProtocols = result;
411 		}
412 		return result;
413 	}
414 
415 	private static String getProperty(String property) {
416 		try {
417 			return SystemReader.getInstance().getProperty(property);
418 		} catch (SecurityException e) {
419 			LOG.warn(JGitText.get().failedReadHttpsProtocols, e);
420 			return null;
421 		}
422 	}
423 
424 	/**
425 	 * Scan a RFC 7230 token as it appears in HTTP headers.
426 	 *
427 	 * @param header
428 	 *            to scan in
429 	 * @param from
430 	 *            index in {@code header} to start scanning at
431 	 * @return the index after the token, that is, on the first non-token
432 	 *         character or {@code header.length}
433 	 * @throws IndexOutOfBoundsException
434 	 *             if {@code from < 0} or {@code from > header.length()}
435 	 *
436 	 * @see <a href="https://tools.ietf.org/html/rfc7230#appendix-B">RFC 7230,
437 	 *      Appendix B: Collected Grammar; "token" production</a>
438 	 * @since 5.10
439 	 */
440 	public static int scanToken(String header, int from) {
441 		int length = header.length();
442 		int i = from;
443 		if (i < 0 || i > length) {
444 			throw new IndexOutOfBoundsException();
445 		}
446 		while (i < length) {
447 			char c = header.charAt(i);
448 			switch (c) {
449 			case '!':
450 			case '#':
451 			case '$':
452 			case '%':
453 			case '&':
454 			case '\'':
455 			case '*':
456 			case '+':
457 			case '-':
458 			case '.':
459 			case '^':
460 			case '_':
461 			case '`':
462 			case '|':
463 			case '~':
464 			case '0':
465 			case '1':
466 			case '2':
467 			case '3':
468 			case '4':
469 			case '5':
470 			case '6':
471 			case '7':
472 			case '8':
473 			case '9':
474 				i++;
475 				break;
476 			default:
477 				if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
478 					i++;
479 					break;
480 				}
481 				return i;
482 			}
483 		}
484 		return i;
485 	}
486 
487 	private HttpSupport() {
488 		// Utility class only.
489 	}
490 }