HttpParser.java

  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. }