Author: markt
Date: Mon Sep  1 18:59:08 2014
New Revision: 1621868

URL: http://svn.apache.org/r1621868
Log:
Add initial implementation of RFC6265/RFC2109 cookie parser

Added:
    tomcat/trunk/java/org/apache/tomcat/util/http/parser/Cookie.java   (with 
props)
    
tomcat/trunk/java/org/apache/tomcat/util/http/parser/LocalStrings.properties   
(with props)

Added: tomcat/trunk/java/org/apache/tomcat/util/http/parser/Cookie.java
URL: 
http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/tomcat/util/http/parser/Cookie.java?rev=1621868&view=auto
==============================================================================
--- tomcat/trunk/java/org/apache/tomcat/util/http/parser/Cookie.java (added)
+++ tomcat/trunk/java/org/apache/tomcat/util/http/parser/Cookie.java Mon Sep  1 
18:59:08 2014
@@ -0,0 +1,425 @@
+/*
+ * 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.nio.charset.StandardCharsets;
+
+import org.apache.juli.logging.Log;
+import org.apache.juli.logging.LogFactory;
+import org.apache.tomcat.util.http.ServerCookie;
+import org.apache.tomcat.util.http.ServerCookies;
+import org.apache.tomcat.util.log.UserDataHelper;
+import org.apache.tomcat.util.res.StringManager;
+
+
+/**
+ * Cookie header parser based on RFC6265 and RFC2109.
+ * <br/>
+ * The parsing of cookies using RFC6265 is more relaxed that the specification
+ * in the following ways:
+ * <ul>
+ *   <li>Values 0x80 to 0xFF are permitted in cookie-octet to support the use 
of
+ *       UTF-8 in cookie values as used by HTML 5.</li>
+ *   <li>For cookies without a value, the '=' is not required after the name as
+ *       some browsers do not sent it.</li>
+ * </ul>
+ */
+public class Cookie {
+
+    private static final Log log = LogFactory.getLog(Cookie.class);
+    private static final UserDataHelper invalidCookieVersionLog = new 
UserDataHelper(log);
+    private static final UserDataHelper invalidCookieLog = new 
UserDataHelper(log);
+    private static final StringManager sm =
+            StringManager.getManager("org.apache.tomcat.util.http.parser");
+
+    private static final boolean isCookieOctet[] = new boolean[256];
+    private static final byte[] VERSION_BYTES = 
"$Version".getBytes(StandardCharsets.ISO_8859_1);
+    private static final byte[] EMPTY_BYTES = new byte[0];
+    private static final byte TAB_BYTE = (byte) 0x09;
+    private static final byte SPACE_BYTE = (byte) 0x20;
+    private static final byte QUOTE_BYTE = (byte) 0x22;
+    private static final byte COMMA_BYTE = (byte) 0x2C;
+    private static final byte SEMICOLON_BYTE = (byte) 0x3B;
+    private static final byte EQUALS_BYTE = (byte) 0x3D;
+    private static final byte SLASH_BYTE = (byte) 0x5C;
+    private static final byte DEL_BYTE = (byte) 0x7F;
+
+
+    static {
+        // %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E (RFC6265)
+        // %x80 to %xFF                            (UTF-8)
+        for (int i = 0; i < 256; i++) {
+            if (i < 0x21 || i == QUOTE_BYTE || i == COMMA_BYTE ||
+                    i == SEMICOLON_BYTE || i == SLASH_BYTE || i == DEL_BYTE) {
+                isCookieOctet[i] = false;
+            } else {
+                isCookieOctet[i] = true;
+            }
+        }
+    }
+
+
+    private Cookie() {
+        // Hide default constructor
+    }
+
+
+    public static void parseCookie(byte[] bytes, int offset, int len,
+            ServerCookies serverCookies) {
+
+        // ByteBuffer is used throughout this parser as it allows the byte[]
+        // and position information to be easily passed between parsing methods
+        ByteBuffer bb = new ByteBuffer(bytes, offset, len);
+
+        // Using RFC6265 parsing rules, check to see if the header starts with 
a
+        // version marker. An RFC2109 version marker may be read using RFC6265
+        // parsing rules. If version 1, use RFC2109. Else use RFC6265.
+
+        skipLWS(bb);
+
+        // Record position in case we need to return.
+        int mark = bb.position();
+
+        SkipResult skipResult = skipBytes(bb, VERSION_BYTES);
+        if (skipResult != SkipResult.FOUND) {
+            // No need to reset position since skipConstant() will have done it
+            parseCookieRfc6265(bb, serverCookies);
+            return;
+        }
+
+        skipLWS(bb);
+
+        skipResult = skipByte(bb, EQUALS_BYTE);
+        if (skipResult != SkipResult.FOUND) {
+            // Need to reset position as skipConstant() will only have reset to
+            // position before it was called
+            bb.position(mark);
+            parseCookieRfc6265(bb, serverCookies);
+            return;
+        }
+
+        skipLWS(bb);
+
+        ByteBuffer value = readCookieValue(bb);
+        if (value != null && value.remaining() == 1 && value.get() == (byte) 
49) {
+            // $Version=1 -> RFC2109
+            parseCookieRfc2109(bb, serverCookies);
+            return;
+        } else {
+            // Unrecognised version.
+            // Ignore this header.
+            logInvalidVersion(value);
+        }
+    }
+
+
+    private static void parseCookieRfc6265(ByteBuffer bb, ServerCookies 
serverCookies) {
+
+        boolean moreToProcess = true;
+
+        while (moreToProcess) {
+            skipLWS(bb);
+
+            ByteBuffer name = readToken(bb);
+            ByteBuffer value = null;
+
+            skipLWS(bb);
+
+            SkipResult skipResult = skipByte(bb, EQUALS_BYTE);
+            if (skipResult == SkipResult.FOUND) {
+                skipLWS(bb);
+                value = readCookieValueRfc6265(bb);
+                if (value == null) {
+                    logInvalidHeader(bb);
+                    // Invalid cookie value. Skip to the next semi-colon
+                    skipUntilSemiColon(bb);
+                    continue;
+                }
+                skipLWS(bb);
+            }
+
+            skipResult = skipByte(bb, SEMICOLON_BYTE);
+            if (skipResult == SkipResult.FOUND) {
+                // NO-OP
+            } else if (skipResult == SkipResult.NOT_FOUND) {
+                logInvalidHeader(bb);
+                // Invalid cookie. Ignore it and skip to the next semi-colon
+                skipUntilSemiColon(bb);
+                continue;
+            } else {
+                // SkipResult.EOF
+                moreToProcess = false;
+            }
+
+            if (name.hasRemaining()) {
+                ServerCookie sc = serverCookies.addCookie();
+                sc.getName().setBytes(name.array(), name.position(), 
name.remaining());
+                if (value == null) {
+                    sc.getValue().setBytes(EMPTY_BYTES, 0, EMPTY_BYTES.length);
+                } else {
+                    sc.getValue().setBytes(value.array(), value.position(), 
value.remaining());
+                }
+            }
+
+        }
+    }
+
+
+    private static void parseCookieRfc2109(ByteBuffer bb, ServerCookies 
serverCookies) {
+        System.out.println("Parse with RFC 2109");
+        // TODO
+    }
+
+
+    private static void skipLWS(ByteBuffer bb) {
+        while(bb.hasRemaining()) {
+            byte b = bb.get();
+            if (b != TAB_BYTE && b != SPACE_BYTE) {
+                bb.rewind();
+                break;
+            }
+        }
+    }
+
+
+    private static void skipUntilSemiColon(ByteBuffer bb) {
+        while(bb.hasRemaining()) {
+            if (bb.get() == EQUALS_BYTE) {
+                break;
+            }
+        }
+    }
+
+
+    private static SkipResult skipByte(ByteBuffer bb, byte target) {
+
+        if (!bb.hasRemaining()) {
+            return SkipResult.EOF;
+        }
+        if (bb.get() == target) {
+            return SkipResult.FOUND;
+        }
+
+        bb.rewind();
+        return SkipResult.NOT_FOUND;
+    }
+
+
+    private static SkipResult skipBytes(ByteBuffer bb, byte[] target) {
+        int mark = bb.position();
+
+        for (int i = 0; i < target.length; i++) {
+            if (!bb.hasRemaining()) {
+                bb.position(mark);
+                return SkipResult.EOF;
+            }
+            if (bb.get() != target[i]) {
+                bb.position(mark);
+                return SkipResult.NOT_FOUND;
+            }
+        }
+        return SkipResult.FOUND;
+    }
+
+
+    /**
+     * Similar to readCookieValueRfc6265() but also allows a comma to terminate
+     * the value (as permitted by RFC2109).
+     */
+    private static ByteBuffer readCookieValue(ByteBuffer bb) {
+        boolean quoted = false;
+        if (bb.hasRemaining()) {
+            if (bb.get() == QUOTE_BYTE) {
+                quoted = true;
+            } else {
+                bb.rewind();
+            }
+        }
+        int start = bb.position();
+        int end = bb.limit();
+        while (bb.hasRemaining()) {
+            byte b = bb.get();
+            if (isCookieOctet[(b & 0xFF)]) {
+                // NO-OP
+            } else if (b == SEMICOLON_BYTE || b == COMMA_BYTE || b == 
SPACE_BYTE || b == TAB_BYTE) {
+                end = bb.position() - 1;
+                bb.position(end);
+                break;
+            } else if (quoted && b == QUOTE_BYTE) {
+                end = bb.position() - 1;
+                break;
+            } else {
+                // Invalid cookie
+                return null;
+            }
+        }
+
+        return new ByteBuffer(bb.bytes, start, end - start);
+    }
+
+
+    /**
+     * Similar to readCookieValue() but treats a comma as part of an invalid
+     * value.
+     */
+    private static ByteBuffer readCookieValueRfc6265(ByteBuffer bb) {
+        boolean quoted = false;
+        if (bb.hasRemaining()) {
+            if (bb.get() == QUOTE_BYTE) {
+                quoted = true;
+            } else {
+                bb.rewind();
+            }
+        }
+        int start = bb.position();
+        int end = bb.limit();
+        while (bb.hasRemaining()) {
+            byte b = bb.get();
+            if (isCookieOctet[(b & 0xFF)]) {
+                // NO-OP
+            } else if (b == SEMICOLON_BYTE || b == SPACE_BYTE || b == 
TAB_BYTE) {
+                end = bb.position() - 1;
+                bb.position(end);
+                break;
+            } else if (quoted && b == QUOTE_BYTE) {
+                end = bb.position() - 1;
+                break;
+            } else {
+                // Invalid cookie
+                return null;
+            }
+        }
+
+        return new ByteBuffer(bb.bytes, start, end - start);
+    }
+
+
+    private static ByteBuffer readToken(ByteBuffer bb) {
+        final int start = bb.position();
+        int end = bb.limit();
+        while (bb.hasRemaining()) {
+            if (!HttpParser.isToken(bb.get())) {
+                end = bb.position() - 1;
+                bb.position(end);
+                break;
+            }
+        }
+
+        return new ByteBuffer(bb.bytes, start, end - start);
+    }
+
+
+    private static void logInvalidHeader(ByteBuffer bb) {
+        UserDataHelper.Mode logMode = invalidCookieLog.getNextMode();
+        if (logMode != null) {
+            String headerValue = new String(bb.array(), bb.position(), 
bb.limit(),
+                        StandardCharsets.UTF_8);
+            String message = sm.getString("cookie.invalidCookieValue", 
headerValue);
+            switch (logMode) {
+                case INFO_THEN_DEBUG:
+                    message += sm.getString("cookie.fallToDebug");
+                    //$FALL-THROUGH$
+                case INFO:
+                    log.info(message);
+                    break;
+                case DEBUG:
+                    log.debug(message);
+            }
+        }
+    }
+
+
+    private static void logInvalidVersion(ByteBuffer value) {
+        UserDataHelper.Mode logMode = invalidCookieVersionLog.getNextMode();
+        if (logMode != null) {
+            String version;
+            if (value == null) {
+                version = sm.getString("cookie.valueNotPresent");
+            } else {
+                version = new String(value.bytes, value.position(), 
value.limit(),
+                        StandardCharsets.UTF_8);
+            }
+            String message = sm.getString("cookie.invalidCookieVersion", 
version);
+            switch (logMode) {
+                case INFO_THEN_DEBUG:
+                    message += sm.getString("cookie.fallToDebug");
+                    //$FALL-THROUGH$
+                case INFO:
+                    log.info(message);
+                    break;
+                case DEBUG:
+                    log.debug(message);
+            }
+        }
+    }
+
+
+    /**
+     * Custom implementation that skips many of the safety checks in
+     * {@link javax.nio.ByteBuffer}.
+     */
+    private static class ByteBuffer {
+
+        private final byte[] bytes;
+        private int limit;
+        private int position = 0;
+
+        public ByteBuffer(byte[] bytes, int offset, int len) {
+            this.bytes = bytes;
+            this.position = offset;
+            this.limit = offset + len;
+        }
+
+        public int position() {
+            return position;
+        }
+
+        public void position(int position) {
+            this.position = position;
+        }
+
+        public int limit() {
+            return limit;
+        }
+
+        public int remaining() {
+            return limit - position;
+        }
+
+        public boolean hasRemaining() {
+            return position < limit;
+        }
+
+        public byte get() {
+            return bytes[position++];
+        }
+
+        public void rewind() {
+            position--;
+        }
+
+        public byte[] array() {
+            return bytes;
+        }
+
+        // For debug purposes
+        @Override
+        public String toString() {
+            return "position [" + position + "], limit [" + limit + "]";
+        }
+    }
+}

Propchange: tomcat/trunk/java/org/apache/tomcat/util/http/parser/Cookie.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: 
tomcat/trunk/java/org/apache/tomcat/util/http/parser/LocalStrings.properties
URL: 
http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/tomcat/util/http/parser/LocalStrings.properties?rev=1621868&view=auto
==============================================================================
--- 
tomcat/trunk/java/org/apache/tomcat/util/http/parser/LocalStrings.properties 
(added)
+++ 
tomcat/trunk/java/org/apache/tomcat/util/http/parser/LocalStrings.properties 
Mon Sep  1 18:59:08 2014
@@ -0,0 +1,19 @@
+# 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.
+
+cookie.fallToDebug=Note: further occurrences of this error will be logged at 
DEBUG level.
+cookie.invalidCookieValue=A cookie header was received [{0}] that contained an 
invalid cookie. That cookie will be ignored.
+cookie.invalidCookieVersion=A cookie header was received using an unrecognised 
cookie version of [{0}]. The header and the cookies it contains will be ignored.
+cookie.valueNotPresent=<not present>
\ No newline at end of file

Propchange: 
tomcat/trunk/java/org/apache/tomcat/util/http/parser/LocalStrings.properties
------------------------------------------------------------------------------
    svn:eol-style = native



---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org
For additional commands, e-mail: dev-h...@tomcat.apache.org

Reply via email to