Package com.planet_ink.coffee_web.http

Source Code of com.planet_ink.coffee_web.http.HTTPReader

package com.planet_ink.coffee_web.http;

import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.io.*;

import com.planet_ink.coffee_web.interfaces.DataBuffers;
import com.planet_ink.coffee_web.interfaces.HTTPIOHandler;
import com.planet_ink.coffee_web.server.WebServer;
import com.planet_ink.coffee_web.util.ChunkSpec;
import com.planet_ink.coffee_web.util.CWDataBuffers;
import com.planet_ink.coffee_web.util.ThrottleSpec;
import com.planet_ink.coffee_web.util.WebAddress;
import com.planet_ink.coffee_web.util.CWConfig;
import com.planet_ink.coffee_common.collections.Pair;
import com.planet_ink.coffee_common.logging.Log;
import com.planet_ink.coffee_common.logging.Log.Type;

/*
Copyright 2012-2014 Bo Zimmerman

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.
*/

/**
* Handler of http request reading duties.  Instances of this class will handle
* reading a request from the first byte to the last, populating an HTTPRequest
* object with the help of that classes internal parsers. 
*
* An internal state of high level request reading is maintained throughout
* the process, as well as statistical information about the age of this request,
* whether it is still running, whether it is being cancelled, etc.
* @author Bo Zimmerman
*
*/
public class HTTPReader implements HTTPIOHandler, Runnable
{
  private static final AtomicLong   idCounter    = new AtomicLong(0);
 
  private volatile boolean       isRunning    = false;// the request is currently getting active thread/read time
  private volatile boolean       closeMe      = false;// the request is closed, along with its channel
  private volatile boolean       closeRequested  = false;// the request is closed, along with its channel

  protected final SocketChannel    chan;          // the channel from which the request is read
  protected final String        name;          // the name of this handler -- to denote a request ID
  protected final boolean        isDebugging;      // true if the log debug channel is on -- an optomization
  private final long          startTime;        // the initial start time of the request, for overall age calculation
  protected final CWConfig      config;          // mini web configuration variables
  protected final WebServer      server;          // mini web server managing this request

  private volatile long        idleTime    = System.currentTimeMillis()// the last time this handler went idle
 
  private volatile CWHTTPRequest    currentReq;            // the parser and pojo of the current request
  private volatile ParseState      currentState  = ParseState.REQ_INLINE;  // the current parse state of this request
  private volatile int        nextChunkSize  = 0;
 
  private volatile HTTPForwarder    forwarder    = null// in the off chance everything is just being forwarded, it goes here
  private volatile ThrottleSpec    outputThrottle  = null;
 
  private final LinkedList<DataBuffers>writeables    = new LinkedList<DataBuffers>();
 
  private final static String      EOLN      = HTTPIOHandler.EOLN;
  private static final Charset    utf8      = Charset.forName("UTF-8");
 
  private enum ParseState  // state enum for high level parsing
  {
    REQ_INLINE,
    REQ_EOLN,
    HDR_INLINE,
    HDR_EOLN,
    CHUNKED_HEADER_INLINE,
    CHUNKED_HEADER_EOLN,
    CHUNKED_ENDER_INLINE,
    CHUNKED_ENDER_EOLN,
    CHUNKED_TRAILER_INLINE,
    CHUNKED_TRAILER_EOLN,
    CHUNKED_BODY,
    BODY,
    FORWARD,
    DONE
  }
 

  /**
   * Constructor takes the server managing this request, and the channel to read from and write to.
   * @param server the web server managing this runner, a place to get the config and register new ops
   * @param chan the channel to read from and write to
   * @param registerOps a list to add to when you need the server to make channel changes
   * @throws IOException
   */
  public HTTPReader(WebServer server, SocketChannel chan) throws IOException
  {
    super();
    this.config=server.getConfig();
    this.server=server;
    this.chan=chan;
    this.isDebugging=config.isDebugging();
    final Logger debugLogger = (isDebugging)?config.getLogger():null;
    final ByteBuffer requestBuffer=ByteBuffer.allocate((int)config.getRequestLineBufBytes());
    final boolean isHttps= this.getReaderType().equals("https");
    final int localPort=chan.socket().getLocalPort();
    final InetAddress clientAddress=chan.socket().getInetAddress();
    final long requestLineSize=config.getRequestLineBufBytes();
    final boolean overwriteDups=config.getDupPolicy()==CWConfig.DupPolicy.OVERWRITE;
    this.currentReq=new CWHTTPRequest(clientAddress,isHttps, localPort, overwriteDups, requestLineSize, debugLogger, config.getDisableFlags(), requestBuffer);
    this.name=getReaderType()+"#"+idCounter.addAndGet(1);
    this.startTime=System.currentTimeMillis();
  }

  /**
   * Returns a descriptive string for whether this is
   * an ssl or http reader
   * @return
   */
  protected String getReaderType()
  {
    return "http";
  }
 
  /**
   * Returns the name of this handler.
   * @return the name of this handler
   */
  @Override
  public String getName()
  {
    return name;
  }
 
  /**
   * Closes the IO channel and marks this handler as closed
   */
  protected void closeChannels()
  {
    if(!closeMe)
    {
      closeMe=true;
      synchronized(this.writeables)
      {
        for(final DataBuffers buf : this.writeables)
          buf.close();
        this.writeables.clear();
      }
      if((isDebugging)&&(chan.isOpen()))
        config.getLogger().finer("Closed request handler '"+name);
      try
      {
        chan.close();
      }catch(final Exception e){}
      if(forwarder!=null)
        forwarder.closeChannels();
    }
  }
 
  /**
   * Closes the IO channel and marks this handler as closed
   * Also waits until this runnable is no longer in progress
   */
  @Override
  public void closeAndWait()
  {
    closeChannels();
    final long time = System.currentTimeMillis();
    while((System.currentTimeMillis()-time<30000) && (isRunning))
    {
      try { Thread.sleep(100); }catch(final Exception e){}
    }
    if(forwarder!=null)
      forwarder.closeAndWait();
  }

  /**
   * Returns true if this request is currently reading/processing the request
   * @return true if in progress
   */
  @Override public boolean isRunning() { return isRunning && ((forwarder==null) || forwarder.isRunning());}

  /**
   * Returns true if this handler is either closed, or needs to be
   * due to timing out in one way or another.
   * @return true if this handler is done
   */
  @Override
  public boolean isCloseable()
  {
    if(closeMe)
      return true;
    final long currentTime=System.currentTimeMillis();
    if((idleTime!=0) && ((currentTime - idleTime) > config.getRequestMaxIdleMs()))
    {
      if (isDebugging) config.getLogger().finest("Idle Timed out: "+this.getName()+" "+(currentTime - idleTime) +">"+ config.getRequestMaxIdleMs());
      return true;
    }
    if((!chan.isOpen()) || (!chan.isConnected()) || (!chan.isRegistered()))
    {
      if (isDebugging) config.getLogger().finest("Disconnected: "+this.getName());
      return true;
    }
    if((startTime!=0) && (currentTime - startTime) > (config.getRequestMaxAliveSecs() * 1000))
    {
      if (isDebugging) config.getLogger().finest("Over Timed Out: "+this.getName()+" "+(currentTime - startTime) +">"+ (config.getRequestMaxAliveSecs() * 1000));
      return true;
    }
    if((forwarder!=null) && (forwarder.isCloseable()))
      return true;
    return false;
  }

 
  /**
   * Starts port forwarding for a normal httpreader
   * @param address the address to forward it to.
   * @param context the context that the initial request asked for
   */
  protected String startPortForwarding(final WebAddress address, final String context) throws HTTPException
  {
    forwarder=null;
    try
    {
     
      final SocketChannel forwarderChannel = SocketChannel.open(address.getAddress());
      if (forwarderChannel == null)
        throw new IOException("Unable to create channel.");
      synchronized(forwarderChannel)
      {
        if(!forwarderChannel.finishConnect())
          throw new HTTPException(HTTPStatus.S500_INTERNAL_ERROR);
        final HTTPForwarder forwarder=new HTTPForwarder(this, server, forwarderChannel);
        forwarderChannel.configureBlocking (false);
        final String webContext=address.getContext();
        final StringBuilder urlPage=new StringBuilder("");
        final String restOfUri=currentReq.getUrlPath().substring(context.length());
        if(webContext.length()>0)
          urlPage.append(webContext);
        if(restOfUri.length()>0)
        {
          if(restOfUri.startsWith("/"))
          {
            if(urlPage.length()==0)
              urlPage.append(restOfUri);
            else
            if(urlPage.charAt(urlPage.length()-1)=='/')
              urlPage.append(restOfUri.substring(1));
            else
              urlPage.append(restOfUri);
          }
          else
          if(urlPage.length()==0)
            urlPage.append('/').append(restOfUri);
          else
          if(urlPage.charAt(urlPage.length()-1)=='/')
            urlPage.append(restOfUri);
          else
            urlPage.append('/').append(restOfUri);
        }
        if(urlPage.length()==0) urlPage.append('/');
        final StringBuilder s=new StringBuilder(currentReq.getMethod().toString()).append(' ').append(urlPage)
              .append(" HTTP/").append(currentReq.getHttpVer()).append(EOLN);
        for(final String headerKey : currentReq.getAllHeaderReferences(true))
          if(headerKey.equalsIgnoreCase(HTTPHeader.HOST.name()))
            s.append(HTTPHeader.HOST.name()).append(": ").append(address.getHost()).append(':').append(address.getPort()).append(EOLN);
          else
            s.append(headerKey).append(": ").append(currentReq.getHeader(headerKey.toLowerCase().trim())).append(EOLN);
        if(config.isDebugging())
          config.getLogger().finer(forwarder.getName()+": "+address.getAddressStr()+": "+s.toString().replace('\n', ' ').replace('\r', ' '));
        server.registerNewHandler(forwarderChannel, forwarder);
        this.forwarder=forwarder;
        return s.toString();
      }
    }
    catch (final IOException e)
    {
      config.getLogger().throwing("", "", e);
      throw new HTTPException(HTTPStatus.S500_INTERNAL_ERROR);
    }
  }
 
  /**
   * Here it is, the mighty mighty state machine.
   * This ginormous function handles high level state for request reading.
   * It is handed a ByteBuffer recently written to, which it then flips
   * and reads out of (in the case of a request state) or simply checks
   * for errors in the case of a body read state. 
   *
   * The buffer may be modified if an actionable request piece is portioned
   * off of it.  It will also modify the internal state of parsing as needed.
   * It will help populate the HTTPRequest object as needed, but will not
   * generate the output.  It may, however, mark the request as complete
   * if the state warrants.
   *
   * The method should exit with the buffer in the same writeable state it
   * was handed.
   *
   * @param buffer the bytebuffer to process data from
   * @throws HTTPException
   */
  private void processBuffer(ByteBuffer buffer) throws HTTPException
  {
    ParseState state = currentState; // since currentState is volatile, lets cache local before tinkering with it
    try
    {
      switch(state)
      {
      case BODY: // while in a body state, there's nothing to do but see if its done
      {
        if(buffer.position() >= buffer.capacity()) // the > part is a little silly
        {
          currentReq.finishRequest();
          state=ParseState.DONE;
        }
        break;
      }
      case FORWARD:
      {
        if(forwarder==null)
          state=ParseState.DONE;
        else
        {
          buffer.flip(); // turn the writeable buffer into a "readable" one
          final ByteBuffer writeableBuf=ByteBuffer.allocate(buffer.remaining());
          writeableBuf.put(buffer);
          writeableBuf.flip();
          forwarder.writeBytesToChannel(new CWDataBuffers(writeableBuf,0,false));
          buffer.clear();
        }
        break;
      }
      default:
      {
        buffer.flip(); // turn the writeable buffer into a "readable" one
        int lastEOLIndex = 0; // the marker for the last place an EOLN was found
        char c;  // current character being examined
        while(buffer.position() < buffer.limit())
        {
          c=(char)buffer.get();
          switch(state)
          {
          case CHUNKED_ENDER_INLINE:
          {
            if(c=='\r')
              state=ParseState.CHUNKED_ENDER_EOLN;
            else
              throw HTTPException.standardException(HTTPStatus.S400_BAD_REQUEST);
            break;
          }
          case CHUNKED_ENDER_EOLN:
          {
            if(c=='\n')
            {
              lastEOLIndex=buffer.position();
              state=ParseState.CHUNKED_HEADER_INLINE;
            }
            else
              throw HTTPException.standardException(HTTPStatus.S400_BAD_REQUEST);
            break;
          }
          case CHUNKED_HEADER_INLINE:
          {
            if(c=='\r')
              state=ParseState.CHUNKED_HEADER_EOLN;
            break;
          }
          case CHUNKED_HEADER_EOLN:
          {
            if (c=='\n')
            {
              final String chunkSizeStr = new String(Arrays.copyOfRange(buffer.array(), lastEOLIndex, buffer.position()-2),utf8).trim();
              lastEOLIndex=buffer.position();
              if (chunkSizeStr.length()>0)
              {
                final String[] parts=chunkSizeStr.split(";");
                this.nextChunkSize = Integer.parseInt(parts[0],16);
                if(this.nextChunkSize == 0) // we've reached the last chunk
                {
                  buffer = currentReq.setToReceiveContentChunkedBody((int)config.getRequestLineBufBytes());
                  buffer.flip();
                  lastEOLIndex=0;
                  state=ParseState.CHUNKED_TRAILER_INLINE;
                }
                else
                {
                   // check for illegal request
                  if((this.nextChunkSize + currentReq.getBufferSize()) > config.getRequestMaxBodyBytes())
                  {
                    throw HTTPException.standardException(HTTPStatus.S413_REQUEST_ENTITY_TOO_LARGE);
                  }
                  buffer = currentReq.setToReceiveContentChunkedBody(this.nextChunkSize);
                  buffer.flip();
                  lastEOLIndex=0;
                  state=ParseState.CHUNKED_BODY;
                }
              }
              else
                throw HTTPException.standardException(HTTPStatus.S400_BAD_REQUEST);
            }
            else
              throw HTTPException.standardException(HTTPStatus.S400_BAD_REQUEST);
            break;
          }
          case REQ_INLINE:
            if(c=='\r')
              state=ParseState.REQ_EOLN;
            break;
          case REQ_EOLN:
          {
            if (c=='\n')
            {
              final String requestLine = new String(Arrays.copyOfRange(buffer.array(), lastEOLIndex, buffer.position()-2),utf8);
              lastEOLIndex=buffer.position();
              state=ParseState.HDR_INLINE;
              if (requestLine.length()>0)
              {
                try
                {
                  currentReq.parseRequest(requestLine);
                }
                catch(final NumberFormatException ne)
                {
                  throw HTTPException.standardException(HTTPStatus.S400_BAD_REQUEST);
                }
              }
              else
              {
                // ignore blank lines here -- perhaps someone telnetted in.
              }
            }
            else // an error! Ignore this line!
            {
              lastEOLIndex=buffer.position();
              state=ParseState.REQ_INLINE;
            }
            break;
          }
          case HDR_INLINE:
            if(c=='\r')
              state=ParseState.HDR_EOLN;
            break;
          case CHUNKED_TRAILER_INLINE:
            if(c=='\r')
              state=ParseState.CHUNKED_TRAILER_EOLN;
            break;
          case CHUNKED_TRAILER_EOLN:
          case HDR_EOLN:
          {
            if (c=='\n')
            {
              final String headerLine = new String(Arrays.copyOfRange(buffer.array(), lastEOLIndex, buffer.position()-2),utf8);
              lastEOLIndex=buffer.position();
              if(headerLine.length()>0)
              {
                String host = currentReq.parseHeaderLine(headerLine);
                if (state == ParseState.CHUNKED_TRAILER_EOLN)
                  state=ParseState.CHUNKED_TRAILER_INLINE;
                else
                  state=ParseState.HDR_INLINE;
               
                if(host!=null)
                {
                  final int x=host.indexOf(':');
                  if(x>0) host=host.substring(0, x); // we only care about the host, we KNOW the port.
                  final Pair<String,WebAddress> forward=config.getPortForward(host,currentReq.getClientPort(),currentReq.getUrlPath());
                  if((forward != null) && (state != ParseState.CHUNKED_TRAILER_INLINE))
                  {
                    final String requestLine=startPortForwarding(forward.second, forward.first);
                    if(forwarder!=null)
                    {
                      final DataBuffers out=new CWDataBuffers();
                      out.add(ByteBuffer.wrap(requestLine.getBytes()),0,false);
                      final ByteBuffer writeableBuf=ByteBuffer.allocate(buffer.remaining());
                      writeableBuf.put(buffer);
                      writeableBuf.flip();
                      out.add(writeableBuf,0,false);
                      forwarder.writeBytesToChannel(out);
                      buffer.clear();
                      state=ParseState.FORWARD;
                    }
                  }
                  outputThrottle = config.getResponseThrottle(host,currentReq.getClientPort(),currentReq.getUrlPath());
                  currentReq.getAllHeaderReferences(true);
                }
              }
              else // a blank line means the end of the header section!!!
              {
                if(state == ParseState.CHUNKED_TRAILER_EOLN)
                {
                  currentReq.finishRequest();
                  state=ParseState.DONE;
                }
                else
                if("chunked".equalsIgnoreCase(currentReq.getHeader(HTTPHeader.TRANSFER_ENCODING.lowerCaseName())))
                {
                  state=ParseState.CHUNKED_HEADER_INLINE;
                  buffer = currentReq.setToReceiveContentChunkedBody(0); // prepare for chunk length/headers
                  buffer.flip();
                  lastEOLIndex=0;
                }
                else
                {
                  //the headers will tell you what to do next "BODY" is too vague
                  final String contentLengthStr=currentReq.getHeader(HTTPHeader.CONTENT_LENGTH.lowerCaseName());
                  if(contentLengthStr!=null)
                  {
                    try
                    {
                      // moment of truth, do we have a body forthcoming?
                      final int contentLength = Integer.parseInt(contentLengthStr);
                      if ((contentLength < 0) || (contentLength > config.getRequestMaxBodyBytes())) // illegal request
                      {
                        throw HTTPException.standardException(HTTPStatus.S413_REQUEST_ENTITY_TOO_LARGE);
                      }
                      else
                      if(contentLength == 0) // no content means we are done .. finish the request
                      {
                        currentReq.setToReceiveContentBody(contentLength);
                        currentReq.finishRequest();
                        state=ParseState.DONE;
                      }
                      else // a positive content length means we should prepare to receive the body
                      {
                        currentReq.setToReceiveContentBody(contentLength);
                        state=ParseState.BODY;
                        // the line buffer might have contained the entire body, so check for that state and finish
                        // if necessary
                        if(currentReq.getBuffer().position() >= currentReq.getBuffer().capacity())
                        {
                          currentReq.finishRequest();
                          state=ParseState.DONE;
                        }
                      }
                    }
                    catch(final NumberFormatException ne)
                    {
                      throw HTTPException.standardException(HTTPStatus.S411_LENGTH_REQUIRED);
                    }
                  }
                  else
                  {
                    // we have an http exception for this, but why be a jerk
                    currentReq.finishRequest();
                    state=ParseState.DONE;
                  }
                }
              }
              // continue
              if((state != ParseState.DONE)
              &&(currentReq.isExpect("100-continue")))
              {
                if(currentReq.getHttpVer()>1.0)
                  writeBytesToChannel(new CWDataBuffers(ByteBuffer.wrap(HTTPIOHandler.CONT_RESPONSE),0,false));
              }
            }
            else // an error! Ignore this line!
            {
              lastEOLIndex=buffer.position();
              state=ParseState.REQ_INLINE;
            }
            break;
          }
          case CHUNKED_BODY: // while in a body state, there's nothing to do but see if its done
          {
            buffer.position(buffer.position()-1);
            final int len = (buffer.limit() >= this.nextChunkSize) ? this.nextChunkSize : buffer.limit();
            buffer = this.currentReq.receiveChunkedContent(len);
            buffer.flip();
            this.nextChunkSize -= len;
            if(this.nextChunkSize <= 0)
              state=ParseState.CHUNKED_ENDER_INLINE;
            break;
          }
          case FORWARD: // you can't get there from here
          case BODY: // you can't get there from here
            break;
          case DONE: // if done, we're done
            break;
          }
        }
       
        // check the new state
        switch(state)
        {
        case DONE:
          // just here to be clear that there's nothing left to do...
        case FORWARD:
          // nothing more to do in this case
          break;
        default:
        {
          // nothing to do ..
          if(lastEOLIndex==0)
          {
            buffer.position(buffer.limit());
            buffer.limit(buffer.capacity());
          }
          else
          if(lastEOLIndex > buffer.limit())
          {
            buffer.clear();
          }
          else
          {
            buffer.position(lastEOLIndex);
            buffer.compact();
          }
         
          if(state==ParseState.REQ_EOLN)
            state=ParseState.REQ_INLINE;
          else
          if(state==ParseState.HDR_EOLN)
            state=ParseState.HDR_INLINE;
          break;
        }
        }
        break;
      }
      }
    }
    catch(final IOException ioe)
    {
      config.getLogger().throwing("", "", ioe);
      throw HTTPException.standardException(HTTPStatus.S500_INTERNAL_ERROR);
    }
    finally
    {
      currentState = state; // the state was cached local, so copy back to memory when done
    }
  }
 
  /**
   * Reads bytes from the local channel into the given buffer, returning
   * the number of bytes read.  This code is parsed out here so that it
   * can be overridden by HTTPSReader
   * @param buffer target buffer for the data read
   * @return the number of bytes read (decoded)
   * @throws IOException
   */
  protected int readBytesFromClient(final ByteBuffer buffer) throws IOException
  {
    return chan.read (buffer);
  }

  /**
   * Reads bytes from the given buffer into the local channel.
   * This code is parsed out here so that it can be overridden by HTTPSReader
   * @param buffers source buffer for the data write
   * @return the number of bytes written
   * @throws IOException
   */
  @Override
  public int writeBlockingBytesToChannel(final DataBuffers buffers) throws IOException
  {
    int written=0;
    DataBuffers bufs=null;
    synchronized(writeables)
    {
      writeables.addLast(buffers);
      while(writeables.size()>0)
      {
        bufs=writeables.getFirst();
        while(bufs.hasNext())
        {
          ByteBuffer buffer=buffers.next();
          while(buffer.remaining()>0)
          {
            long maxToWrite = buffer.remaining();
            if(outputThrottle != null)
            {
              maxToWrite=outputThrottle.request(maxToWrite);
              if(maxToWrite < buffer.remaining())
                buffer = bufs.splitTopBuffer((int)maxToWrite);
            }
            final int bytesWritten=chan.write(buffer);
            if(bytesWritten>=0)
            {
              written+=bytesWritten;
            }
            if(outputThrottle != null)
            {
              outputThrottle.registerWritten(bytesWritten);
            }
            if(buffer.remaining()>0)
            {
              try{Thread.sleep(1);}catch(final Exception e){}
            }
          }
        }
        writeables.removeFirst();
      }
      return written;
    }
  }
 
  /**
   * Reads bytes from the given buffer into the local channel.
   * This code is parsed out here so that it can be overridden by HTTPSReader
   * @param buffers source buffer for the data write
   * @throws IOException
   */
  @Override
  public void writeBytesToChannel(final DataBuffers buffers) throws IOException
  {
    synchronized(this.writeables)
    {
      writeables.addLast(buffers);
    }
    handleWrites();
  }
 
  /**
   * Actual do an async write from the internal buffers.  Writes as much as
   * it can, then, when it can't write any more, register channel for more
   * interest.
   * @throws IOException
   */
  protected void handleWrites() throws IOException
  {
    DataBuffers bufs = null;
    synchronized(writeables)
    {
      while(writeables.size()>0)
      {
        bufs=writeables.getFirst();
        while(bufs.hasNext())
        {
          ByteBuffer buf=bufs.next();
          if(outputThrottle != null)
          {
            final long maxToWrite=outputThrottle.request(buf.remaining());
            if(maxToWrite < buf.remaining())
              buf = bufs.splitTopBuffer((int)maxToWrite);
          }
          final int written = chan.write(buf);
          if(outputThrottle != null)
          {
            outputThrottle.registerWritten(written);
          }
          if(buf.remaining()>0)
          {
            server.registerChannelInterest(chan, SelectionKey.OP_WRITE | SelectionKey.OP_READ);
            return;
          }
        }
        bufs.close();
        writeables.removeFirst();
      }
    }
  }

  /**
   * Main handler for the request reading and processing.  For a single request, it all happens here.
   * The bytes are read out of the channel until there's none left to read.  processBuffer above is called
   * to manage the state of parsing.
   *
   * When parsing is complete, if the request is done, output is generated and written to the channel.
   * Otherwise, we fall out and wait to be called again when more data is available to be read.
   */
  @Override
  public void run()
  {
    synchronized(this) // don't let mulitple readers in at the same time, ever.
    {
      isRunning=true; // for record keeping purposes
      idleTime=0;
      try // begin of the IO error handling and record integrity block
      {
        int bytesRead=0; // read bytes until we can't any more
        boolean announcedAlready=!isDebugging;
        final boolean accessLogging = config.getLogger().isLoggable(Level.FINE);
        final StringBuilder accessLog = ( accessLogging )? new StringBuilder() : null;
        try // begin of the http exception handling block
        {
          while (!closeMe && (( bytesRead = readBytesFromClient(currentReq.getBuffer())) > 0) )
          {
            if(!announcedAlready)
            {
              config.getLogger().finer("Processing handler '"+name+"'");
              announcedAlready=true;
            }
            processBuffer(currentReq.getBuffer()); // process any data received
            if(currentReq.isFinished()) // if the request was completed, generate output!
            {
              final HTTPReqProcessor processor = new HTTPReqProcessor(config);
              final DataBuffers bufs = processor.generateOutput(currentReq);
             
              if(accessLog != null)
                accessLog.append(Log.makeLogEntry(Log.Type.access, Thread.currentThread().getName(),
                  currentReq.getClientAddress().getHostAddress()
                  +" "+currentReq.getHost()
                  +" \""+currentReq.getFullRequest()+" \" "+processor.getLastHttpStatusCode()+" "+bufs.getLength())).append("\n");
              writeBytesToChannel(bufs);
              // after output, prepare for a second request on this channel
              final String closeHeader = currentReq.getHeader(HTTPHeader.CONNECTION.lowerCaseName());
              if((closeHeader != null) && (closeHeader.trim().equalsIgnoreCase("close")))
                closeRequested = true;
              else
              {
                currentReq=new CWHTTPRequest(currentReq);
                currentState=ParseState.REQ_INLINE;
              }
            }
          }
        }
        catch(final HTTPException me) // if an exception is generated, go ahead and send it out
        {
          final DataBuffers bufs=me.generateOutput(currentReq);
          writeBytesToChannel(bufs);
          if(accessLog != null)
            accessLog.append(Log.makeLogEntry(Log.Type.access, Thread.currentThread().getName(),
              currentReq.getClientAddress().getHostAddress()
              +" "+currentReq.getHost()
              +" \""+currentReq.getFullRequest()+"\" "+me.getStatus().getStatusCode()+" "+bufs.getLength())).append("\n");
          // have to assume any exception caused
          // before a finish is malformed and needs a closed connection.
          if(currentReq.isFinished())
          {
            final String closeHeader = currentReq.getHeader(HTTPHeader.CONNECTION.lowerCaseName());
            if((closeHeader != null) && (closeHeader.trim().equalsIgnoreCase("close")))
              closeRequested = true;
            else
            {
              currentReq=new CWHTTPRequest(currentReq);
              currentState=ParseState.REQ_INLINE;
            }
          }
          else
            closeChannels();
        }
        finally
        {
          if((accessLogging)&&(accessLog!=null)&&(accessLog.length()>1))
          {
            if(config.getLogger().getClass().equals(Log.class))
              ((Log)config.getLogger()).rawStandardOut(Type.access,accessLog.substring(0,accessLog.length()-1),Integer.MIN_VALUE);
            else
              config.getLogger().fine(accessLog.substring(0,accessLog.length()-1));
          }
        }
        handleWrites();
         // if eof is reached, close this channel and mark it for deletion by the web server
        if(!closeMe)
        {
          if ((bytesRead < 0)
          || (!chan.isConnected())
          || (!chan.isOpen())
          || (closeRequested && (writeables.size()==0)))
          {
            closeChannels();
            currentState=ParseState.DONE;
          }
        }
       
      }
      catch(final IOException e)
      {
        if(isDebugging) config.getLogger().finer("Closing "+getName()+" due to: "+e.getClass().getName()+": "+e.getMessage());
        closeChannels();
        currentState=ParseState.DONE; // a common case when client closes first
      }
      catch(final Exception e)
      {
        closeChannels();
        currentState=ParseState.DONE;
        config.getLogger().throwing("", "", e);
      }
      finally
      {
        isRunning=false;
        idleTime=System.currentTimeMillis();
      }
    }
  }
}
TOP

Related Classes of com.planet_ink.coffee_web.http.HTTPReader

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.