You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

HttpParser.java 8.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. /*
  2. * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> and others
  3. *
  4. * This program and the accompanying materials are made available under the
  5. * terms of the Eclipse Distribution License v. 1.0 which is available at
  6. * https://www.eclipse.org/org/documents/edl-v10.php.
  7. *
  8. * SPDX-License-Identifier: BSD-3-Clause
  9. */
  10. package org.eclipse.jgit.internal.transport.sshd.proxy;
  11. import java.util.ArrayList;
  12. import java.util.Iterator;
  13. import java.util.List;
  14. /**
  15. * A basic parser for HTTP response headers. Handles status lines and
  16. * authentication headers (WWW-Authenticate, Proxy-Authenticate).
  17. *
  18. * @see <a href="https://tools.ietf.org/html/rfc7230">RFC 7230</a>
  19. * @see <a href="https://tools.ietf.org/html/rfc7235">RFC 7235</a>
  20. */
  21. public final class HttpParser {
  22. /**
  23. * An exception indicating some problem parsing HTPP headers.
  24. */
  25. public static class ParseException extends Exception {
  26. private static final long serialVersionUID = -1634090143702048640L;
  27. }
  28. private HttpParser() {
  29. // No instantiation
  30. }
  31. /**
  32. * Parse a HTTP response status line.
  33. *
  34. * @param line
  35. * to parse
  36. * @return the {@link StatusLine}
  37. * @throws ParseException
  38. * if the line cannot be parsed or has the wrong HTTP version
  39. */
  40. public static StatusLine parseStatusLine(String line)
  41. throws ParseException {
  42. // Format is HTTP/<version> Code Reason
  43. int firstBlank = line.indexOf(' ');
  44. if (firstBlank < 0) {
  45. throw new ParseException();
  46. }
  47. int secondBlank = line.indexOf(' ', firstBlank + 1);
  48. if (secondBlank < 0) {
  49. // Accept the line even if the (according to RFC 2616 mandatory)
  50. // reason is missing.
  51. secondBlank = line.length();
  52. }
  53. int resultCode;
  54. try {
  55. resultCode = Integer.parseUnsignedInt(
  56. line.substring(firstBlank + 1, secondBlank));
  57. } catch (NumberFormatException e) {
  58. throw new ParseException();
  59. }
  60. // Again, accept even if the reason is missing
  61. String reason = ""; //$NON-NLS-1$
  62. if (secondBlank < line.length()) {
  63. reason = line.substring(secondBlank + 1);
  64. }
  65. return new StatusLine(line.substring(0, firstBlank), resultCode,
  66. reason);
  67. }
  68. /**
  69. * Extract the authentication headers from the header lines. It is assumed
  70. * that the first element in {@code reply} is the raw status line as
  71. * received from the server. It is skipped. Line processing stops on the
  72. * first empty line thereafter.
  73. *
  74. * @param reply
  75. * The complete (header) lines of the HTTP response
  76. * @param authenticationHeader
  77. * to look for (including the terminating ':'!)
  78. * @return a list of {@link AuthenticationChallenge}s found.
  79. */
  80. public static List<AuthenticationChallenge> getAuthenticationHeaders(
  81. List<String> reply, String authenticationHeader) {
  82. List<AuthenticationChallenge> challenges = new ArrayList<>();
  83. Iterator<String> lines = reply.iterator();
  84. // We know we have at least one line. Skip the response line.
  85. lines.next();
  86. StringBuilder value = null;
  87. while (lines.hasNext()) {
  88. String line = lines.next();
  89. if (line.isEmpty()) {
  90. break;
  91. }
  92. if (Character.isWhitespace(line.charAt(0))) {
  93. // Continuation line.
  94. if (value == null) {
  95. // Skip if we have no current value
  96. continue;
  97. }
  98. // Skip leading whitespace
  99. int i = skipWhiteSpace(line, 1);
  100. value.append(' ').append(line, i, line.length());
  101. continue;
  102. }
  103. if (value != null) {
  104. parseChallenges(challenges, value.toString());
  105. value = null;
  106. }
  107. int firstColon = line.indexOf(':');
  108. if (firstColon > 0 && authenticationHeader
  109. .equalsIgnoreCase(line.substring(0, firstColon + 1))) {
  110. value = new StringBuilder(line.substring(firstColon + 1));
  111. }
  112. }
  113. if (value != null) {
  114. parseChallenges(challenges, value.toString());
  115. }
  116. return challenges;
  117. }
  118. private static void parseChallenges(
  119. List<AuthenticationChallenge> challenges,
  120. String header) {
  121. // Comma-separated list of challenges, each itself a scheme name
  122. // followed optionally by either: a comma-separated list of key=value
  123. // pairs, where the value may be a quoted string with backslash escapes,
  124. // or a single token value, which itself may end in zero or more '='
  125. // characters. Ugh.
  126. int length = header.length();
  127. for (int i = 0; i < length;) {
  128. int start = skipWhiteSpace(header, i);
  129. int end = scanToken(header, start);
  130. if (end <= start) {
  131. break;
  132. }
  133. AuthenticationChallenge challenge = new AuthenticationChallenge(
  134. header.substring(start, end));
  135. challenges.add(challenge);
  136. i = parseChallenge(challenge, header, end);
  137. }
  138. }
  139. private static int parseChallenge(AuthenticationChallenge challenge,
  140. String header, int from) {
  141. int length = header.length();
  142. boolean first = true;
  143. for (int start = from; start <= length; first = false) {
  144. // Now we have either a single token, which may end in zero or more
  145. // equal signs, or a comma-separated list of key=value pairs (with
  146. // optional legacy whitespace around the equals sign), where the
  147. // value can be either a token or a quoted string.
  148. start = skipWhiteSpace(header, start);
  149. int end = scanToken(header, start);
  150. if (end == start) {
  151. // Nothing found. Either at end or on a comma.
  152. if (start < header.length() && header.charAt(start) == ',') {
  153. return start + 1;
  154. }
  155. return start;
  156. }
  157. int next = skipWhiteSpace(header, end);
  158. // Comma, or equals sign, or end of string
  159. if (next >= length || header.charAt(next) != '=') {
  160. if (first) {
  161. // It must be a token
  162. challenge.setToken(header.substring(start, end));
  163. if (next < length && header.charAt(next) == ',') {
  164. next++;
  165. }
  166. return next;
  167. }
  168. // This token must be the name of the next authentication
  169. // scheme.
  170. return start;
  171. }
  172. int nextStart = skipWhiteSpace(header, next + 1);
  173. if (nextStart >= length) {
  174. if (next == end) {
  175. // '=' immediately after the key, no value: key must be the
  176. // token, and the equals sign is part of the token
  177. challenge.setToken(header.substring(start, end + 1));
  178. } else {
  179. // Key without value...
  180. challenge.addArgument(header.substring(start, end), null);
  181. }
  182. return nextStart;
  183. }
  184. if (nextStart == end + 1 && header.charAt(nextStart) == '=') {
  185. // More than one equals sign: must be the single token.
  186. end = nextStart + 1;
  187. while (end < length && header.charAt(end) == '=') {
  188. end++;
  189. }
  190. challenge.setToken(header.substring(start, end));
  191. end = skipWhiteSpace(header, end);
  192. if (end < length && header.charAt(end) == ',') {
  193. end++;
  194. }
  195. return end;
  196. }
  197. if (header.charAt(nextStart) == ',') {
  198. if (next == end) {
  199. // '=' immediately after the key, no value: key must be the
  200. // token, and the equals sign is part of the token
  201. challenge.setToken(header.substring(start, end + 1));
  202. return nextStart + 1;
  203. }
  204. // Key without value...
  205. challenge.addArgument(header.substring(start, end), null);
  206. start = nextStart + 1;
  207. } else {
  208. if (header.charAt(nextStart) == '"') {
  209. int[] nextEnd = { nextStart + 1 };
  210. String value = scanQuotedString(header, nextStart + 1,
  211. nextEnd);
  212. challenge.addArgument(header.substring(start, end), value);
  213. start = nextEnd[0];
  214. } else {
  215. int nextEnd = scanToken(header, nextStart);
  216. challenge.addArgument(header.substring(start, end),
  217. header.substring(nextStart, nextEnd));
  218. start = nextEnd;
  219. }
  220. start = skipWhiteSpace(header, start);
  221. if (start < length && header.charAt(start) == ',') {
  222. start++;
  223. }
  224. }
  225. }
  226. return length;
  227. }
  228. private static int skipWhiteSpace(String header, int i) {
  229. int length = header.length();
  230. while (i < length && Character.isWhitespace(header.charAt(i))) {
  231. i++;
  232. }
  233. return i;
  234. }
  235. private static int scanToken(String header, int from) {
  236. int length = header.length();
  237. int i = from;
  238. while (i < length) {
  239. char c = header.charAt(i);
  240. switch (c) {
  241. case '!':
  242. case '#':
  243. case '$':
  244. case '%':
  245. case '&':
  246. case '\'':
  247. case '*':
  248. case '+':
  249. case '-':
  250. case '.':
  251. case '^':
  252. case '_':
  253. case '`':
  254. case '|':
  255. case '0':
  256. case '1':
  257. case '2':
  258. case '3':
  259. case '4':
  260. case '5':
  261. case '6':
  262. case '7':
  263. case '8':
  264. case '9':
  265. i++;
  266. break;
  267. default:
  268. if (c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z') {
  269. i++;
  270. break;
  271. }
  272. return i;
  273. }
  274. }
  275. return i;
  276. }
  277. private static String scanQuotedString(String header, int from, int[] to) {
  278. StringBuilder result = new StringBuilder();
  279. int length = header.length();
  280. boolean quoted = false;
  281. int i = from;
  282. while (i < length) {
  283. char c = header.charAt(i++);
  284. if (quoted) {
  285. result.append(c);
  286. quoted = false;
  287. } else if (c == '\\') {
  288. quoted = true;
  289. } else if (c == '"') {
  290. break;
  291. } else {
  292. result.append(c);
  293. }
  294. }
  295. to[0] = i;
  296. return result.toString();
  297. }
  298. }