/FileExtraction/Java_unseen/dlitz_resin/modules/kernel/src/com/caucho/vfs/HttpStream.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.vfs;

import com.caucho.util.Alarm;
import com.caucho.util.CharBuffer;
import com.caucho.util.L10N;
import com.caucho.util.Log;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ConnectException;
import java.net.Socket;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Underlying stream handling HTTP requests.
 */
class HttpStream extends StreamImpl {
  private static final L10N L = new L10N(HttpStream.class);
  
  private static final Logger log
    = Logger.getLogger(HttpStream.class.getName());
  // reserved headers that should not be passed to the HTTP server
  private static HashMap<String,String> _reserved;

  private static final Object LOCK = new Object();
  
  // Saved keepalive stream for a new request.
  private static HttpStream _savedStream;
  // Time the stream was saved
  private static long _saveTime;
  
  private static boolean _isKeepaliveAllowed;
  
  private long _socketTimeout = 30000L;

  private boolean _isSSL;

  private Socket _s;
  private InputStream _is;
  private OutputStream _os;
  private ReadStream _rs;
  private WriteStream _ws;

  // The server's host name
  private String _host;
  // The server's port
  private int _port;

  private String _virtualHost;

  // the method
  private String _method;
  // true for a HEAD stream
  private boolean _isHead;
  // true for a POST stream
  private boolean _isPost;
  
  // true for an HTTP 1.1 client
  private boolean _isHttp11 = true;

  // buffer containing the POST data
  private MemoryStream _tempStream;

  // true if keepalive is allowed
  private boolean _isKeepalive = _isKeepaliveAllowed;
  // true after the request has been sent
  private boolean _didGet;
  // content length from the returned response
  private int _contentLength;
  // true if the response was chunked
  private boolean _isChunked;
  // length of the current chunk, -1 on eof
  private int _chunkLength;
  // the request is done
  private boolean _isRequestDone;

  private HashMap<String,Object> _attributes;

  // Used to read unread bytes on recycle.
  private byte []_tempBuffer;

  /**
   * Create a new HTTP stream.
   */
  private HttpStream(Path path, String host, int port, Socket s)
    throws IOException
  {
    _s = s;

    _host = host;
    _port = port;
    
    _is = _s.getInputStream();
    _os = _s.getOutputStream();

    _ws = VfsStream.openWrite(_os);
    _rs = VfsStream.openRead(_is, _ws);

    _attributes = new HashMap<String,Object>();

    init(path);
  }

  /**
   * Opens a new HTTP stream for reading, i.e. a GET request.
   *
   * @param path the URL for the stream
   *
   * @return the opened stream
   */
  static HttpStreamWrapper openRead(HttpPath path) throws IOException
  {
    HttpStream stream = createStream(path);
    stream._isPost = false;

    return new HttpStreamWrapper(stream);
  }

  public static void setAllowKeepalive(boolean isAllowKeepalive)
  {
    _isKeepaliveAllowed = isAllowKeepalive;
  }
  
  /**
   * Opens a new HTTP stream for reading and writing, i.e. a POST request.
   *
   * @param path the URL for the stream
   *
   * @return the opened stream
   */
  static HttpStreamWrapper openReadWrite(HttpPath path) throws IOException
  {
    HttpStream stream = createStream(path);
    stream._isPost = true;

    return new HttpStreamWrapper(stream);
  }

  /**
   * Creates a new HTTP stream.  If there is a saved connection to
   * the same host, use it.
   *
   * @param path the URL for the stream
   *
   * @return the opened stream
   */
  static private HttpStream createStream(HttpPath path) throws IOException
  {
    String host = path.getHost();
    int port = path.getPort();

    HttpStream stream = null;
    long streamTime = 0;
    synchronized (LOCK) {
      if (_savedStream != null
          && host.equals(_savedStream.getHost())
          && port == _savedStream.getPort()) {
        stream = _savedStream;
        streamTime = _saveTime;
        _savedStream = null;
      }
    }

    if (stream != null) {
      long now;
      
      now = Alarm.getCurrentTime();
      
      if (now < streamTime + 5000) {
        // if the stream is still valid, use it
        stream.init(path);
        return stream;
      }
      else {
        // if the stream has timed out, close it
        try {
          stream._isKeepalive = false;
          stream.close();
        } catch (IOException e) {
          log.log(Level.FINE, e.toString(), e);
        }
      }
    }

    Socket s;

    try {
      s = new Socket(host, port);
      
      if (path instanceof HttpsPath) {
        SSLContext context = SSLContext.getInstance("TLS");

        javax.net.ssl.TrustManager tm =
          new javax.net.ssl.X509TrustManager() {
            public java.security.cert.X509Certificate[]
              getAcceptedIssuers() {
              return null;
            }
            public void checkClientTrusted(
                                           java.security.cert.X509Certificate[] cert, String foo) {
            }
            public void checkServerTrusted(
                                           java.security.cert.X509Certificate[] cert, String foo) {
            }
          };

      
        context.init(null, new javax.net.ssl.TrustManager[] { tm }, null);
        SSLSocketFactory factory = context.getSocketFactory();

        s = factory.createSocket(s, host, port, true);
      }
    } catch (ConnectException e) {
      throw new ConnectException(path.getURL() + ": " + e.getMessage());
    } catch (Exception e) {
      throw new ConnectException(path.getURL() + ": " + e.toString());
    }

    int socketTimeout = 300 * 1000;

    try {
      s.setSoTimeout(socketTimeout);
    } catch (Exception e) {
    }
          
    return new HttpStream(path, host, port, s);
  }

  /**
   * Initializes the stream for the next request.
   */
  private void init(Path path)
  {
    _contentLength = -1;
    _isChunked = false;
    _isRequestDone = false;
    _didGet = false;
    _isPost = false;
    _isHead = false;
    _method = null;
    _attributes.clear();
    
    setPath(path);

    if (path instanceof HttpPath)
      _virtualHost = ((HttpPath) path).getVirtualHost();
  }

  /**
   * Set if this should be an SSL connection.
   */
  public void setSSL(boolean isSSL)
  {
    _isSSL = isSSL;
  }

  /**
   * Set if this should be an SSL connection.
   */
  public boolean isSSL()
  {
    return _isSSL;
  }

  /**
   * Sets the method
   */
  public void setMethod(String method)
  {
    _method = method;
  }

  /**
   * Sets true if we're only interested in the head.
   */
  public void setHead(boolean isHead)
  {
    _isHead = isHead;
  }

  /**
   * Returns the stream's host.
   */
  public String getHost()
  {
    return _host;
  }

  /**
   * Returns the stream's port.
   */
  public int getPort()
  {
    return _port;
  }
  
  /**
   * Sets the http version.
   */
  public void setHttp10()
  {
    _isHttp11 = false;
  }
  
  /**
   * Sets the http version.
   */
  public void setHttp11()
  {
    _isHttp11 = true;
  }
  
  /**
   * Returns a header from the response returned from the HTTP server.
   *
   * @param name name of the header
   * @return the header value.
   */
  public Object getAttribute(String name)
    throws IOException
  {
    if (! _didGet)
      getConnInput();
    
    return _attributes.get(name.toLowerCase(Locale.ENGLISH));
  }

  /**
   * Returns an iterator of the returned header names.
   */
  public Iterator getAttributeNames()
    throws IOException
  {
    if (! _didGet)
      getConnInput();

    return _attributes.keySet().iterator();
  }

  /**
   * Sets a header for the request.
   */
  public void setAttribute(String name, Object value)
  {
    if (name.equals("method"))
      setMethod((String) value);
    else if (name.equals("socket-timeout")) {
      if (value instanceof Integer) {
        int socketTimeout = ((Integer) value).intValue();

        if (socketTimeout > 0) {
          try {
            if (_s != null)
              _s.setSoTimeout(socketTimeout);
          } catch (Exception e) {

          }
        }
      }
    }
    else {
      Object oldValue = _attributes.put(name.toLowerCase(Locale.ENGLISH), value);

      if (oldValue instanceof String[]) {
        String []old = (String []) oldValue;
        String []newValue = new String[old.length + 1];
        System.arraycopy(old, 0, newValue, 0, old.length);
        newValue[old.length] = String.valueOf(value);
        _attributes.put(name.toLowerCase(Locale.ENGLISH), newValue);
      }
      else if (oldValue != null) {
        String []newValue = new String[] { String.valueOf(oldValue),
                                           String.valueOf(value) };
        _attributes.put(name.toLowerCase(Locale.ENGLISH), newValue);
      }
    }
  }

  /**
   * Remove a header for the request.
   */
  public void removeAttribute(String name)
  {
    _attributes.remove(name.toLowerCase(Locale.ENGLISH));
  }

  /**
   * Sets the timeout.
   */
  public void setSocketTimeout(long timeout)
    throws SocketException
  {
    if (_s != null)
      _s.setSoTimeout((int) timeout);
  }

  /**
   * The stream is always writable (?)
   */
  public boolean canWrite()
  {
    return true;
  }

  /**
   * Writes a buffer to the underlying stream.
   *
   * @param buffer the byte array to write.
   * @param offset the offset into the byte array.
   * @param length the number of bytes to write.
   * @param isEnd true when the write is flushing a close.
   */
  public void write(byte []buf, int offset, int length, boolean isEnd)
    throws IOException
  {
    if (! _isPost)
      return;

    if (_tempStream == null)
      _tempStream = new MemoryStream();

    _tempStream.write(buf, offset, length, isEnd);
  }

  /**
   * The stream is readable.
   */
  public boolean canRead()
  {
    return true;
  }

  /**
   * Read data from the connection.  If the request hasn't yet been sent
   * to the server, send it.
   */
  public int read(byte []buf, int offset, int length) throws IOException
  {
    try {
      return readInt(buf, offset, length);
    } catch (IOException e) {
      _isKeepalive = false;
      throw e;
    } catch (RuntimeException e) {
      _isKeepalive = false;
      throw e;
    }
  }
  
  /**
   * Read data from the connection.  If the request hasn't yet been sent
   * to the server, send it.
   */
  public int readInt(byte []buf, int offset, int length) throws IOException
  {
    if (! _didGet)
      getConnInput();

    if (_isRequestDone)
      return -1;

    try {
      int len = length;

      if (_isChunked) {
        if (_chunkLength == 0) {
          int ch;

          for (ch = _rs.read();
               ch >= 0 && (ch == '\r' || ch == '\n' || ch == ' ');
               ch = _rs.read()) {
          }
        
          for (; ch >= 0 && ch != '\n'; ch = _rs.read()) {
            if (ch >= '0' && ch <= '9')
              _chunkLength = 16 * _chunkLength + ch - '0';
            else if (ch >= 'a' && ch <= 'f')
              _chunkLength = 16 * _chunkLength + ch - 'a' + 10;
            else if (ch >= 'A' && ch <= 'F')
              _chunkLength = 16 * _chunkLength + ch - 'A' + 10;
          }

          if (_chunkLength == 0) {
            _isRequestDone = true;
            return -1;
          }
        }
        else if (_chunkLength < 0)
          return -1;
      
        if (_chunkLength < len)
          len = _chunkLength;
      }
      else if (_contentLength < 0) {
      }
      else if (_contentLength == 0) {
        _isRequestDone = true;
        return -1;
      }
      else if (_contentLength < len)
        len = _contentLength;

      len = _rs.read(buf, offset, len);

      if (len < 0) {
      }
      else if (_isChunked)
        _chunkLength -= len;
      else if (_contentLength > 0)
        _contentLength -= len;

      return len;
    } catch (IOException e) {
      _isKeepalive = false;
      throw e;
    } catch (RuntimeException e) {
      _isKeepalive = false;
      throw e;
    }
  }

  /**
   * Sends the request and initializes the response.
   */
  private void getConnInput() throws IOException
  {
    if (_didGet)
      return;

    try {
      getConnInputImpl();
    } catch (IOException e) {
      _isKeepalive = false;
      throw e;
    } catch (RuntimeException e) {
      _isKeepalive = false;
      throw e;
    }
  }

  /**
   * Send the request to the server, wait for the response and parse
   * the headers.
   */
  private void getConnInputImpl() throws IOException
  {
    if (_didGet)
      return;

    _didGet = true;
    
    if (log.isLoggable(Level.FINER))
      log.finer(this + " connect " + _method + " post=" + _isPost);

    if (_method != null) {
      _ws.print(_method);
      _ws.print(' ');
    }
    else if (_isPost)
      _ws.print("POST ");
    else if (_isHead)
      _ws.print("HEAD ");
    else
      _ws.print("GET ");

    // Not splitting query? Also fullpath?
    _ws.print(_path.getPath());

    if (_path.getQuery() != null) {
      _ws.print("?");
      _ws.print(_path.getQuery());
    }
    
    if (_isHttp11) {
      _ws.print(" HTTP/1.1\r\n");
      Object host = getAttribute("host");
      _ws.print("Host: ");
      if (host != null) {
        _ws.print(host);
      }
      else if (_virtualHost != null)
        _ws.print(_virtualHost);
      else {
        _ws.print(_path.getHost());
        if (_path.getPort() != 80) {
      _ws.print(":");
      _ws.print(String.valueOf(_path.getPort()));
        }
      }
    }
    else
      _ws.print(" HTTP/1.0\r\n");
    
    _ws.print("\r\n");
    
    Object userAgent = getAttribute("User-Agent");
    if (userAgent == null)
      _ws.print("User-Agent: Mozilla/4.0 (compatible; Resin 1.0; JDK)\r\n");
    else
      _ws.print("User-Agent: " + userAgent + "\r\n");
    Iterator iter = getAttributeNames();
    while (iter.hasNext()) {
      String name = (String) iter.next();
      if (_reserved.get(name.toLowerCase(Locale.ENGLISH)) == null) {
        Object value = getAttribute(name);
        if (value instanceof String[]) {
          String []values = (String []) value;
          for (int i = 0; i < values.length; i++) {
            _ws.print(name + ": " + values[i] + "\r\n");
          }
        }
        else
          _ws.print(name + ": " + value + "\r\n");
      }
    }
    if (! _isKeepalive)
      _ws.print("Connection: close\r\n");
    
    if (_isPost) {
      int writeLength = 0;
      if (_tempStream != null)
        writeLength = _tempStream.getLength();
      
      Object contentLength = getAttribute("Content-Length");

      if (contentLength != null) {
        long len = 0;
        
        if (contentLength instanceof Number)
          len = ((Number) contentLength).longValue();
        else {
          String lenStr = contentLength.toString().trim();
          
          for (int i = 0; i < lenStr.length(); i++) {
            char ch = lenStr.charAt(i);
            
            if ('0' <= ch && ch <= '9') 
              len = len * 10 + ch - '0';
            else
              break;
          }
        }
        
        // server/1963
        if (len != writeLength) {
          throw new IOException(L.l("Content-Length={0} but only received {1}",
                                    len, "" + writeLength));
        }
      }

      _ws.print("Content-Length: " + writeLength);
      _ws.print("\r\n");
    }
    _ws.print("\r\n");

    if (_isPost) {
      MemoryStream tempStream = _tempStream;
      _tempStream = null;
      if (tempStream != null) {
        tempStream.writeToStream(_ws);
        tempStream.destroy();
      }
    }

    _attributes.clear();

    parseHeaders();

    if (_isHead)
      _isRequestDone = true;
  }

  /**
   * Parse the headers returned from the server.
   */
  private void parseHeaders() throws IOException
  {
    CharBuffer line = new CharBuffer();

    // Skip blank lines
    int count = 0;
    do {
      line.clear();
      if (! _rs.readln(line)) {
        _isKeepalive = false;
        return;
      }
    } while (line.length() == 0 && ++count < 10);

    if (line.length() == 0) {
      _isKeepalive = false;
      return;
    }

    if (line.startsWith("HTTP/1.1 100")) {
      count = 100;
      do {
        line.clear();
        if (! _rs.readln(line)) {
          _isKeepalive = false;
          return;
        }
      } while (line.length() != 0 && count-- > 0);
      
      count = 100;
      do {
        line.clear();
        if (! _rs.readln(line)) {
          _isKeepalive = false;
          return;
        }
      } while (line.length() == 0 && count-- > 0);
    }

    if (line.length() == 0) {
      _isKeepalive = false;
      return;
    }

    int i = 0;
    for (i = 0; i < line.length() && line.charAt(i) != ' '; i++) {
    }
    
    for (; i < line.length() && line.charAt(i) == ' '; i++) {
    }
    
    int status = 0;
    for (; i < line.length(); i++) {
      char ch = line.charAt(i);
      if (ch >= '0' && ch <= '9')
        status = 10 * status + ch - '0';
      else
        break;
    }

    if (status != 200)
      _isKeepalive = false;
    else if (! line.startsWith("HTTP/1.1 "))
      _isKeepalive = false;

    _attributes.put("status", String.valueOf(status));
    _attributes.put("status-message", line.toString());

    CharBuffer key = new CharBuffer();
    while (true) {
      line.clear();
      if (! _rs.readln(line) || line.length() == 0)
          break;

      int lineLength = line.length();
      
      for (i = 0;
           i < lineLength && Character.isWhitespace(line.charAt(i));
           i++) {
      }

      key.clear();
      for (;
           i < lineLength && ! Character.isWhitespace(line.charAt(i))
             && line.charAt(i) != ':';
           i++) {
        key.append((char) line.charAt(i));
      }

      for (;
           i < lineLength && Character.isWhitespace(line.charAt(i));
           i++) {
      }

      if (key.length() == 0 || lineLength <= i || line.charAt(i) != ':')
        continue;

      for (i++;
           i < lineLength && Character.isWhitespace(line.charAt(i));
           i++) {
      }

      key.toLowerCase();
      String value = line.substring(i);

      if (log.isLoggable(Level.FINE))
        log.fine(key + ": " + value);
      
      if (key.matchesIgnoreCase("content-length")) {
        _contentLength = Integer.parseInt(value.trim());
      }
      else if (key.matchesIgnoreCase("connection")
               && value.equalsIgnoreCase("close")) {
        _isKeepalive = false;
      }
      else if (key.matchesIgnoreCase("transfer-encoding")
               && value.equalsIgnoreCase("chunked")) {
        
        _isChunked = true;
        _chunkLength = 0;
      }

      String keyString = key.toLowerCase().toString();
      
      String oldValue = (String) _attributes.put(keyString, value);

      if (oldValue != null) {
        value = oldValue + '\n' + value;
        _attributes.put(keyString, value);
      }
    }
  }

  /**
   * Returns the bytes still available.
   */
  public int getAvailable() throws IOException
  {
    if (! _didGet)
      getConnInput();

    // php/164q
    if (_isRequestDone)
      return 0;
    else if (_contentLength > 0)
      return _contentLength;
    else
      return _rs.getAvailable();
  }

  /**
   * Close the connection.
   */
  public void close() throws IOException
  {
    if (_isKeepalive) {
      // If recycling, read any unread data
      if (! _didGet)
        getConnInput();

      if (! _isRequestDone) {
        if (_tempBuffer == null)
          _tempBuffer = new byte[256];

        try {
          while (read(_tempBuffer, 0, _tempBuffer.length) > 0) {
          }
        } catch (IOException e) {
          _isKeepalive = false;
        }
      }
    }

    if (_isKeepalive) {
      HttpStream oldSaved;
      
      long now;
      
      now = Alarm.getCurrentTime();
      
      synchronized (LOCK) {
        oldSaved = _savedStream;
        _savedStream = this;
        _saveTime = now;
      }

      if (oldSaved != null && oldSaved != this) {
        oldSaved._isKeepalive = false;
        oldSaved.close();
      }

      return;
    }

    try {
      try {
        if (_ws != null)
          _ws.close();
      } catch (Throwable e) {
      }
      _ws = null;

      try {
        if (_rs != null)
          _rs.close();
      } catch (Throwable e) {
      }
      _rs = null;

      try {
        if (_os != null)
          _os.close();
      } catch (Throwable e) {
      }
      _os = null;

      try {
        if (_is != null)
          _is.close();
      } catch (Throwable e) {
      }
      _is = null;
    } finally {
      if (_s != null)
        _s.close();
      _s = null;
    }
  }

  @Override
  public String toString()
  {
    return getClass().getSimpleName() + "[" + _s + "]";
  }

  static {
    _reserved = new HashMap<String,String>();
    _reserved.put("user-agent", "");
    _reserved.put("content-length", "");
    //_reserved.put("content-encoding", "");
    _reserved.put("connection", "");
    //_reserved.put("host", "");
  }
}
