All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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