View Javadoc
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  
12  package org.eclipse.jgit.transport;
13  
14  import java.io.IOException;
15  import java.net.URISyntaxException;
16  import java.text.MessageFormat;
17  import java.util.Arrays;
18  import java.util.Collections;
19  import java.util.List;
20  import java.util.Set;
21  import java.util.function.Supplier;
22  
23  import org.eclipse.jgit.annotations.NonNull;
24  import org.eclipse.jgit.errors.ConfigInvalidException;
25  import org.eclipse.jgit.internal.JGitText;
26  import org.eclipse.jgit.lib.Config;
27  import org.eclipse.jgit.lib.StoredConfig;
28  import org.eclipse.jgit.util.StringUtils;
29  import org.eclipse.jgit.util.SystemReader;
30  import org.slf4j.Logger;
31  import org.slf4j.LoggerFactory;
32  
33  /**
34   * A representation of the "http.*" config values in a git
35   * {@link org.eclipse.jgit.lib.Config}. git provides for setting values for
36   * specific URLs through "http.&lt;url&gt;.*" subsections. git always considers
37   * only the initial original URL for such settings, not any redirected URL.
38   *
39   * @since 4.9
40   */
41  public class HttpConfig {
42  
43  	private static final Logger LOG = LoggerFactory.getLogger(HttpConfig.class);
44  
45  	private static final String FTP = "ftp"; //$NON-NLS-1$
46  
47  	/** git config section key for http settings. */
48  	public static final String HTTP = "http"; //$NON-NLS-1$
49  
50  	/** git config key for the "followRedirects" setting. */
51  	public static final String FOLLOW_REDIRECTS_KEY = "followRedirects"; //$NON-NLS-1$
52  
53  	/** git config key for the "maxRedirects" setting. */
54  	public static final String MAX_REDIRECTS_KEY = "maxRedirects"; //$NON-NLS-1$
55  
56  	/** git config key for the "postBuffer" setting. */
57  	public static final String POST_BUFFER_KEY = "postBuffer"; //$NON-NLS-1$
58  
59  	/** git config key for the "sslVerify" setting. */
60  	public static final String SSL_VERIFY_KEY = "sslVerify"; //$NON-NLS-1$
61  
62  	/**
63  	 * git config key for the "userAgent" setting.
64  	 *
65  	 * @since 5.10
66  	 */
67  	public static final String USER_AGENT = "userAgent"; //$NON-NLS-1$
68  
69  	/**
70  	 * git config key for the "extraHeader" setting.
71  	 *
72  	 * @since 5.10
73  	 */
74  	public static final String EXTRA_HEADER = "extraHeader"; //$NON-NLS-1$
75  
76  	/**
77  	 * git config key for the "cookieFile" setting.
78  	 *
79  	 * @since 5.4
80  	 */
81  	public static final String COOKIE_FILE_KEY = "cookieFile"; //$NON-NLS-1$
82  
83  	/**
84  	 * git config key for the "saveCookies" setting.
85  	 *
86  	 * @since 5.4
87  	 */
88  	public static final String SAVE_COOKIES_KEY = "saveCookies"; //$NON-NLS-1$
89  
90  	/**
91  	 * Custom JGit config key which holds the maximum number of cookie files to
92  	 * keep in the cache.
93  	 *
94  	 * @since 5.4
95  	 */
96  	public static final String COOKIE_FILE_CACHE_LIMIT_KEY = "cookieFileCacheLimit"; //$NON-NLS-1$
97  
98  	private static final int DEFAULT_COOKIE_FILE_CACHE_LIMIT = 10;
99  
100 	private static final String MAX_REDIRECT_SYSTEM_PROPERTY = "http.maxRedirects"; //$NON-NLS-1$
101 
102 	private static final int DEFAULT_MAX_REDIRECTS = 5;
103 
104 	private static final int MAX_REDIRECTS = (new Supplier<Integer>() {
105 
106 		@Override
107 		public Integer get() {
108 			String rawValue = SystemReader.getInstance()
109 					.getProperty(MAX_REDIRECT_SYSTEM_PROPERTY);
110 			Integer value = Integer.valueOf(DEFAULT_MAX_REDIRECTS);
111 			if (rawValue != null) {
112 				try {
113 					value = Integer.valueOf(Integer.parseUnsignedInt(rawValue));
114 				} catch (NumberFormatException e) {
115 					LOG.warn(MessageFormat.format(
116 							JGitText.get().invalidSystemProperty,
117 							MAX_REDIRECT_SYSTEM_PROPERTY, rawValue, value));
118 				}
119 			}
120 			return value;
121 		}
122 	}).get().intValue();
123 
124 	private static final String ENV_HTTP_USER_AGENT = "GIT_HTTP_USER_AGENT"; //$NON-NLS-1$
125 
126 	/**
127 	 * Config values for http.followRedirect.
128 	 */
129 	public enum HttpRedirectMode implements Config.ConfigEnum {
130 
131 		/** Always follow redirects (up to the http.maxRedirects limit). */
132 		TRUE("true"), //$NON-NLS-1$
133 		/**
134 		 * Only follow redirects on the initial GET request. This is the
135 		 * default.
136 		 */
137 		INITIAL("initial"), //$NON-NLS-1$
138 		/** Never follow redirects. */
139 		FALSE("false"); //$NON-NLS-1$
140 
141 		private final String configValue;
142 
143 		private HttpRedirectMode(String configValue) {
144 			this.configValue = configValue;
145 		}
146 
147 		@Override
148 		public String toConfigValue() {
149 			return configValue;
150 		}
151 
152 		@Override
153 		public boolean matchConfigValue(String s) {
154 			return configValue.equals(s);
155 		}
156 	}
157 
158 	private int postBuffer;
159 
160 	private boolean sslVerify;
161 
162 	private HttpRedirectMode followRedirects;
163 
164 	private int maxRedirects;
165 
166 	private String userAgent;
167 
168 	private List<String> extraHeaders;
169 
170 	private String cookieFile;
171 
172 	private boolean saveCookies;
173 
174 	private int cookieFileCacheLimit;
175 
176 	/**
177 	 * Get the "http.postBuffer" setting
178 	 *
179 	 * @return the value of the "http.postBuffer" setting
180 	 */
181 	public int getPostBuffer() {
182 		return postBuffer;
183 	}
184 
185 	/**
186 	 * Get the "http.sslVerify" setting
187 	 *
188 	 * @return the value of the "http.sslVerify" setting
189 	 */
190 	public boolean isSslVerify() {
191 		return sslVerify;
192 	}
193 
194 	/**
195 	 * Get the "http.followRedirects" setting
196 	 *
197 	 * @return the value of the "http.followRedirects" setting
198 	 */
199 	public HttpRedirectMode getFollowRedirects() {
200 		return followRedirects;
201 	}
202 
203 	/**
204 	 * Get the "http.maxRedirects" setting
205 	 *
206 	 * @return the value of the "http.maxRedirects" setting
207 	 */
208 	public int getMaxRedirects() {
209 		return maxRedirects;
210 	}
211 
212 	/**
213 	 * Get the "http.userAgent" setting
214 	 *
215 	 * @return the value of the "http.userAgent" setting
216 	 * @since 5.10
217 	 */
218 	public String getUserAgent() {
219 		return userAgent;
220 	}
221 
222 	/**
223 	 * Get the "http.extraHeader" setting
224 	 *
225 	 * @return the value of the "http.extraHeader" setting
226 	 * @since 5.10
227 	 */
228 	@NonNull
229 	public List<String> getExtraHeaders() {
230 		return extraHeaders == null ? Collections.emptyList() : extraHeaders;
231 	}
232 
233 	/**
234 	 * Get the "http.cookieFile" setting
235 	 *
236 	 * @return the value of the "http.cookieFile" setting
237 	 *
238 	 * @since 5.4
239 	 */
240 	public String getCookieFile() {
241 		return cookieFile;
242 	}
243 
244 	/**
245 	 * Get the "http.saveCookies" setting
246 	 *
247 	 * @return the value of the "http.saveCookies" setting
248 	 *
249 	 * @since 5.4
250 	 */
251 	public boolean getSaveCookies() {
252 		return saveCookies;
253 	}
254 
255 	/**
256 	 * Get the "http.cookieFileCacheLimit" setting (gives the maximum number of
257 	 * cookie files to keep in the LRU cache)
258 	 *
259 	 * @return the value of the "http.cookieFileCacheLimit" setting
260 	 *
261 	 * @since 5.4
262 	 */
263 	public int getCookieFileCacheLimit() {
264 		return cookieFileCacheLimit;
265 	}
266 
267 	/**
268 	 * Creates a new {@link org.eclipse.jgit.transport.HttpConfig} tailored to
269 	 * the given {@link org.eclipse.jgit.transport.URIish}.
270 	 *
271 	 * @param config
272 	 *            to read the {@link org.eclipse.jgit.transport.HttpConfig} from
273 	 * @param uri
274 	 *            to get the configuration values for
275 	 */
276 	public HttpConfig(Config config, URIish uri) {
277 		init(config, uri);
278 	}
279 
280 	/**
281 	 * Creates a {@link org.eclipse.jgit.transport.HttpConfig} that reads values
282 	 * solely from the user config.
283 	 *
284 	 * @param uri
285 	 *            to get the configuration values for
286 	 */
287 	public HttpConfig(URIish uri) {
288 		StoredConfig userConfig = null;
289 		try {
290 			userConfig = SystemReader.getInstance().getUserConfig();
291 		} catch (IOException | ConfigInvalidException e) {
292 			// Log it and then work with default values.
293 			LOG.error(e.getMessage(), e);
294 			init(new Config(), uri);
295 			return;
296 		}
297 		init(userConfig, uri);
298 	}
299 
300 	private void init(Config config, URIish uri) {
301 		// Set defaults from the section first
302 		int postBufferSize = config.getInt(HTTP, POST_BUFFER_KEY,
303 				1 * 1024 * 1024);
304 		boolean sslVerifyFlag = config.getBoolean(HTTP, SSL_VERIFY_KEY, true);
305 		HttpRedirectMode followRedirectsMode = config.getEnum(
306 				HttpRedirectMode.values(), HTTP, null,
307 				FOLLOW_REDIRECTS_KEY, HttpRedirectMode.INITIAL);
308 		int redirectLimit = config.getInt(HTTP, MAX_REDIRECTS_KEY,
309 				MAX_REDIRECTS);
310 		if (redirectLimit < 0) {
311 			redirectLimit = MAX_REDIRECTS;
312 		}
313 		String agent = config.getString(HTTP, null, USER_AGENT);
314 		if (agent != null) {
315 			agent = UserAgent.clean(agent);
316 		}
317 		userAgent = agent;
318 		String[] headers = config.getStringList(HTTP, null, EXTRA_HEADER);
319 		// https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpextraHeader
320 		// "an empty value will reset the extra headers to the empty list."
321 		int start = findLastEmpty(headers) + 1;
322 		if (start > 0) {
323 			headers = Arrays.copyOfRange(headers, start, headers.length);
324 		}
325 		extraHeaders = Arrays.asList(headers);
326 		cookieFile = config.getString(HTTP, null, COOKIE_FILE_KEY);
327 		saveCookies = config.getBoolean(HTTP, SAVE_COOKIES_KEY, false);
328 		cookieFileCacheLimit = config.getInt(HTTP, COOKIE_FILE_CACHE_LIMIT_KEY,
329 				DEFAULT_COOKIE_FILE_CACHE_LIMIT);
330 		String match = findMatch(config.getSubsections(HTTP), uri);
331 
332 		if (match != null) {
333 			// Override with more specific items
334 			postBufferSize = config.getInt(HTTP, match, POST_BUFFER_KEY,
335 					postBufferSize);
336 			sslVerifyFlag = config.getBoolean(HTTP, match, SSL_VERIFY_KEY,
337 					sslVerifyFlag);
338 			followRedirectsMode = config.getEnum(HttpRedirectMode.values(),
339 					HTTP, match, FOLLOW_REDIRECTS_KEY, followRedirectsMode);
340 			int newMaxRedirects = config.getInt(HTTP, match, MAX_REDIRECTS_KEY,
341 					redirectLimit);
342 			if (newMaxRedirects >= 0) {
343 				redirectLimit = newMaxRedirects;
344 			}
345 			String uriSpecificUserAgent = config.getString(HTTP, match,
346 					USER_AGENT);
347 			if (uriSpecificUserAgent != null) {
348 				userAgent = UserAgent.clean(uriSpecificUserAgent);
349 			}
350 			String[] uriSpecificExtraHeaders = config.getStringList(HTTP, match,
351 					EXTRA_HEADER);
352 			if (uriSpecificExtraHeaders.length > 0) {
353 				start = findLastEmpty(uriSpecificExtraHeaders) + 1;
354 				if (start > 0) {
355 					uriSpecificExtraHeaders = Arrays.copyOfRange(
356 							uriSpecificExtraHeaders, start,
357 							uriSpecificExtraHeaders.length);
358 				}
359 				extraHeaders = Arrays.asList(uriSpecificExtraHeaders);
360 			}
361 			String urlSpecificCookieFile = config.getString(HTTP, match,
362 					COOKIE_FILE_KEY);
363 			if (urlSpecificCookieFile != null) {
364 				cookieFile = urlSpecificCookieFile;
365 			}
366 			saveCookies = config.getBoolean(HTTP, match, SAVE_COOKIES_KEY,
367 					saveCookies);
368 		}
369 		// Environment overrides config
370 		agent = SystemReader.getInstance().getenv(ENV_HTTP_USER_AGENT);
371 		if (!StringUtils.isEmptyOrNull(agent)) {
372 			userAgent = UserAgent.clean(agent);
373 		}
374 		postBuffer = postBufferSize;
375 		sslVerify = sslVerifyFlag;
376 		followRedirects = followRedirectsMode;
377 		maxRedirects = redirectLimit;
378 	}
379 
380 	private int findLastEmpty(String[] values) {
381 		for (int i = values.length - 1; i >= 0; i--) {
382 			if (values[i] == null) {
383 				return i;
384 			}
385 		}
386 		return -1;
387 	}
388 
389 	/**
390 	 * Determines the best match from a set of subsection names (representing
391 	 * prefix URLs) for the given {@link URIish}.
392 	 *
393 	 * @param names
394 	 *            to match against the {@code uri}
395 	 * @param uri
396 	 *            to find a match for
397 	 * @return the best matching subsection name, or {@code null} if no
398 	 *         subsection matches
399 	 */
400 	private String findMatch(Set<String> names, URIish uri) {
401 		String bestMatch = null;
402 		int bestMatchLength = -1;
403 		boolean withUser = false;
404 		String uPath = uri.getPath();
405 		boolean hasPath = !StringUtils.isEmptyOrNull(uPath);
406 		if (hasPath) {
407 			uPath = normalize(uPath);
408 			if (uPath == null) {
409 				// Normalization failed; warning was logged.
410 				return null;
411 			}
412 		}
413 		for (String s : names) {
414 			try {
415 				URIish candidate = new URIish(s);
416 				// Scheme and host must match case-insensitively
417 				if (!compare(uri.getScheme(), candidate.getScheme())
418 						|| !compare(uri.getHost(), candidate.getHost())) {
419 					continue;
420 				}
421 				// Ports must match after default ports have been substituted
422 				if (defaultedPort(uri.getPort(),
423 						uri.getScheme()) != defaultedPort(candidate.getPort(),
424 								candidate.getScheme())) {
425 					continue;
426 				}
427 				// User: if present in candidate, must match
428 				boolean hasUser = false;
429 				if (candidate.getUser() != null) {
430 					if (!candidate.getUser().equals(uri.getUser())) {
431 						continue;
432 					}
433 					hasUser = true;
434 				}
435 				// Path: prefix match, longer is better
436 				String cPath = candidate.getPath();
437 				int matchLength = -1;
438 				if (StringUtils.isEmptyOrNull(cPath)) {
439 					matchLength = 0;
440 				} else {
441 					if (!hasPath) {
442 						continue;
443 					}
444 					// Paths can match only on segments
445 					matchLength = segmentCompare(uPath, cPath);
446 					if (matchLength < 0) {
447 						continue;
448 					}
449 				}
450 				// A longer path match is always preferred even over a user
451 				// match. If the path matches are equal, a match with user wins
452 				// over a match without user.
453 				if (matchLength > bestMatchLength
454 						|| (!withUser && hasUser && matchLength >= 0
455 								&& matchLength == bestMatchLength)) {
456 					bestMatch = s;
457 					bestMatchLength = matchLength;
458 					withUser = hasUser;
459 				}
460 			} catch (URISyntaxException e) {
461 				LOG.warn(MessageFormat
462 						.format(JGitText.get().httpConfigInvalidURL, s));
463 			}
464 		}
465 		return bestMatch;
466 	}
467 
468 	private boolean compare(String a, String b) {
469 		if (a == null) {
470 			return b == null;
471 		}
472 		return a.equalsIgnoreCase(b);
473 	}
474 
475 	private int defaultedPort(int port, String scheme) {
476 		if (port >= 0) {
477 			return port;
478 		}
479 		if (FTP.equalsIgnoreCase(scheme)) {
480 			return 21;
481 		} else if (HTTP.equalsIgnoreCase(scheme)) {
482 			return 80;
483 		} else {
484 			return 443; // https
485 		}
486 	}
487 
488 	static int segmentCompare(String uriPath, String m) {
489 		// Precondition: !uriPath.isEmpty() && !m.isEmpty(),and u must already
490 		// be normalized
491 		String matchPath = normalize(m);
492 		if (matchPath == null || !uriPath.startsWith(matchPath)) {
493 			return -1;
494 		}
495 		// We can match only on a segment boundary: either both paths are equal,
496 		// or if matchPath does not end in '/', there is a '/' in uriPath right
497 		// after the match.
498 		int uLength = uriPath.length();
499 		int mLength = matchPath.length();
500 		if (mLength == uLength || matchPath.charAt(mLength - 1) == '/'
501 				|| (mLength < uLength && uriPath.charAt(mLength) == '/')) {
502 			return mLength;
503 		}
504 		return -1;
505 	}
506 
507 	static String normalize(String path) {
508 		// C-git resolves . and .. segments
509 		int i = 0;
510 		int length = path.length();
511 		StringBuilder builder = new StringBuilder(length);
512 		builder.append('/');
513 		if (length > 0 && path.charAt(0) == '/') {
514 			i = 1;
515 		}
516 		while (i < length) {
517 			int slash = path.indexOf('/', i);
518 			if (slash < 0) {
519 				slash = length;
520 			}
521 			if (slash == i || (slash == i + 1 && path.charAt(i) == '.')) {
522 				// Skip /. or also double slashes
523 			} else if (slash == i + 2 && path.charAt(i) == '.'
524 					&& path.charAt(i + 1) == '.') {
525 				// Remove previous segment if we have "/.."
526 				int l = builder.length() - 2; // Skip terminating slash.
527 				while (l >= 0 && builder.charAt(l) != '/') {
528 					l--;
529 				}
530 				if (l < 0) {
531 					LOG.warn(MessageFormat.format(
532 							JGitText.get().httpConfigCannotNormalizeURL, path));
533 					return null;
534 				}
535 				builder.setLength(l + 1);
536 			} else {
537 				// Include the slash, if any
538 				builder.append(path, i, Math.min(length, slash + 1));
539 			}
540 			i = slash + 1;
541 		}
542 		if (builder.length() > 1 && builder.charAt(builder.length() - 1) == '/'
543 				&& length > 0 && path.charAt(length - 1) != '/') {
544 			// . or .. normalization left a trailing slash when the original
545 			// path had none at the end
546 			builder.setLength(builder.length() - 1);
547 		}
548 		return builder.toString();
549 	}
550 }