Package de.netseeker.ejoe.io

Source Code of de.netseeker.ejoe.io.HttpChannel

/*********************************************************************
* 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;
    }
}
TOP

Related Classes of de.netseeker.ejoe.io.HttpChannel

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.