/*********************************************************************
* HttpChannel.java
* created on 01.04.2006 by netseeker
* $Source$
* $Date$
* $Revision$
*
* ====================================================================
*
* Copyright 2006 netseeker aka Michael Manske
*
* 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.
* ====================================================================
*
* This file is part of the de.netseeker.ejoe.io framework.
* For more information on the author, please see
* <http://www.manskes.de/>.
*
*********************************************************************/
package de.netseeker.ejoe.io;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.SocketTimeoutException;
import java.net.URLDecoder;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.text.ParseException;
import java.util.logging.Level;
import java.util.logging.Logger;
import de.netseeker.ejoe.ConnectionHeader;
import de.netseeker.ejoe.EJConstants;
import de.netseeker.ejoe.cache.ByteBufferAllocator;
import de.netseeker.ejoe.http.HttpHeaderParser;
import de.netseeker.ejoe.http.HttpRequest;
import de.netseeker.ejoe.http.HttpRequestParser;
import de.netseeker.ejoe.http.HttpResponse;
import de.netseeker.ejoe.http.HttpResponseParser;
/**
* @author netseeker
* @since 0.3.9.1
*/
class HttpChannel extends DataChannel
{
private static final Logger logger = Logger.getLogger( HttpChannel.class.getName() );
private static HttpChannel dataChannel = new HttpChannel();
/**
* Singleton with hidden constructor
*/
private HttpChannel()
{
super();
}
/**
* @return
*/
public static DataChannel getInstance()
{
return dataChannel;
}
/**
* @param header
* @param channel
* @param timeout
* @param magicBuf
* @return
* @throws IOException
* @throws ParseException
*/
public ConnectionHeader handshake( final ConnectionHeader header, SocketChannel channel, byte[] prereadHead,
long timeout ) throws IOException, ParseException
{
ConnectionHeader receiverHeader;
String host = null;
ByteBuffer magicBuf = null;
ByteBuffer hBuffer = null;
// shall we act as clientside and initialize the handshake?
if ( header.isClient() )
{
// create a new HTTP HEAD operation and send our client header and the requested adapter (if any)
hBuffer = new HttpRequest( header, HttpRequest.HTTP_HEAD ).toByteBuffer();
semiBlockingWrite( channel, hBuffer, timeout );
// something strange happened - at least the handshake data must be written in one operation
if ( hBuffer.hasRemaining() ) return null;
}
// read and parse the HTTP header - this can also preread one or more bytes of none-header-data (content)
HttpHeaderParser parser = readHttpHeader( channel, prereadHead, timeout, header.isClient() );
// little bit paranoia
if ( parser == null )
{
return null;
}
// get the read ByteBuffer
magicBuf = parser.getByteHeader();
if ( header.isClient() )
{
// did the HTTP parser already read the content? if so the ByteBuffer has already the required size.
if ( magicBuf == null )
{
// allocate an appropiate ByteBuffer
magicBuf = ByteBufferAllocator.allocate( parser.getContentLength() );
}
// did the HTTP parser already read the complete content (the eight bits of the header byte)?
if ( magicBuf.position() < (parser.getContentLength() - 1) )
{
// read the data part
semiBlockingRead( channel, magicBuf, timeout );
if ( magicBuf.hasRemaining() ) return null;
}
magicBuf.flip();
if ( logger.isLoggable( Level.FINEST ) )
{
logger.log( Level.FINEST, "HTTP-Header read: "
+ IOUtil.bBitsToSBits( IOUtil.byteToBBits( magicBuf.get() ) ) );
magicBuf.rewind();
}
receiverHeader = new ConnectionHeader( channel, host, header.isClient(), magicBuf.get() );
}
// shall we act as server side and answer to the handshake request?
else
{
receiverHeader = new ConnectionHeader( channel, host, header.isClient() );
// the HTTP header must contain the eight header bits in the requested URI
receiverHeader.fromString( ((HttpRequestParser) parser).getUri() );
// does the http header overwrite the compression setting?
receiverHeader.setCompression( header.hasCompression() && parser.hasCompression() );
// does the http header overwrite the setting for using a persistent connection?
receiverHeader.setPersistent( parser.isPersistentConnection() && header.isPersistent() );
// answer to the client request and send our serverside headerbyte
// clients as browsers (IE, Firefox, Opera) or similiar doesn't understand our handshake
// hence we send our handshake response only when the client tells us that it is able
// to parse our response
if ( receiverHeader.isHandshakeResponseAware() )
{
// create a HTTP response with just our server header byte as content
HttpResponse response = new HttpResponse( header, HttpResponse.HTTP_OK );
response.addData( header.toByte() );
logger.log( Level.FINEST, "Sending server headerbyte: " + header );
hBuffer = response.toByteBuffer();
semiBlockingWrite( channel, hBuffer, timeout );
if ( hBuffer.hasRemaining() ) return null;
}
// ok, a unknown client which might have already send some content with it's first request
else if ( parser.hasPrereadContent() )
{
receiverHeader.setWaitingBuffer( parser.getByteHeader() );
}
}
receiverHeader.setHttp( true );
receiverHeader.setConnected( true );
return receiverHeader;
}
/**
* Reads a HTTP header from a socket channel and validates and parses the header Reading HTTP headers is much more
* tricky then reading the usual EJOE headers because in case of HTTP we can't deal with fix-sized headers. We have
* to read until the stream ends or we have detected a complete HTTP header. Doing so can result in preread content
* which we must be able to handle at later time.
*
* @param channel
* @param timeout
* @param isRequest
* @return
* @throws IOException
* @throws ParseException
* @throws ConnectionTimeoutException
*/
private HttpHeaderParser readHttpHeader( SocketChannel channel, byte[] preReadData, long timeout, boolean isRequest )
throws IOException, ParseException
{
HttpHeaderParser httpHeaderParser = null;
// prepare a byte buffer which will receive the HTTP header [ + preread content ]
ByteBuffer headerBuf = ByteBufferAllocator.allocate( EJConstants.HTTP_BYTEBUFFER_PREALLOC );
// do we already have some preread content?
if ( preReadData != null )
{
// if so put it at the beginning of the byte buffer
headerBuf.put( preReadData );
}
int readControl = 0;
int limit = -1;
long timestamp = System.currentTimeMillis();
long timePeriod = -1;
do
{
limit = headerBuf.limit();
// increase the byte buffer if it's free memory falls below 20 percent
if ( headerBuf.remaining() <= (limit / 5) )
{
logger.log( Level.FINEST, "Allocating additional " + (limit / 2) + "b for buffer with " + limit + "b" );
headerBuf = ByteBufferAllocator.reAllocate( headerBuf, limit + (limit / 2) );
}
readControl = channel.read( headerBuf );
timePeriod = System.currentTimeMillis() - timestamp;
}
while ( readControl > -1 && !HttpHeaderParser.isComplete( headerBuf ) && (timePeriod < timeout) );
// timeout occured?
if ( timePeriod >= timeout )
{
throw new SocketTimeoutException();
}
else if ( readControl == -1 && headerBuf.position() == 0 )
{
throw new ClosedChannelException();
}
// stream ended but HTTP header isn't complete?
else if ( !HttpHeaderParser.isComplete( headerBuf ) )
{
throw new ParseException( "Received HTTP Header missing finalizing line terminators (\\r\\n\\r\\n)!",
readControl );
}
if ( logger.isLoggable( Level.FINEST ) )
{
logger.log( Level.FINEST, "HTTP Header read: complete=" + HttpHeaderParser.isComplete( headerBuf )
+ ", readControl=" + readControl + ", bytes read: " + headerBuf.position() );
}
headerBuf.flip();
if ( logger.isLoggable( Level.FINE ) )
{
logger.log( Level.FINE, IOUtil.decodeToString( headerBuf ) );
headerBuf.position( 0 );
}
// instantiate an appropiate HTTP header parser depending on whether the data are a HTTP request or a response
if ( isRequest )
{
httpHeaderParser = new HttpResponseParser( headerBuf );
}
else
{
httpHeaderParser = new HttpRequestParser( headerBuf );
}
// validate the data
if ( !httpHeaderParser.isValid() )
{
headerBuf.rewind();
throw new ParseException( httpHeaderParser.getClass().getName() + ": Invalid HTTP header detected!!!\n"
+ IOUtil.decodeToString( headerBuf ), 0 );
}
return httpHeaderParser;
}
/*
* (non-Javadoc)
*
* @see de.netseeker.ejoe.io.DataChannel#readHeader(de.netseeker.ejoe.ConnectionHeader, long)
*/
public int readHeader( ConnectionHeader header, long timeout ) throws IOException
{
HttpHeaderParser parser = null;
try
{
parser = readHttpHeader( header.getChannel(), null, timeout, header.isClient() );
}
catch ( ParseException e )
{
throw new IOException( e.getMessage() );
}
if ( parser.hasPrereadContent() )
{
header.setWaitingBuffer( parser.getByteHeader() );
}
return parser.getContentLength();
}
/*
* (non-Javadoc)
*
* @see de.netseeker.ejoe.io.DataChannel#writeHeader(de.netseeker.ejoe.ConnectionHeader, java.nio.ByteBuffer, long)
*/
public void writeHeader( ConnectionHeader header, ByteBuffer buffer, long timeout ) throws IOException
{
ByteBuffer headerBuf = null;
SocketChannel channel = header.getChannel();
boolean noBuffer = (buffer == null);
int length = !noBuffer ? buffer.limit() : 0;
if ( !noBuffer ) buffer.mark();
if ( header.isClient() )
{
HttpRequest request = new HttpRequest( header, (header.getAttachementInfo() != null) ? header
.getAttachementInfo().toString() : HttpRequest.HTTP_POST );
if ( !noBuffer ) request.addData( buffer );
headerBuf = request.toByteBuffer();
if ( logger.isLoggable( Level.FINEST ) )
{
logger.log( Level.FINEST, "Preparing to write client request with " + headerBuf.limit() + " bytes:\n"
+ IOUtil.decodeToString( headerBuf ) );
}
}
else
{
HttpResponse response = new HttpResponse( header, (header.getAttachementInfo() != null) ? header
.getAttachementInfo().toString() : HttpResponse.HTTP_OK );
if ( !noBuffer ) response.addData( buffer );
headerBuf = response.toByteBuffer();
if ( logger.isLoggable( Level.FINEST ) )
{
logger.log( Level.FINEST, "Preparing to write server response with " + headerBuf.limit() + " bytes:\n"
+ IOUtil.decodeToString( headerBuf ) );
}
}
try
{
semiBlockingWrite( channel, headerBuf, timeout );
IOUtil.setSendBufferSize( channel.socket(), length );
}
catch ( IncompleteIOException ioe )
{
logger.log( Level.FINEST, "Incomplete header write detected, skip this request." );
throw new IncompleteIOException( null, SelectionKey.OP_WRITE );
}
finally
{
ByteBufferAllocator.collect( headerBuf );
if ( !noBuffer ) buffer.reset();
}
}
/*
* (non-Javadoc)
*
* @see de.netseeker.ejoe.io.DataChannel#decode(java.nio.ByteBuffer)
*/
public ByteBuffer decode( ByteBuffer buffer ) throws UnsupportedEncodingException
{
byte[] ejdata = IOUtil.encodeToBytes( EJConstants.HTTP_PARAM_NAME );
if ( buffer.limit() > ejdata.length )
{
for ( int i = 0; i < ejdata.length; i++ )
{
if ( buffer.get( i ) != ejdata[i] )
{
return buffer;
}
}
buffer.position( ejdata.length + 1 );
buffer.compact();
buffer.flip();
String request = IOUtil.decodeToString( buffer );
request = URLDecoder.decode( request, EJConstants.EJOE_DEFAULT_CHARSET );
buffer = IOUtil.encodeToByteBuffer( request );
}
return buffer;
}
}