/* * 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.hmux; import com.caucho.util.Alarm; import com.caucho.vfs.*; 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 HmuxStream extends StreamImpl { private static final Logger log = Logger.getLogger(HmuxStream.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 HmuxStream _savedStream; // Time the stream was saved private static long _saveTime; 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; // buffer containing the POST data private MemoryStream _tempStream; // true if keepalive is allowed private boolean _isKeepalive = true; // true after the request has been sent private boolean _didGet; // 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 HmuxStream(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 HmuxStreamWrapper openRead(HmuxPath path) throws IOException { HmuxStream stream = createStream(path); stream._isPost = false; return new HmuxStreamWrapper(stream); } /** * 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 HmuxStreamWrapper openReadWrite(HmuxPath path) throws IOException { HmuxStream stream = createStream(path); stream._isPost = true; return new HmuxStreamWrapper(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 HmuxStream createStream(HmuxPath path) throws IOException { String host = path.getHost(); int port = path.getPort(); HmuxStream 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) { } // if the stream is still valid, use it else if (Alarm.getCurrentTime() < streamTime + 5000) { stream.init(path); return stream; } // if the stream has timed out, close it else { try { stream._isKeepalive = false; stream.close(); } catch (IOException e) { log.log(Level.FINE, e.toString(), e); } } Socket s; try { s = new Socket(host, port); } 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 HmuxStream(path, host, port, s); } /** * Initializes the stream for the next request. */ private void init(Path path) { _isRequestDone = false; _didGet = false; _isPost = false; _isHead = false; _method = null; _attributes.clear(); setPath(path); if (path instanceof HmuxPath) _virtualHost = ((HmuxPath) 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; } /** * 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 _attributes.put(name.toLowerCase(Locale.ENGLISH), value); } /** * 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 (_chunkLength == 0) { if (! readData()) _chunkLength = -1; } if (_chunkLength < 0) return -1; if (_chunkLength < len) len = _chunkLength; len = _rs.read(buf, offset, len); if (len < 0) { } else _chunkLength -= 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; _ws.write('C'); _ws.write(0); _ws.write(0); if (_method != null) { writeString(HmuxRequest.HMUX_METHOD, _method); } else if (_isPost) { writeString(HmuxRequest.HMUX_METHOD, "POST"); } else if (_isHead) writeString(HmuxRequest.HMUX_METHOD, "HEAD"); else writeString(HmuxRequest.HMUX_METHOD, "GET"); if (_virtualHost != null) writeString(HmuxRequest.HMUX_SERVER_NAME, _virtualHost); else { writeString(HmuxRequest.HMUX_SERVER_NAME, _path.getHost()); _ws.print(_path.getHost()); if (_path.getPort() != 80) { writeString(HmuxRequest.CSE_SERVER_PORT, String.valueOf(_path.getPort())); } } // Not splitting query? Also fullpath? writeString(HmuxRequest.HMUX_URI, _path.getPath()); if (_path.getQuery() != null) writeString(HmuxRequest.CSE_QUERY_STRING, _path.getQuery()); Iterator iter = getAttributeNames(); while (iter.hasNext()) { String name = (String) iter.next(); if (_reserved.get(name.toLowerCase(Locale.ENGLISH)) == null) { writeString(HmuxRequest.HMUX_HEADER, name); writeString(HmuxRequest.HMUX_STRING, getAttribute(name)); } } if (_isPost) { MemoryStream tempStream = _tempStream; _tempStream = null; if (tempStream != null) { TempBuffer tb = TempBuffer.allocate(); byte []buffer = tb.getBuffer(); int sublen; ReadStream postIn = tempStream.openReadAndSaveBuffer(); while ((sublen = postIn.read(buffer, 0, buffer.length)) > 0) { _ws.write('D'); _ws.write(sublen >> 8); _ws.write(sublen); _ws.write(buffer, 0, sublen); } tempStream.destroy(); TempBuffer.free(tb); tb = null; } } _attributes.clear(); _ws.write('Q'); readData(); if (_isHead) _isRequestDone = true; } private void writeString(int code, String string) throws IOException { WriteStream ws = _ws; ws.write((byte) code); int len = string.length(); ws.write(len >> 8); ws.write(len); ws.print(string); } private void writeString(int code, Object obj) throws IOException { String string = String.valueOf(obj); WriteStream ws = _ws; ws.write((byte) code); int len = string.length(); ws.write(len >> 8); ws.write(len); ws.print(string); } /** * Parse the headers returned from the server. */ private boolean readData() throws IOException { boolean isDebug = log.isLoggable(Level.FINE); int code; ReadStream is = _rs; while ((code = is.read()) > 0) { switch (code) { case HmuxRequest.HMUX_CHANNEL: is.read(); is.read(); break; case HmuxRequest.HMUX_QUIT: case HmuxRequest.HMUX_EXIT: is.close(); if (isDebug) log.fine("HMUX: " + (char) code); return false; case HmuxRequest.HMUX_YIELD: break; case HmuxRequest.HMUX_STATUS: String value = readString(is); _attributes.put("status", value.substring(0, 3)); if (isDebug) log.fine("HMUX: " + (char) code + " " + value); break; case HmuxRequest.HMUX_DATA: _chunkLength = 256 * (is.read() & 0xff) + (is.read() & 0xff); if (isDebug) log.fine("HMUX: " + (char) code + " " + _chunkLength); return true; default: int len = 256 * (is.read() & 0xff) + (is.read() & 0xff); if (isDebug) log.fine("HMUX: " + (char) code + " " + len); is.skip(len); break; } } return false; } private String readString(ReadStream is) throws IOException { int len = 256 * (is.read() & 0xff) + is.read(); char []buf = new char[len]; is.readAll(buf, 0, len); return new String(buf); } /** * Returns the bytes still available. */ public int getAvailable() throws IOException { if (! _didGet) getConnInput(); 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 (com.caucho.server.util.CauchoSystem.isTesting()) _isKeepalive = false; // XXX: if (_isKeepalive) { HmuxStream oldSaved; long 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; } } static { _reserved = new HashMap<String,String>(); _reserved.put("user-agent", ""); _reserved.put("content-length", ""); _reserved.put("content-encoding", ""); _reserved.put("connection", ""); _reserved.put("host", ""); } }