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 10KB

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