/*************************************************************************** * Copyright (C) by Fabrizio Montesi * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU Library General Public License as * * published by the Free Software Foundation; either version 2 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 General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this program; if not, write to the * * Free Software Foundation, Inc., * * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * * * * For details about the authors of this software, see the AUTHORS file. * ***************************************************************************/ package jolie.net.http; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.net.URLDecoder; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; import jolie.lang.parse.Scanner; public class HttpParser { private final static String URL_DECODER_ENC = "UTF-8"; private static final String HTTP = "HTTP"; private static final String GET = "GET"; private static final String POST = "POST"; private static final String PUT = "PUT"; private static final String HEAD = "HEAD"; private static final String DELETE = "DELETE"; private static final String TRACE = "TRACE"; private static final String CONNECT = "CONNECT"; private static final String OPTIONS = "OPTIONS"; private static final Pattern cookiesSplitPattern = Pattern.compile( ";" ); private static final Pattern cookieNameValueSplitPattern = Pattern.compile( "=" ); private final HttpScanner scanner; private Scanner.Token token; private void getToken() throws IOException { token = scanner.getToken(); } public HttpParser( InputStream istream ) throws IOException { scanner = new HttpScanner( istream, URI.create( "urn:network" ) ); } private void tokenAssert( Scanner.TokenType type ) throws IOException { if ( token.isNot( type ) ) throwException(); } private void throwException() throws IOException { throw new IOException( "Malformed HTTP header" ); } private void parseHeaderProperties( HttpMessage message ) throws IOException { String name, value; getToken(); HttpMessage.Cookie cookie; while( token.is( Scanner.TokenType.ID ) ) { name = token.content().toLowerCase(); getToken(); tokenAssert( Scanner.TokenType.COLON ); value = scanner.readLine(); if ( "set-cookie".equals( name ) ) { //cookie = parseSetCookie( value ); if ( (cookie=parseSetCookie( value )) != null ) { message.addSetCookie( cookie ); } } else if ( "cookie".equals( name ) ) { String ss[] = value.split( ";" ); for( String s : ss ) { String nv[] = s.trim().split( "=" ); if ( nv.length > 1 ) { message.addCookie( nv[0], nv[1] ); } } } else if ( "user-agent".equals( name ) ) { message.setUserAgent( value ); message.setProperty( name, value ); } else { message.setProperty( name, value ); } getToken(); } } private HttpMessage.Cookie parseSetCookie( String cookieString ) { String ss[] = cookiesSplitPattern.split( cookieString ); if ( cookieString.isEmpty() == false && ss.length > 0 ) { boolean secure = false; String domain = ""; String path = ""; String expires = ""; String nameValue[] = cookieNameValueSplitPattern.split( ss[ 0 ], 2 ); if ( ss.length > 1 ) { String kv[]; for( int i = 1; i < ss.length; i++ ) { if ( "secure".equals( ss[ i ] ) ) { secure = true; } else { kv = cookieNameValueSplitPattern.split( ss[ i ], 2 ); if ( kv.length > 1 ) { kv[ 0 ] = kv[ 0 ].trim(); if ( "expires".equalsIgnoreCase( kv[ 0 ] ) ) { expires = kv[ 1 ]; } else if ( "path".equalsIgnoreCase( kv[ 0 ] ) ) { path = kv[ 1 ]; } else if ( "domain".equalsIgnoreCase( kv[ 0 ] ) ) { domain = kv[ 1 ]; } } } } } return new HttpMessage.Cookie( nameValue[0], nameValue[1], domain, path, expires, secure ); } return null; } private HttpMessage parseRequest() throws IOException { HttpMessage message = null; if ( token.isKeyword( GET ) ) { message = new HttpMessage( HttpMessage.Type.GET ); } else if ( token.isKeyword( POST ) ) { message = new HttpMessage( HttpMessage.Type.POST ); } else if ( token.isKeyword( OPTIONS ) || token.isKeyword( CONNECT ) || token.isKeyword( HEAD ) || token.isKeyword( PUT ) || token.isKeyword( DELETE ) || token.isKeyword( TRACE ) ) { message = new HttpMessage( HttpMessage.Type.UNSUPPORTED ); } else { throw new IOException( "Unknown HTTP request type: " + token.content() + "(" + token.type() + ")" ); } message.setRequestPath( URLDecoder.decode( scanner.readWord().substring( 1 ), URL_DECODER_ENC ) ); getToken(); if ( !token.isKeyword( HTTP ) ) throw new IOException( "Invalid HTTP header: expected HTTP version" ); if ( (char)scanner.currentCharacter() != '/' ) throw new IOException( "Expected HTTP version" ); String version = scanner.readWord(); if ( "1.0".equals( version ) ) message.setVersion( HttpMessage.Version.HTTP_1_0 ); else if ( "1.1".equals( version ) ) message.setVersion( HttpMessage.Version.HTTP_1_1 ); else throw new IOException( "Unsupported HTTP version specified: " + version ); return message; } private HttpMessage parseMessageType() throws IOException { if ( token.isKeyword( HTTP ) ) { return parseResponse(); } else { return parseRequest(); } } private HttpMessage parseResponse() throws IOException { HttpMessage message = new HttpMessage( HttpMessage.Type.RESPONSE ); if ( (char)scanner.currentCharacter() != '/' ) throw new IOException( "Expected HTTP version" ); String version = scanner.readWord(); if ( !( "1.1".equals( version ) || "1.0".equals( version ) ) ) throw new IOException( "Unsupported HTTP version specified: " + version ); getToken(); tokenAssert( Scanner.TokenType.INT ); message.setStatusCode( Integer.parseInt( token.content() ) ); message.setReason( scanner.readLine() ); return message; } @SuppressWarnings( "empty-statement" ) public static void blockingRead( InputStream stream, byte[] buffer, int offset, int length ) throws IOException { int r = 0; while( (r+=stream.read( buffer, offset+r, length-r )) < length ); } private static final int BLOCK_SIZE = 1024; public static byte[] readAll( InputStream stream ) { int r = 0; ByteArrayOutputStream c = new ByteArrayOutputStream(); byte[] tmp = new byte[ BLOCK_SIZE ]; try { while( (r=stream.read( tmp, 0, BLOCK_SIZE )) != -1 ) { c.write( tmp, 0, r ); tmp = new byte[ BLOCK_SIZE ]; } } catch( IOException e ) { // End of stream } return c.toByteArray(); } private void readContent( HttpMessage message ) throws IOException { String p; int contentLength = 0; p = message.getProperty( "content-length" ); if ( p != null && !p.isEmpty() ) { try { contentLength = Integer.parseInt( p ); if ( contentLength == 0 ) { message.setContent( new byte[0] ); return; } } catch( NumberFormatException e ) { contentLength = 0; } } boolean chunked = false; p = message.getProperty( "transfer-encoding" ); if ( p != null && p.equals( "chunked" ) ) chunked = true; byte buffer[] = null; if ( chunked ) { InputStream stream = scanner.inputStream(); List< byte[] > chunks = new ArrayList< byte[] > (); byte[] chunk; int l; int total = 0; boolean keepRun = true; String lStr = scanner.readWord(); while( keepRun ) { l = Integer.parseInt( lStr, 16 ); if ( l > 0 ) { scanner.eatSeparators(); total += l; chunk = new byte[ l ]; chunk[0] = (byte) (scanner.currentCharacter()); blockingRead( stream, chunk, 1, l - 1 ); chunks.add( chunk ); scanner.readChar(); scanner.eatSeparators(); lStr = scanner.readWord( false ); } else keepRun = false; } ByteBuffer b = ByteBuffer.allocate( total ); for( byte[] c : chunks ) b.put( c ); buffer = b.array(); } else if ( contentLength > 0 ) { buffer = new byte[ contentLength ]; InputStream stream = scanner.inputStream(); blockingRead( stream, buffer, 0, contentLength ); } else { HttpMessage.Version version = message.version(); if ( // Will the connection be closed? // HTTP 1.1 ((version == null || version.equals( HttpMessage.Version.HTTP_1_1 )) && message.getPropertyOrEmptyString( "connection" ).equalsIgnoreCase( "close" )) || // HTTP 1.0 (version.equals( HttpMessage.Version.HTTP_1_0 ) && !message.getPropertyOrEmptyString( "connection" ).equalsIgnoreCase( "keep-alive" ) ) ) { buffer = readAll( scanner.inputStream() ); } } message.setContent( buffer ); } public HttpMessage parse() throws IOException { getToken(); HttpMessage message = parseMessageType(); parseHeaderProperties( message ); readContent( message ); scanner.eatSeparatorsUntilEOF(); return message; } }