Author: markt Date: Thu Nov 1 21:29:16 2012 New Revision: 1404773 URL: http://svn.apache.org/viewvc?rev=1404773&view=rev Log: Initial implementation of a possible solution for BZ 54060. TODO before using this solution: - lots more test cases - check performance
Added: tomcat/trunk/java/org/apache/tomcat/util/http/parser/HttpParser2.java (with props) tomcat/trunk/test/org/apache/tomcat/util/http/parser/TestHttpParser2.java (with props) Added: tomcat/trunk/java/org/apache/tomcat/util/http/parser/HttpParser2.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/tomcat/util/http/parser/HttpParser2.java?rev=1404773&view=auto ============================================================================== --- tomcat/trunk/java/org/apache/tomcat/util/http/parser/HttpParser2.java (added) +++ tomcat/trunk/java/org/apache/tomcat/util/http/parser/HttpParser2.java Thu Nov 1 21:29:16 2012 @@ -0,0 +1,276 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.tomcat.util.http.parser; + +import java.io.IOException; +import java.io.StringReader; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +/** + * HTTP header value parser implementation. Parsing HTTP headers as per RFC2616 + * is not always as simple as it first appears. For headers that only use tokens + * the simple approach will normally be sufficient. However, for the other + * headers, while simple code meets 99.9% of cases, there are often some edge + * cases that make things far more complicated. + * + * The purpose of this parser is to let the parser worry about the edge cases. + * It provides tolerant (where safe to do so) parsing of HTTP header values + * assuming that wrapped header lines have already been unwrapped. (The Tomcat + * header processing code does the unwrapping.) + * + * Provides parsing of the following HTTP header values as per RFC 2616: + * - Authorization for DIGEST authentication + * + * Support for additional headers will be provided as required. + * + * TODO: Check the performance of this parser against the current Digest header + * parsing code. + * + * TODO: Add support for parsing content-type and replace HttpParser + */ +public class HttpParser2 { + + private static final Integer FIELD_TYPE_TOKEN = Integer.valueOf(0); + private static final Integer FIELD_TYPE_QUOTED_STRING = Integer.valueOf(1); + private static final Integer FIELD_TYPE_TOKEN_OR_QUOTED_STRING = Integer.valueOf(2); + private static final Integer FIELD_TYPE_LHEX = Integer.valueOf(3); + private static final Integer FIELD_TYPE_QUOTED_LHEX = Integer.valueOf(4); + + private static final Map<String,Integer> fieldTypes = new HashMap<>(); + + private static final boolean isToken[] = new boolean[128]; + private static final boolean isHex[] = new boolean[128]; + + static { + // Digest field types + fieldTypes.put("username", FIELD_TYPE_QUOTED_STRING); + fieldTypes.put("realm", FIELD_TYPE_QUOTED_STRING); + fieldTypes.put("nonce", FIELD_TYPE_QUOTED_STRING); + fieldTypes.put("digest-uri", FIELD_TYPE_QUOTED_STRING); + fieldTypes.put("response", FIELD_TYPE_QUOTED_LHEX); + fieldTypes.put("algorithm", FIELD_TYPE_TOKEN); + fieldTypes.put("cnonce", FIELD_TYPE_QUOTED_STRING); + fieldTypes.put("opaque", FIELD_TYPE_QUOTED_STRING); + fieldTypes.put("qop", FIELD_TYPE_TOKEN); + fieldTypes.put("nc", FIELD_TYPE_LHEX); + + // Setup the flag arrays + for (int i = 0; i < 128; i++) { + if (i < 32) { + isToken[i] = false; + } else if (i == '(' || i == ')' || i == '<' || i == '>' || i == '@' || + i == ',' || i == ';' || i == ':' || i == '\\' || i == '\"' || + i == '/' || i == '[' || i == ']' || i == '?' || i == '=' || + i == '{' || i == '}' || i == ' ' || i == '\t') { + isToken[i] = false; + } else { + isToken[i] = true; + } + + if (i >= '0' && i <= '9' || i >= 'A' && i <= 'F' || + i >= 'a' && i <= 'f') { + isHex[i] = true; + } else { + isHex[i] = false; + } + } + } + + /** + * Parses an HTTP Authorization header for DIGEST authentication as per RFC + * 2617 section 3.2.2. + * + * @param input The header value to parse + * + * @return A map of directives and values as {@link String}s. Although the + * values returned are {@link String}s they will have been + * validated to ensure that they conform to RFC 2617. + * + * @throws IllegalArgumentException If the header does not conform to RFC + * 2617 + * @throws IOException If an error occurs while reading the input + */ + public static Map<String,String> parseAuthorizationDigest ( + StringReader input) throws IllegalArgumentException, IOException { + + Map<String,String> result = new HashMap<>(); + + swallowConstant(input, "Digest", false); + skipLws(input); + // All field names are valid tokens + String field = readToken(input); + while (field != null) { + skipLws(input); + swallowConstant(input, "=", false); + skipLws(input); + String value = null; + Integer type = fieldTypes.get(field.toLowerCase(Locale.US)); + if (type == null) { + // auth-param = token "=" ( token | quoted-string ) + type = FIELD_TYPE_TOKEN_OR_QUOTED_STRING; + } + switch (type.intValue()) { + case 0: + // FIELD_TYPE_TOKEN + value = readToken(input); + break; + case 1: + // FIELD_TYPE_QUOTED_STRING + value = readQuotedString(input); + break; + case 2: + // FIELD_TYPE_TOKEN_OR_QUOTED_STRING + value = readTokenOrQuotedString(input); + break; + case 3: + // FIELD_TYPE_LHEX + value = readLhex(input); + break; + case 4: + // FIELD_TYPE_QUOTED_LHEX + value = readQuotedLhex(input); + break; + default: + // Error + throw new IllegalArgumentException( + "TODO i18n: Unsupported type"); + } + + result.put(field, value); + + skipLws(input); + if (!swallowConstant(input, ",", true)) { + break; + } + skipLws(input); + field = readToken(input); + } + + return result; + } + + private static boolean swallowConstant(StringReader input, String constant, + boolean optional) throws IOException { + int len = constant.length(); + + for (int i = 0; i < len; i++) { + int c = input.read(); + if (c != constant.charAt(i)) { + if (optional) { + input.skip(i); + return false; + } else { + throw new IllegalArgumentException( + "TODO I18N: Failed to parse input for [" + constant + + "]"); + } + } + } + return true; + } + + private static void skipLws(StringReader input) throws IOException { + char c = (char) input.read(); + while (c == 32 || c == 9) { + c = (char) input.read(); + } + + // Skip back so non-LWS character is available for next read + input.skip(-1); + } + + private static String readToken(StringReader input) throws IOException { + StringBuilder result = new StringBuilder(); + + char c = (char) input.read(); + while (c != 65535 && isToken[c]) { + result.append(c); + c = (char) input.read(); + } + // Skip back so non-token character is available for next read + input.skip(-1); + + return result.toString(); + } + + private static String readQuotedString(StringReader input) + throws IOException { + + char c = (char) input.read(); + if (c != '"') { + throw new IllegalArgumentException( + "TODO i18n: Quoted string must start with a quote"); + } + + StringBuilder result = new StringBuilder(); + + c = (char) input.read(); + while (c != '"') { + if (c == '\\') { + c = (char) input.read(); + result.append(c); + } else { + result.append(c); + } + c = (char) input.read(); + } + + return result.toString(); + } + + private static String readTokenOrQuotedString(StringReader input) + throws IOException { + char c = (char) input.read(); + input.skip(-1); + + if (c == '"') { + return readQuotedString(input); + } else { + return readToken(input); + } + } + + /* + * Parses lower case hex but permits upper case hex to be used (converting + * it to lower case before returning). + */ + private static String readLhex(StringReader input) throws IOException { + StringBuilder result = new StringBuilder(); + + char c = (char) input.read(); + while (isHex[c]) { + result.append(c); + c = (char) input.read(); + } + // Skip back so non-hex character is available for next read + input.skip(-1); + + return result.toString().toLowerCase(); + } + + private static String readQuotedLhex(StringReader input) + throws IOException { + + swallowConstant(input, "\"", false); + String result = readLhex(input); + swallowConstant(input, "\"", false); + + return result; + } +} Propchange: tomcat/trunk/java/org/apache/tomcat/util/http/parser/HttpParser2.java ------------------------------------------------------------------------------ svn:eol-style = native Added: tomcat/trunk/test/org/apache/tomcat/util/http/parser/TestHttpParser2.java URL: http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/tomcat/util/http/parser/TestHttpParser2.java?rev=1404773&view=auto ============================================================================== --- tomcat/trunk/test/org/apache/tomcat/util/http/parser/TestHttpParser2.java (added) +++ tomcat/trunk/test/org/apache/tomcat/util/http/parser/TestHttpParser2.java Thu Nov 1 21:29:16 2012 @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.tomcat.util.http.parser; + +import java.io.StringReader; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; + +public class TestHttpParser2 { + + @Test + public void testBug54060a() throws Exception { + String header = "Digest username=\"mthornton\", " + + "realm=\"optrak.com\", " + + "nonce=\"1351427243671:c1d6360150712149bae931a3ed7cb498\", " + + "uri=\"/files/junk.txt\", " + + "response=\"c5c2410bfc46753e83a8f007888b0d2e\", " + + "opaque=\"DB85C1A73933A7EB586D10E4BF2924EF\", " + + "qop=auth, " + + "nc=00000001, " + + "cnonce=\"9926cb3c334ede11\""; + + StringReader input = new StringReader(header); + + Map<String,String> result = HttpParser2.parseAuthorizationDigest(input); + + Assert.assertEquals("mthornton", result.get("username")); + Assert.assertEquals("optrak.com", result.get("realm")); + Assert.assertEquals("1351427243671:c1d6360150712149bae931a3ed7cb498", + result.get("nonce")); + Assert.assertEquals("/files/junk.txt", result.get("uri")); + Assert.assertEquals("c5c2410bfc46753e83a8f007888b0d2e", + result.get("response")); + Assert.assertEquals("DB85C1A73933A7EB586D10E4BF2924EF", + result.get("opaque")); + Assert.assertEquals("auth", result.get("qop")); + Assert.assertEquals("00000001", result.get("nc")); + Assert.assertEquals("9926cb3c334ede11", result.get("cnonce")); + } + + @Test + public void testBug54060b() throws Exception { + String header = "Digest username=\"mthornton\", " + + "realm=\"optrak.com\", " + + "nonce=\"1351427480964:a01c16fed5168d72a2b5267395a2022e\", " + + "uri=\"/files\", " + + "algorithm=MD5, " + + "response=\"f310c44b87efc0bc0a7aab7096fd36b6\", " + + "opaque=\"DB85C1A73933A7EB586D10E4BF2924EF\", " + + "cnonce=\"MHg3ZjA3ZGMwMTUwMTA6NzI2OToxMzUxNDI3NDgw\", " + + "nc=00000001, " + + "qop=auth"; + + StringReader input = new StringReader(header); + + Map<String,String> result = HttpParser2.parseAuthorizationDigest(input); + + Assert.assertEquals("mthornton", result.get("username")); + Assert.assertEquals("optrak.com", result.get("realm")); + Assert.assertEquals("1351427480964:a01c16fed5168d72a2b5267395a2022e", + result.get("nonce")); + Assert.assertEquals("/files", result.get("uri")); + Assert.assertEquals("MD5", result.get("algorithm")); + Assert.assertEquals("f310c44b87efc0bc0a7aab7096fd36b6", + result.get("response")); + Assert.assertEquals("DB85C1A73933A7EB586D10E4BF2924EF", + result.get("opaque")); + Assert.assertEquals("MHg3ZjA3ZGMwMTUwMTA6NzI2OToxMzUxNDI3NDgw", + result.get("cnonce")); + Assert.assertEquals("00000001", result.get("nc")); + Assert.assertEquals("auth", result.get("qop")); + } + + @Test + public void testBug54060c() throws Exception { + String header = "Digest username=\"mthornton\", qop=auth"; + + StringReader input = new StringReader(header); + + Map<String,String> result = HttpParser2.parseAuthorizationDigest(input); + + Assert.assertEquals("mthornton", result.get("username")); + Assert.assertEquals("auth", result.get("qop")); + } + + @Test + public void testBug54060d() throws Exception { + String header = "Digest username=\"mthornton\"," + + "qop=auth," + + "cnonce=\"9926cb3c334ede11\""; + + StringReader input = new StringReader(header); + + Map<String,String> result = HttpParser2.parseAuthorizationDigest(input); + + Assert.assertEquals("mthornton", result.get("username")); + Assert.assertEquals("auth", result.get("qop")); + Assert.assertEquals("9926cb3c334ede11", result.get("cnonce")); + } +} Propchange: tomcat/trunk/test/org/apache/tomcat/util/http/parser/TestHttpParser2.java ------------------------------------------------------------------------------ svn:eol-style = native --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org