1 /*
2  * JBoss, Home of Professional Open Source.
3  * Copyright 2014 Red Hat, Inc., and individual contributors
4  * as indicated by the @author tags.
5  *
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  *     http://www.apache.org/licenses/LICENSE-2.0
11  *
12  *  Unless required by applicable law or agreed to in writing, software
13  *  distributed under the License is distributed on an "AS IS" BASIS,
14  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  *  See the License for the specific language governing permissions and
16  *  limitations under the License.
17  */

18 package io.undertow.util;
19
20 import static io.undertow.UndertowMessages.MESSAGES;
21
22 import java.util.LinkedHashMap;
23 import java.util.Map;
24
25 /**
26  * Utility to parse the tokens contained within a HTTP header.
27  *
28  * @author <a href="mailto:darran.lofthouse@jboss.com">Darran Lofthouse</a>
29  */

30 public class HeaderTokenParser<E extends HeaderToken> {
31
32     private static final char EQUALS = '=';
33     private static final char COMMA = ',';
34     private static final char QUOTE = '"';
35     private static final char ESCAPE = '\\';
36
37     private final Map<String, E> expectedTokens;
38
39     public HeaderTokenParser(final Map<String, E> expectedTokens) {
40         this.expectedTokens = expectedTokens;
41     }
42
43     public Map<E, String> parseHeader(final String header) {
44         char[] headerChars = header.toCharArray();
45
46         // The LinkedHashMap is used so that the parameter order can also be retained.
47         Map<E, String> response = new LinkedHashMap<>();
48
49         SearchingFor searchingFor = SearchingFor.START_OF_NAME;
50         int nameStart = 0;
51         E currentToken = null;
52         int valueStart = 0;
53
54         int escapeCount = 0;
55         boolean containsEscapes = false;
56
57         for (int i = 0; i < headerChars.length; i++) {
58             switch (searchingFor) {
59                 case START_OF_NAME:
60                     // Eliminate any white space before the name of the parameter.
61                     if (headerChars[i] != COMMA && !Character.isWhitespace(headerChars[i])) {
62                         nameStart = i;
63                         searchingFor = SearchingFor.EQUALS_SIGN;
64                     }
65                     break;
66                 case EQUALS_SIGN:
67                     if (headerChars[i] == EQUALS) {
68                         String paramName = String.valueOf(headerChars, nameStart, i - nameStart);
69                         currentToken = expectedTokens.get(paramName);
70                         if (currentToken == null) {
71                             throw MESSAGES.unexpectedTokenInHeader(paramName);
72                         }
73                         searchingFor = SearchingFor.START_OF_VALUE;
74                     }
75                     break;
76                 case START_OF_VALUE:
77                     if (!Character.isWhitespace(headerChars[i])) {
78                         if (headerChars[i] == QUOTE && currentToken.isAllowQuoted()) {
79                             valueStart = i + 1;
80                             searchingFor = SearchingFor.LAST_QUOTE;
81                         } else {
82                             valueStart = i;
83                             searchingFor = SearchingFor.END_OF_VALUE;
84                         }
85                     }
86                     break;
87                 case LAST_QUOTE:
88                     if (headerChars[i] == ESCAPE) {
89                         escapeCount++;
90                         containsEscapes = true;
91                     } else if (headerChars[i] == QUOTE && (escapeCount % 2 == 0)) {
92                         String value = String.valueOf(headerChars, valueStart, i - valueStart);
93                         if(containsEscapes) {
94                             StringBuilder sb = new StringBuilder();
95                             boolean lastEscape = false;
96                             for(int j = 0; j < value.length(); ++j) {
97                                 char c = value.charAt(j);
98                                 if(c == ESCAPE && !lastEscape) {
99                                     lastEscape = true;
100                                 } else {
101                                     lastEscape = false;
102                                     sb.append(c);
103                                 }
104                             }
105                             value = sb.toString();
106                             containsEscapes = false;
107                         }
108                         response.put(currentToken, value);
109
110                         searchingFor = SearchingFor.START_OF_NAME;
111                         escapeCount = 0;
112                     } else {
113                         escapeCount = 0;
114                     }
115                     break;
116                 case END_OF_VALUE:
117                     if (headerChars[i] == COMMA || Character.isWhitespace(headerChars[i])) {
118                         String value = String.valueOf(headerChars, valueStart, i - valueStart);
119                         response.put(currentToken, value);
120
121                         searchingFor = SearchingFor.START_OF_NAME;
122                     }
123                     break;
124             }
125         }
126
127         if (searchingFor == SearchingFor.END_OF_VALUE) {
128             // Special case where we reached the end of the array containing the header values.
129             String value = String.valueOf(headerChars, valueStart, headerChars.length - valueStart);
130             response.put(currentToken, value);
131         } else if (searchingFor != SearchingFor.START_OF_NAME) {
132             // Somehow we are still in the middle of searching for a current value.
133             throw MESSAGES.invalidHeader();
134         }
135
136         return response;
137     }
138
139     enum SearchingFor {
140         START_OF_NAME, EQUALS_SIGN, START_OF_VALUE, LAST_QUOTE, END_OF_VALUE;
141     }
142
143 }
144