blob: 1942342c45af6f254096f7244319590223ba760f [file] [log] [blame]
/*
* Copyright (C) 2010, Google Inc.
* Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> 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<String> 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.
* <p>
* Roughly the same as <code>c.getResponseCode()</code> 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) ? "<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.
* <p>
* Roughly the same as <code>c.getResponseCode()</code> 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) ? "<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.
* <p>
* 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.
* <p>
* <br>
* <table>
* <caption>TLS versions</caption>
* <tr>
* <td>SSLContext.getInstance()</td>
* <td>OpenJDK</td>
* <td>IDM JDK</td>
* </tr>
* <tr>
* <td>"TLS"</td>
* <td>Supported: TLSv1, TLSV1.1, TLSv1.2 (+ TLSv1.3)<br>
* Enabled: TLSv1, TLSV1.1, TLSv1.2 (+ TLSv1.3)</td>
* <td>Supported: TLSv1, TLSV1.1, TLSv1.2<br>
* Enabled: TLSv1</td>
* </tr>
* <tr>
* <td>"TLSv1.2"</td>
* <td>Supported: TLSv1, TLSV1.1, TLSv1.2<br>
* Enabled: TLSv1, TLSV1.1, TLSv1.2</td>
* <td>Supported: TLSv1, TLSV1.1, TLSv1.2<br>
* Enabled: TLSv1.2</td>
* </tr>
* </table>
*
* @param socket
* to configure
* @see <a href=
* "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
* of SSLContext.getInstance("TLS") on IBM JDK</a>
* @see <a href=
* "https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html#InstallationAndCustomization">Customizing
* JSSE about https.protocols</a>
* @since 5.7
*/
public static void configureTLS(SSLSocket socket) {
// 1. Enable all available TLS protocol versions
Set<String> 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<String> configured = getConfiguredProtocols();
if (!configured.isEmpty()) {
enabled.retainAll(configured);
}
if (!enabled.isEmpty()) {
socket.setEnabledProtocols(enabled.toArray(new String[0]));
}
}
private static Set<String> getConfiguredProtocols() {
Set<String> 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 <a href="https://tools.ietf.org/html/rfc7230#appendix-B">RFC 7230,
* Appendix B: Collected Grammar; "token" production</a>
* @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.
}
}