View Javadoc
1   /*
2    * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
3    * and other copyright owners as documented in the project's IP log.
4    *
5    * This program and the accompanying materials are made available
6    * under the terms of the Eclipse Distribution License v1.0 which
7    * accompanies this distribution, is reproduced below, and is
8    * available at http://www.eclipse.org/org/documents/edl-v10.php
9    *
10   * All rights reserved.
11   *
12   * Redistribution and use in source and binary forms, with or
13   * without modification, are permitted provided that the following
14   * conditions are met:
15   *
16   * - Redistributions of source code must retain the above copyright
17   *   notice, this list of conditions and the following disclaimer.
18   *
19   * - Redistributions in binary form must reproduce the above
20   *   copyright notice, this list of conditions and the following
21   *   disclaimer in the documentation and/or other materials provided
22   *   with the distribution.
23   *
24   * - Neither the name of the Eclipse Foundation, Inc. nor the
25   *   names of its contributors may be used to endorse or promote
26   *   products derived from this software without specific prior
27   *   written permission.
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42   */
43  package org.eclipse.jgit.internal.transport.sshd.proxy;
44  
45  import java.util.ArrayList;
46  import java.util.Iterator;
47  import java.util.List;
48  
49  /**
50   * A basic parser for HTTP response headers. Handles status lines and
51   * authentication headers (WWW-Authenticate, Proxy-Authenticate).
52   *
53   * @see <a href="https://tools.ietf.org/html/rfc7230">RFC 7230</a>
54   * @see <a href="https://tools.ietf.org/html/rfc7235">RFC 7235</a>
55   */
56  public final class HttpParser {
57  
58  	/**
59  	 * An exception indicating some problem parsing HTPP headers.
60  	 */
61  	public static class ParseException extends Exception {
62  
63  		private static final long serialVersionUID = -1634090143702048640L;
64  
65  	}
66  
67  	private HttpParser() {
68  		// No instantiation
69  	}
70  
71  	/**
72  	 * Parse a HTTP response status line.
73  	 *
74  	 * @param line
75  	 *            to parse
76  	 * @return the {@link StatusLine}
77  	 * @throws ParseException
78  	 *             if the line cannot be parsed or has the wrong HTTP version
79  	 */
80  	public static StatusLine parseStatusLine(String line)
81  			throws ParseException {
82  		// Format is HTTP/<version> Code Reason
83  		int firstBlank = line.indexOf(' ');
84  		if (firstBlank < 0) {
85  			throw new ParseException();
86  		}
87  		int secondBlank = line.indexOf(' ', firstBlank + 1);
88  		if (secondBlank < 0) {
89  			// Accept the line even if the (according to RFC 2616 mandatory)
90  			// reason is missing.
91  			secondBlank = line.length();
92  		}
93  		int resultCode;
94  		try {
95  			resultCode = Integer.parseUnsignedInt(
96  					line.substring(firstBlank + 1, secondBlank));
97  		} catch (NumberFormatException e) {
98  			throw new ParseException();
99  		}
100 		// Again, accept even if the reason is missing
101 		String reason = ""; //$NON-NLS-1$
102 		if (secondBlank < line.length()) {
103 			reason = line.substring(secondBlank + 1);
104 		}
105 		return new StatusLine(line.substring(0, firstBlank), resultCode,
106 				reason);
107 	}
108 
109 	/**
110 	 * Extract the authentication headers from the header lines. It is assumed
111 	 * that the first element in {@code reply} is the raw status line as
112 	 * received from the server. It is skipped. Line processing stops on the
113 	 * first empty line thereafter.
114 	 *
115 	 * @param reply
116 	 *            The complete (header) lines of the HTTP response
117 	 * @param authenticationHeader
118 	 *            to look for (including the terminating ':'!)
119 	 * @return a list of {@link AuthenticationChallenge}s found.
120 	 */
121 	public static List<AuthenticationChallenge> getAuthenticationHeaders(
122 			List<String> reply, String authenticationHeader) {
123 		List<AuthenticationChallenge> challenges = new ArrayList<>();
124 		Iterator<String> lines = reply.iterator();
125 		// We know we have at least one line. Skip the response line.
126 		lines.next();
127 		StringBuilder value = null;
128 		while (lines.hasNext()) {
129 			String line = lines.next();
130 			if (line.isEmpty()) {
131 				break;
132 			}
133 			if (Character.isWhitespace(line.charAt(0))) {
134 				// Continuation line.
135 				if (value == null) {
136 					// Skip if we have no current value
137 					continue;
138 				}
139 				// Skip leading whitespace
140 				int i = skipWhiteSpace(line, 1);
141 				value.append(' ').append(line, i, line.length());
142 				continue;
143 			}
144 			if (value != null) {
145 				parseChallenges(challenges, value.toString());
146 				value = null;
147 			}
148 			int firstColon = line.indexOf(':');
149 			if (firstColon > 0 && authenticationHeader
150 					.equalsIgnoreCase(line.substring(0, firstColon + 1))) {
151 				value = new StringBuilder(line.substring(firstColon + 1));
152 			}
153 		}
154 		if (value != null) {
155 			parseChallenges(challenges, value.toString());
156 		}
157 		return challenges;
158 	}
159 
160 	private static void parseChallenges(
161 			List<AuthenticationChallenge> challenges,
162 			String header) {
163 		// Comma-separated list of challenges, each itself a scheme name
164 		// followed optionally by either: a comma-separated list of key=value
165 		// pairs, where the value may be a quoted string with backslash escapes,
166 		// or a single token value, which itself may end in zero or more '='
167 		// characters. Ugh.
168 		int length = header.length();
169 		for (int i = 0; i < length;) {
170 			int start = skipWhiteSpace(header, i);
171 			int end = scanToken(header, start);
172 			if (end <= start) {
173 				break;
174 			}
175 			AuthenticationChallenge challenge = new AuthenticationChallenge(
176 					header.substring(start, end));
177 			challenges.add(challenge);
178 			i = parseChallenge(challenge, header, end);
179 		}
180 	}
181 
182 	private static int parseChallenge(AuthenticationChallenge challenge,
183 			String header, int from) {
184 		int length = header.length();
185 		boolean first = true;
186 		for (int start = from; start <= length; first = false) {
187 			// Now we have either a single token, which may end in zero or more
188 			// equal signs, or a comma-separated list of key=value pairs (with
189 			// optional legacy whitespace around the equals sign), where the
190 			// value can be either a token or a quoted string.
191 			start = skipWhiteSpace(header, start);
192 			int end = scanToken(header, start);
193 			if (end == start) {
194 				// Nothing found. Either at end or on a comma.
195 				if (start < header.length() && header.charAt(start) == ',') {
196 					return start + 1;
197 				}
198 				return start;
199 			}
200 			int next = skipWhiteSpace(header, end);
201 			// Comma, or equals sign, or end of string
202 			if (next >= length || header.charAt(next) != '=') {
203 				if (first) {
204 					// It must be a token
205 					challenge.setToken(header.substring(start, end));
206 					if (next < length && header.charAt(next) == ',') {
207 						next++;
208 					}
209 					return next;
210 				} else {
211 					// This token must be the name of the next authentication
212 					// scheme.
213 					return start;
214 				}
215 			}
216 			int nextStart = skipWhiteSpace(header, next + 1);
217 			if (nextStart >= length) {
218 				if (next == end) {
219 					// '=' immediately after the key, no value: key must be the
220 					// token, and the equals sign is part of the token
221 					challenge.setToken(header.substring(start, end + 1));
222 				} else {
223 					// Key without value...
224 					challenge.addArgument(header.substring(start, end), null);
225 				}
226 				return nextStart;
227 			}
228 			if (nextStart == end + 1 && header.charAt(nextStart) == '=') {
229 				// More than one equals sign: must be the single token.
230 				end = nextStart + 1;
231 				while (end < length && header.charAt(end) == '=') {
232 					end++;
233 				}
234 				challenge.setToken(header.substring(start, end));
235 				end = skipWhiteSpace(header, end);
236 				if (end < length && header.charAt(end) == ',') {
237 					end++;
238 				}
239 				return end;
240 			}
241 			if (header.charAt(nextStart) == ',') {
242 				if (next == end) {
243 					// '=' immediately after the key, no value: key must be the
244 					// token, and the equals sign is part of the token
245 					challenge.setToken(header.substring(start, end + 1));
246 					return nextStart + 1;
247 				} else {
248 					// Key without value...
249 					challenge.addArgument(header.substring(start, end), null);
250 					start = nextStart + 1;
251 				}
252 			} else {
253 				if (header.charAt(nextStart) == '"') {
254 					int nextEnd[] = { nextStart + 1 };
255 					String value = scanQuotedString(header, nextStart + 1,
256 							nextEnd);
257 					challenge.addArgument(header.substring(start, end), value);
258 					start = nextEnd[0];
259 				} else {
260 					int nextEnd = scanToken(header, nextStart);
261 					challenge.addArgument(header.substring(start, end),
262 							header.substring(nextStart, nextEnd));
263 					start = nextEnd;
264 				}
265 				start = skipWhiteSpace(header, start);
266 				if (start < length && header.charAt(start) == ',') {
267 					start++;
268 				}
269 			}
270 		}
271 		return length;
272 	}
273 
274 	private static int skipWhiteSpace(String header, int i) {
275 		int length = header.length();
276 		while (i < length && Character.isWhitespace(header.charAt(i))) {
277 			i++;
278 		}
279 		return i;
280 	}
281 
282 	private static int scanToken(String header, int from) {
283 		int length = header.length();
284 		int i = from;
285 		while (i < length) {
286 			char c = header.charAt(i);
287 			switch (c) {
288 			case '!':
289 			case '#':
290 			case '$':
291 			case '%':
292 			case '&':
293 			case '\'':
294 			case '*':
295 			case '+':
296 			case '-':
297 			case '.':
298 			case '^':
299 			case '_':
300 			case '`':
301 			case '|':
302 			case '0':
303 			case '1':
304 			case '2':
305 			case '3':
306 			case '4':
307 			case '5':
308 			case '6':
309 			case '7':
310 			case '8':
311 			case '9':
312 				i++;
313 				break;
314 			default:
315 				if (c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z') {
316 					i++;
317 					break;
318 				}
319 				return i;
320 			}
321 		}
322 		return i;
323 	}
324 
325 	private static String scanQuotedString(String header, int from, int[] to) {
326 		StringBuilder result = new StringBuilder();
327 		int length = header.length();
328 		boolean quoted = false;
329 		int i = from;
330 		while (i < length) {
331 			char c = header.charAt(i++);
332 			if (quoted) {
333 				result.append(c);
334 				quoted = false;
335 			} else if (c == '\\') {
336 				quoted = true;
337 			} else if (c == '"') {
338 				break;
339 			} else {
340 				result.append(c);
341 			}
342 		}
343 		to[0] = i;
344 		return result.toString();
345 	}
346 }