1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one or more
3  * contributor license agreements.  See the NOTICE file distributed with
4  * this work for additional information regarding copyright ownership.
5  * The ASF licenses this file to You under the Apache License, Version 2.0
6  * (the "License"); you may not use this file except in compliance with
7  * the License.  You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */

17 package org.apache.tomcat.util.http.parser;
18
19 import java.nio.charset.StandardCharsets;
20
21 import org.apache.juli.logging.Log;
22 import org.apache.juli.logging.LogFactory;
23 import org.apache.tomcat.util.http.ServerCookie;
24 import org.apache.tomcat.util.http.ServerCookies;
25 import org.apache.tomcat.util.log.UserDataHelper;
26 import org.apache.tomcat.util.res.StringManager;
27
28
29 /**
30  * <p>Cookie header parser based on RFC6265 and RFC2109.</p>
31  * <p>The parsing of cookies using RFC6265 is more relaxed that the
32  * specification in the following ways:</p>
33  * <ul>
34  *   <li>Values 0x80 to 0xFF are permitted in cookie-octet to support the use of
35  *       UTF-8 in cookie values as used by HTML 5.</li>
36  *   <li>For cookies without a value, the '=' is not required after the name as
37  *       some browsers do not sent it.</li>
38  * </ul>
39  * <p>The parsing of cookies using RFC2109 is more relaxed that the
40  * specification in the following ways:</p>
41  * <ul>
42  *   <li>Values for the path attribute that contain a / character do not have to
43  *       be quoted even though / is not permitted in a token.</li>
44  * </ul>
45  *
46  * <p>Implementation note:<br>
47  * This class has been carefully tuned to ensure that it has equal or better
48  * performance than the original Netscape/RFC2109 cookie parser. Before
49  * committing and changes, ensure that the TesterCookiePerformance unit test
50  * continues to give results within 1% for the old and new parsers.</p>
51  */

52 public class Cookie {
53
54     private static final Log log = LogFactory.getLog(Cookie.class);
55     private static final UserDataHelper invalidCookieVersionLog = new UserDataHelper(log);
56     private static final UserDataHelper invalidCookieLog = new UserDataHelper(log);
57     private static final StringManager sm =
58             StringManager.getManager("org.apache.tomcat.util.http.parser");
59
60     private static final boolean isCookieOctet[] = new boolean[256];
61     private static final boolean isText[] = new boolean[256];
62     private static final byte[] VERSION_BYTES = "$Version".getBytes(StandardCharsets.ISO_8859_1);
63     private static final byte[] PATH_BYTES = "$Path".getBytes(StandardCharsets.ISO_8859_1);
64     private static final byte[] DOMAIN_BYTES = "$Domain".getBytes(StandardCharsets.ISO_8859_1);
65     private static final byte[] EMPTY_BYTES = new byte[0];
66     private static final byte TAB_BYTE = (byte) 0x09;
67     private static final byte SPACE_BYTE = (byte) 0x20;
68     private static final byte QUOTE_BYTE = (byte) 0x22;
69     private static final byte COMMA_BYTE = (byte) 0x2C;
70     private static final byte FORWARDSLASH_BYTE = (byte) 0x2F;
71     private static final byte SEMICOLON_BYTE = (byte) 0x3B;
72     private static final byte EQUALS_BYTE = (byte) 0x3D;
73     private static final byte SLASH_BYTE = (byte) 0x5C;
74     private static final byte DEL_BYTE = (byte) 0x7F;
75
76
77     static {
78         // %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E (RFC6265)
79         // %x80 to %xFF                                 (UTF-8)
80         for (int i = 0; i < 256; i++) {
81             if (i < 0x21 || i == QUOTE_BYTE || i == COMMA_BYTE ||
82                     i == SEMICOLON_BYTE || i == SLASH_BYTE || i == DEL_BYTE) {
83                 isCookieOctet[i] = false;
84             } else {
85                 isCookieOctet[i] = true;
86             }
87         }
88         for (int i = 0; i < 256; i++) {
89             if (i < TAB_BYTE || (i > TAB_BYTE && i < SPACE_BYTE) || i == DEL_BYTE) {
90                 isText[i] = false;
91             } else {
92                 isText[i] = true;
93             }
94         }
95     }
96
97
98     private Cookie() {
99         // Hide default constructor
100     }
101
102
103     public static void parseCookie(byte[] bytes, int offset, int len,
104             ServerCookies serverCookies) {
105
106         // ByteBuffer is used throughout this parser as it allows the byte[]
107         // and position information to be easily passed between parsing methods
108         ByteBuffer bb = new ByteBuffer(bytes, offset, len);
109
110         // Using RFC6265 parsing rules, check to see if the header starts with a
111         // version marker. An RFC2109 version marker may be read using RFC6265
112         // parsing rules. If version 1, use RFC2109. Else use RFC6265.
113
114         skipLWS(bb);
115
116         // Record position in case we need to return.
117         int mark = bb.position();
118
119         SkipResult skipResult = skipBytes(bb, VERSION_BYTES);
120         if (skipResult != SkipResult.FOUND) {
121             // No need to reset position since skipBytes() will have done it
122             parseCookieRfc6265(bb, serverCookies);
123             return;
124         }
125
126         skipLWS(bb);
127
128         skipResult = skipByte(bb, EQUALS_BYTE);
129         if (skipResult != SkipResult.FOUND) {
130             // Need to reset position as skipByte() will only have reset to
131             // position before it was called
132             bb.position(mark);
133             parseCookieRfc6265(bb, serverCookies);
134             return;
135         }
136
137         skipLWS(bb);
138
139         ByteBuffer value = readCookieValue(bb);
140         if (value != null && value.remaining() == 1) {
141             int version = value.get() - '0';
142             if (version == 1 || version == 0) {
143                 // $Version=1 -> RFC2109
144                 // $Version=0 -> RFC2109
145                 skipLWS(bb);
146                 byte b = bb.get();
147                 if (b == SEMICOLON_BYTE || b == COMMA_BYTE) {
148                     parseCookieRfc2109(bb, serverCookies, version);
149                 }
150             } else {
151                 // Unrecognised version.
152                 // Ignore this header.
153                 value.rewind();
154                 logInvalidVersion(value);
155             }
156         } else {
157             // Unrecognised version.
158             // Ignore this header.
159             logInvalidVersion(value);
160         }
161     }
162
163
164     public static String unescapeCookieValueRfc2109(String input) {
165         if (input == null || input.length() < 2) {
166             return input;
167         }
168         if (input.charAt(0) != '"' && input.charAt(input.length() - 1) != '"') {
169             return input;
170         }
171
172         StringBuilder sb = new StringBuilder(input.length());
173         char[] chars = input.toCharArray();
174         boolean escaped = false;
175
176         for (int i = 1; i < input.length() - 1; i++) {
177             if (chars[i] == '\\') {
178                 escaped = true;
179             } else if (escaped) {
180                 escaped = false;
181                 if (chars[i] < 128) {
182                     sb.append(chars[i]);
183                 } else {
184                     sb.append('\\');
185                     sb.append(chars[i]);
186                 }
187             } else {
188                 sb.append(chars[i]);
189             }
190         }
191         return sb.toString();
192     }
193
194
195     private static void parseCookieRfc6265(ByteBuffer bb, ServerCookies serverCookies) {
196
197         boolean moreToProcess = true;
198
199         while (moreToProcess) {
200             skipLWS(bb);
201
202             ByteBuffer name = readToken(bb);
203             ByteBuffer value = null;
204
205             skipLWS(bb);
206
207             SkipResult skipResult = skipByte(bb, EQUALS_BYTE);
208             if (skipResult == SkipResult.FOUND) {
209                 skipLWS(bb);
210                 value = readCookieValueRfc6265(bb);
211                 if (value == null) {
212                     logInvalidHeader(bb);
213                     // Invalid cookie value. Skip to the next semi-colon
214                     skipUntilSemiColon(bb);
215                     continue;
216                 }
217                 skipLWS(bb);
218             }
219
220             skipResult = skipByte(bb, SEMICOLON_BYTE);
221             if (skipResult == SkipResult.FOUND) {
222                 // NO-OP
223             } else if (skipResult == SkipResult.NOT_FOUND) {
224                 logInvalidHeader(bb);
225                 // Invalid cookie. Ignore it and skip to the next semi-colon
226                 skipUntilSemiColon(bb);
227                 continue;
228             } else {
229                 // SkipResult.EOF
230                 moreToProcess = false;
231             }
232
233             if (name.hasRemaining()) {
234                 ServerCookie sc = serverCookies.addCookie();
235                 sc.getName().setBytes(name.array(), name.position(), name.remaining());
236                 if (value == null) {
237                     sc.getValue().setBytes(EMPTY_BYTES, 0, EMPTY_BYTES.length);
238                 } else {
239                     sc.getValue().setBytes(value.array(), value.position(), value.remaining());
240                 }
241             }
242         }
243     }
244
245
246     private static void parseCookieRfc2109(ByteBuffer bb, ServerCookies serverCookies,
247             int version) {
248
249         boolean moreToProcess = true;
250
251         while (moreToProcess) {
252             skipLWS(bb);
253
254             boolean parseAttributes = true;
255
256             ByteBuffer name = readToken(bb);
257             ByteBuffer value = null;
258             ByteBuffer path = null;
259             ByteBuffer domain = null;
260
261             skipLWS(bb);
262
263             SkipResult skipResult = skipByte(bb, EQUALS_BYTE);
264             if (skipResult == SkipResult.FOUND) {
265                 skipLWS(bb);
266                 value = readCookieValueRfc2109(bb, false);
267                 if (value == null) {
268                     skipInvalidCookie(bb);
269                     continue;
270                 }
271                 skipLWS(bb);
272             }
273
274             skipResult = skipByte(bb, COMMA_BYTE);
275             if (skipResult == SkipResult.FOUND) {
276                 parseAttributes = false;
277             }
278             skipResult = skipByte(bb, SEMICOLON_BYTE);
279             if (skipResult == SkipResult.EOF) {
280                 parseAttributes = false;
281                 moreToProcess = false;
282             } else if (skipResult == SkipResult.NOT_FOUND) {
283                 skipInvalidCookie(bb);
284                 continue;
285             }
286
287             if (parseAttributes) {
288                 skipResult = skipBytes(bb, PATH_BYTES);
289                 if (skipResult == SkipResult.FOUND) {
290                     skipLWS(bb);
291                     skipResult = skipByte(bb, EQUALS_BYTE);
292                     if (skipResult != SkipResult.FOUND) {
293                         skipInvalidCookie(bb);
294                         continue;
295                     }
296                     path = readCookieValueRfc2109(bb, true);
297                     if (path == null) {
298                         skipInvalidCookie(bb);
299                         continue;
300                     }
301                     skipLWS(bb);
302
303                     skipResult = skipByte(bb, COMMA_BYTE);
304                     if (skipResult == SkipResult.FOUND) {
305                         parseAttributes = false;
306                     }
307                     skipResult = skipByte(bb, SEMICOLON_BYTE);
308                     if (skipResult == SkipResult.EOF) {
309                         parseAttributes = false;
310                         moreToProcess = false;
311                     } else if (skipResult == SkipResult.NOT_FOUND) {
312                         skipInvalidCookie(bb);
313                         continue;
314                     }
315                 }
316             }
317
318             if (parseAttributes) {
319                 skipResult = skipBytes(bb, DOMAIN_BYTES);
320                 if (skipResult == SkipResult.FOUND) {
321                     skipLWS(bb);
322                     skipResult = skipByte(bb, EQUALS_BYTE);
323                     if (skipResult != SkipResult.FOUND) {
324                         skipInvalidCookie(bb);
325                         continue;
326                     }
327                     domain = readCookieValueRfc2109(bb, false);
328                     if (domain == null) {
329                         skipInvalidCookie(bb);
330                         continue;
331                     }
332
333                     skipResult = skipByte(bb, COMMA_BYTE);
334                     if (skipResult == SkipResult.FOUND) {
335                         parseAttributes = false;
336                     }
337                     skipResult = skipByte(bb, SEMICOLON_BYTE);
338                     if (skipResult == SkipResult.EOF) {
339                         parseAttributes = false;
340                         moreToProcess = false;
341                     } else if (skipResult == SkipResult.NOT_FOUND) {
342                         skipInvalidCookie(bb);
343                         continue;
344                     }
345                 }
346             }
347
348             if (name.hasRemaining() && value != null && value.hasRemaining()) {
349                 ServerCookie sc = serverCookies.addCookie();
350                 sc.setVersion(version);
351                 sc.getName().setBytes(name.array(), name.position(), name.remaining());
352                 sc.getValue().setBytes(value.array(), value.position(), value.remaining());
353                 if (domain != null) {
354                     sc.getDomain().setBytes(domain.array(),  domain.position(),  domain.remaining());
355                 }
356                 if (path != null) {
357                     sc.getPath().setBytes(path.array(),  path.position(),  path.remaining());
358                 }
359             }
360         }
361     }
362
363
364     private static void skipInvalidCookie(ByteBuffer bb) {
365         logInvalidHeader(bb);
366         // Invalid cookie value. Skip to the next semi-colon
367         skipUntilSemiColonOrComma(bb);
368     }
369
370
371     private static void skipLWS(ByteBuffer bb) {
372         while(bb.hasRemaining()) {
373             byte b = bb.get();
374             if (b != TAB_BYTE && b != SPACE_BYTE) {
375                 bb.rewind();
376                 break;
377             }
378         }
379     }
380
381
382     private static void skipUntilSemiColon(ByteBuffer bb) {
383         while(bb.hasRemaining()) {
384             if (bb.get() == SEMICOLON_BYTE) {
385                 break;
386             }
387         }
388     }
389
390
391     private static void skipUntilSemiColonOrComma(ByteBuffer bb) {
392         while(bb.hasRemaining()) {
393             byte b = bb.get();
394             if (b == SEMICOLON_BYTE || b == COMMA_BYTE) {
395                 break;
396             }
397         }
398     }
399
400
401     private static SkipResult skipByte(ByteBuffer bb, byte target) {
402
403         if (!bb.hasRemaining()) {
404             return SkipResult.EOF;
405         }
406         if (bb.get() == target) {
407             return SkipResult.FOUND;
408         }
409
410         bb.rewind();
411         return SkipResult.NOT_FOUND;
412     }
413
414
415     private static SkipResult skipBytes(ByteBuffer bb, byte[] target) {
416         int mark = bb.position();
417
418         for (int i = 0; i < target.length; i++) {
419             if (!bb.hasRemaining()) {
420                 bb.position(mark);
421                 return SkipResult.EOF;
422             }
423             if (bb.get() != target[i]) {
424                 bb.position(mark);
425                 return SkipResult.NOT_FOUND;
426             }
427         }
428         return SkipResult.FOUND;
429     }
430
431
432     /**
433      * Similar to readCookieValueRfc6265() but also allows a comma to terminate
434      * the value (as permitted by RFC2109).
435      */

436     private static ByteBuffer readCookieValue(ByteBuffer bb) {
437         boolean quoted = false;
438         if (bb.hasRemaining()) {
439             if (bb.get() == QUOTE_BYTE) {
440                 quoted = true;
441             } else {
442                 bb.rewind();
443             }
444         }
445         int start = bb.position();
446         int end = bb.limit();
447         while (bb.hasRemaining()) {
448             byte b = bb.get();
449             if (isCookieOctet[(b & 0xFF)]) {
450                 // NO-OP
451             } else if (b == SEMICOLON_BYTE || b == COMMA_BYTE || b == SPACE_BYTE || b == TAB_BYTE) {
452                 end = bb.position() - 1;
453                 bb.position(end);
454                 break;
455             } else if (quoted && b == QUOTE_BYTE) {
456                 end = bb.position() - 1;
457                 break;
458             } else {
459                 // Invalid cookie
460                 return null;
461             }
462         }
463
464         return new ByteBuffer(bb.bytes, start, end - start);
465     }
466
467
468     /**
469      * Similar to readCookieValue() but treats a comma as part of an invalid
470      * value.
471      */

472     private static ByteBuffer readCookieValueRfc6265(ByteBuffer bb) {
473         boolean quoted = false;
474         if (bb.hasRemaining()) {
475             if (bb.get() == QUOTE_BYTE) {
476                 quoted = true;
477             } else {
478                 bb.rewind();
479             }
480         }
481         int start = bb.position();
482         int end = bb.limit();
483         while (bb.hasRemaining()) {
484             byte b = bb.get();
485             if (isCookieOctet[(b & 0xFF)]) {
486                 // NO-OP
487             } else if (b == SEMICOLON_BYTE || b == SPACE_BYTE || b == TAB_BYTE) {
488                 end = bb.position() - 1;
489                 bb.position(end);
490                 break;
491             } else if (quoted && b == QUOTE_BYTE) {
492                 end = bb.position() - 1;
493                 break;
494             } else {
495                 // Invalid cookie
496                 return null;
497             }
498         }
499
500         return new ByteBuffer(bb.bytes, start, end - start);
501     }
502
503
504     private static ByteBuffer readCookieValueRfc2109(ByteBuffer bb, boolean allowForwardSlash) {
505         if (!bb.hasRemaining()) {
506             return null;
507         }
508
509         if (bb.peek() == QUOTE_BYTE) {
510             return readQuotedString(bb);
511         } else {
512             if (allowForwardSlash) {
513                 return readTokenAllowForwardSlash(bb);
514             } else {
515                 return readToken(bb);
516             }
517         }
518     }
519
520
521     private static ByteBuffer readToken(ByteBuffer bb) {
522         final int start = bb.position();
523         int end = bb.limit();
524         while (bb.hasRemaining()) {
525             if (!HttpParser.isToken(bb.get())) {
526                 end = bb.position() - 1;
527                 bb.position(end);
528                 break;
529             }
530         }
531
532         return new ByteBuffer(bb.bytes, start, end - start);
533     }
534
535
536     private static ByteBuffer readTokenAllowForwardSlash(ByteBuffer bb) {
537         final int start = bb.position();
538         int end = bb.limit();
539         while (bb.hasRemaining()) {
540             byte b = bb.get();
541             if (b != FORWARDSLASH_BYTE && !HttpParser.isToken(b)) {
542                 end = bb.position() - 1;
543                 bb.position(end);
544                 break;
545             }
546         }
547
548         return new ByteBuffer(bb.bytes, start, end - start);
549     }
550
551
552     private static ByteBuffer readQuotedString(ByteBuffer bb) {
553         int start = bb.position();
554
555         // Read the opening quote
556         bb.get();
557         boolean escaped = false;
558         while (bb.hasRemaining()) {
559             byte b = bb.get();
560             if (b == SLASH_BYTE) {
561                 // Escaping another character
562                 escaped = true;
563             } else if (escaped && b > (byte) -1) {
564                 escaped = false;
565             } else if (b == QUOTE_BYTE) {
566                 return new ByteBuffer(bb.bytes, start, bb.position() - start);
567             } else if (isText[b & 0xFF]) {
568                 escaped = false;
569             } else {
570                 return null;
571             }
572         }
573
574         return null;
575     }
576
577
578     private static void logInvalidHeader(ByteBuffer bb) {
579         UserDataHelper.Mode logMode = invalidCookieLog.getNextMode();
580         if (logMode != null) {
581             String headerValue = new String(bb.array(), bb.position(), bb.limit() - bb.position(),
582                         StandardCharsets.UTF_8);
583             String message = sm.getString("cookie.invalidCookieValue", headerValue);
584             switch (logMode) {
585                 case INFO_THEN_DEBUG:
586                     message += sm.getString("cookie.fallToDebug");
587                     //$FALL-THROUGH$
588                 case INFO:
589                     log.info(message);
590                     break;
591                 case DEBUG:
592                     log.debug(message);
593             }
594         }
595     }
596
597
598     private static void logInvalidVersion(ByteBuffer value) {
599         UserDataHelper.Mode logMode = invalidCookieVersionLog.getNextMode();
600         if (logMode != null) {
601             String version;
602             if (value == null) {
603                 version = sm.getString("cookie.valueNotPresent");
604             } else {
605                 version = new String(value.bytes, value.position(),
606                         value.limit() - value.position(), StandardCharsets.UTF_8);
607             }
608             String message = sm.getString("cookie.invalidCookieVersion", version);
609             switch (logMode) {
610                 case INFO_THEN_DEBUG:
611                     message += sm.getString("cookie.fallToDebug");
612                     //$FALL-THROUGH$
613                 case INFO:
614                     log.info(message);
615                     break;
616                 case DEBUG:
617                     log.debug(message);
618             }
619         }
620     }
621
622
623     /**
624      * Custom implementation that skips many of the safety checks in
625      * {@link java.nio.ByteBuffer}.
626      */

627     private static class ByteBuffer {
628
629         private final byte[] bytes;
630         private int limit;
631         private int position = 0;
632
633         public ByteBuffer(byte[] bytes, int offset, int len) {
634             this.bytes = bytes;
635             this.position = offset;
636             this.limit = offset + len;
637         }
638
639         public int position() {
640             return position;
641         }
642
643         public void position(int position) {
644             this.position = position;
645         }
646
647         public int limit() {
648             return limit;
649         }
650
651         public int remaining() {
652             return limit - position;
653         }
654
655         public boolean hasRemaining() {
656             return position < limit;
657         }
658
659         public byte get() {
660             return bytes[position++];
661         }
662
663         public byte peek() {
664             return bytes[position];
665         }
666
667         public void rewind() {
668             position--;
669         }
670
671         public byte[] array() {
672             return bytes;
673         }
674
675         // For debug purposes
676         @Override
677         public String toString() {
678             return "position [" + position + "], limit [" + limit + "]";
679         }
680     }
681 }
682