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