/* * Copyright (C) 2010, Google Inc. * Copyright (C) 2008, Shawn O. Pearce and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at * https://www.eclipse.org/org/documents/edl-v10.php. * * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.util; import static java.nio.charset.StandardCharsets.UTF_8; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.ConnectException; import java.net.Proxy; import java.net.ProxySelector; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLEncoder; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.text.MessageFormat; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; import javax.net.ssl.SSLSocket; import javax.net.ssl.TrustManager; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.transport.http.HttpConnection; import org.eclipse.jgit.transport.http.NoCheckX509TrustManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Extra utilities to support usage of HTTP. */ public class HttpSupport { private final static Logger LOG = LoggerFactory .getLogger(HttpSupport.class); /** The {@code GET} HTTP method. */ public static final String METHOD_GET = "GET"; //$NON-NLS-1$ /** The {@code HEAD} HTTP method. * @since 4.3 */ public static final String METHOD_HEAD = "HEAD"; //$NON-NLS-1$ /** The {@code POST} HTTP method. * @since 4.3 */ public static final String METHOD_PUT = "PUT"; //$NON-NLS-1$ /** The {@code POST} HTTP method. */ public static final String METHOD_POST = "POST"; //$NON-NLS-1$ /** The {@code Cache-Control} header. */ public static final String HDR_CACHE_CONTROL = "Cache-Control"; //$NON-NLS-1$ /** The {@code Pragma} header. */ public static final String HDR_PRAGMA = "Pragma"; //$NON-NLS-1$ /** The {@code User-Agent} header. */ public static final String HDR_USER_AGENT = "User-Agent"; //$NON-NLS-1$ /** * The {@code Server} header. * @since 4.0 */ public static final String HDR_SERVER = "Server"; //$NON-NLS-1$ /** The {@code Date} header. */ public static final String HDR_DATE = "Date"; //$NON-NLS-1$ /** The {@code Expires} header. */ public static final String HDR_EXPIRES = "Expires"; //$NON-NLS-1$ /** The {@code ETag} header. */ public static final String HDR_ETAG = "ETag"; //$NON-NLS-1$ /** The {@code If-None-Match} header. */ public static final String HDR_IF_NONE_MATCH = "If-None-Match"; //$NON-NLS-1$ /** The {@code Last-Modified} header. */ public static final String HDR_LAST_MODIFIED = "Last-Modified"; //$NON-NLS-1$ /** The {@code If-Modified-Since} header. */ public static final String HDR_IF_MODIFIED_SINCE = "If-Modified-Since"; //$NON-NLS-1$ /** The {@code Accept} header. */ public static final String HDR_ACCEPT = "Accept"; //$NON-NLS-1$ /** The {@code Content-Type} header. */ public static final String HDR_CONTENT_TYPE = "Content-Type"; //$NON-NLS-1$ /** The {@code Content-Length} header. */ public static final String HDR_CONTENT_LENGTH = "Content-Length"; //$NON-NLS-1$ /** The {@code Content-Encoding} header. */ public static final String HDR_CONTENT_ENCODING = "Content-Encoding"; //$NON-NLS-1$ /** The {@code Content-Range} header. */ public static final String HDR_CONTENT_RANGE = "Content-Range"; //$NON-NLS-1$ /** The {@code Accept-Ranges} header. */ public static final String HDR_ACCEPT_RANGES = "Accept-Ranges"; //$NON-NLS-1$ /** The {@code If-Range} header. */ public static final String HDR_IF_RANGE = "If-Range"; //$NON-NLS-1$ /** The {@code Range} header. */ public static final String HDR_RANGE = "Range"; //$NON-NLS-1$ /** The {@code Accept-Encoding} header. */ public static final String HDR_ACCEPT_ENCODING = "Accept-Encoding"; //$NON-NLS-1$ /** * The {@code Location} header. * @since 4.7 */ public static final String HDR_LOCATION = "Location"; //$NON-NLS-1$ /** The {@code gzip} encoding value for {@link #HDR_ACCEPT_ENCODING}. */ public static final String ENCODING_GZIP = "gzip"; //$NON-NLS-1$ /** * The {@code x-gzip} encoding value for {@link #HDR_ACCEPT_ENCODING}. * @since 4.6 */ public static final String ENCODING_X_GZIP = "x-gzip"; //$NON-NLS-1$ /** The standard {@code text/plain} MIME type. */ public static final String TEXT_PLAIN = "text/plain"; //$NON-NLS-1$ /** The {@code Authorization} header. */ public static final String HDR_AUTHORIZATION = "Authorization"; //$NON-NLS-1$ /** The {@code WWW-Authenticate} header. */ public static final String HDR_WWW_AUTHENTICATE = "WWW-Authenticate"; //$NON-NLS-1$ /** * The {@code Cookie} header. * * @since 5.4 */ public static final String HDR_COOKIE = "Cookie"; //$NON-NLS-1$ /** * The {@code Set-Cookie} header. * * @since 5.4 */ public static final String HDR_SET_COOKIE = "Set-Cookie"; //$NON-NLS-1$ /** * The {@code Set-Cookie2} header. * * @since 5.4 */ public static final String HDR_SET_COOKIE2 = "Set-Cookie2"; //$NON-NLS-1$ private static Set configuredHttpsProtocols; /** * URL encode a value string into an output buffer. * * @param urlstr * the output buffer. * @param key * value which must be encoded to protected special characters. */ public static void encode(StringBuilder urlstr, String key) { if (key == null || key.length() == 0) return; try { urlstr.append(URLEncoder.encode(key, UTF_8.name())); } catch (UnsupportedEncodingException e) { throw new RuntimeException(JGitText.get().couldNotURLEncodeToUTF8, e); } } /** * Translates the provided URL into application/x-www-form-urlencoded * format. * * @param url * The URL to translate. * @param keepPathSlash * Whether or not to keep "/" in the URL (i.e. don't translate * them to "%2F"). * * @return The translated URL. * @since 5.13.1 */ public static String urlEncode(String url, boolean keepPathSlash) { String encoded; try { encoded = URLEncoder.encode(url, UTF_8.name()); } catch (UnsupportedEncodingException e) { throw new RuntimeException(JGitText.get().couldNotURLEncodeToUTF8, e); } if (keepPathSlash) { encoded = encoded.replace("%2F", "/"); //$NON-NLS-1$ //$NON-NLS-2$ } return encoded; } /** * Get the HTTP response code from the request. *

* Roughly the same as c.getResponseCode() but the * ConnectException is translated to be more understandable. * * @param c * connection the code should be obtained from. * @return r HTTP status code, usually 200 to indicate success. See * {@link org.eclipse.jgit.transport.http.HttpConnection} for other * defined constants. * @throws java.io.IOException * communications error prevented obtaining the response code. * @since 3.3 */ public static int response(HttpConnection c) throws IOException { try { return c.getResponseCode(); } catch (ConnectException ce) { final URL url = c.getURL(); final String host = (url == null) ? "" : url.getHost(); //$NON-NLS-1$ // The standard J2SE error message is not very useful. // if ("Connection timed out: connect".equals(ce.getMessage())) //$NON-NLS-1$ throw new ConnectException(MessageFormat.format(JGitText.get().connectionTimeOut, host)); throw new ConnectException(ce.getMessage() + " " + host); //$NON-NLS-1$ } } /** * Get the HTTP response code from the request. *

* Roughly the same as c.getResponseCode() but the * ConnectException is translated to be more understandable. * * @param c * connection the code should be obtained from. * @return r HTTP status code, usually 200 to indicate success. See * {@link org.eclipse.jgit.transport.http.HttpConnection} for other * defined constants. * @throws java.io.IOException * communications error prevented obtaining the response code. */ public static int response(java.net.HttpURLConnection c) throws IOException { try { return c.getResponseCode(); } catch (ConnectException ce) { final URL url = c.getURL(); final String host = (url == null) ? "" : url.getHost(); //$NON-NLS-1$ // The standard J2SE error message is not very useful. // if ("Connection timed out: connect".equals(ce.getMessage())) //$NON-NLS-1$ throw new ConnectException(MessageFormat.format( JGitText.get().connectionTimeOut, host)); throw new ConnectException(ce.getMessage() + " " + host); //$NON-NLS-1$ } } /** * Extract a HTTP header from the response. * * @param c * connection the header should be obtained from. * @param headerName * the header name * @return the header value * @throws java.io.IOException * communications error prevented obtaining the header. * @since 4.7 */ public static String responseHeader(final HttpConnection c, final String headerName) throws IOException { return c.getHeaderField(headerName); } /** * Determine the proxy server (if any) needed to obtain a URL. * * @param proxySelector * proxy support for the caller. * @param u * location of the server caller wants to talk to. * @return proxy to communicate with the supplied URL. * @throws java.net.ConnectException * the proxy could not be computed as the supplied URL could not * be read. This failure should never occur. */ public static Proxy proxyFor(ProxySelector proxySelector, URL u) throws ConnectException { try { URI uri = new URI(u.getProtocol(), null, u.getHost(), u.getPort(), null, null, null); return proxySelector.select(uri).get(0); } catch (URISyntaxException e) { final ConnectException err; err = new ConnectException(MessageFormat.format(JGitText.get().cannotDetermineProxyFor, u)); err.initCause(e); throw err; } } /** * Disable SSL and hostname verification for given HTTP connection * * @param conn * a {@link org.eclipse.jgit.transport.http.HttpConnection} * object. * @throws java.io.IOException * if an IO error occurred * @since 4.3 */ public static void disableSslVerify(HttpConnection conn) throws IOException { TrustManager[] trustAllCerts = { new NoCheckX509TrustManager() }; try { conn.configure(null, trustAllCerts, null); conn.setHostnameVerifier((name, session) -> true); } catch (KeyManagementException | NoSuchAlgorithmException e) { throw new IOException(e.getMessage(), e); } } /** * Enables all supported TLS protocol versions on the socket given. If * system property "https.protocols" is set, only protocols specified there * are enabled. *

* This is primarily a mechanism to deal with using TLS on IBM JDK. IBM JDK * returns sockets that support all TLS protocol versions but have only the * one specified in the context enabled. Oracle or OpenJDK return sockets * that have all available protocols enabled already, up to the one * specified. *

*
* * * * * * * * * * * * * * * * * *
TLS versions
SSLContext.getInstance()OpenJDKIDM JDK
"TLS"Supported: TLSv1, TLSV1.1, TLSv1.2 (+ TLSv1.3)
* Enabled: TLSv1, TLSV1.1, TLSv1.2 (+ TLSv1.3)
Supported: TLSv1, TLSV1.1, TLSv1.2
* Enabled: TLSv1
"TLSv1.2"Supported: TLSv1, TLSV1.1, TLSv1.2
* Enabled: TLSv1, TLSV1.1, TLSv1.2
Supported: TLSv1, TLSV1.1, TLSv1.2
* Enabled: TLSv1.2
* * @param socket * to configure * @see Behavior * of SSLContext.getInstance("TLS") on IBM JDK * @see Customizing * JSSE about https.protocols * @since 5.7 */ public static void configureTLS(SSLSocket socket) { // 1. Enable all available TLS protocol versions Set enabled = new LinkedHashSet<>( Arrays.asList(socket.getEnabledProtocols())); for (String s : socket.getSupportedProtocols()) { if (s.startsWith("TLS")) { //$NON-NLS-1$ enabled.add(s); } } // 2. Respect the https.protocols system property Set configured = getConfiguredProtocols(); if (!configured.isEmpty()) { enabled.retainAll(configured); } if (!enabled.isEmpty()) { socket.setEnabledProtocols(enabled.toArray(new String[0])); } } private static Set getConfiguredProtocols() { Set result = configuredHttpsProtocols; if (result == null) { String configured = getProperty("https.protocols"); //$NON-NLS-1$ if (StringUtils.isEmptyOrNull(configured)) { result = Collections.emptySet(); } else { result = new LinkedHashSet<>( Arrays.asList(configured.split("\\s*,\\s*"))); //$NON-NLS-1$ } configuredHttpsProtocols = result; } return result; } private static String getProperty(String property) { try { return SystemReader.getInstance().getProperty(property); } catch (SecurityException e) { LOG.warn(JGitText.get().failedReadHttpsProtocols, e); return null; } } /** * Scan a RFC 7230 token as it appears in HTTP headers. * * @param header * to scan in * @param from * index in {@code header} to start scanning at * @return the index after the token, that is, on the first non-token * character or {@code header.length} * @throws IndexOutOfBoundsException * if {@code from < 0} or {@code from > header.length()} * * @see RFC 7230, * Appendix B: Collected Grammar; "token" production * @since 5.10 */ public static int scanToken(String header, int from) { int length = header.length(); int i = from; if (i < 0 || i > length) { throw new IndexOutOfBoundsException(); } while (i < length) { char c = header.charAt(i); switch (c) { case '!': case '#': case '$': case '%': case '&': case '\'': case '*': case '+': case '-': case '.': case '^': case '_': case '`': case '|': case '~': case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': i++; break; default: if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { i++; break; } return i; } } return i; } private HttpSupport() { // Utility class only. } }