/FileExtraction/Java_unseen/dlitz_resin/modules/resin/src/com/caucho/server/http/HttpRequest.java
/*
 * Copyright (c) 1998-2011 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source 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, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.server.http;

import java.io.IOException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.caucho.env.meter.ActiveTimeMeter;
import com.caucho.env.meter.AverageMeter;
import com.caucho.env.meter.MeterService;
import com.caucho.network.listen.ProtocolConnection;
import com.caucho.network.listen.SocketLink;
import com.caucho.network.listen.SocketLinkDuplexController;
import com.caucho.network.listen.SocketLinkDuplexListener;
import com.caucho.network.listen.TcpSocketLink;
import com.caucho.server.cluster.Server;
import com.caucho.server.dispatch.BadRequestException;
import com.caucho.server.dispatch.Invocation;
import com.caucho.util.CharBuffer;
import com.caucho.util.CharSegment;
import com.caucho.util.L10N;
import com.caucho.vfs.ClientDisconnectException;
import com.caucho.vfs.QSocket;
import com.caucho.vfs.ReadStream;
import com.caucho.vfs.SocketTimeoutException;

/**
 * Handles a new request from an HTTP connection.
 */
public class HttpRequest extends AbstractHttpRequest
  implements ProtocolConnection
{
  private static final L10N L = new L10N(HttpRequest.class);
  
  private static final Logger log
    = Logger.getLogger(HttpRequest.class.getName());

  public static final int HTTP_0_9 = 0x0009;
  public static final int HTTP_1_0 = 0x0100;
  public static final int HTTP_1_1 = 0x0101;

  private static final CharBuffer _getCb = new CharBuffer("GET");
  private static final CharBuffer _headCb = new CharBuffer("HEAD");
  private static final CharBuffer _postCb = new CharBuffer("POST");

  private static final String REQUEST_TIME_PROBE
    = "Resin|Http|Request";
  private static final String REQUEST_READ_BYTES_PROBE
  = "Resin|Http|Request Read Bytes";
  private static final String REQUEST_WRITE_BYTES_PROBE
  = "Resin|Http|Request Write Bytes";

  private final CharBuffer _method     // "GET"
    = new CharBuffer();
  private String _methodString;

  private final CharBuffer _uriHost    // www.caucho.com:8080
    = new CharBuffer();
  private CharSequence _host;

  private byte []_uri;                 // "/path/test.jsp/Junk?query=7"
  private int _uriLength;

  private final CharBuffer _protocol   // "HTTP/1.0"
    = new CharBuffer();
  private int _version;

  private char []_headerBuffer;

  private CharSegment []_headerKeys;
  private CharSegment []_headerValues;
  private int _headerSize;

  private ChunkedInputStream _chunkedInputStream = new ChunkedInputStream();
  private ContentLengthStream _contentLengthStream = new ContentLengthStream();
  private RawInputStream _rawInputStream = new RawInputStream();

  private ActiveTimeMeter _requestTimeProbe;
  private AverageMeter _requestReadBytesProbe;
  private AverageMeter _requestWriteBytesProbe;

  /**
   * Creates a new HttpRequest.  New connections reuse the request.
   *
   * @param server the owning server.
   */
  public HttpRequest(Server server, SocketLink conn)
  {
    super(server, conn);

    _requestTimeProbe
      = MeterService.createActiveTimeMeter(REQUEST_TIME_PROBE);

    _requestReadBytesProbe
      = MeterService.createAverageMeter(REQUEST_READ_BYTES_PROBE, "");

    _requestWriteBytesProbe
      = MeterService.createAverageMeter(REQUEST_WRITE_BYTES_PROBE, "");
  }

  @Override
  public HttpResponse createResponse()
  {
    return new HttpResponse(this, getConnection().getWriteStream());
  }

  /**
   * Return true if the request waits for a read before beginning.
   */
  @Override
  public final boolean isWaitForRead()
  {
    return true;
  }

  /**
   * Returns true if the request exists
   */
  @Override
  public boolean hasRequest()
  {
    return getRequestFacade() != null;
  }

  /**
   * Returns true for the top-level request, but false for any include()
   * or forward()
   */
  public boolean isTop()
  {
    return true;
  }

  protected boolean checkLogin()
  {
    return true;
  }

  //
  // HTTP request properties
  //

  /**
   * Returns a buffer containing the request method.
   */
  public CharSegment getMethodBuffer()
  {
    return _method;
  }

  /**
   * Returns the HTTP method (GET, POST, HEAD, etc.)
   */
  @Override
  public String getMethod()
  {
    if (_methodString == null) {
      CharSegment cb = getMethodBuffer();
      if (cb.length() == 0) {
        _methodString = "GET";
        return _methodString;
      }

      switch (cb.charAt(0)) {
      case 'G':
        _methodString = cb.equals(_getCb) ? "GET" : cb.toString();
        break;

      case 'H':
        _methodString = cb.equals(_headCb) ? "HEAD" : cb.toString();
        break;

      case 'P':
        _methodString = cb.equals(_postCb) ? "POST" : cb.toString();
        break;

      default:
        _methodString = cb.toString();
      }
    }

    return _methodString;
  }

  /**
   * Returns the virtual host of the request
   */
  @Override
  protected CharSequence getHost()
  {
    if (_host != null)
      return _host;

    String virtualHost = getConnection().getVirtualHost();
    if (virtualHost != null)
      _host = virtualHost;
    else if (_uriHost.length() > 0)
      _host = _uriHost;
    else
      _host = _hostHeader;

    return _host;
  }

  /**
   * Returns the virtual host from the invocation
   */
  private CharSequence getInvocationHost()
    throws IOException
  {
    if (_host != null)
      return _host;

    String virtualHost = getConnection().getVirtualHost();
    if (virtualHost != null)
      return virtualHost;
    else if (_host != null) {
    }
    else if (_uriHost.length() > 0) {
      _host = _uriHost;
    }
    else if (_hostHeader != null) {
      _host = _hostHeader;
    }
    else if (HTTP_1_1 <= getVersion())
      throw new BadRequestException("HTTP/1.1 requires a Host header (Remote IP=" + getRemoteHost() + ")");

    return _host;
  }

  /**
   * Returns the byte buffer containing the request URI
   */
  @Override
  public byte []getUriBuffer()
  {
    return _uri;
  }

  /**
   * Returns the length of the request URI
   */
  @Override
  public int getUriLength()
  {
    return _uriLength;
  }

  /**
   * Returns the protocol.
   */
  @Override
  public String getProtocol()
  {
    switch (_version) {
    case HTTP_1_1:
      return "HTTP/1.1";
    case HTTP_1_0:
      return "HTTP/1.0";
    case HTTP_0_9:
    default:
      return "HTTP/0.9";
    }
  }

  /**
   * Returns a char segment containing the protocol.
   */
  public CharSegment getProtocolBuffer()
  {
    return _protocol;
  }

  /**
   * Returns the HTTP version of the request based on getProtocol().
   */
  int getVersion()
  {
    if (_version > 0)
      return _version;

    CharSegment protocol = getProtocolBuffer();
    if (protocol.equals("HTTP/1.1")) {
      _version = HTTP_1_1;
      return HTTP_1_1;
    }
    else if (protocol.equals("HTTP/1.0")) {
      _version = HTTP_1_0;
      return _version;
    }
    else if (protocol.equals("HTTP/0.9")) {
      _version = HTTP_0_9;
      return HTTP_0_9;
    }
    else if (protocol.length() < 8) {
      _version = HTTP_0_9;
      return _version;
    }

    int i = protocol.indexOf('/');
    int len = protocol.length();
    int major = 0;
    for (i++; i < len; i++) {
      char ch = protocol.charAt(i);

      if ('0' <= ch && ch <= '9')
        major = 10 * major + ch - '0';
      else if (ch == '.')
        break;
      else {
        _version = HTTP_1_0;
        return _version;
      }
    }

    int minor = 0;
    for (i++; i < len; i++) {
      char ch = protocol.charAt(i);

      if ('0' <= ch && ch <= '9')
        minor = 10 * minor + ch - '0';
      else
        break;
    }

    _version = 256 * major + minor;

    return _version;
  }

  //
  // HTTP request headers
  //

  /**
   * Returns the header.
   */
  @Override
  public String getHeader(String key)
  {
    CharSegment buf = getHeaderBuffer(key);
    
    if (buf != null)
      return buf.toString();
    else
      return null;
  }

  /**
   * Returns the number of headers.
   */
  @Override
  public int getHeaderSize()
  {
    return _headerSize;
  }

  /**
   * Returns the header key
   */
  @Override
  public CharSegment getHeaderKey(int index)
  {
    return _headerKeys[index];
  }

  /**
   * Returns the header value
   */
  @Override
  public CharSegment getHeaderValue(int index)
  {
    return _headerValues[index];
  }

  /**
   * Returns the matching header.
   *
   * @param testBuf header key
   * @param length length of the key.
   */
  public CharSegment getHeaderBuffer(char []testBuf, int length)
  {
    char []keyBuf = _headerBuffer;
    CharSegment []headerKeys = _headerKeys;

    for (int i = _headerSize - 1; i >= 0; i--) {
      CharSegment key = headerKeys[i];

      if (key.length() != length)
        continue;

      int offset = key.getOffset();
      int j;
      for (j = length - 1; j >= 0; j--) {
        char a = testBuf[j];
        char b = keyBuf[offset + j];
        if (a == b)
          continue;

        if (a >= 'A' && a <= 'Z')
          a += 'a' - 'A';
        if (b >= 'A' && b <= 'Z')
          b += 'a' - 'A';
        if (a != b)
          break;
      }

      if (j < 0)
        return _headerValues[i];
    }

    return null;
  }

  /**
   * Returns the header value for the key, returned as a CharSegment.
   */
  @Override
  public CharSegment getHeaderBuffer(String key)
  {
    int i = matchNextHeader(0, key);

    if (i >= 0)
      return _headerValues[i];
    else
      return null;
  }

  /**
   * Fills an ArrayList with the header values matching the key.
   *
   * @param values ArrayList which will contain the maching values.
   * @param key the header key to select.
   */
  @Override
  public void getHeaderBuffers(String key, ArrayList<CharSegment> values)
  {
    int i = -1;
    while ((i = matchNextHeader(i + 1, key)) >= 0)
      values.add(_headerValues[i]);
  }

  /**
   * Return an enumeration of headers matching a key.
   *
   * @param key the header key to match.
   * @return the enumeration of the headers.
   */
  @Override
  public Enumeration<String> getHeaders(String key)
  {
    ArrayList<String> values = new ArrayList<String>();
    
    int i = -1;
    while ((i = matchNextHeader(i + 1, key)) >= 0)
      values.add(_headerValues[i].toString());

    return Collections.enumeration(values);
  }

  /**
   * Returns the index of the next header matching the key.
   *
   * @param i header index to start search
   * @param key header key to match
   *
   * @return the index of the next header matching, or -1.
   */
  private int matchNextHeader(int i, String key)
  {
    int size = _headerSize;
    int length = key.length();

    char []keyBuf = _headerBuffer;

    for (; i < size; i++) {
      CharSegment header = _headerKeys[i];

      if (header.length() != length)
        continue;

      int offset = header.getOffset();

      int j;
      for (j = 0; j < length; j++) {
        char a = key.charAt(j);
        char b = keyBuf[offset + j];
        if (a == b)
          continue;

        if (a >= 'A' && a <= 'Z')
          a += 'a' - 'A';
        if (b >= 'A' && b <= 'Z')
          b += 'a' - 'A';
        if (a != b)
          break;
      }

      if (j == length)
        return i;
    }

    return -1;
  }

  /**
   * Returns an enumeration of all the header keys.
   */
  @Override
  public Enumeration<String> getHeaderNames()
  {
    ArrayList<String> names = new ArrayList<String>();

    for (int i = 0; i < _headerSize; i++) {
      CharSegment name = _headerKeys[i];

      int j;
      for (j = 0; j < names.size(); j++) {
        String oldName = names.get(j);
        if (name.matches(oldName))
          break;
      }
      if (j == names.size())
        names.add(j, name.toString());
    }

    return Collections.enumeration(names);
  }

  /**
   * Adds a new header.  Used only by the caching to simulate
   * If-Modified-Since.
   *
   * @param key the key of the new header
   * @param value the value for the new header
   */
  @Override
  public void setHeader(String key, String value)
  {
    int tail;

    if (_headerSize > 0) {
      tail = (_headerValues[_headerSize - 1].getOffset()
              + _headerValues[_headerSize - 1].getLength());
    }
    else
      tail = 0;

    char []headerBuffer = _headerBuffer;
    for (int i = key.length() - 1; i >= 0; i--)
      headerBuffer[tail + i] = key.charAt(i);

    _headerKeys[_headerSize].init(headerBuffer, tail, key.length());

    tail += key.length();

    for (int i = value.length() - 1; i >= 0; i--)
      headerBuffer[tail + i] = value.charAt(i);

    _headerValues[_headerSize].init(headerBuffer, tail, value.length());
    _headerSize++;
  }

  //
  // attribute management
  //

  /**
   * Initialize any special attributes.
   */
  @Override
  protected void initAttributes(HttpServletRequestImpl request)
  {
    SocketLink conn = getConnection();

    if (! (conn instanceof TcpSocketLink))
      return;
    
    TcpSocketLink tcpConn = (TcpSocketLink) conn;
    
    if (! conn.isSecure())
      return;

    QSocket socket = tcpConn.getSocket();

    String cipherSuite = socket.getCipherSuite();
    request.setAttribute("javax.servlet.request.cipher_suite", cipherSuite);

    int keySize = socket.getCipherBits();
    if (keySize != 0)
      request.setAttribute("javax.servlet.request.key_size",
                           new Integer(keySize));

    try {
      X509Certificate []certs = socket.getClientCertificates();
      if (certs != null && certs.length > 0) {
        request.setAttribute("javax.servlet.request.X509Certificate",
                             certs); //spec mandates array
      }
    } catch (Exception e) {
      log.log(Level.FINER, e.toString(), e);
    }
  }

  //
  // session management
  //

  /**
   * For SSL connections, use the SSL identifier.
   */
  @Override
  public String findSessionIdFromConnection()
  {
    SocketLink link = getConnection();
    
    TcpSocketLink tcpConn = null;
    
    if (link instanceof TcpSocketLink)
      tcpConn = (TcpSocketLink) getConnection();

    if (tcpConn == null || ! tcpConn.isSecure())
      return null;

    /*
    QSocket socket = tcpConn.getSocket(); // XXX:
    if (! (socket instanceof SSLSocket))
      return null;

    SSLSession sslSession = ((SSLSocket) socket).getSession();
    if (sslSession == null)
      return null;

    byte []sessionId = sslSession.getId();
    if (sessionId == null)
      return null;

    CharBuffer cb = CharBuffer.allocate();
    Base64.encode(cb, sessionId, 0, sessionId.length);
    for (int i = cb.length() - 1; i >= 0; i--) {
      char ch = cb.charAt(i);
      if (ch == '/')
        cb.setCharAt(i, '-');
    }

    return cb.close();
    */
    return null;
  }

  //
  // stream management
  //

  /**
   * Returns a stream for reading POST data.
   */
  @Override
  public boolean initStream(ReadStream readStream, ReadStream rawRead)
    throws IOException
  {
    // ReadStream readStream = getReadStream();

    // needed to avoid auto-flush on read conflicting with partially
    // generated response
    rawRead.setSibling(null);

    if (getConnection().isDuplex()) {
      _rawInputStream.init(rawRead);
      readStream.init(_rawInputStream, null);
      return true;
    }

    long contentLength = getLongContentLength();

    if (contentLength < 0 && HTTP_1_1 <= getVersion()
        && getHeader("Transfer-Encoding") != null) {
      _chunkedInputStream.init(rawRead);
      readStream.init(_chunkedInputStream, null);
      return true;
    }
    // Otherwise use content-length
    else if (contentLength >= 0) {
      _contentLengthStream.init(rawRead, contentLength);
      readStream.init(_contentLengthStream, null);

      return true;
    }
    else if (getMethod().equals("POST")) {
      _contentLengthStream.init(rawRead, 0);
      readStream.init(_contentLengthStream, null);

      throw new com.caucho.server.dispatch.BadRequestException("POST requires content-length");
    }

    else {
      _contentLengthStream.init(rawRead, 0);
      readStream.init(_contentLengthStream, null);

      return false;
    }
  }

  @Override
  protected void skip()
    throws IOException
  {
    if (getMethod() == "GET")
      return;

    super.skip();
  }

  /**
   * Returns the raw input stream.
   */
  @Override
  public ReadStream getRawInput()
  {
    return getRawRead();
  }

  /**
   * Handles a new HTTP request.
   *
   * <p>Note: ClientDisconnectException must be rethrown to
   * the caller.
   *
   * @return true if the connection should stay open (keepalive)
   */
  @Override
  public boolean handleRequest()
    throws IOException
  {
    boolean isInvocation = false;

    Server server = getServer();
    Thread thread = Thread.currentThread();
    ClassLoader oldLoader = thread.getContextClassLoader();
    long startTime = 0;
    
    long startReadBytes = getRawRead().getPosition();
    long startWriteBytes = getRawWrite().getPosition();

    try {
      thread.setContextClassLoader(server.getClassLoader());
      
      startRequest(server.allocateHttpBuffer());

      if (! parseRequest()) {
        if (log.isLoggable(Level.FINER)) {
          log.finer(dbgId() + " empty request");
        }
        
        return false;
      }

      CharSequence host = getInvocationHost();

      Invocation invocation = getInvocation(host, _uri, _uriLength);

      if (invocation == null) {
        if (log.isLoggable(Level.FINER)) {
          log.finer(dbgId() + " empty invocation");
        }
        
        return false;
      }

      HttpServletRequestImpl requestFacade = getRequestFacade();

      requestFacade.setInvocation(invocation);

      isInvocation = true;
      startTime = _requestTimeProbe.start();
      startInvocation();

      invocation.service(requestFacade, getResponseFacade());
    } catch (ClientDisconnectException e) {
      clientDisconnect();
      /*
      CauchoResponse response = getResponseFacade();
      
      if (response != null)
        response.killCache();

      killKeepalive();
      */

      throw e;
    } catch (Throwable e) {
      log.log(Level.FINE, e.toString(), e);

      CauchoResponse response = getResponseFacade();
      
      if (response != null)
        response.killCache();
      
      killKeepalive("http handleRequest exception: " + e);

      sendRequestError(e);

      return false;
    } finally {
      if (isInvocation) {
        finishInvocation();
      }

      if (! isSuspend()) {
        finishRequest();
      }

      if (startTime > 0) {
        _requestTimeProbe.end(startTime);
        
        long endReadBytes = getRawRead().getPosition();
        long endWriteBytes = getRawWrite().getPosition();
        
        _requestReadBytesProbe.add(endReadBytes - startReadBytes);
        _requestWriteBytesProbe.add(endWriteBytes - startWriteBytes);
      }

      thread.setContextClassLoader(oldLoader);
    }

    return true;
  }

  private boolean parseRequest()
    throws IOException
  {
    try {
      ReadStream is = getRawRead();

      if (! readRequest(is)) {
        clearRequest();

        return false;
      }

      if (log.isLoggable(Level.FINE)) {
        log.fine(dbgId() + _method + " "
                 + new String(_uri, 0, _uriLength) + " " + _protocol);
        log.fine(dbgId() + "Remote-IP: " + getRemoteHost()
                 + ":" + getRemotePort());
      }

      parseHeaders(is);

      return true;
    } catch (ClientDisconnectException e) {
      throw e;
    } catch (SocketTimeoutException e) {
      log.log(Level.FINER, e.toString(), e);
      
      return false;
    } catch (ArrayIndexOutOfBoundsException e) {
      log.log(Level.FINER, e.toString(), e);
      
      throw new BadRequestException(L.l("Invalid request: URL or headers are too long"), e);
    } catch (Throwable e) {
      log.log(Level.FINER, e.toString(), e);
      
      throw new BadRequestException(String.valueOf(e), e);
    }
  }

  /**
   * Clear the request variables in preparation for a new request.
   *
   * @param s the read stream for the request
   */
  @Override
  protected void startRequest(HttpBufferStore httpBuffer)
    throws IOException
  {
    super.startRequest(httpBuffer);

    _method.clear();
    _methodString = null;
    _protocol.clear();

    _uriLength = 0;
    _uri = httpBuffer.getUriBuffer();

    _uriHost.clear();
    _host = null;

    _headerSize = 0;
    _headerBuffer = httpBuffer.getHeaderBuffer();
    _headerKeys = httpBuffer.getHeaderKeys();
    _headerValues = httpBuffer.getHeaderValues();
  }

  /**
   * Read the first line of a request:
   *
   * GET [http://www.caucho.com[:80]]/path [HTTP/1.x]
   *
   * @return true if the request is valid
   */
  private boolean readRequest(ReadStream s)
    throws IOException
  {
    // server/12o3 - default to 1.0 for error messages in request
    _version = HTTP_1_0;
    
    byte []readBuffer = s.getBuffer();
    int readOffset = s.getOffset();
    int readLength = s.getLength();
    int ch;

    // skip leading whitespace
    do {
      if (readLength <= readOffset) {
        if ((readLength = s.fillBuffer()) < 0)
          return false;

        readOffset = 0;
      }

      ch = readBuffer[readOffset++];
    } while (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n');

    char []buffer = _method.getBuffer();
    int length = buffer.length;
    int offset = 0;

    // scan method
    while (true) {
      if (length <= offset) {
      }
      else if ('a' <= ch && ch <= 'z')
        buffer[offset++] = ((char) (ch + 'A' - 'a'));
      else if (ch > ' ')
        buffer[offset++] = (char) ch;
      else
        break;

      if (readLength <= readOffset) {
        if ((readLength = s.fillBuffer()) < 0)
          return false;

        readOffset = 0;
      }
      
      ch = readBuffer[readOffset++];
    }
    
    _method.setLength(offset);

    // skip whitespace
    while (ch == ' ' || ch == '\t') {
      if (readLength <= readOffset) {
        if ((readLength = s.fillBuffer()) < 0)
          return false;

        readOffset = 0;
      }

      ch = readBuffer[readOffset++];
    }

    byte []uriBuffer = _uri;
    int uriLength = 0;

    // skip 'http:'
    if (ch != '/') {
      while (ch > ' ' && ch != '/') {
        if (readLength <= readOffset) {
          if ((readLength = s.fillBuffer()) < 0)
            return false;
          readOffset = 0;
        }
        ch = readBuffer[readOffset++];
      }
      
      if (ch != '/') {
        log.warning("Invalid Request (method='" + _method + "' url ch=0x" + Integer.toHexString(ch & 0xff) + ") (IP=" + getRemoteHost() + ")");
        
        throw new BadRequestException("Invalid Request(Remote IP=" + getRemoteHost() + ")");
      }

      if (readLength <= readOffset) {
        if ((readLength = s.fillBuffer()) < 0) {
          if (ch == '/') {
            uriBuffer[uriLength++] = (byte) ch;
            _uriLength = uriLength;
          }
          
          _version = 0;

          return true;
        }
        readOffset = 0;
      }

      int ch1 = readBuffer[readOffset++];

      if (ch1 != '/') {
        uriBuffer[uriLength++] = (byte) ch;
        ch = ch1;
      }
      else {
        // read host
        host:
        while (true) {
          if (readLength <= readOffset) {
            if ((readLength = s.fillBuffer()) < 0) {
              _version = 0;
              
              return true;
            }
            readOffset = 0;
          }
          ch = readBuffer[readOffset++];

          switch (ch) {
          case ' ': case '\t': case '\n': case '\r':
            break host;

          case '?':
            break host;

          case '/':
            break host;

          default:
            _uriHost.append((char) ch);
            break;
          }
        }
      }
    }

    // read URI
    uri:
    while (true) {
      switch (ch) {
      case ' ': case '\t': case '\n': case '\r':
        break uri;

      default:
        // There's no check for over-running the length because
        // allowing resizing would allow a DOS memory attack and
        // also lets us save a bit of efficiency.
        uriBuffer[uriLength++] = (byte) ch;
        break;
      }

      if (readLength <= readOffset) {
        readOffset = 0;
        if ((readLength = s.fillBuffer()) < 0) {
          _uriLength = uriLength;
          _version = 0;
          return true;
        }
      }
      ch = readBuffer[readOffset++];
    }
    
    _uriLength = uriLength;
    _version = 0;

    // skip whitespace
    while (ch == ' ' || ch == '\t') {
      if (readLength <= readOffset) {
        readOffset = 0;
        if ((readLength = s.fillBuffer()) < 0)
          return true;
      }
      ch = readBuffer[readOffset++];
    }

    buffer = _protocol.getBuffer();
    length = buffer.length;
    offset = 0;
    // scan protocol
    while (ch != ' ' && ch != '\t' && ch != '\r' && ch != '\n') {
      if (length <= offset) {
      }
      else if ('a' <= ch && ch <= 'z')
        buffer[offset++] = ((char) (ch + 'A' - 'a'));
      else
        buffer[offset++] = (char) ch;

      if (readLength <= readOffset) {
        readOffset = 0;
        if ((readLength = s.fillBuffer()) < 0) {
          _protocol.setLength(offset);
          return true;
        }
      }
      ch = readBuffer[readOffset++];
    }

    _protocol.setLength(offset);

    if (offset != 8) {
      _protocol.append("HTTP/0.9");
      _version = HTTP_0_9;
    }
    else if (buffer[7] == '1') // && _protocol.equals(_http11Cb))
      _version = HTTP_1_1;
    else if (buffer[7] == '0') // && _protocol.equals(_http10Cb))
      _version = HTTP_1_0;
    else
      _version = HTTP_0_9;

    // skip to end of line
    while (ch != '\n') {
      if (readLength <= readOffset) {
        if ((readLength = s.fillBuffer()) < 0)
          return true;
        readOffset = 0;
      }
      ch = readBuffer[readOffset++];
    }

    s.setOffset(readOffset);

    return true;
  }

  /**
   * Parses headers from the read stream.
   *
   * @param s the input read stream
   */
  private void parseHeaders(ReadStream s) throws IOException
  {
    // This is still slowest part of the web server.  I don't see how
    // to improve it much more, but there must be a way.
    int version = getVersion();

    if (version < HTTP_1_0) {
      return;
    }

    if (version < HTTP_1_1)
      killKeepalive("http client version less than 1.1: " + version);

    byte []readBuffer = s.getBuffer();
    int readOffset = s.getOffset();
    int readLength = s.getLength();

    char []headerBuffer = _headerBuffer;
    int headerOffset = 1;
    headerBuffer[0] = 'z';
    int headerSize = 0;
    _headerSize = 0;

    CharSegment []headerKeys = _headerKeys;
    CharSegment []headerValues = _headerValues;

    boolean isLogFine = log.isLoggable(Level.FINE);

    while (true) {
      int ch;

      int keyOffset = headerOffset;

      // scan the key
      while (true) {
        if (readLength <= readOffset) {
          readOffset = 0;
          if ((readLength = s.fillBuffer()) <= 0)
            return;
        }
        ch = readBuffer[readOffset++];

        if (ch == '\n') {
          s.setOffset(readOffset);
          return;
        }
        else if (ch == ':')
          break;

        headerBuffer[headerOffset++] = (char) ch;
      }

      while (headerBuffer[headerOffset - 1] == ' ')
        headerOffset--;

      int keyLength = headerOffset - keyOffset;
      headerKeys[headerSize].init(headerBuffer, keyOffset, keyLength);

      do {
        if (readLength <= readOffset) {
          readOffset = 0;
          if ((readLength = s.fillBuffer()) <= 0)
            return;
        }
        ch = readBuffer[readOffset++];
      } while (ch == ' ' || ch == '\t');

      int valueOffset = headerOffset;

      // scan the value
      while (true) {
        if (readLength <= readOffset) {
          readOffset = 0;
          if ((readLength = s.fillBuffer()) <= 0)
            break;
        }

        if (ch == '\n') {
          int ch1 = readBuffer[readOffset];

          if (ch1 == ' ' || ch1 == '\t') {
            ch = ' ';
            readOffset++;

            if (headerBuffer[headerOffset - 1] == '\r')
              headerOffset--;
          }
          else
            break;
        }

        headerBuffer[headerOffset++] = (char) ch;

        ch = readBuffer[readOffset++];
      }

      while (headerBuffer[headerOffset - 1] <= ' ')
        headerOffset--;

      int valueLength = headerOffset - valueOffset;
      headerValues[headerSize].init(headerBuffer, valueOffset, valueLength);

      if (isLogFine) {
        log.fine(dbgId() +
                 headerKeys[headerSize] + ": " + headerValues[headerSize]);
      }

      if (addHeaderInt(headerBuffer, keyOffset, keyLength,
                       headerValues[headerSize])) {
        headerSize++;
      }

      _headerSize = headerSize;
    }
  }

  //
  // upgrade to duplex
  //


  /**
   * Upgrade to duplex
   */
  @Override
  public SocketLinkDuplexController startDuplex(SocketLinkDuplexListener handler)
  {
    TcpSocketLink conn = (TcpSocketLink) getConnection();

    SocketLinkDuplexController context = conn.startDuplex(handler);

    _rawInputStream.init(conn.getReadStream());
    getReadStream().setSource(_rawInputStream);
    
    return context;
  }

  /**
   * Cleans up at the end of the invocation
   */
  @Override
  public void finishRequest()
    throws IOException
  {
    super.finishRequest();

    skip();
  }

  @Override
  protected String dbgId()
  {
    String serverId = getServer().getServerId();
    int connId = getConnectionId();

    if ("".equals(serverId))
      return "Http[" + connId + "] ";
    else
      return "Http[" + serverId + ", " + connId + "] ";
  }

  @Override
  public String toString()
  {
    String serverId = getServer().getServerId();
    int connId = getConnectionId();

    if ("".equals(serverId))
      return "HttpRequest[" + connId + "]";
    else {
      return ("HttpRequest[" + serverId + ", " + connId + "]");
    }
  }
}
