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.2KB

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