/* * JBoss, Home of Professional Open Source. * Copyright 2014 Red Hat, Inc., and individual contributors * as indicated by the @author tags. * * Licensed 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 io.undertow.util; import static io.undertow.UndertowMessages.MESSAGES; import java.util.LinkedHashMap; import java.util.Map; /** * Utility to parse the tokens contained within a HTTP header. * * @author <a href="mailto:darran.lofthouse@jboss.com">Darran Lofthouse</a> */ public class HeaderTokenParser<E extends HeaderToken> { private static final char EQUALS = '='; private static final char COMMA = ','; private static final char QUOTE = '"'; private static final char ESCAPE = '\\'; private final Map<String, E> expectedTokens; public HeaderTokenParser(final Map<String, E> expectedTokens) { this.expectedTokens = expectedTokens; } public Map<E, String> parseHeader(final String header) { char[] headerChars = header.toCharArray(); // The LinkedHashMap is used so that the parameter order can also be retained. Map<E, String> response = new LinkedHashMap<>(); SearchingFor searchingFor = SearchingFor.START_OF_NAME; int nameStart = 0; E currentToken = null; int valueStart = 0; int escapeCount = 0; boolean containsEscapes = false; for (int i = 0; i < headerChars.length; i++) { switch (searchingFor) { case START_OF_NAME: // Eliminate any white space before the name of the parameter. if (headerChars[i] != COMMA && !Character.isWhitespace(headerChars[i])) { nameStart = i; searchingFor = SearchingFor.EQUALS_SIGN; } break; case EQUALS_SIGN: if (headerChars[i] == EQUALS) { String paramName = String.valueOf(headerChars, nameStart, i - nameStart); currentToken = expectedTokens.get(paramName); if (currentToken == null) { throw MESSAGES.unexpectedTokenInHeader(paramName); } searchingFor = SearchingFor.START_OF_VALUE; } break; case START_OF_VALUE: if (!Character.isWhitespace(headerChars[i])) { if (headerChars[i] == QUOTE && currentToken.isAllowQuoted()) { valueStart = i + 1; searchingFor = SearchingFor.LAST_QUOTE; } else { valueStart = i; searchingFor = SearchingFor.END_OF_VALUE; } } break; case LAST_QUOTE: if (headerChars[i] == ESCAPE) { escapeCount++; containsEscapes = true; } else if (headerChars[i] == QUOTE && (escapeCount % 2 == 0)) { String value = String.valueOf(headerChars, valueStart, i - valueStart); if(containsEscapes) { StringBuilder sb = new StringBuilder(); boolean lastEscape = false; for(int j = 0; j < value.length(); ++j) { char c = value.charAt(j); if(c == ESCAPE && !lastEscape) { lastEscape = true; } else { lastEscape = false; sb.append(c); } } value = sb.toString(); containsEscapes = false; } response.put(currentToken, value); searchingFor = SearchingFor.START_OF_NAME; escapeCount = 0; } else { escapeCount = 0; } break; case END_OF_VALUE: if (headerChars[i] == COMMA || Character.isWhitespace(headerChars[i])) { String value = String.valueOf(headerChars, valueStart, i - valueStart); response.put(currentToken, value); searchingFor = SearchingFor.START_OF_NAME; } break; } } if (searchingFor == SearchingFor.END_OF_VALUE) { // Special case where we reached the end of the array containing the header values. String value = String.valueOf(headerChars, valueStart, headerChars.length - valueStart); response.put(currentToken, value); } else if (searchingFor != SearchingFor.START_OF_NAME) { // Somehow we are still in the middle of searching for a current value. throw MESSAGES.invalidHeader(); } return response; } enum SearchingFor { START_OF_NAME, EQUALS_SIGN, START_OF_VALUE, LAST_QUOTE, END_OF_VALUE; } }