![JAR search and dependency download from the Maven repository](/logo.png)
net.officefloor.web.tokenise.HttpRequestTokeniser Maven / Gradle / Ivy
/*
* OfficeFloor - http://www.officefloor.net
* Copyright (C) 2005-2018 Daniel Sagenschneider
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
package net.officefloor.web.tokenise;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.Arrays;
import net.officefloor.server.http.HttpException;
import net.officefloor.server.http.HttpHeader;
import net.officefloor.server.http.HttpRequest;
import net.officefloor.server.http.ServerHttpConnection;
import net.officefloor.web.build.HttpArgumentParser;
import net.officefloor.web.build.HttpValueLocation;
import net.officefloor.web.escalation.BadRequestHttpException;
import net.officefloor.web.value.load.ValueLoader;
/**
* Tokenises the {@link HttpRequest} for the path, parameters, fragment.
*
* @author Daniel Sagenschneider
*/
public class HttpRequestTokeniser {
/**
* {@link ThreadLocal} for the {@link ParseState}.
*/
private static ThreadLocal parseState = new ThreadLocal() {
@Override
protected ParseState initialValue() {
return new ParseState();
}
};
/**
* State of parsing.
*/
private static class ParseState implements CharSequence {
/**
* Buffer with reasonable default size.
*/
private char[] buffer = new char[256];
/**
* Length of {@link CharSequence}.
*/
private int length = 0;
/*
* ============= CharSequence ================
*/
@Override
public int length() {
return this.length;
}
@Override
public char charAt(int index) {
return this.buffer[index];
}
@Override
public CharSequence subSequence(int start, int end) {
throw new UnsupportedOperationException("Should not sub sequence " + this.getClass().getName());
}
@Override
public String toString() {
return new String(this.buffer, 0, this.length);
}
}
/**
* Tokenises the {@link HttpRequest} for the arguments to the
* {@link ValueLoader}.
*
* @param request
* {@link HttpRequest} to be tokenised.
* @param valueLoader
* {@link ValueLoader}.
* @param argumentParsers
* {@link HttpArgumentParser} instances.
* @throws HttpException
* If fails to tokenise the {@link HttpRequest}.
*/
public static void tokeniseHttpRequest(HttpRequest request, HttpArgumentParser[] argumentParsers,
ValueLoader valueLoader) throws HttpException {
// Obtain the parse state (ready for use)
ParseState state = parseState.get();
// Load the query string arguments
String requestUri = request.getUri();
// Values to aid in parsing
boolean isPathProcessed = false;
int begin = 0; // start of contents
int end = -1;
String name = null;
String value = null;
boolean isRequireDecode = false;
// Iterate over the contents, loading the parameters
PARSING: for (int i = 0; i < requestUri.length(); i++) {
char character = requestUri.charAt(i);
// Handle based on character
switch (character) {
case '?':
// If not processing path then just include
if (!isPathProcessed) {
// No longer processing path
isPathProcessed = true;
begin = i + 1; // after '?'
}
break;
case '=':
// Flag to now obtain value
end = i; // before '='
name = decode(requestUri.substring(begin, end), isRequireDecode, state);
begin = i + 1; // after '='
end = -1;
break;
case '+': // space
case '%': // escaping
// Requires translating
isRequireDecode = true;
break;
case '&':
case ';':
// Have parameter name/value, so load
end = i; // before terminator
value = decode(requestUri.substring(begin, end), isRequireDecode, state);
valueLoader.loadValue(name, value, HttpValueLocation.QUERY);
name = null;
// Reset for next parameter name/value
begin = i + 1; // after terminator
end = -1;
isRequireDecode = false;
break;
case '#':
// Determine previous (path/parameter)
if (isPathProcessed) {
// At end of parameters as have fragment
end = i; // before '#'
break PARSING;
}
}
}
// Determine if load final value
if ((name != null) && (begin > 0)) {
// Obtain the value
if (end == -1) {
end = requestUri.length();
}
value = decode(requestUri.substring(begin, end), isRequireDecode, state);
// Load the value
valueLoader.loadValue(name, value, HttpValueLocation.QUERY);
}
// Load the header arguments (and determine content type)
String contentType = null;
for (HttpHeader header : request.getHeaders()) {
name = header.getName();
value = header.getValue();
// Load the header value
valueLoader.loadValue(name, value, HttpValueLocation.HEADER);
// Handle specific header values
if ("content-type".equalsIgnoreCase(name)) {
// Capture content type, for later parsing content
contentType = header.getValue();
}
}
// Load content arguments
if ((contentType != null) && (argumentParsers != null)) {
for (int i = 0; i < argumentParsers.length; i++) {
HttpArgumentParser parser = argumentParsers[i];
if (contentType.equals(parser.getContentType())) {
parser.parse(request, valueLoader);
return; // use only first matching
}
}
}
}
/**
* Tokenises the application/x-www-form-urlencoded
entity.
*
* @param request
* {@link HttpRequest}.
* @param valueLoader
* {@link ValueLoader}.
* @throws HttpException
* If fails to tokenise the form content.
*/
public static void tokeniseFormEntity(HttpRequest request, ValueLoader valueLoader) throws HttpException {
// Obtain the parse state (ready for use)
ParseState state = parseState.get();
// Values to aid in parsing
String name = null;
String value = null;
boolean isRequireDecode = false;
// Parse the name / value pairs
int index = 0;
try {
Reader reader = new InputStreamReader(request.getEntity().createBrowseInputStream(),
ServerHttpConnection.DEFAULT_HTTP_ENTITY_CHARSET);
for (int character = reader.read(); character != -1; character = reader.read()) {
// Load the value (ensuring enough space for value)
if (index >= state.buffer.length) {
state.buffer = Arrays.copyOf(state.buffer, state.buffer.length * 2);
}
state.buffer[index] = (char) character;
// Handle based on character
switch (character) {
case '=':
// Flag to now obtain value
state.length = index; // before '='
name = decode(state, isRequireDecode, state);
index = -1; // reset to start
break;
case '+': // space
case '%': // escaping
// Requires translating
isRequireDecode = true;
break;
case '&':
case ';':
// Have parameter name/value, so load
state.length = index; // before terminator
value = decode(state, isRequireDecode, state);
valueLoader.loadValue(name, value, HttpValueLocation.ENTITY);
name = null;
// Reset for next parameter name/value
index = -1; // reset to start
isRequireDecode = false;
break;
}
// Increment for next index
index++;
}
} catch (IOException ex) {
throw new HttpException(ex);
}
// Determine if load final value
if (name != null) {
// Ensure have value
state.length = index;
value = decode(state, isRequireDecode, state);
// Load the value
valueLoader.loadValue(name, value, HttpValueLocation.ENTITY);
}
}
/**
* Enum providing the escape state for translating.
*/
private static enum EscapeState {
NONE, HIGH, LOW
}
/**
* Decodes the text.
*
* @param text
* Text to be decoded.
* @param state
* {@link ParseState}.
* @return Decoded text.
* @throws HttpException
* If fails to translate.
*/
private static String decode(CharSequence text, boolean isRequireDecode, ParseState state) throws HttpException {
// Determine if require decode
if (!isRequireDecode) {
// No decode required
return text.toString();
}
// Obtain the temporary buffer
char[] buffer = state.buffer;
// Ensure temporary buffer large enough
if ((buffer == null) || (buffer.length < text.length())) {
// Increase buffer size (translation should not be bigger)
buffer = new char[text.length()];
// Make available for further translations
state.buffer = buffer;
}
// Iterate over parameter text translating
int charIndex = 0;
EscapeState escape = EscapeState.NONE;
byte highBits = 0;
for (int i = 0; i < text.length(); i++) {
char character = text.charAt(i);
// Handle on whether escaping
switch (escape) {
case NONE:
// Not escaped so handle character
switch (character) {
case '+':
// Translate to space
buffer[charIndex++] = ' ';
break;
case '%':
// Escaping
escape = EscapeState.HIGH;
break;
default:
// No translation needed of character
buffer[charIndex++] = character;
break;
}
break;
case HIGH:
// Obtain the high bits for escaping
highBits = decodeEscapedCharToBits(character);
escape = EscapeState.LOW;
break;
case LOW:
// Have low bits, so obtain escaped character
byte lowBits = decodeEscapedCharToBits(character);
character = (char) ((highBits << 4) | lowBits);
// Load the character and no longer escaped
buffer[charIndex++] = character;
escape = EscapeState.NONE;
break;
}
}
// Should always be in non-escape state after translating
if (escape != EscapeState.NONE) {
throw new BadRequestHttpException(null,
"Invalid parameter text as escaping not complete: '" + text.toString() + "'");
}
// Return the translated text
return new String(buffer, 0, charIndex);
}
/**
* Decode the character to the 4 bits as per escaping of HTTP.
*
* @param character
* Character to translate.
* @return Corresponding 4 bits for character.
* @throws HttpException
* If invalid character for escaping.
*/
private static byte decodeEscapedCharToBits(char character) throws HttpException {
// Obtain the bits for the character
int bits;
if (('0' <= character) && (character <= '9')) {
bits = character - '0';
} else if (('A' <= character) && (character <= 'F')) {
bits = (character - 'A') + 0xA;
} else if (('a' <= character) && (character <= 'f')) {
bits = (character - 'a') + 0xA;
} else {
// Invalid character for escaping
throw new BadRequestHttpException(null, "Invalid character for escaping: " + character);
}
// Return the bits
return (byte) bits;
}
/**
* All access via static methods.
*/
private HttpRequestTokeniser() {
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy