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.      * Translates the provided URL into application/x-www-form-urlencoded
  149.      * format.
  150.      *
  151.      * @param url
  152.      *            The URL to translate.
  153.      * @param keepPathSlash
  154.      *            Whether or not to keep "/" in the URL (i.e. don't translate
  155.      *            them to "%2F").
  156.      *
  157.      * @return The translated URL.
  158.      * @since 5.13.1
  159.      */
  160.     public static String urlEncode(String url, boolean keepPathSlash) {
  161.         String encoded;
  162.         try {
  163.             encoded = URLEncoder.encode(url, UTF_8.name());
  164.         } catch (UnsupportedEncodingException e) {
  165.             throw new RuntimeException(JGitText.get().couldNotURLEncodeToUTF8,
  166.                     e);
  167.         }
  168.         if (keepPathSlash) {
  169.             encoded = encoded.replace("%2F", "/"); //$NON-NLS-1$ //$NON-NLS-2$
  170.         }
  171.         return encoded;
  172.     }

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

  201.     /**
  202.      * Get the HTTP response code from the request.
  203.      * <p>
  204.      * Roughly the same as <code>c.getResponseCode()</code> but the
  205.      * ConnectException is translated to be more understandable.
  206.      *
  207.      * @param c
  208.      *            connection the code should be obtained from.
  209.      * @return r HTTP status code, usually 200 to indicate success. See
  210.      *         {@link org.eclipse.jgit.transport.http.HttpConnection} for other
  211.      *         defined constants.
  212.      * @throws java.io.IOException
  213.      *             communications error prevented obtaining the response code.
  214.      */
  215.     public static int response(java.net.HttpURLConnection c)
  216.             throws IOException {
  217.         try {
  218.             return c.getResponseCode();
  219.         } catch (ConnectException ce) {
  220.             final URL url = c.getURL();
  221.             final String host = (url == null) ? "<null>" : url.getHost(); //$NON-NLS-1$
  222.             // The standard J2SE error message is not very useful.
  223.             //
  224.             if ("Connection timed out: connect".equals(ce.getMessage())) //$NON-NLS-1$
  225.                 throw new ConnectException(MessageFormat.format(
  226.                         JGitText.get().connectionTimeOut, host));
  227.             throw new ConnectException(ce.getMessage() + " " + host); //$NON-NLS-1$
  228.         }
  229.     }

  230.     /**
  231.      * Extract a HTTP header from the response.
  232.      *
  233.      * @param c
  234.      *            connection the header should be obtained from.
  235.      * @param headerName
  236.      *            the header name
  237.      * @return the header value
  238.      * @throws java.io.IOException
  239.      *             communications error prevented obtaining the header.
  240.      * @since 4.7
  241.      */
  242.     public static String responseHeader(final HttpConnection c,
  243.             final String headerName) throws IOException {
  244.         return c.getHeaderField(headerName);
  245.     }

  246.     /**
  247.      * Determine the proxy server (if any) needed to obtain a URL.
  248.      *
  249.      * @param proxySelector
  250.      *            proxy support for the caller.
  251.      * @param u
  252.      *            location of the server caller wants to talk to.
  253.      * @return proxy to communicate with the supplied URL.
  254.      * @throws java.net.ConnectException
  255.      *             the proxy could not be computed as the supplied URL could not
  256.      *             be read. This failure should never occur.
  257.      */
  258.     public static Proxy proxyFor(ProxySelector proxySelector, URL u)
  259.             throws ConnectException {
  260.         try {
  261.             URI uri = new URI(u.getProtocol(), null, u.getHost(), u.getPort(),
  262.                     null, null, null);
  263.             return proxySelector.select(uri).get(0);
  264.         } catch (URISyntaxException e) {
  265.             final ConnectException err;
  266.             err = new ConnectException(MessageFormat.format(JGitText.get().cannotDetermineProxyFor, u));
  267.             err.initCause(e);
  268.             throw err;
  269.         }
  270.     }

  271.     /**
  272.      * Disable SSL and hostname verification for given HTTP connection
  273.      *
  274.      * @param conn
  275.      *            a {@link org.eclipse.jgit.transport.http.HttpConnection}
  276.      *            object.
  277.      * @throws java.io.IOException
  278.      * @since 4.3
  279.      */
  280.     public static void disableSslVerify(HttpConnection conn)
  281.             throws IOException {
  282.         TrustManager[] trustAllCerts = {
  283.                 new NoCheckX509TrustManager() };
  284.         try {
  285.             conn.configure(null, trustAllCerts, null);
  286.             conn.setHostnameVerifier((name, session) -> true);
  287.         } catch (KeyManagementException | NoSuchAlgorithmException e) {
  288.             throw new IOException(e.getMessage(), e);
  289.         }
  290.     }

  291.     /**
  292.      * Enables all supported TLS protocol versions on the socket given. If
  293.      * system property "https.protocols" is set, only protocols specified there
  294.      * are enabled.
  295.      * <p>
  296.      * This is primarily a mechanism to deal with using TLS on IBM JDK. IBM JDK
  297.      * returns sockets that support all TLS protocol versions but have only the
  298.      * one specified in the context enabled. Oracle or OpenJDK return sockets
  299.      * that have all available protocols enabled already, up to the one
  300.      * specified.
  301.      * <p>
  302.      * <table>
  303.      * <tr>
  304.      * <td>SSLContext.getInstance()</td>
  305.      * <td>OpenJDK</td>
  306.      * <td>IDM JDK</td>
  307.      * </tr>
  308.      * <tr>
  309.      * <td>"TLS"</td>
  310.      * <td>Supported: TLSv1, TLSV1.1, TLSv1.2 (+ TLSv1.3)<br />
  311.      * Enabled: TLSv1, TLSV1.1, TLSv1.2 (+ TLSv1.3)</td>
  312.      * <td>Supported: TLSv1, TLSV1.1, TLSv1.2<br />
  313.      * Enabled: TLSv1</td>
  314.      * </tr>
  315.      * <tr>
  316.      * <td>"TLSv1.2"</td>
  317.      * <td>Supported: TLSv1, TLSV1.1, TLSv1.2<br />
  318.      * Enabled: TLSv1, TLSV1.1, TLSv1.2</td>
  319.      * <td>Supported: TLSv1, TLSV1.1, TLSv1.2<br />
  320.      * Enabled: TLSv1.2</td>
  321.      * </tr>
  322.      * </table>
  323.      *
  324.      * @param socket
  325.      *            to configure
  326.      * @see <a href=
  327.      *      "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
  328.      *      of SSLContext.getInstance("TLS") on IBM JDK</a>
  329.      * @see <a href=
  330.      *      "https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html#InstallationAndCustomization">Customizing
  331.      *      JSSE about https.protocols</a>
  332.      * @since 5.7
  333.      */
  334.     public static void configureTLS(SSLSocket socket) {
  335.         // 1. Enable all available TLS protocol versions
  336.         Set<String> enabled = new LinkedHashSet<>(
  337.                 Arrays.asList(socket.getEnabledProtocols()));
  338.         for (String s : socket.getSupportedProtocols()) {
  339.             if (s.startsWith("TLS")) { //$NON-NLS-1$
  340.                 enabled.add(s);
  341.             }
  342.         }
  343.         // 2. Respect the https.protocols system property
  344.         Set<String> configured = getConfiguredProtocols();
  345.         if (!configured.isEmpty()) {
  346.             enabled.retainAll(configured);
  347.         }
  348.         if (!enabled.isEmpty()) {
  349.             socket.setEnabledProtocols(enabled.toArray(new String[0]));
  350.         }
  351.     }

  352.     private static Set<String> getConfiguredProtocols() {
  353.         Set<String> result = configuredHttpsProtocols;
  354.         if (result == null) {
  355.             String configured = getProperty("https.protocols"); //$NON-NLS-1$
  356.             if (StringUtils.isEmptyOrNull(configured)) {
  357.                 result = Collections.emptySet();
  358.             } else {
  359.                 result = new LinkedHashSet<>(
  360.                         Arrays.asList(configured.split("\\s*,\\s*"))); //$NON-NLS-1$
  361.             }
  362.             configuredHttpsProtocols = result;
  363.         }
  364.         return result;
  365.     }

  366.     private static String getProperty(String property) {
  367.         try {
  368.             return SystemReader.getInstance().getProperty(property);
  369.         } catch (SecurityException e) {
  370.             LOG.warn(JGitText.get().failedReadHttpsProtocols, e);
  371.             return null;
  372.         }
  373.     }

  374.     /**
  375.      * Scan a RFC 7230 token as it appears in HTTP headers.
  376.      *
  377.      * @param header
  378.      *            to scan in
  379.      * @param from
  380.      *            index in {@code header} to start scanning at
  381.      * @return the index after the token, that is, on the first non-token
  382.      *         character or {@code header.length}
  383.      * @throws IndexOutOfBoundsException
  384.      *             if {@code from < 0} or {@code from > header.length()}
  385.      *
  386.      * @see <a href="https://tools.ietf.org/html/rfc7230#appendix-B">RFC 7230,
  387.      *      Appendix B: Collected Grammar; "token" production</a>
  388.      * @since 5.10
  389.      */
  390.     public static int scanToken(String header, int from) {
  391.         int length = header.length();
  392.         int i = from;
  393.         if (i < 0 || i > length) {
  394.             throw new IndexOutOfBoundsException();
  395.         }
  396.         while (i < length) {
  397.             char c = header.charAt(i);
  398.             switch (c) {
  399.             case '!':
  400.             case '#':
  401.             case '$':
  402.             case '%':
  403.             case '&':
  404.             case '\'':
  405.             case '*':
  406.             case '+':
  407.             case '-':
  408.             case '.':
  409.             case '^':
  410.             case '_':
  411.             case '`':
  412.             case '|':
  413.             case '~':
  414.             case '0':
  415.             case '1':
  416.             case '2':
  417.             case '3':
  418.             case '4':
  419.             case '5':
  420.             case '6':
  421.             case '7':
  422.             case '8':
  423.             case '9':
  424.                 i++;
  425.                 break;
  426.             default:
  427.                 if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
  428.                     i++;
  429.                     break;
  430.                 }
  431.                 return i;
  432.             }
  433.         }
  434.         return i;
  435.     }

  436.     private HttpSupport() {
  437.         // Utility class only.
  438.     }
  439. }