View Javadoc
1   /*
2    * Copyright (C) 2008, 2010, Google Inc.
3    * Copyright (C) 2017, Thomas Wolf <thomas.wolf@paranor.ch>
4    * and other copyright owners as documented in the project's IP log.
5    *
6    * This program and the accompanying materials are made available
7    * under the terms of the Eclipse Distribution License v1.0 which
8    * accompanies this distribution, is reproduced below, and is
9    * available at http://www.eclipse.org/org/documents/edl-v10.php
10   *
11   * All rights reserved.
12   *
13   * Redistribution and use in source and binary forms, with or
14   * without modification, are permitted provided that the following
15   * conditions are met:
16   *
17   * - Redistributions of source code must retain the above copyright
18   *   notice, this list of conditions and the following disclaimer.
19   *
20   * - Redistributions in binary form must reproduce the above
21   *   copyright notice, this list of conditions and the following
22   *   disclaimer in the documentation and/or other materials provided
23   *   with the distribution.
24   *
25   * - Neither the name of the Eclipse Foundation, Inc. nor the
26   *   names of its contributors may be used to endorse or promote
27   *   products derived from this software without specific prior
28   *   written permission.
29   *
30   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
31   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
32   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
33   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
34   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
35   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
36   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
37   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
38   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
39   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
40   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
41   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
42   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
43   */
44  
45  package org.eclipse.jgit.transport;
46  
47  import java.io.IOException;
48  import java.net.URISyntaxException;
49  import java.text.MessageFormat;
50  import java.util.Set;
51  import java.util.function.Supplier;
52  
53  import org.eclipse.jgit.errors.ConfigInvalidException;
54  import org.eclipse.jgit.internal.JGitText;
55  import org.eclipse.jgit.lib.Config;
56  import org.eclipse.jgit.storage.file.FileBasedConfig;
57  import org.eclipse.jgit.util.FS;
58  import org.eclipse.jgit.util.StringUtils;
59  import org.eclipse.jgit.util.SystemReader;
60  import org.slf4j.Logger;
61  import org.slf4j.LoggerFactory;
62  
63  /**
64   * A representation of the "http.*" config values in a git
65   * {@link org.eclipse.jgit.lib.Config}. git provides for setting values for
66   * specific URLs through "http.&lt;url&gt;.*" subsections. git always considers
67   * only the initial original URL for such settings, not any redirected URL.
68   *
69   * @since 4.9
70   */
71  public class HttpConfig {
72  
73  	private static final Logger LOG = LoggerFactory.getLogger(HttpConfig.class);
74  
75  	private static final String FTP = "ftp"; //$NON-NLS-1$
76  
77  	/** git config section key for http settings. */
78  	public static final String HTTP = "http"; //$NON-NLS-1$
79  
80  	/** git config key for the "followRedirects" setting. */
81  	public static final String FOLLOW_REDIRECTS_KEY = "followRedirects"; //$NON-NLS-1$
82  
83  	/** git config key for the "maxRedirects" setting. */
84  	public static final String MAX_REDIRECTS_KEY = "maxRedirects"; //$NON-NLS-1$
85  
86  	/** git config key for the "postBuffer" setting. */
87  	public static final String POST_BUFFER_KEY = "postBuffer"; //$NON-NLS-1$
88  
89  	/** git config key for the "sslVerify" setting. */
90  	public static final String SSL_VERIFY_KEY = "sslVerify"; //$NON-NLS-1$
91  
92  	private static final String MAX_REDIRECT_SYSTEM_PROPERTY = "http.maxRedirects"; //$NON-NLS-1$
93  
94  	private static final int DEFAULT_MAX_REDIRECTS = 5;
95  
96  	private static final int MAX_REDIRECTS = (new Supplier<Integer>() {
97  
98  		@Override
99  		public Integer get() {
100 			String rawValue = SystemReader.getInstance()
101 					.getProperty(MAX_REDIRECT_SYSTEM_PROPERTY);
102 			Integer value = Integer.valueOf(DEFAULT_MAX_REDIRECTS);
103 			if (rawValue != null) {
104 				try {
105 					value = Integer.valueOf(Integer.parseUnsignedInt(rawValue));
106 				} catch (NumberFormatException e) {
107 					LOG.warn(MessageFormat.format(
108 							JGitText.get().invalidSystemProperty,
109 							MAX_REDIRECT_SYSTEM_PROPERTY, rawValue, value));
110 				}
111 			}
112 			return value;
113 		}
114 	}).get().intValue();
115 
116 	/**
117 	 * Config values for http.followRedirect.
118 	 */
119 	public enum HttpRedirectMode implements Config.ConfigEnum {
120 
121 		/** Always follow redirects (up to the http.maxRedirects limit). */
122 		TRUE("true"), //$NON-NLS-1$
123 		/**
124 		 * Only follow redirects on the initial GET request. This is the
125 		 * default.
126 		 */
127 		INITIAL("initial"), //$NON-NLS-1$
128 		/** Never follow redirects. */
129 		FALSE("false"); //$NON-NLS-1$
130 
131 		private final String configValue;
132 
133 		private HttpRedirectMode(String configValue) {
134 			this.configValue = configValue;
135 		}
136 
137 		@Override
138 		public String toConfigValue() {
139 			return configValue;
140 		}
141 
142 		@Override
143 		public boolean matchConfigValue(String s) {
144 			return configValue.equals(s);
145 		}
146 	}
147 
148 	private int postBuffer;
149 
150 	private boolean sslVerify;
151 
152 	private HttpRedirectMode followRedirects;
153 
154 	private int maxRedirects;
155 
156 	/**
157 	 * Get the "http.postBuffer" setting
158 	 *
159 	 * @return the value of the "http.postBuffer" setting
160 	 */
161 	public int getPostBuffer() {
162 		return postBuffer;
163 	}
164 
165 	/**
166 	 * Get the "http.sslVerify" setting
167 	 *
168 	 * @return the value of the "http.sslVerify" setting
169 	 */
170 	public boolean isSslVerify() {
171 		return sslVerify;
172 	}
173 
174 	/**
175 	 * Get the "http.followRedirects" setting
176 	 *
177 	 * @return the value of the "http.followRedirects" setting
178 	 */
179 	public HttpRedirectMode getFollowRedirects() {
180 		return followRedirects;
181 	}
182 
183 	/**
184 	 * Get the "http.maxRedirects" setting
185 	 *
186 	 * @return the value of the "http.maxRedirects" setting
187 	 */
188 	public int getMaxRedirects() {
189 		return maxRedirects;
190 	}
191 
192 	/**
193 	 * Creates a new {@link org.eclipse.jgit.transport.HttpConfig} tailored to
194 	 * the given {@link org.eclipse.jgit.transport.URIish}.
195 	 *
196 	 * @param config
197 	 *            to read the {@link org.eclipse.jgit.transport.HttpConfig} from
198 	 * @param uri
199 	 *            to get the configuration values for
200 	 */
201 	public HttpConfig(Config config, URIish uri) {
202 		init(config, uri);
203 	}
204 
205 	/**
206 	 * Creates a {@link org.eclipse.jgit.transport.HttpConfig} that reads values
207 	 * solely from the user config.
208 	 *
209 	 * @param uri
210 	 *            to get the configuration values for
211 	 */
212 	public HttpConfig(URIish uri) {
213 		FileBasedConfig userConfig = SystemReader.getInstance()
214 				.openUserConfig(null, FS.DETECTED);
215 		try {
216 			userConfig.load();
217 		} catch (IOException | ConfigInvalidException e) {
218 			// Log it and then work with default values.
219 			LOG.error(MessageFormat.format(JGitText.get().userConfigFileInvalid,
220 					userConfig.getFile().getAbsolutePath(), e));
221 			init(new Config(), uri);
222 			return;
223 		}
224 		init(userConfig, uri);
225 	}
226 
227 	private void init(Config config, URIish uri) {
228 		// Set defaults from the section first
229 		int postBufferSize = config.getInt(HTTP, POST_BUFFER_KEY,
230 				1 * 1024 * 1024);
231 		boolean sslVerifyFlag = config.getBoolean(HTTP, SSL_VERIFY_KEY, true);
232 		HttpRedirectMode followRedirectsMode = config.getEnum(
233 				HttpRedirectMode.values(), HTTP, null,
234 				FOLLOW_REDIRECTS_KEY, HttpRedirectMode.INITIAL);
235 		int redirectLimit = config.getInt(HTTP, MAX_REDIRECTS_KEY,
236 				MAX_REDIRECTS);
237 		if (redirectLimit < 0) {
238 			redirectLimit = MAX_REDIRECTS;
239 		}
240 		String match = findMatch(config.getSubsections(HTTP), uri);
241 		if (match != null) {
242 			// Override with more specific items
243 			postBufferSize = config.getInt(HTTP, match, POST_BUFFER_KEY,
244 					postBufferSize);
245 			sslVerifyFlag = config.getBoolean(HTTP, match, SSL_VERIFY_KEY,
246 					sslVerifyFlag);
247 			followRedirectsMode = config.getEnum(HttpRedirectMode.values(),
248 					HTTP, match, FOLLOW_REDIRECTS_KEY, followRedirectsMode);
249 			int newMaxRedirects = config.getInt(HTTP, match, MAX_REDIRECTS_KEY,
250 					redirectLimit);
251 			if (newMaxRedirects >= 0) {
252 				redirectLimit = newMaxRedirects;
253 			}
254 		}
255 		postBuffer = postBufferSize;
256 		sslVerify = sslVerifyFlag;
257 		followRedirects = followRedirectsMode;
258 		maxRedirects = redirectLimit;
259 	}
260 
261 	/**
262 	 * Determines the best match from a set of subsection names (representing
263 	 * prefix URLs) for the given {@link URIish}.
264 	 *
265 	 * @param names
266 	 *            to match against the {@code uri}
267 	 * @param uri
268 	 *            to find a match for
269 	 * @return the best matching subsection name, or {@code null} if no
270 	 *         subsection matches
271 	 */
272 	private String findMatch(Set<String> names, URIish uri) {
273 		String bestMatch = null;
274 		int bestMatchLength = -1;
275 		boolean withUser = false;
276 		String uPath = uri.getPath();
277 		boolean hasPath = !StringUtils.isEmptyOrNull(uPath);
278 		if (hasPath) {
279 			uPath = normalize(uPath);
280 			if (uPath == null) {
281 				// Normalization failed; warning was logged.
282 				return null;
283 			}
284 		}
285 		for (String s : names) {
286 			try {
287 				URIish candidate = new URIish(s);
288 				// Scheme and host must match case-insensitively
289 				if (!compare(uri.getScheme(), candidate.getScheme())
290 						|| !compare(uri.getHost(), candidate.getHost())) {
291 					continue;
292 				}
293 				// Ports must match after default ports have been substituted
294 				if (defaultedPort(uri.getPort(),
295 						uri.getScheme()) != defaultedPort(candidate.getPort(),
296 								candidate.getScheme())) {
297 					continue;
298 				}
299 				// User: if present in candidate, must match
300 				boolean hasUser = false;
301 				if (candidate.getUser() != null) {
302 					if (!candidate.getUser().equals(uri.getUser())) {
303 						continue;
304 					}
305 					hasUser = true;
306 				}
307 				// Path: prefix match, longer is better
308 				String cPath = candidate.getPath();
309 				int matchLength = -1;
310 				if (StringUtils.isEmptyOrNull(cPath)) {
311 					matchLength = 0;
312 				} else {
313 					if (!hasPath) {
314 						continue;
315 					}
316 					// Paths can match only on segments
317 					matchLength = segmentCompare(uPath, cPath);
318 					if (matchLength < 0) {
319 						continue;
320 					}
321 				}
322 				// A longer path match is always preferred even over a user
323 				// match. If the path matches are equal, a match with user wins
324 				// over a match without user.
325 				if (matchLength > bestMatchLength || !withUser && hasUser
326 						&& matchLength >= 0 && matchLength == bestMatchLength) {
327 					bestMatch = s;
328 					bestMatchLength = matchLength;
329 					withUser = hasUser;
330 				}
331 			} catch (URISyntaxException e) {
332 				LOG.warn(MessageFormat
333 						.format(JGitText.get().httpConfigInvalidURL, s));
334 			}
335 		}
336 		return bestMatch;
337 	}
338 
339 	private boolean compare(String a, String b) {
340 		if (a == null) {
341 			return b == null;
342 		}
343 		return a.equalsIgnoreCase(b);
344 	}
345 
346 	private int defaultedPort(int port, String scheme) {
347 		if (port >= 0) {
348 			return port;
349 		}
350 		if (FTP.equalsIgnoreCase(scheme)) {
351 			return 21;
352 		} else if (HTTP.equalsIgnoreCase(scheme)) {
353 			return 80;
354 		} else {
355 			return 443; // https
356 		}
357 	}
358 
359 	static int segmentCompare(String uriPath, String m) {
360 		// Precondition: !uriPath.isEmpty() && !m.isEmpty(),and u must already
361 		// be normalized
362 		String matchPath = normalize(m);
363 		if (matchPath == null || !uriPath.startsWith(matchPath)) {
364 			return -1;
365 		}
366 		// We can match only on a segment boundary: either both paths are equal,
367 		// or if matchPath does not end in '/', there is a '/' in uriPath right
368 		// after the match.
369 		int uLength = uriPath.length();
370 		int mLength = matchPath.length();
371 		if (mLength == uLength || matchPath.charAt(mLength - 1) == '/'
372 				|| mLength < uLength && uriPath.charAt(mLength) == '/') {
373 			return mLength;
374 		}
375 		return -1;
376 	}
377 
378 	static String normalize(String path) {
379 		// C-git resolves . and .. segments
380 		int i = 0;
381 		int length = path.length();
382 		StringBuilder builder = new StringBuilder(length);
383 		builder.append('/');
384 		if (length > 0 && path.charAt(0) == '/') {
385 			i = 1;
386 		}
387 		while (i < length) {
388 			int slash = path.indexOf('/', i);
389 			if (slash < 0) {
390 				slash = length;
391 			}
392 			if (slash == i || slash == i + 1 && path.charAt(i) == '.') {
393 				// Skip /. or also double slashes
394 			} else if (slash == i + 2 && path.charAt(i) == '.'
395 					&& path.charAt(i + 1) == '.') {
396 				// Remove previous segment if we have "/.."
397 				int l = builder.length() - 2; // Skip terminating slash.
398 				while (l >= 0 && builder.charAt(l) != '/') {
399 					l--;
400 				}
401 				if (l < 0) {
402 					LOG.warn(MessageFormat.format(
403 							JGitText.get().httpConfigCannotNormalizeURL, path));
404 					return null;
405 				}
406 				builder.setLength(l + 1);
407 			} else {
408 				// Include the slash, if any
409 				builder.append(path, i, Math.min(length, slash + 1));
410 			}
411 			i = slash + 1;
412 		}
413 		if (builder.length() > 1 && builder.charAt(builder.length() - 1) == '/'
414 				&& length > 0 && path.charAt(length - 1) != '/') {
415 			// . or .. normalization left a trailing slash when the original
416 			// path had none at the end
417 			builder.setLength(builder.length() - 1);
418 		}
419 		return builder.toString();
420 	}
421 }