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