/* * HttpRequest.java * * Created on Jul 23, 2007, 3:58:31 PM * * A HTTP request * */ package com.pugh.sockso.web; import com.pugh.sockso.Utils; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; public class HttpRequest implements Request { public static final int MAX_HEADERS = 100; private final Server server; protected final Map<String,String> cookies; protected final Map<String,String> arguments; private final Map<String,String> headers; private final Map<String,UploadFile> files; private String[] params = null; private String host = null, statusLine = null; /** * Reads and processes a HTTP request. Supports reading cookies, * http headers, accessing the requested URL and reading * POST data and query strings * * @param server the server the request is to * @param stream the input stream for the request * * @throws IOException * @throws BadRequestException * */ public HttpRequest( final Server server ) { this.server = server; this.cookies = new HashMap<String,String>(); this.arguments = new HashMap<String,String>(); this.headers = new HashMap<String,String>(); this.files = new HashMap<String,UploadFile>(); } /** * processes the request from the given input stream * * @param stream * * @throws java.io.IOException * @throws com.pugh.sockso.web.BadRequestException * */ @Override public void process( final InputStream stream ) throws IOException, BadRequestException, EmptyRequestException { final InputBuffer buffer = new InputBuffer( stream, 100 ); readStatusLine( buffer.readLine() ); readHeaders( buffer ); readBody( buffer ); } /** * reads the status line of the http request * * @param stream input stream * * @throws IOException * @throws BadRequestException * */ protected void readStatusLine( final String rawStatusLine ) throws IOException, BadRequestException, EmptyRequestException { statusLine = rawStatusLine; // look for IE6 "strangeness" if ( statusLine.equals("") ) { throw new EmptyRequestException(); } final Pattern p = Pattern.compile( "(GET|POST) (.+?) HTTP/\\d\\.\\d" ); final Matcher m = p.matcher( statusLine ); log.debug( "Status Line: '" + statusLine + "'" ); if ( !m.matches() ) { throw new BadRequestException( "Invalid HTTP status line", 400 ); } String resource = m.group( 2 ); final int queryStringIndex = resource.indexOf( "?" ); // extract query string from resource if it exists if ( queryStringIndex != -1 ) { processUrlEncodedData( resource.substring(queryStringIndex+1) ); resource = resource.substring( 0, queryStringIndex ); } // work out params params = resource .substring(1) .split( "/" ); } /** * read the http headers from the request * * @param stream input stream * * @throws IOException * */ private void readHeaders( final InputBuffer buffer ) throws IOException { // set limit on how many headers we'll read incase the request is malformed int headerCount = 0; while ( headerCount++ < MAX_HEADERS ) { final String line = buffer.readLine(); if ( line == null || line.equals("") ) break; log.debug( "HTTP Header: " + line ); final Pattern p2 = Pattern.compile( "(.*?): (.*)" ); final Matcher m2 = p2.matcher( line ); if ( !m2.matches() ) continue; // invalid header format, ignore final String name = m2.group( 1 ).toLowerCase(); final String value = m2.group( 2 ); headers.put( name.toLowerCase(), value ); if ( name.equals("host") ) host = value; else if ( name.equals("cookie") ) addCookies( value ); } } /** * reads the body of the http request for any post * data that may have been sent * * @param in input stream * */ private void readBody( final InputBuffer buffer ) throws IOException { final int contentLength = getContentLength(); // work out what type of data we've received, it needs to be processed // differently depending on the content type header we got. if ( getHeader("content-type").contains("multipart/form-data") ) { //log.debug( "Multipart Form Data: ~" +contentLength+ " bytes" ); processMultipartData( buffer ); } else { final String postData = buffer.readString( contentLength ); //log.debug( "URL Encoded Post Data: '" +postData+ "'" ); processUrlEncodedData( postData ); } } /** * tries to return the content length header provided by the client. if * it's not present or malformed (not a number) then return -1 * * @return content length or -1 * */ private int getContentLength() { try { return Integer.parseInt( getHeader("content-length") ); } catch ( NumberFormatException e ) { // ignore badness, we'll just return -1 next... } return -1; } /** * processes data that has been submitted via the multipart/form-data * type, could be uploaded files and stuff... * * @param urlEncData the post data * */ private void processMultipartData( final InputBuffer buffer ) throws IOException { final String boundary = getMultipartBoundary(); final String startMarker = "--" +boundary; final String endMarker = startMarker+ "--"; while ( true ) { final String marker = buffer.readLine(); // have we reached the end? if ( marker.equals(endMarker) ) { return; } else if ( marker.equals(startMarker) ) { final MultipartSection ms = new MultipartSection(); ms.process( buffer, boundary ); // if we've managed to extract a filename then we'll treat // this section as a file upload... if ( !ms.getFilename().equals("") ) { //log.debug( "Multipart File: " + ms.getFilename() ); files.put( ms.getName(), new UploadFile( ms.getFilename(), ms.getContentType(), ms.getData(), ms.getFilename(), ms.getTemporaryFile() )); } // else treat as normal argument else { //log.debug( "Multipart Arg: " + ms.getName() + " = " + ms.getData() ); arguments.put( ms.getName(), ms.getData() ); } } // in case no more data... else if ( marker.equals("") ) { break; } } } /** * extracts the boundary being used to seperate data in multipart * type, returns null if it can't be found * * @return boundary if found, null otherwise * */ protected String getMultipartBoundary() { final String contentType = getHeader( "content-type" ); final Pattern pattern = Pattern.compile( ".*boundary=(.*)" ); final Matcher matcher = pattern.matcher( contentType ); return ( matcher.matches() ) ? matcher.group( 1 ) : null; } /** * process request data that is in standard url encoded form * * @param urlEncData the post data * */ private void processUrlEncodedData( final String data ) { // extract arguments from post data final String[] pairs = data.split( "&" ); for ( final String pairData : pairs ) { final String[] pair = pairData.split( "=" ); if ( pair.length == 2 ) { final String name = Utils.URLDecode( pair[0] ); final String value = Utils.URLDecode( pair[1] ); //log.debug( "URL Encoded Argument: " + name + "=" + value ); arguments.put( name, value ); } } } /** * returns the http status line * * @return status line * */ @Override public String getResource() { return statusLine; } /** * returns the value of a named HTTP header, if the header * was not in the request the empty string is returned * * NB: case-insensitive * * @param name name of the http header * @param header value, or null * */ @Override public String getHeader( final String name ) { final String value = headers.get( name.toLowerCase() ); return value == null ? "" : value; } /** * tries to return a named argument that was received from * the request data (POST) * * @param name argument name * @return string value * */ @Override public String getArgument( final String name ) { final String value = arguments.get( name ); return value == null ? "" : value; } /** * indicates if an argument was present * */ @Override public boolean hasArgument( final String name ) { return !getArgument(name).equals(""); } /** * returns a file that has been uploaded in the request by the * name it was given in the form. if the file isn't found then * returns null * * @param name file parameter name * @return uploaded file, or null if not found * */ @Override public UploadFile getFile( final String name ) { return files.get( name ); } /** * adds some cookies to the cookies in the request. the cookie * data is URL decoded (this isn't a standard, just a reccomendation, * i read the RFC and couldn't find any mention for how they should be encoded) * * @param cookie the cookie to add * */ protected void addCookies( final String cookieData ) { final String[] pairs = cookieData.split( ";" ); for ( final String pairData : pairs ) { final String[] pair = pairData.split( "=" ); if ( pair.length == 2 ) { final String name = Utils.URLDecode( pair[0] ).trim(); final String value = Utils.URLDecode( pair[1] ).trim(); cookies.put( name, value ); //log.debug( "HttpRequestCookie: " + name + "=" + value ); } } } /** * return a named cookie * * @return value of cookie * */ @Override public String getCookie( final String name ) { final String value = cookies.get( name ); return value == null ? "" : value; } /** * returns the host to use for this request when replying. hopefully * the user sent us something we can use, otherwise we'll hafta try * and guess... * * @return the host to reply with * */ @Override public String getHost() { return host != null ? host : server.getHost(); } /** * returns the parameter at the specified index * * @param index the index of the parameter to fetch * @return the parameter value * */ @Override public String getUrlParam( final int index ) { return ( index < params.length ) ? Utils.URLDecode(params[ index ]) : ""; } /** * returns the number of parameters in the request * * @return parameter count * */ @Override public int getParamCount() { return params.length; } /** * strips the initial command arguments from a string that should * then only contain custom url arguments (eg. "tr123/ar456") * * the skip argument can be used if the url is "/command/type/ARGS", * rather than "/command/ARGS". * * @param skipFirstArg skip an argument in list * @return the array with just the custom args * */ @Override public String[] getPlayParams( final boolean skipFirstArg ) { return getPlayParams( skipFirstArg ? 1 : 0 ); } @Override public String[] getPlayParams( final int skipNumArgs ) { final int offset = 1 + skipNumArgs; final String[] custArgs = new String[ params.length - offset ]; System.arraycopy( params, offset, custArgs, 0, params.length - offset ); return custArgs; } /** * returns users preferred 2 char lang code from "Accept-Language", if * nothing is found then the empty string is returned. * * @todo give respect to weightings * * @return 2 char lang code or "" * */ @Override public String getPreferredLangCode() { final String langs = getHeader( "Accept-Language" ); final String[] locales = langs.split( "," ); // split apart lang/locale and weightings String lang = ""; if ( locales.length > 0 ) { final String[] weightingPair = locales[0].split( ";" ); // split off possible weighting "q=0.5" if ( weightingPair.length > 0 ) { final String[] langPair = weightingPair[0].split( "-" ); // split off locale "eg-GB" if ( langPair.length > 0 ) lang = langPair[ 0 ]; } } return lang; } /** * when we're done with the request we should delete any temporary files * that may have been created. * * @throws java.lang.Throwable * */ @Override protected void finalize() throws Throwable { for ( final String key : files.keySet() ) { final UploadFile file = getFile( key ); final File tempFile = file.getTemporaryFile(); if ( tempFile != null ) { tempFile.delete(); } } } }