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 	 * Get the HTTP response code from the request.
190 	 * <p>
191 	 * Roughly the same as <code>c.getResponseCode()</code> but the
192 	 * ConnectException is translated to be more understandable.
193 	 *
194 	 * @param c
195 	 *            connection the code should be obtained from.
196 	 * @return r HTTP status code, usually 200 to indicate success. See
197 	 *         {@link org.eclipse.jgit.transport.http.HttpConnection} for other
198 	 *         defined constants.
199 	 * @throws java.io.IOException
200 	 *             communications error prevented obtaining the response code.
201 	 * @since 3.3
202 	 */
203 	public static int response(HttpConnection c) throws IOException {
204 		try {
205 			return c.getResponseCode();
206 		} catch (ConnectException ce) {
207 			final URL url = c.getURL();
208 			final String host = (url == null) ? "<null>" : url.getHost(); //$NON-NLS-1$
209 			// The standard J2SE error message is not very useful.
210 			//
211 			if ("Connection timed out: connect".equals(ce.getMessage())) //$NON-NLS-1$
212 				throw new ConnectException(MessageFormat.format(JGitText.get().connectionTimeOut, host));
213 			throw new ConnectException(ce.getMessage() + " " + host); //$NON-NLS-1$
214 		}
215 	}
216 
217 	/**
218 	 * Get the HTTP response code from the request.
219 	 * <p>
220 	 * Roughly the same as <code>c.getResponseCode()</code> but the
221 	 * ConnectException is translated to be more understandable.
222 	 *
223 	 * @param c
224 	 *            connection the code should be obtained from.
225 	 * @return r HTTP status code, usually 200 to indicate success. See
226 	 *         {@link org.eclipse.jgit.transport.http.HttpConnection} for other
227 	 *         defined constants.
228 	 * @throws java.io.IOException
229 	 *             communications error prevented obtaining the response code.
230 	 */
231 	public static int response(java.net.HttpURLConnection c)
232 			throws IOException {
233 		try {
234 			return c.getResponseCode();
235 		} catch (ConnectException ce) {
236 			final URL url = c.getURL();
237 			final String host = (url == null) ? "<null>" : url.getHost(); //$NON-NLS-1$
238 			// The standard J2SE error message is not very useful.
239 			//
240 			if ("Connection timed out: connect".equals(ce.getMessage())) //$NON-NLS-1$
241 				throw new ConnectException(MessageFormat.format(
242 						JGitText.get().connectionTimeOut, host));
243 			throw new ConnectException(ce.getMessage() + " " + host); //$NON-NLS-1$
244 		}
245 	}
246 
247 	/**
248 	 * Extract a HTTP header from the response.
249 	 *
250 	 * @param c
251 	 *            connection the header should be obtained from.
252 	 * @param headerName
253 	 *            the header name
254 	 * @return the header value
255 	 * @throws java.io.IOException
256 	 *             communications error prevented obtaining the header.
257 	 * @since 4.7
258 	 */
259 	public static String responseHeader(final HttpConnection c,
260 			final String headerName) throws IOException {
261 		return c.getHeaderField(headerName);
262 	}
263 
264 	/**
265 	 * Determine the proxy server (if any) needed to obtain a URL.
266 	 *
267 	 * @param proxySelector
268 	 *            proxy support for the caller.
269 	 * @param u
270 	 *            location of the server caller wants to talk to.
271 	 * @return proxy to communicate with the supplied URL.
272 	 * @throws java.net.ConnectException
273 	 *             the proxy could not be computed as the supplied URL could not
274 	 *             be read. This failure should never occur.
275 	 */
276 	public static Proxy proxyFor(ProxySelector proxySelector, URL u)
277 			throws ConnectException {
278 		try {
279 			URI uri = new URI(u.getProtocol(), null, u.getHost(), u.getPort(),
280 					null, null, null);
281 			return proxySelector.select(uri).get(0);
282 		} catch (URISyntaxException e) {
283 			final ConnectException err;
284 			err = new ConnectException(MessageFormat.format(JGitText.get().cannotDetermineProxyFor, u));
285 			err.initCause(e);
286 			throw err;
287 		}
288 	}
289 
290 	/**
291 	 * Disable SSL and hostname verification for given HTTP connection
292 	 *
293 	 * @param conn
294 	 *            a {@link org.eclipse.jgit.transport.http.HttpConnection}
295 	 *            object.
296 	 * @throws java.io.IOException
297 	 * @since 4.3
298 	 */
299 	public static void disableSslVerify(HttpConnection conn)
300 			throws IOException {
301 		TrustManager[] trustAllCerts = {
302 				new NoCheckX509TrustManager() };
303 		try {
304 			conn.configure(null, trustAllCerts, null);
305 			conn.setHostnameVerifier((name, session) -> true);
306 		} catch (KeyManagementException | NoSuchAlgorithmException e) {
307 			throw new IOException(e.getMessage(), e);
308 		}
309 	}
310 
311 	/**
312 	 * Enables all supported TLS protocol versions on the socket given. If
313 	 * system property "https.protocols" is set, only protocols specified there
314 	 * are enabled.
315 	 * <p>
316 	 * This is primarily a mechanism to deal with using TLS on IBM JDK. IBM JDK
317 	 * returns sockets that support all TLS protocol versions but have only the
318 	 * one specified in the context enabled. Oracle or OpenJDK return sockets
319 	 * that have all available protocols enabled already, up to the one
320 	 * specified.
321 	 * <p>
322 	 * <table>
323 	 * <tr>
324 	 * <td>SSLContext.getInstance()</td>
325 	 * <td>OpenJDK</td>
326 	 * <td>IDM JDK</td>
327 	 * </tr>
328 	 * <tr>
329 	 * <td>"TLS"</td>
330 	 * <td>Supported: TLSv1, TLSV1.1, TLSv1.2 (+ TLSv1.3)<br />
331 	 * Enabled: TLSv1, TLSV1.1, TLSv1.2 (+ TLSv1.3)</td>
332 	 * <td>Supported: TLSv1, TLSV1.1, TLSv1.2<br />
333 	 * Enabled: TLSv1</td>
334 	 * </tr>
335 	 * <tr>
336 	 * <td>"TLSv1.2"</td>
337 	 * <td>Supported: TLSv1, TLSV1.1, TLSv1.2<br />
338 	 * Enabled: TLSv1, TLSV1.1, TLSv1.2</td>
339 	 * <td>Supported: TLSv1, TLSV1.1, TLSv1.2<br />
340 	 * Enabled: TLSv1.2</td>
341 	 * </tr>
342 	 * </table>
343 	 *
344 	 * @param socket
345 	 *            to configure
346 	 * @see <a href=
347 	 *      "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
348 	 *      of SSLContext.getInstance("TLS") on IBM JDK</a>
349 	 * @see <a href=
350 	 *      "https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html#InstallationAndCustomization">Customizing
351 	 *      JSSE about https.protocols</a>
352 	 * @since 5.7
353 	 */
354 	public static void configureTLS(SSLSocket socket) {
355 		// 1. Enable all available TLS protocol versions
356 		Set<String> enabled = new LinkedHashSet<>(
357 				Arrays.asList(socket.getEnabledProtocols()));
358 		for (String s : socket.getSupportedProtocols()) {
359 			if (s.startsWith("TLS")) { //$NON-NLS-1$
360 				enabled.add(s);
361 			}
362 		}
363 		// 2. Respect the https.protocols system property
364 		Set<String> configured = getConfiguredProtocols();
365 		if (!configured.isEmpty()) {
366 			enabled.retainAll(configured);
367 		}
368 		if (!enabled.isEmpty()) {
369 			socket.setEnabledProtocols(enabled.toArray(new String[0]));
370 		}
371 	}
372 
373 	private static Set<String> getConfiguredProtocols() {
374 		Set<String> result = configuredHttpsProtocols;
375 		if (result == null) {
376 			String configured = getProperty("https.protocols"); //$NON-NLS-1$
377 			if (StringUtils.isEmptyOrNull(configured)) {
378 				result = Collections.emptySet();
379 			} else {
380 				result = new LinkedHashSet<>(
381 						Arrays.asList(configured.split("\\s*,\\s*"))); //$NON-NLS-1$
382 			}
383 			configuredHttpsProtocols = result;
384 		}
385 		return result;
386 	}
387 
388 	private static String getProperty(String property) {
389 		try {
390 			return SystemReader.getInstance().getProperty(property);
391 		} catch (SecurityException e) {
392 			LOG.warn(JGitText.get().failedReadHttpsProtocols, e);
393 			return null;
394 		}
395 	}
396 
397 	/**
398 	 * Scan a RFC 7230 token as it appears in HTTP headers.
399 	 *
400 	 * @param header
401 	 *            to scan in
402 	 * @param from
403 	 *            index in {@code header} to start scanning at
404 	 * @return the index after the token, that is, on the first non-token
405 	 *         character or {@code header.length}
406 	 * @throws IndexOutOfBoundsException
407 	 *             if {@code from < 0} or {@code from > header.length()}
408 	 *
409 	 * @see <a href="https://tools.ietf.org/html/rfc7230#appendix-B">RFC 7230,
410 	 *      Appendix B: Collected Grammar; "token" production</a>
411 	 * @since 5.10
412 	 */
413 	public static int scanToken(String header, int from) {
414 		int length = header.length();
415 		int i = from;
416 		if (i < 0 || i > length) {
417 			throw new IndexOutOfBoundsException();
418 		}
419 		while (i < length) {
420 			char c = header.charAt(i);
421 			switch (c) {
422 			case '!':
423 			case '#':
424 			case '$':
425 			case '%':
426 			case '&':
427 			case '\'':
428 			case '*':
429 			case '+':
430 			case '-':
431 			case '.':
432 			case '^':
433 			case '_':
434 			case '`':
435 			case '|':
436 			case '~':
437 			case '0':
438 			case '1':
439 			case '2':
440 			case '3':
441 			case '4':
442 			case '5':
443 			case '6':
444 			case '7':
445 			case '8':
446 			case '9':
447 				i++;
448 				break;
449 			default:
450 				if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
451 					i++;
452 					break;
453 				}
454 				return i;
455 			}
456 		}
457 		return i;
458 	}
459 
460 	private HttpSupport() {
461 		// Utility class only.
462 	}
463 }