HttpConfig.java

  1. /*
  2.  * Copyright (C) 2008, 2010, Google Inc.
  3.  * Copyright (C) 2017, Thomas Wolf <thomas.wolf@paranor.ch> 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.transport;

  12. import java.io.IOException;
  13. import java.net.URISyntaxException;
  14. import java.text.MessageFormat;
  15. import java.util.Arrays;
  16. import java.util.Collections;
  17. import java.util.List;
  18. import java.util.Set;
  19. import java.util.function.Supplier;

  20. import org.eclipse.jgit.annotations.NonNull;
  21. import org.eclipse.jgit.errors.ConfigInvalidException;
  22. import org.eclipse.jgit.internal.JGitText;
  23. import org.eclipse.jgit.lib.Config;
  24. import org.eclipse.jgit.lib.StoredConfig;
  25. import org.eclipse.jgit.util.StringUtils;
  26. import org.eclipse.jgit.util.SystemReader;
  27. import org.slf4j.Logger;
  28. import org.slf4j.LoggerFactory;

  29. /**
  30.  * A representation of the "http.*" config values in a git
  31.  * {@link org.eclipse.jgit.lib.Config}. git provides for setting values for
  32.  * specific URLs through "http.&lt;url&gt;.*" subsections. git always considers
  33.  * only the initial original URL for such settings, not any redirected URL.
  34.  *
  35.  * @since 4.9
  36.  */
  37. public class HttpConfig {

  38.     private static final Logger LOG = LoggerFactory.getLogger(HttpConfig.class);

  39.     private static final String FTP = "ftp"; //$NON-NLS-1$

  40.     /** git config section key for http settings. */
  41.     public static final String HTTP = "http"; //$NON-NLS-1$

  42.     /** git config key for the "followRedirects" setting. */
  43.     public static final String FOLLOW_REDIRECTS_KEY = "followRedirects"; //$NON-NLS-1$

  44.     /** git config key for the "maxRedirects" setting. */
  45.     public static final String MAX_REDIRECTS_KEY = "maxRedirects"; //$NON-NLS-1$

  46.     /** git config key for the "postBuffer" setting. */
  47.     public static final String POST_BUFFER_KEY = "postBuffer"; //$NON-NLS-1$

  48.     /** git config key for the "sslVerify" setting. */
  49.     public static final String SSL_VERIFY_KEY = "sslVerify"; //$NON-NLS-1$

  50.     /**
  51.      * git config key for the "userAgent" setting.
  52.      *
  53.      * @since 5.10
  54.      */
  55.     public static final String USER_AGENT = "userAgent"; //$NON-NLS-1$

  56.     /**
  57.      * git config key for the "extraHeader" setting.
  58.      *
  59.      * @since 5.10
  60.      */
  61.     public static final String EXTRA_HEADER = "extraHeader"; //$NON-NLS-1$

  62.     /**
  63.      * git config key for the "cookieFile" setting.
  64.      *
  65.      * @since 5.4
  66.      */
  67.     public static final String COOKIE_FILE_KEY = "cookieFile"; //$NON-NLS-1$

  68.     /**
  69.      * git config key for the "saveCookies" setting.
  70.      *
  71.      * @since 5.4
  72.      */
  73.     public static final String SAVE_COOKIES_KEY = "saveCookies"; //$NON-NLS-1$

  74.     /**
  75.      * Custom JGit config key which holds the maximum number of cookie files to
  76.      * keep in the cache.
  77.      *
  78.      * @since 5.4
  79.      */
  80.     public static final String COOKIE_FILE_CACHE_LIMIT_KEY = "cookieFileCacheLimit"; //$NON-NLS-1$

  81.     private static final int DEFAULT_COOKIE_FILE_CACHE_LIMIT = 10;

  82.     private static final String MAX_REDIRECT_SYSTEM_PROPERTY = "http.maxRedirects"; //$NON-NLS-1$

  83.     private static final int DEFAULT_MAX_REDIRECTS = 5;

  84.     private static final int MAX_REDIRECTS = (new Supplier<Integer>() {

  85.         @Override
  86.         public Integer get() {
  87.             String rawValue = SystemReader.getInstance()
  88.                     .getProperty(MAX_REDIRECT_SYSTEM_PROPERTY);
  89.             Integer value = Integer.valueOf(DEFAULT_MAX_REDIRECTS);
  90.             if (rawValue != null) {
  91.                 try {
  92.                     value = Integer.valueOf(Integer.parseUnsignedInt(rawValue));
  93.                 } catch (NumberFormatException e) {
  94.                     LOG.warn(MessageFormat.format(
  95.                             JGitText.get().invalidSystemProperty,
  96.                             MAX_REDIRECT_SYSTEM_PROPERTY, rawValue, value));
  97.                 }
  98.             }
  99.             return value;
  100.         }
  101.     }).get().intValue();

  102.     private static final String ENV_HTTP_USER_AGENT = "GIT_HTTP_USER_AGENT"; //$NON-NLS-1$

  103.     /**
  104.      * Config values for http.followRedirect.
  105.      */
  106.     public enum HttpRedirectMode implements Config.ConfigEnum {

  107.         /** Always follow redirects (up to the http.maxRedirects limit). */
  108.         TRUE("true"), //$NON-NLS-1$
  109.         /**
  110.          * Only follow redirects on the initial GET request. This is the
  111.          * default.
  112.          */
  113.         INITIAL("initial"), //$NON-NLS-1$
  114.         /** Never follow redirects. */
  115.         FALSE("false"); //$NON-NLS-1$

  116.         private final String configValue;

  117.         private HttpRedirectMode(String configValue) {
  118.             this.configValue = configValue;
  119.         }

  120.         @Override
  121.         public String toConfigValue() {
  122.             return configValue;
  123.         }

  124.         @Override
  125.         public boolean matchConfigValue(String s) {
  126.             return configValue.equals(s);
  127.         }
  128.     }

  129.     private int postBuffer;

  130.     private boolean sslVerify;

  131.     private HttpRedirectMode followRedirects;

  132.     private int maxRedirects;

  133.     private String userAgent;

  134.     private List<String> extraHeaders;

  135.     private String cookieFile;

  136.     private boolean saveCookies;

  137.     private int cookieFileCacheLimit;

  138.     /**
  139.      * Get the "http.postBuffer" setting
  140.      *
  141.      * @return the value of the "http.postBuffer" setting
  142.      */
  143.     public int getPostBuffer() {
  144.         return postBuffer;
  145.     }

  146.     /**
  147.      * Get the "http.sslVerify" setting
  148.      *
  149.      * @return the value of the "http.sslVerify" setting
  150.      */
  151.     public boolean isSslVerify() {
  152.         return sslVerify;
  153.     }

  154.     /**
  155.      * Get the "http.followRedirects" setting
  156.      *
  157.      * @return the value of the "http.followRedirects" setting
  158.      */
  159.     public HttpRedirectMode getFollowRedirects() {
  160.         return followRedirects;
  161.     }

  162.     /**
  163.      * Get the "http.maxRedirects" setting
  164.      *
  165.      * @return the value of the "http.maxRedirects" setting
  166.      */
  167.     public int getMaxRedirects() {
  168.         return maxRedirects;
  169.     }

  170.     /**
  171.      * Get the "http.userAgent" setting
  172.      *
  173.      * @return the value of the "http.userAgent" setting
  174.      * @since 5.10
  175.      */
  176.     public String getUserAgent() {
  177.         return userAgent;
  178.     }

  179.     /**
  180.      * Get the "http.extraHeader" setting
  181.      *
  182.      * @return the value of the "http.extraHeader" setting
  183.      * @since 5.10
  184.      */
  185.     @NonNull
  186.     public List<String> getExtraHeaders() {
  187.         return extraHeaders == null ? Collections.emptyList() : extraHeaders;
  188.     }

  189.     /**
  190.      * Get the "http.cookieFile" setting
  191.      *
  192.      * @return the value of the "http.cookieFile" setting
  193.      *
  194.      * @since 5.4
  195.      */
  196.     public String getCookieFile() {
  197.         return cookieFile;
  198.     }

  199.     /**
  200.      * Get the "http.saveCookies" setting
  201.      *
  202.      * @return the value of the "http.saveCookies" setting
  203.      *
  204.      * @since 5.4
  205.      */
  206.     public boolean getSaveCookies() {
  207.         return saveCookies;
  208.     }

  209.     /**
  210.      * Get the "http.cookieFileCacheLimit" setting (gives the maximum number of
  211.      * cookie files to keep in the LRU cache)
  212.      *
  213.      * @return the value of the "http.cookieFileCacheLimit" setting
  214.      *
  215.      * @since 5.4
  216.      */
  217.     public int getCookieFileCacheLimit() {
  218.         return cookieFileCacheLimit;
  219.     }

  220.     /**
  221.      * Creates a new {@link org.eclipse.jgit.transport.HttpConfig} tailored to
  222.      * the given {@link org.eclipse.jgit.transport.URIish}.
  223.      *
  224.      * @param config
  225.      *            to read the {@link org.eclipse.jgit.transport.HttpConfig} from
  226.      * @param uri
  227.      *            to get the configuration values for
  228.      */
  229.     public HttpConfig(Config config, URIish uri) {
  230.         init(config, uri);
  231.     }

  232.     /**
  233.      * Creates a {@link org.eclipse.jgit.transport.HttpConfig} that reads values
  234.      * solely from the user config.
  235.      *
  236.      * @param uri
  237.      *            to get the configuration values for
  238.      */
  239.     public HttpConfig(URIish uri) {
  240.         StoredConfig userConfig = null;
  241.         try {
  242.             userConfig = SystemReader.getInstance().getUserConfig();
  243.         } catch (IOException | ConfigInvalidException e) {
  244.             // Log it and then work with default values.
  245.             LOG.error(e.getMessage(), e);
  246.             init(new Config(), uri);
  247.             return;
  248.         }
  249.         init(userConfig, uri);
  250.     }

  251.     private void init(Config config, URIish uri) {
  252.         // Set defaults from the section first
  253.         int postBufferSize = config.getInt(HTTP, POST_BUFFER_KEY,
  254.                 1 * 1024 * 1024);
  255.         boolean sslVerifyFlag = config.getBoolean(HTTP, SSL_VERIFY_KEY, true);
  256.         HttpRedirectMode followRedirectsMode = config.getEnum(
  257.                 HttpRedirectMode.values(), HTTP, null,
  258.                 FOLLOW_REDIRECTS_KEY, HttpRedirectMode.INITIAL);
  259.         int redirectLimit = config.getInt(HTTP, MAX_REDIRECTS_KEY,
  260.                 MAX_REDIRECTS);
  261.         if (redirectLimit < 0) {
  262.             redirectLimit = MAX_REDIRECTS;
  263.         }
  264.         String agent = config.getString(HTTP, null, USER_AGENT);
  265.         if (agent != null) {
  266.             agent = UserAgent.clean(agent);
  267.         }
  268.         userAgent = agent;
  269.         String[] headers = config.getStringList(HTTP, null, EXTRA_HEADER);
  270.         // https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpextraHeader
  271.         // "an empty value will reset the extra headers to the empty list."
  272.         int start = findLastEmpty(headers) + 1;
  273.         if (start > 0) {
  274.             headers = Arrays.copyOfRange(headers, start, headers.length);
  275.         }
  276.         extraHeaders = Arrays.asList(headers);
  277.         cookieFile = config.getString(HTTP, null, COOKIE_FILE_KEY);
  278.         saveCookies = config.getBoolean(HTTP, SAVE_COOKIES_KEY, false);
  279.         cookieFileCacheLimit = config.getInt(HTTP, COOKIE_FILE_CACHE_LIMIT_KEY,
  280.                 DEFAULT_COOKIE_FILE_CACHE_LIMIT);
  281.         String match = findMatch(config.getSubsections(HTTP), uri);

  282.         if (match != null) {
  283.             // Override with more specific items
  284.             postBufferSize = config.getInt(HTTP, match, POST_BUFFER_KEY,
  285.                     postBufferSize);
  286.             sslVerifyFlag = config.getBoolean(HTTP, match, SSL_VERIFY_KEY,
  287.                     sslVerifyFlag);
  288.             followRedirectsMode = config.getEnum(HttpRedirectMode.values(),
  289.                     HTTP, match, FOLLOW_REDIRECTS_KEY, followRedirectsMode);
  290.             int newMaxRedirects = config.getInt(HTTP, match, MAX_REDIRECTS_KEY,
  291.                     redirectLimit);
  292.             if (newMaxRedirects >= 0) {
  293.                 redirectLimit = newMaxRedirects;
  294.             }
  295.             String uriSpecificUserAgent = config.getString(HTTP, match,
  296.                     USER_AGENT);
  297.             if (uriSpecificUserAgent != null) {
  298.                 userAgent = UserAgent.clean(uriSpecificUserAgent);
  299.             }
  300.             String[] uriSpecificExtraHeaders = config.getStringList(HTTP, match,
  301.                     EXTRA_HEADER);
  302.             if (uriSpecificExtraHeaders.length > 0) {
  303.                 start = findLastEmpty(uriSpecificExtraHeaders) + 1;
  304.                 if (start > 0) {
  305.                     uriSpecificExtraHeaders = Arrays.copyOfRange(
  306.                             uriSpecificExtraHeaders, start,
  307.                             uriSpecificExtraHeaders.length);
  308.                 }
  309.                 extraHeaders = Arrays.asList(uriSpecificExtraHeaders);
  310.             }
  311.             String urlSpecificCookieFile = config.getString(HTTP, match,
  312.                     COOKIE_FILE_KEY);
  313.             if (urlSpecificCookieFile != null) {
  314.                 cookieFile = urlSpecificCookieFile;
  315.             }
  316.             saveCookies = config.getBoolean(HTTP, match, SAVE_COOKIES_KEY,
  317.                     saveCookies);
  318.         }
  319.         // Environment overrides config
  320.         agent = SystemReader.getInstance().getenv(ENV_HTTP_USER_AGENT);
  321.         if (!StringUtils.isEmptyOrNull(agent)) {
  322.             userAgent = UserAgent.clean(agent);
  323.         }
  324.         postBuffer = postBufferSize;
  325.         sslVerify = sslVerifyFlag;
  326.         followRedirects = followRedirectsMode;
  327.         maxRedirects = redirectLimit;
  328.     }

  329.     private int findLastEmpty(String[] values) {
  330.         for (int i = values.length - 1; i >= 0; i--) {
  331.             if (values[i] == null) {
  332.                 return i;
  333.             }
  334.         }
  335.         return -1;
  336.     }

  337.     /**
  338.      * Determines the best match from a set of subsection names (representing
  339.      * prefix URLs) for the given {@link URIish}.
  340.      *
  341.      * @param names
  342.      *            to match against the {@code uri}
  343.      * @param uri
  344.      *            to find a match for
  345.      * @return the best matching subsection name, or {@code null} if no
  346.      *         subsection matches
  347.      */
  348.     private String findMatch(Set<String> names, URIish uri) {
  349.         String bestMatch = null;
  350.         int bestMatchLength = -1;
  351.         boolean withUser = false;
  352.         String uPath = uri.getPath();
  353.         boolean hasPath = !StringUtils.isEmptyOrNull(uPath);
  354.         if (hasPath) {
  355.             uPath = normalize(uPath);
  356.             if (uPath == null) {
  357.                 // Normalization failed; warning was logged.
  358.                 return null;
  359.             }
  360.         }
  361.         for (String s : names) {
  362.             try {
  363.                 URIish candidate = new URIish(s);
  364.                 // Scheme and host must match case-insensitively
  365.                 if (!compare(uri.getScheme(), candidate.getScheme())
  366.                         || !compare(uri.getHost(), candidate.getHost())) {
  367.                     continue;
  368.                 }
  369.                 // Ports must match after default ports have been substituted
  370.                 if (defaultedPort(uri.getPort(),
  371.                         uri.getScheme()) != defaultedPort(candidate.getPort(),
  372.                                 candidate.getScheme())) {
  373.                     continue;
  374.                 }
  375.                 // User: if present in candidate, must match
  376.                 boolean hasUser = false;
  377.                 if (candidate.getUser() != null) {
  378.                     if (!candidate.getUser().equals(uri.getUser())) {
  379.                         continue;
  380.                     }
  381.                     hasUser = true;
  382.                 }
  383.                 // Path: prefix match, longer is better
  384.                 String cPath = candidate.getPath();
  385.                 int matchLength = -1;
  386.                 if (StringUtils.isEmptyOrNull(cPath)) {
  387.                     matchLength = 0;
  388.                 } else {
  389.                     if (!hasPath) {
  390.                         continue;
  391.                     }
  392.                     // Paths can match only on segments
  393.                     matchLength = segmentCompare(uPath, cPath);
  394.                     if (matchLength < 0) {
  395.                         continue;
  396.                     }
  397.                 }
  398.                 // A longer path match is always preferred even over a user
  399.                 // match. If the path matches are equal, a match with user wins
  400.                 // over a match without user.
  401.                 if (matchLength > bestMatchLength
  402.                         || (!withUser && hasUser && matchLength >= 0
  403.                                 && matchLength == bestMatchLength)) {
  404.                     bestMatch = s;
  405.                     bestMatchLength = matchLength;
  406.                     withUser = hasUser;
  407.                 }
  408.             } catch (URISyntaxException e) {
  409.                 LOG.warn(MessageFormat
  410.                         .format(JGitText.get().httpConfigInvalidURL, s));
  411.             }
  412.         }
  413.         return bestMatch;
  414.     }

  415.     private boolean compare(String a, String b) {
  416.         if (a == null) {
  417.             return b == null;
  418.         }
  419.         return a.equalsIgnoreCase(b);
  420.     }

  421.     private int defaultedPort(int port, String scheme) {
  422.         if (port >= 0) {
  423.             return port;
  424.         }
  425.         if (FTP.equalsIgnoreCase(scheme)) {
  426.             return 21;
  427.         } else if (HTTP.equalsIgnoreCase(scheme)) {
  428.             return 80;
  429.         } else {
  430.             return 443; // https
  431.         }
  432.     }

  433.     static int segmentCompare(String uriPath, String m) {
  434.         // Precondition: !uriPath.isEmpty() && !m.isEmpty(),and u must already
  435.         // be normalized
  436.         String matchPath = normalize(m);
  437.         if (matchPath == null || !uriPath.startsWith(matchPath)) {
  438.             return -1;
  439.         }
  440.         // We can match only on a segment boundary: either both paths are equal,
  441.         // or if matchPath does not end in '/', there is a '/' in uriPath right
  442.         // after the match.
  443.         int uLength = uriPath.length();
  444.         int mLength = matchPath.length();
  445.         if (mLength == uLength || matchPath.charAt(mLength - 1) == '/'
  446.                 || (mLength < uLength && uriPath.charAt(mLength) == '/')) {
  447.             return mLength;
  448.         }
  449.         return -1;
  450.     }

  451.     static String normalize(String path) {
  452.         // C-git resolves . and .. segments
  453.         int i = 0;
  454.         int length = path.length();
  455.         StringBuilder builder = new StringBuilder(length);
  456.         builder.append('/');
  457.         if (length > 0 && path.charAt(0) == '/') {
  458.             i = 1;
  459.         }
  460.         while (i < length) {
  461.             int slash = path.indexOf('/', i);
  462.             if (slash < 0) {
  463.                 slash = length;
  464.             }
  465.             if (slash == i || (slash == i + 1 && path.charAt(i) == '.')) {
  466.                 // Skip /. or also double slashes
  467.             } else if (slash == i + 2 && path.charAt(i) == '.'
  468.                     && path.charAt(i + 1) == '.') {
  469.                 // Remove previous segment if we have "/.."
  470.                 int l = builder.length() - 2; // Skip terminating slash.
  471.                 while (l >= 0 && builder.charAt(l) != '/') {
  472.                     l--;
  473.                 }
  474.                 if (l < 0) {
  475.                     LOG.warn(MessageFormat.format(
  476.                             JGitText.get().httpConfigCannotNormalizeURL, path));
  477.                     return null;
  478.                 }
  479.                 builder.setLength(l + 1);
  480.             } else {
  481.                 // Include the slash, if any
  482.                 builder.append(path, i, Math.min(length, slash + 1));
  483.             }
  484.             i = slash + 1;
  485.         }
  486.         if (builder.length() > 1 && builder.charAt(builder.length() - 1) == '/'
  487.                 && length > 0 && path.charAt(length - 1) != '/') {
  488.             // . or .. normalization left a trailing slash when the original
  489.             // path had none at the end
  490.             builder.setLength(builder.length() - 1);
  491.         }
  492.         return builder.toString();
  493.     }
  494. }