HttpSupport.java

  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. package org.eclipse.jgit.util;

  12. import static java.nio.charset.StandardCharsets.UTF_8;

  13. import java.io.IOException;
  14. import java.io.UnsupportedEncodingException;
  15. import java.net.ConnectException;
  16. import java.net.Proxy;
  17. import java.net.ProxySelector;
  18. import java.net.URI;
  19. import java.net.URISyntaxException;
  20. import java.net.URL;
  21. import java.net.URLEncoder;
  22. import java.security.KeyManagementException;
  23. import java.security.NoSuchAlgorithmException;
  24. import java.text.MessageFormat;
  25. import java.util.Arrays;
  26. import java.util.Collections;
  27. import java.util.LinkedHashSet;
  28. import java.util.Set;

  29. import javax.net.ssl.SSLSocket;
  30. import javax.net.ssl.TrustManager;

  31. import org.eclipse.jgit.internal.JGitText;
  32. import org.eclipse.jgit.transport.http.HttpConnection;
  33. import org.eclipse.jgit.transport.http.NoCheckX509TrustManager;
  34. import org.slf4j.Logger;
  35. import org.slf4j.LoggerFactory;

  36. /**
  37.  * Extra utilities to support usage of HTTP.
  38.  */
  39. public class HttpSupport {
  40.     private final static Logger LOG = LoggerFactory
  41.             .getLogger(HttpSupport.class);

  42.     /** The {@code GET} HTTP method. */
  43.     public static final String METHOD_GET = "GET"; //$NON-NLS-1$

  44.     /** The {@code HEAD} HTTP method.
  45.      * @since 4.3 */
  46.     public static final String METHOD_HEAD = "HEAD"; //$NON-NLS-1$

  47.     /** The {@code POST} HTTP method.
  48.      * @since 4.3 */
  49.     public static final String METHOD_PUT = "PUT"; //$NON-NLS-1$

  50.     /** The {@code POST} HTTP method. */
  51.     public static final String METHOD_POST = "POST"; //$NON-NLS-1$

  52.     /** The {@code Cache-Control} header. */
  53.     public static final String HDR_CACHE_CONTROL = "Cache-Control"; //$NON-NLS-1$

  54.     /** The {@code Pragma} header. */
  55.     public static final String HDR_PRAGMA = "Pragma"; //$NON-NLS-1$

  56.     /** The {@code User-Agent} header. */
  57.     public static final String HDR_USER_AGENT = "User-Agent"; //$NON-NLS-1$

  58.     /**
  59.      * The {@code Server} header.
  60.      * @since 4.0
  61.      */
  62.     public static final String HDR_SERVER = "Server"; //$NON-NLS-1$

  63.     /** The {@code Date} header. */
  64.     public static final String HDR_DATE = "Date"; //$NON-NLS-1$

  65.     /** The {@code Expires} header. */
  66.     public static final String HDR_EXPIRES = "Expires"; //$NON-NLS-1$

  67.     /** The {@code ETag} header. */
  68.     public static final String HDR_ETAG = "ETag"; //$NON-NLS-1$

  69.     /** The {@code If-None-Match} header. */
  70.     public static final String HDR_IF_NONE_MATCH = "If-None-Match"; //$NON-NLS-1$

  71.     /** The {@code Last-Modified} header. */
  72.     public static final String HDR_LAST_MODIFIED = "Last-Modified"; //$NON-NLS-1$

  73.     /** The {@code If-Modified-Since} header. */
  74.     public static final String HDR_IF_MODIFIED_SINCE = "If-Modified-Since"; //$NON-NLS-1$

  75.     /** The {@code Accept} header. */
  76.     public static final String HDR_ACCEPT = "Accept"; //$NON-NLS-1$

  77.     /** The {@code Content-Type} header. */
  78.     public static final String HDR_CONTENT_TYPE = "Content-Type"; //$NON-NLS-1$

  79.     /** The {@code Content-Length} header. */
  80.     public static final String HDR_CONTENT_LENGTH = "Content-Length"; //$NON-NLS-1$

  81.     /** The {@code Content-Encoding} header. */
  82.     public static final String HDR_CONTENT_ENCODING = "Content-Encoding"; //$NON-NLS-1$

  83.     /** The {@code Content-Range} header. */
  84.     public static final String HDR_CONTENT_RANGE = "Content-Range"; //$NON-NLS-1$

  85.     /** The {@code Accept-Ranges} header. */
  86.     public static final String HDR_ACCEPT_RANGES = "Accept-Ranges"; //$NON-NLS-1$

  87.     /** The {@code If-Range} header. */
  88.     public static final String HDR_IF_RANGE = "If-Range"; //$NON-NLS-1$

  89.     /** The {@code Range} header. */
  90.     public static final String HDR_RANGE = "Range"; //$NON-NLS-1$

  91.     /** The {@code Accept-Encoding} header. */
  92.     public static final String HDR_ACCEPT_ENCODING = "Accept-Encoding"; //$NON-NLS-1$

  93.     /**
  94.      * The {@code Location} header.
  95.      * @since 4.7
  96.      */
  97.     public static final String HDR_LOCATION = "Location"; //$NON-NLS-1$

  98.     /** The {@code gzip} encoding value for {@link #HDR_ACCEPT_ENCODING}. */
  99.     public static final String ENCODING_GZIP = "gzip"; //$NON-NLS-1$

  100.     /**
  101.      * The {@code x-gzip} encoding value for {@link #HDR_ACCEPT_ENCODING}.
  102.      * @since 4.6
  103.      */
  104.     public static final String ENCODING_X_GZIP = "x-gzip"; //$NON-NLS-1$

  105.     /** The standard {@code text/plain} MIME type. */
  106.     public static final String TEXT_PLAIN = "text/plain"; //$NON-NLS-1$

  107.     /** The {@code Authorization} header. */
  108.     public static final String HDR_AUTHORIZATION = "Authorization"; //$NON-NLS-1$

  109.     /** The {@code WWW-Authenticate} header. */
  110.     public static final String HDR_WWW_AUTHENTICATE = "WWW-Authenticate"; //$NON-NLS-1$

  111.     /**
  112.      * The {@code Cookie} header.
  113.      *
  114.      * @since 5.4
  115.      */
  116.     public static final String HDR_COOKIE = "Cookie"; //$NON-NLS-1$

  117.     /**
  118.      * The {@code Set-Cookie} header.
  119.      *
  120.      * @since 5.4
  121.      */
  122.     public static final String HDR_SET_COOKIE = "Set-Cookie"; //$NON-NLS-1$

  123.     /**
  124.      * The {@code Set-Cookie2} header.
  125.      *
  126.      * @since 5.4
  127.      */
  128.     public static final String HDR_SET_COOKIE2 = "Set-Cookie2"; //$NON-NLS-1$

  129.     private static Set<String> configuredHttpsProtocols;

  130.     /**
  131.      * URL encode a value string into an output buffer.
  132.      *
  133.      * @param urlstr
  134.      *            the output buffer.
  135.      * @param key
  136.      *            value which must be encoded to protected special characters.
  137.      */
  138.     public static void encode(StringBuilder urlstr, String key) {
  139.         if (key == null || key.length() == 0)
  140.             return;
  141.         try {
  142.             urlstr.append(URLEncoder.encode(key, UTF_8.name()));
  143.         } catch (UnsupportedEncodingException e) {
  144.             throw new RuntimeException(JGitText.get().couldNotURLEncodeToUTF8, e);
  145.         }
  146.     }

  147.     /**
  148.      * Get the HTTP response code from the request.
  149.      * <p>
  150.      * Roughly the same as <code>c.getResponseCode()</code> but the
  151.      * ConnectException is translated to be more understandable.
  152.      *
  153.      * @param c
  154.      *            connection the code should be obtained from.
  155.      * @return r HTTP status code, usually 200 to indicate success. See
  156.      *         {@link org.eclipse.jgit.transport.http.HttpConnection} for other
  157.      *         defined constants.
  158.      * @throws java.io.IOException
  159.      *             communications error prevented obtaining the response code.
  160.      * @since 3.3
  161.      */
  162.     public static int response(HttpConnection c) throws IOException {
  163.         try {
  164.             return c.getResponseCode();
  165.         } catch (ConnectException ce) {
  166.             final URL url = c.getURL();
  167.             final String host = (url == null) ? "<null>" : url.getHost(); //$NON-NLS-1$
  168.             // The standard J2SE error message is not very useful.
  169.             //
  170.             if ("Connection timed out: connect".equals(ce.getMessage())) //$NON-NLS-1$
  171.                 throw new ConnectException(MessageFormat.format(JGitText.get().connectionTimeOut, host));
  172.             throw new ConnectException(ce.getMessage() + " " + host); //$NON-NLS-1$
  173.         }
  174.     }

  175.     /**
  176.      * Get the HTTP response code from the request.
  177.      * <p>
  178.      * Roughly the same as <code>c.getResponseCode()</code> but the
  179.      * ConnectException is translated to be more understandable.
  180.      *
  181.      * @param c
  182.      *            connection the code should be obtained from.
  183.      * @return r HTTP status code, usually 200 to indicate success. See
  184.      *         {@link org.eclipse.jgit.transport.http.HttpConnection} for other
  185.      *         defined constants.
  186.      * @throws java.io.IOException
  187.      *             communications error prevented obtaining the response code.
  188.      */
  189.     public static int response(java.net.HttpURLConnection c)
  190.             throws IOException {
  191.         try {
  192.             return c.getResponseCode();
  193.         } catch (ConnectException ce) {
  194.             final URL url = c.getURL();
  195.             final String host = (url == null) ? "<null>" : url.getHost(); //$NON-NLS-1$
  196.             // The standard J2SE error message is not very useful.
  197.             //
  198.             if ("Connection timed out: connect".equals(ce.getMessage())) //$NON-NLS-1$
  199.                 throw new ConnectException(MessageFormat.format(
  200.                         JGitText.get().connectionTimeOut, host));
  201.             throw new ConnectException(ce.getMessage() + " " + host); //$NON-NLS-1$
  202.         }
  203.     }

  204.     /**
  205.      * Extract a HTTP header from the response.
  206.      *
  207.      * @param c
  208.      *            connection the header should be obtained from.
  209.      * @param headerName
  210.      *            the header name
  211.      * @return the header value
  212.      * @throws java.io.IOException
  213.      *             communications error prevented obtaining the header.
  214.      * @since 4.7
  215.      */
  216.     public static String responseHeader(final HttpConnection c,
  217.             final String headerName) throws IOException {
  218.         return c.getHeaderField(headerName);
  219.     }

  220.     /**
  221.      * Determine the proxy server (if any) needed to obtain a URL.
  222.      *
  223.      * @param proxySelector
  224.      *            proxy support for the caller.
  225.      * @param u
  226.      *            location of the server caller wants to talk to.
  227.      * @return proxy to communicate with the supplied URL.
  228.      * @throws java.net.ConnectException
  229.      *             the proxy could not be computed as the supplied URL could not
  230.      *             be read. This failure should never occur.
  231.      */
  232.     public static Proxy proxyFor(ProxySelector proxySelector, URL u)
  233.             throws ConnectException {
  234.         try {
  235.             URI uri = new URI(u.getProtocol(), null, u.getHost(), u.getPort(),
  236.                     null, null, null);
  237.             return proxySelector.select(uri).get(0);
  238.         } catch (URISyntaxException e) {
  239.             final ConnectException err;
  240.             err = new ConnectException(MessageFormat.format(JGitText.get().cannotDetermineProxyFor, u));
  241.             err.initCause(e);
  242.             throw err;
  243.         }
  244.     }

  245.     /**
  246.      * Disable SSL and hostname verification for given HTTP connection
  247.      *
  248.      * @param conn
  249.      *            a {@link org.eclipse.jgit.transport.http.HttpConnection}
  250.      *            object.
  251.      * @throws java.io.IOException
  252.      * @since 4.3
  253.      */
  254.     public static void disableSslVerify(HttpConnection conn)
  255.             throws IOException {
  256.         TrustManager[] trustAllCerts = {
  257.                 new NoCheckX509TrustManager() };
  258.         try {
  259.             conn.configure(null, trustAllCerts, null);
  260.             conn.setHostnameVerifier((name, session) -> true);
  261.         } catch (KeyManagementException | NoSuchAlgorithmException e) {
  262.             throw new IOException(e.getMessage(), e);
  263.         }
  264.     }

  265.     /**
  266.      * Enables all supported TLS protocol versions on the socket given. If
  267.      * system property "https.protocols" is set, only protocols specified there
  268.      * are enabled.
  269.      * <p>
  270.      * This is primarily a mechanism to deal with using TLS on IBM JDK. IBM JDK
  271.      * returns sockets that support all TLS protocol versions but have only the
  272.      * one specified in the context enabled. Oracle or OpenJDK return sockets
  273.      * that have all available protocols enabled already, up to the one
  274.      * specified.
  275.      * <p>
  276.      * <table>
  277.      * <tr>
  278.      * <td>SSLContext.getInstance()</td>
  279.      * <td>OpenJDK</td>
  280.      * <td>IDM JDK</td>
  281.      * </tr>
  282.      * <tr>
  283.      * <td>"TLS"</td>
  284.      * <td>Supported: TLSv1, TLSV1.1, TLSv1.2 (+ TLSv1.3)<br />
  285.      * Enabled: TLSv1, TLSV1.1, TLSv1.2 (+ TLSv1.3)</td>
  286.      * <td>Supported: TLSv1, TLSV1.1, TLSv1.2<br />
  287.      * Enabled: TLSv1</td>
  288.      * </tr>
  289.      * <tr>
  290.      * <td>"TLSv1.2"</td>
  291.      * <td>Supported: TLSv1, TLSV1.1, TLSv1.2<br />
  292.      * Enabled: TLSv1, TLSV1.1, TLSv1.2</td>
  293.      * <td>Supported: TLSv1, TLSV1.1, TLSv1.2<br />
  294.      * Enabled: TLSv1.2</td>
  295.      * </tr>
  296.      * </table>
  297.      *
  298.      * @param socket
  299.      *            to configure
  300.      * @see <a href=
  301.      *      "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
  302.      *      of SSLContext.getInstance("TLS") on IBM JDK</a>
  303.      * @see <a href=
  304.      *      "https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html#InstallationAndCustomization">Customizing
  305.      *      JSSE about https.protocols</a>
  306.      * @since 5.7
  307.      */
  308.     public static void configureTLS(SSLSocket socket) {
  309.         // 1. Enable all available TLS protocol versions
  310.         Set<String> enabled = new LinkedHashSet<>(
  311.                 Arrays.asList(socket.getEnabledProtocols()));
  312.         for (String s : socket.getSupportedProtocols()) {
  313.             if (s.startsWith("TLS")) { //$NON-NLS-1$
  314.                 enabled.add(s);
  315.             }
  316.         }
  317.         // 2. Respect the https.protocols system property
  318.         Set<String> configured = getConfiguredProtocols();
  319.         if (!configured.isEmpty()) {
  320.             enabled.retainAll(configured);
  321.         }
  322.         if (!enabled.isEmpty()) {
  323.             socket.setEnabledProtocols(enabled.toArray(new String[0]));
  324.         }
  325.     }

  326.     private static Set<String> getConfiguredProtocols() {
  327.         Set<String> result = configuredHttpsProtocols;
  328.         if (result == null) {
  329.             String configured = getProperty("https.protocols"); //$NON-NLS-1$
  330.             if (StringUtils.isEmptyOrNull(configured)) {
  331.                 result = Collections.emptySet();
  332.             } else {
  333.                 result = new LinkedHashSet<>(
  334.                         Arrays.asList(configured.split("\\s*,\\s*"))); //$NON-NLS-1$
  335.             }
  336.             configuredHttpsProtocols = result;
  337.         }
  338.         return result;
  339.     }

  340.     private static String getProperty(String property) {
  341.         try {
  342.             return SystemReader.getInstance().getProperty(property);
  343.         } catch (SecurityException e) {
  344.             LOG.warn(JGitText.get().failedReadHttpsProtocols, e);
  345.             return null;
  346.         }
  347.     }

  348.     /**
  349.      * Scan a RFC 7230 token as it appears in HTTP headers.
  350.      *
  351.      * @param header
  352.      *            to scan in
  353.      * @param from
  354.      *            index in {@code header} to start scanning at
  355.      * @return the index after the token, that is, on the first non-token
  356.      *         character or {@code header.length}
  357.      * @throws IndexOutOfBoundsException
  358.      *             if {@code from < 0} or {@code from > header.length()}
  359.      *
  360.      * @see <a href="https://tools.ietf.org/html/rfc7230#appendix-B">RFC 7230,
  361.      *      Appendix B: Collected Grammar; "token" production</a>
  362.      * @since 5.10
  363.      */
  364.     public static int scanToken(String header, int from) {
  365.         int length = header.length();
  366.         int i = from;
  367.         if (i < 0 || i > length) {
  368.             throw new IndexOutOfBoundsException();
  369.         }
  370.         while (i < length) {
  371.             char c = header.charAt(i);
  372.             switch (c) {
  373.             case '!':
  374.             case '#':
  375.             case '$':
  376.             case '%':
  377.             case '&':
  378.             case '\'':
  379.             case '*':
  380.             case '+':
  381.             case '-':
  382.             case '.':
  383.             case '^':
  384.             case '_':
  385.             case '`':
  386.             case '|':
  387.             case '~':
  388.             case '0':
  389.             case '1':
  390.             case '2':
  391.             case '3':
  392.             case '4':
  393.             case '5':
  394.             case '6':
  395.             case '7':
  396.             case '8':
  397.             case '9':
  398.                 i++;
  399.                 break;
  400.             default:
  401.                 if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
  402.                     i++;
  403.                     break;
  404.                 }
  405.                 return i;
  406.             }
  407.         }
  408.         return i;
  409.     }

  410.     private HttpSupport() {
  411.         // Utility class only.
  412.     }
  413. }