blob: dc82f46197c81c04b4d212ee9c478a4d5f140a32 [file] [log] [blame]
/*
* Copyright (C) 2008, 2010, Google Inc.
* Copyright (C) 2017, Thomas Wolf <thomas.wolf@paranor.ch> 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.transport;
import java.io.IOException;
import java.net.URISyntaxException;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.util.StringUtils;
import org.eclipse.jgit.util.SystemReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A representation of the "http.*" config values in a git
* {@link org.eclipse.jgit.lib.Config}. git provides for setting values for
* specific URLs through "http.&lt;url&gt;.*" subsections. git always considers
* only the initial original URL for such settings, not any redirected URL.
*
* @since 4.9
*/
public class HttpConfig {
private static final Logger LOG = LoggerFactory.getLogger(HttpConfig.class);
private static final String FTP = "ftp"; //$NON-NLS-1$
/** git config section key for http settings. */
public static final String HTTP = "http"; //$NON-NLS-1$
/** git config key for the "followRedirects" setting. */
public static final String FOLLOW_REDIRECTS_KEY = "followRedirects"; //$NON-NLS-1$
/** git config key for the "maxRedirects" setting. */
public static final String MAX_REDIRECTS_KEY = "maxRedirects"; //$NON-NLS-1$
/** git config key for the "postBuffer" setting. */
public static final String POST_BUFFER_KEY = "postBuffer"; //$NON-NLS-1$
/** git config key for the "sslVerify" setting. */
public static final String SSL_VERIFY_KEY = "sslVerify"; //$NON-NLS-1$
/**
* git config key for the "userAgent" setting.
*
* @since 5.10
*/
public static final String USER_AGENT = "userAgent"; //$NON-NLS-1$
/**
* git config key for the "extraHeader" setting.
*
* @since 5.10
*/
public static final String EXTRA_HEADER = "extraHeader"; //$NON-NLS-1$
/**
* git config key for the "cookieFile" setting.
*
* @since 5.4
*/
public static final String COOKIE_FILE_KEY = "cookieFile"; //$NON-NLS-1$
/**
* git config key for the "saveCookies" setting.
*
* @since 5.4
*/
public static final String SAVE_COOKIES_KEY = "saveCookies"; //$NON-NLS-1$
/**
* Custom JGit config key which holds the maximum number of cookie files to
* keep in the cache.
*
* @since 5.4
*/
public static final String COOKIE_FILE_CACHE_LIMIT_KEY = "cookieFileCacheLimit"; //$NON-NLS-1$
private static final int DEFAULT_COOKIE_FILE_CACHE_LIMIT = 10;
private static final String MAX_REDIRECT_SYSTEM_PROPERTY = "http.maxRedirects"; //$NON-NLS-1$
private static final int DEFAULT_MAX_REDIRECTS = 5;
private static final int MAX_REDIRECTS = (new Supplier<Integer>() {
@Override
public Integer get() {
String rawValue = SystemReader.getInstance()
.getProperty(MAX_REDIRECT_SYSTEM_PROPERTY);
Integer value = Integer.valueOf(DEFAULT_MAX_REDIRECTS);
if (rawValue != null) {
try {
value = Integer.valueOf(Integer.parseUnsignedInt(rawValue));
} catch (NumberFormatException e) {
LOG.warn(MessageFormat.format(
JGitText.get().invalidSystemProperty,
MAX_REDIRECT_SYSTEM_PROPERTY, rawValue, value));
}
}
return value;
}
}).get().intValue();
private static final String ENV_HTTP_USER_AGENT = "GIT_HTTP_USER_AGENT"; //$NON-NLS-1$
/**
* Config values for http.followRedirect.
*/
public enum HttpRedirectMode implements Config.ConfigEnum {
/** Always follow redirects (up to the http.maxRedirects limit). */
TRUE("true"), //$NON-NLS-1$
/**
* Only follow redirects on the initial GET request. This is the
* default.
*/
INITIAL("initial"), //$NON-NLS-1$
/** Never follow redirects. */
FALSE("false"); //$NON-NLS-1$
private final String configValue;
private HttpRedirectMode(String configValue) {
this.configValue = configValue;
}
@Override
public String toConfigValue() {
return configValue;
}
@Override
public boolean matchConfigValue(String s) {
return configValue.equals(s);
}
}
private int postBuffer;
private boolean sslVerify;
private HttpRedirectMode followRedirects;
private int maxRedirects;
private String userAgent;
private List<String> extraHeaders;
private String cookieFile;
private boolean saveCookies;
private int cookieFileCacheLimit;
/**
* Get the "http.postBuffer" setting
*
* @return the value of the "http.postBuffer" setting
*/
public int getPostBuffer() {
return postBuffer;
}
/**
* Get the "http.sslVerify" setting
*
* @return the value of the "http.sslVerify" setting
*/
public boolean isSslVerify() {
return sslVerify;
}
/**
* Get the "http.followRedirects" setting
*
* @return the value of the "http.followRedirects" setting
*/
public HttpRedirectMode getFollowRedirects() {
return followRedirects;
}
/**
* Get the "http.maxRedirects" setting
*
* @return the value of the "http.maxRedirects" setting
*/
public int getMaxRedirects() {
return maxRedirects;
}
/**
* Get the "http.userAgent" setting
*
* @return the value of the "http.userAgent" setting
* @since 5.10
*/
public String getUserAgent() {
return userAgent;
}
/**
* Get the "http.extraHeader" setting
*
* @return the value of the "http.extraHeader" setting
* @since 5.10
*/
@NonNull
public List<String> getExtraHeaders() {
return extraHeaders == null ? Collections.emptyList() : extraHeaders;
}
/**
* Get the "http.cookieFile" setting
*
* @return the value of the "http.cookieFile" setting
*
* @since 5.4
*/
public String getCookieFile() {
return cookieFile;
}
/**
* Get the "http.saveCookies" setting
*
* @return the value of the "http.saveCookies" setting
*
* @since 5.4
*/
public boolean getSaveCookies() {
return saveCookies;
}
/**
* Get the "http.cookieFileCacheLimit" setting (gives the maximum number of
* cookie files to keep in the LRU cache)
*
* @return the value of the "http.cookieFileCacheLimit" setting
*
* @since 5.4
*/
public int getCookieFileCacheLimit() {
return cookieFileCacheLimit;
}
/**
* Creates a new {@link org.eclipse.jgit.transport.HttpConfig} tailored to
* the given {@link org.eclipse.jgit.transport.URIish}.
*
* @param config
* to read the {@link org.eclipse.jgit.transport.HttpConfig} from
* @param uri
* to get the configuration values for
*/
public HttpConfig(Config config, URIish uri) {
init(config, uri);
}
/**
* Creates a {@link org.eclipse.jgit.transport.HttpConfig} that reads values
* solely from the user config.
*
* @param uri
* to get the configuration values for
*/
public HttpConfig(URIish uri) {
StoredConfig userConfig = null;
try {
userConfig = SystemReader.getInstance().getUserConfig();
} catch (IOException | ConfigInvalidException e) {
// Log it and then work with default values.
LOG.error(e.getMessage(), e);
init(new Config(), uri);
return;
}
init(userConfig, uri);
}
private void init(Config config, URIish uri) {
// Set defaults from the section first
int postBufferSize = config.getInt(HTTP, POST_BUFFER_KEY,
1 * 1024 * 1024);
boolean sslVerifyFlag = config.getBoolean(HTTP, SSL_VERIFY_KEY, true);
HttpRedirectMode followRedirectsMode = config.getEnum(
HttpRedirectMode.values(), HTTP, null,
FOLLOW_REDIRECTS_KEY, HttpRedirectMode.INITIAL);
int redirectLimit = config.getInt(HTTP, MAX_REDIRECTS_KEY,
MAX_REDIRECTS);
if (redirectLimit < 0) {
redirectLimit = MAX_REDIRECTS;
}
String agent = config.getString(HTTP, null, USER_AGENT);
if (agent != null) {
agent = UserAgent.clean(agent);
}
userAgent = agent;
String[] headers = config.getStringList(HTTP, null, EXTRA_HEADER);
// https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpextraHeader
// "an empty value will reset the extra headers to the empty list."
int start = findLastEmpty(headers) + 1;
if (start > 0) {
headers = Arrays.copyOfRange(headers, start, headers.length);
}
extraHeaders = Arrays.asList(headers);
cookieFile = config.getString(HTTP, null, COOKIE_FILE_KEY);
saveCookies = config.getBoolean(HTTP, SAVE_COOKIES_KEY, false);
cookieFileCacheLimit = config.getInt(HTTP, COOKIE_FILE_CACHE_LIMIT_KEY,
DEFAULT_COOKIE_FILE_CACHE_LIMIT);
String match = findMatch(config.getSubsections(HTTP), uri);
if (match != null) {
// Override with more specific items
postBufferSize = config.getInt(HTTP, match, POST_BUFFER_KEY,
postBufferSize);
sslVerifyFlag = config.getBoolean(HTTP, match, SSL_VERIFY_KEY,
sslVerifyFlag);
followRedirectsMode = config.getEnum(HttpRedirectMode.values(),
HTTP, match, FOLLOW_REDIRECTS_KEY, followRedirectsMode);
int newMaxRedirects = config.getInt(HTTP, match, MAX_REDIRECTS_KEY,
redirectLimit);
if (newMaxRedirects >= 0) {
redirectLimit = newMaxRedirects;
}
String uriSpecificUserAgent = config.getString(HTTP, match,
USER_AGENT);
if (uriSpecificUserAgent != null) {
userAgent = UserAgent.clean(uriSpecificUserAgent);
}
String[] uriSpecificExtraHeaders = config.getStringList(HTTP, match,
EXTRA_HEADER);
if (uriSpecificExtraHeaders.length > 0) {
start = findLastEmpty(uriSpecificExtraHeaders) + 1;
if (start > 0) {
uriSpecificExtraHeaders = Arrays.copyOfRange(
uriSpecificExtraHeaders, start,
uriSpecificExtraHeaders.length);
}
extraHeaders = Arrays.asList(uriSpecificExtraHeaders);
}
String urlSpecificCookieFile = config.getString(HTTP, match,
COOKIE_FILE_KEY);
if (urlSpecificCookieFile != null) {
cookieFile = urlSpecificCookieFile;
}
saveCookies = config.getBoolean(HTTP, match, SAVE_COOKIES_KEY,
saveCookies);
}
// Environment overrides config
agent = SystemReader.getInstance().getenv(ENV_HTTP_USER_AGENT);
if (!StringUtils.isEmptyOrNull(agent)) {
userAgent = UserAgent.clean(agent);
}
postBuffer = postBufferSize;
sslVerify = sslVerifyFlag;
followRedirects = followRedirectsMode;
maxRedirects = redirectLimit;
}
private int findLastEmpty(String[] values) {
for (int i = values.length - 1; i >= 0; i--) {
if (values[i] == null) {
return i;
}
}
return -1;
}
/**
* Determines the best match from a set of subsection names (representing
* prefix URLs) for the given {@link URIish}.
*
* @param names
* to match against the {@code uri}
* @param uri
* to find a match for
* @return the best matching subsection name, or {@code null} if no
* subsection matches
*/
private String findMatch(Set<String> names, URIish uri) {
String bestMatch = null;
int bestMatchLength = -1;
boolean withUser = false;
String uPath = uri.getPath();
boolean hasPath = !StringUtils.isEmptyOrNull(uPath);
if (hasPath) {
uPath = normalize(uPath);
if (uPath == null) {
// Normalization failed; warning was logged.
return null;
}
}
for (String s : names) {
try {
URIish candidate = new URIish(s);
// Scheme and host must match case-insensitively
if (!compare(uri.getScheme(), candidate.getScheme())
|| !compare(uri.getHost(), candidate.getHost())) {
continue;
}
// Ports must match after default ports have been substituted
if (defaultedPort(uri.getPort(),
uri.getScheme()) != defaultedPort(candidate.getPort(),
candidate.getScheme())) {
continue;
}
// User: if present in candidate, must match
boolean hasUser = false;
if (candidate.getUser() != null) {
if (!candidate.getUser().equals(uri.getUser())) {
continue;
}
hasUser = true;
}
// Path: prefix match, longer is better
String cPath = candidate.getPath();
int matchLength = -1;
if (StringUtils.isEmptyOrNull(cPath)) {
matchLength = 0;
} else {
if (!hasPath) {
continue;
}
// Paths can match only on segments
matchLength = segmentCompare(uPath, cPath);
if (matchLength < 0) {
continue;
}
}
// A longer path match is always preferred even over a user
// match. If the path matches are equal, a match with user wins
// over a match without user.
if (matchLength > bestMatchLength
|| (!withUser && hasUser && matchLength >= 0
&& matchLength == bestMatchLength)) {
bestMatch = s;
bestMatchLength = matchLength;
withUser = hasUser;
}
} catch (URISyntaxException e) {
LOG.warn(MessageFormat
.format(JGitText.get().httpConfigInvalidURL, s));
}
}
return bestMatch;
}
private boolean compare(String a, String b) {
if (a == null) {
return b == null;
}
return a.equalsIgnoreCase(b);
}
private int defaultedPort(int port, String scheme) {
if (port >= 0) {
return port;
}
if (FTP.equalsIgnoreCase(scheme)) {
return 21;
} else if (HTTP.equalsIgnoreCase(scheme)) {
return 80;
} else {
return 443; // https
}
}
static int segmentCompare(String uriPath, String m) {
// Precondition: !uriPath.isEmpty() && !m.isEmpty(),and u must already
// be normalized
String matchPath = normalize(m);
if (matchPath == null || !uriPath.startsWith(matchPath)) {
return -1;
}
// We can match only on a segment boundary: either both paths are equal,
// or if matchPath does not end in '/', there is a '/' in uriPath right
// after the match.
int uLength = uriPath.length();
int mLength = matchPath.length();
if (mLength == uLength || matchPath.charAt(mLength - 1) == '/'
|| (mLength < uLength && uriPath.charAt(mLength) == '/')) {
return mLength;
}
return -1;
}
static String normalize(String path) {
// C-git resolves . and .. segments
int i = 0;
int length = path.length();
StringBuilder builder = new StringBuilder(length);
builder.append('/');
if (length > 0 && path.charAt(0) == '/') {
i = 1;
}
while (i < length) {
int slash = path.indexOf('/', i);
if (slash < 0) {
slash = length;
}
if (slash == i || (slash == i + 1 && path.charAt(i) == '.')) {
// Skip /. or also double slashes
} else if (slash == i + 2 && path.charAt(i) == '.'
&& path.charAt(i + 1) == '.') {
// Remove previous segment if we have "/.."
int l = builder.length() - 2; // Skip terminating slash.
while (l >= 0 && builder.charAt(l) != '/') {
l--;
}
if (l < 0) {
LOG.warn(MessageFormat.format(
JGitText.get().httpConfigCannotNormalizeURL, path));
return null;
}
builder.setLength(l + 1);
} else {
// Include the slash, if any
builder.append(path, i, Math.min(length, slash + 1));
}
i = slash + 1;
}
if (builder.length() > 1 && builder.charAt(builder.length() - 1) == '/'
&& length > 0 && path.charAt(length - 1) != '/') {
// . or .. normalization left a trailing slash when the original
// path had none at the end
builder.setLength(builder.length() - 1);
}
return builder.toString();
}
}