// ======================================================================== // $Id: HttpConnection.java,v 1.94 2006/02/28 12:45:01 gregwilkins Exp $ // Copyright 199-2004 Mort Bay Consulting Pty. Ltd. // ------------------------------------------------------------------------ // 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. // ======================================================================== package org.browsermob.proxy.jetty.http; import org.apache.commons.logging.Log; import org.browsermob.proxy.jetty.log.LogFactory; import org.browsermob.proxy.jetty.util.*; import javax.net.ssl.SSLSocket; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetAddress; import java.net.Socket; import java.util.Enumeration; import java.util.List; /* ------------------------------------------------------------ */ /** A HTTP Connection. * This class provides the generic HTTP handling for * a connection to a HTTP server. An instance of HttpConnection * is normally created by a HttpListener and then given control * in order to run the protocol handling before and after passing * a request to the HttpServer of the HttpListener. * * This class is not synchronized as it should only ever be known * to a single thread. * * @see HttpListener * @see HttpServer * @version $Id: HttpConnection.java,v 1.94 2006/02/28 12:45:01 gregwilkins Exp $ * @author Greg Wilkins (gregw) */ public class HttpConnection implements OutputObserver { private static Log log = LogFactory.getLog(HttpConnection.class); /* ------------------------------------------------------------ */ private static ThreadLocal __threadConnection=new ThreadLocal(); /** Support for FRC2068 Continues. * If true, then 100 Continues will be sent when expected or for POST requests. If false, 100 Continues will * only be sent if expected. Can be configured with the org.mortbay.http.HttpConnection.2068Continue system * property. */ private static boolean __2068_Continues=Boolean.getBoolean("org.browsermob.proxy.jetty.http.HttpConnection.2068Continue"); /* ------------------------------------------------------------ */ protected HttpRequest _request; protected HttpResponse _response; protected boolean _persistent; protected boolean _keepAlive; protected int _dotVersion; private HttpListener _listener; private HttpInputStream _inputStream; private HttpOutputStream _outputStream; private boolean _close; private boolean _firstWrite; private boolean _completing; private Thread _handlingThread; private InetAddress _remoteInetAddress; private String _remoteAddr; private String _remoteHost; private HttpServer _httpServer; private Object _connection; private boolean _throttled ; private boolean _statsOn; private long _tmpTime; private long _openTime; private long _reqTime; private int _requests; private Object _object; private HttpTunnel _tunnel ; private boolean _resolveRemoteHost; /* ------------------------------------------------------------ */ /** Constructor. * @param listener The listener that created this connection. * @param remoteAddr The address of the remote end or null. * @param in InputStream to read request(s) from. * @param out OutputputStream to write response(s) to. * @param connection The underlying connection object, most likely * a socket. This is not used by HttpConnection other than to make * it available via getConnection(). */ public HttpConnection(HttpListener listener, InetAddress remoteAddr, InputStream in, OutputStream out, Object connection) { if(log.isDebugEnabled())log.debug("new HttpConnection: "+connection); _listener=listener; _remoteInetAddress=remoteAddr; int bufferSize=listener==null?4096:listener.getBufferSize(); int reserveSize=listener==null?512:listener.getBufferReserve(); _inputStream=new HttpInputStream(in,bufferSize); _outputStream=new HttpOutputStream(out,bufferSize,reserveSize); _outputStream.addObserver(this); _firstWrite=false; if (_listener!=null) _httpServer=_listener.getHttpServer(); _connection=connection; _statsOn=_httpServer!=null && _httpServer.getStatsOn(); if (_statsOn) { _openTime=System.currentTimeMillis(); _httpServer.statsOpenConnection(); } _reqTime=0; _requests=0; _request = new HttpRequest(this); _response = new HttpResponse(this); _resolveRemoteHost=_listener!=null && _listener.getHttpServer()!=null && _listener.getHttpServer().getResolveRemoteHost(); } /* ------------------------------------------------------------ */ /** Get the ThreadLocal HttpConnection. * The ThreadLocal HttpConnection is set by the handle() method. * @return HttpConnection for this thread. */ static HttpConnection getHttpConnection() { return (HttpConnection)__threadConnection.get(); } /* ------------------------------------------------------------ */ /** Get the Remote address. * @return the remote address */ public InetAddress getRemoteInetAddress() { return _remoteInetAddress; } /* ------------------------------------------------------------ */ /** Get the Remote address. * @return the remote host name */ public String getRemoteAddr() { if (_remoteAddr==null) { if (_remoteInetAddress==null) return "127.0.0.1"; _remoteAddr=_remoteInetAddress.getHostAddress(); } return _remoteAddr; } /* ------------------------------------------------------------ */ /** Get the Remote address. * @return the remote host name */ public String getRemoteHost() { if (_remoteHost==null) { if (_resolveRemoteHost) { if (_remoteInetAddress==null) return "localhost"; _remoteHost=_remoteInetAddress.getHostName(); } else { if (_remoteInetAddress==null) return "127.0.0.1"; _remoteHost=getRemoteAddr(); } } return _remoteHost; } /* ------------------------------------------------------------ */ /** Get the connections InputStream. * @return the connections InputStream */ public HttpInputStream getInputStream() { return _inputStream; } /* ------------------------------------------------------------ */ /** Get the connections OutputStream. * @return the connections OutputStream */ public HttpOutputStream getOutputStream() { return _outputStream; } /* ------------------------------------------------------------ */ /** Get the underlying connection object. * This opaque object, most likely a socket. This is not used by * HttpConnection other than to make it available via getConnection(). * @return Connection abject */ public Object getConnection() { return _connection; } /* ------------------------------------------------------------ */ /** Get the request. * @return the request */ public HttpRequest getRequest() { return _request; } /* ------------------------------------------------------------ */ /** Get the response. * @return the response */ public HttpResponse getResponse() { return _response; } /* ------------------------------------------------------------ */ /** Force the connection to not be persistent. */ public void forceClose() { _persistent=false; _close=true; } /* ------------------------------------------------------------ */ /** Close the connection. * This method calls close on the input and output streams and * interrupts any thread in the handle method. * may be specialized to close sockets etc. * @exception IOException */ public void close() throws IOException { try{ _completing=true; if (_connection instanceof Socket && !(_connection instanceof SSLSocket)) ((Socket)_connection).shutdownOutput(); _outputStream.close(); _inputStream.close(); } finally { if (_handlingThread!=null && Thread.currentThread()!=_handlingThread) _handlingThread.interrupt(); } } /* ------------------------------------------------------------ */ /** Get the connections listener. * @return HttpListener that created this Connection. */ public HttpListener getListener() { return _listener; } /* ------------------------------------------------------------ */ /** Get the listeners HttpServer . * Conveniance method equivalent to getListener().getHttpServer(). * @return HttpServer. */ public HttpServer getHttpServer() { return _httpServer; } /* ------------------------------------------------------------ */ /** Get the listeners Default scheme. * Conveniance method equivalent to getListener().getDefaultProtocol(). * @return HttpServer. */ public String getDefaultScheme() { return _listener.getDefaultScheme(); } /* ------------------------------------------------------------ */ /** Get the listeners HttpServer. * But if the name is 0.0.0.0, then the real interface address is used. * @return HttpServer. */ public String getServerName() { String host=_listener.getHost(); if (InetAddrPort.__0_0_0_0.equals(host) && _connection instanceof Socket) host = ((Socket)_connection).getLocalAddress().getHostName(); return host; } /* ------------------------------------------------------------ */ /** Get the listeners HttpServer. * @return HttpServer. */ public String getServerAddr() { if (_connection instanceof Socket) return ((Socket)_connection).getLocalAddress().getHostAddress(); return _listener.getHost(); } /* ------------------------------------------------------------ */ /** Get the listeners Port . * Conveniance method equivalent to getListener().getPort(). * @return local port. */ public int getServerPort() { return _listener.getPort(); } /* ------------------------------------------------------------ */ /** Get the remote Port . * @return remote port. */ public int getRemotePort() { if (_connection instanceof Socket) return ((Socket)_connection).getPort(); return 0; } /* ------------------------------------------------------------ */ /** * @return True if this connections state has been altered due * to low resources. */ public boolean isThrottled() { return _throttled; } /* ------------------------------------------------------------ */ /** * @param throttled True if this connections state has been altered due * to low resources. */ public void setThrottled(boolean throttled) { _throttled = throttled; } /* ------------------------------------------------------------ */ /** Get associated object. * Used by a particular HttpListener implementation to associate * private datastructures with the connection. * @return An object associated with the connecton by setObject. */ public Object getObject() { return _object; } /* ------------------------------------------------------------ */ /** Set associated object. * Used by a particular HttpListener implementation to associate * private datastructures with the connection. * @param o An object associated with the connecton. */ public void setObject(Object o) { _object=o; } /* ------------------------------------------------------------ */ /** * @return The HttpTunnel set for the connection or null. */ public HttpTunnel getHttpTunnel() { return _tunnel; } /* ------------------------------------------------------------ */ /** Set a HttpTunnel for the connection. * A HTTP tunnel is used if the connection is to be taken over for * non-HTTP communications. An example of this is the CONNECT method * in proxy handling. If a HttpTunnel is set on a connection, then it's * handle method is called bu the next call to handleNext(). * @param tunnel The HttpTunnel set for the connection or null. */ public void setHttpTunnel(HttpTunnel tunnel) { _tunnel = tunnel; } /* ------------------------------------------------------------ */ /* Verify HTTP/1.0 request * @exception HttpException problem with the request. * @exception IOException problem with the connection. */ private void verifyHTTP_1_0() { // Set content length int content_length= _request.getIntField(HttpFields.__ContentLength); if (content_length>=0) _inputStream.setContentLength(content_length); else if (content_length<0) { // TODO - can't do this check because IE does this after // a redirect. // Can't have content without a content length // String content_type=_request.getField(HttpFields.__ContentType); // if (content_type!=null && content_type.length()>0) // throw new HttpException(_HttpResponse.__411_Length_Required); _inputStream.setContentLength(0); } // Check netscape proxy connection - this is not strictly correct. if (!_keepAlive && HttpFields.__KeepAlive.equalsIgnoreCase(_request.getField(HttpFields.__ProxyConnection))) _keepAlive=true; // persistent connections in HTTP/1.0 only if requested. _persistent=_keepAlive; } /* ------------------------------------------------------------ */ /* Verify HTTP/1.1 request * @exception HttpException problem with the request. * @exception IOException problem with the connection. */ private void verifyHTTP_1_1() throws HttpException, IOException { // Check Host Field exists String host=_request.getField(HttpFields.__Host); if (host==null) throw new HttpException(HttpResponse.__400_Bad_Request); // check and enable requests transfer encodings. String transfer_coding= _request.getField(HttpFields.__TransferEncoding); if (transfer_coding!=null && transfer_coding.length()>0) { // Handling of codings other than chunking is now // the responsibility of handlers, filters or servlets. // Thanks to the compression filter, we now don't know if // what we can handle here. if (transfer_coding.equalsIgnoreCase(HttpFields.__Chunked) || StringUtil.endsWithIgnoreCase(transfer_coding,HttpFields.__Chunked)) _inputStream.setChunking(); else if (StringUtil.asciiToLowerCase(transfer_coding) .indexOf(HttpFields.__Chunked)>=0) throw new HttpException(HttpResponse.__400_Bad_Request); } // Check input content length can be determined int content_length=_request.getIntField(HttpFields.__ContentLength); String content_type=_request.getField(HttpFields.__ContentType); if (!_inputStream.isChunking()) { // If we have a content length, use it if (content_length>=0) _inputStream.setContentLength(content_length); // else if we have no content else if (content_type==null || content_type.length()==0) _inputStream.setContentLength(0); // else we need a content length else { // TODO - can't do this check as IE stuff up on // a redirect. // throw new HttpException(HttpResponse.__411_Length_Required); _inputStream.setContentLength(0); } } // Handle Continue Expectations String expect=_request.getField(HttpFields.__Expect); if (expect!=null && expect.length()>0) { if (StringUtil.asciiToLowerCase(expect).equals(HttpFields.__ExpectContinue)) { _inputStream.setExpectContinues(_outputStream.getOutputStream()); } else throw new HttpException(HttpResponse.__417_Expectation_Failed); } else if (__2068_Continues && _inputStream.available()<=0 && (HttpRequest.__PUT.equals(_request.getMethod()) || HttpRequest.__POST.equals(_request.getMethod()))) { // Send continue for RFC 2068 exception OutputStream real_out=_outputStream.getOutputStream(); real_out.write(HttpResponse.__Continue); real_out.flush(); } // Persistent unless requested otherwise _persistent=!_close; } /* ------------------------------------------------------------ */ /** Output Notifications. * Trigger header and/or filters from output stream observations. * Also finalizes method of indicating response content length. * Called as a result of the connection subscribing for notifications * to the HttpOutputStream. * @see HttpOutputStream * @param out The output stream observed. * @param action The action. */ public void outputNotify(OutputStream out, int action, Object ignoredData) throws IOException { if (_response==null) return; switch(action) { case OutputObserver.__FIRST_WRITE: if (!_firstWrite) { firstWrite(); _firstWrite=true; } break; case OutputObserver.__RESET_BUFFER: resetBuffer(); break; case OutputObserver.__COMMITING: commit(); break; case OutputObserver.__CLOSING: if (_response!=null) { completing(); if (!_response.isCommitted() && _request.getState()==HttpMessage.__MSG_RECEIVED) commit(); } break; case OutputObserver.__CLOSED: break; } } /* ------------------------------------------------------------ */ /** Setup the reponse output stream. * Use the current state of the request and response, to set tranfer * parameters such as chunking and content length. */ protected void firstWrite() throws IOException { if (_response.isCommitted()) return; // Nobble the OutputStream for HEAD requests if (HttpRequest.__HEAD.equals(_request.getMethod())) _outputStream.nullOutput(); int length=_response.getIntField(HttpFields.__ContentLength); if (length>=0) _outputStream.setContentLength(length); } /** * Reset headers after response reset */ private void resetBuffer() { } /* ------------------------------------------------------------ */ /* Signal that the next commit/flush is the last */ void completing() { _completing=true; } /* ------------------------------------------------------------ */ protected void commit() throws IOException { if (_response.isCommitted()) return; int status=_response.getStatus(); int length=-1; // Check if there is missing content expectations if (_inputStream.getExpectContinues()!=null) { // No input read yet - so assume it never will be _inputStream.setExpectContinues(null); _inputStream.unsafeSetContentLength(0); } // Handler forced close, listener stopped or no idle threads left. boolean has_close=HttpFields.__Close.equals(_response.getField(HttpFields.__Connection)); if (!_persistent || _close || _listener!=null && (!_listener.isStarted()||_listener.isOutOfResources())) { _close=true; if (!has_close) _response.setField(HttpFields.__Connection,HttpFields.__Close); has_close=true; } if (_close) _persistent=false; // Determine how to limit content length if (_persistent) { switch(_dotVersion) { case 1: { String transfer_coding=_response.getField(HttpFields.__TransferEncoding); if (transfer_coding==null || transfer_coding.length()==0 || HttpFields.__Identity.equalsIgnoreCase(transfer_coding)) { // if (can have content and no content length) if (status!=HttpResponse.__304_Not_Modified && status!=HttpResponse.__204_No_Content && _response.getField(HttpFields.__ContentLength)==null) { if (_completing) { length=_outputStream.getBytesWritten(); _response.setContentLength(length); } else { // Chunk it! _response.setField(HttpFields.__TransferEncoding,HttpFields.__Chunked); _outputStream.setChunking(); } } } else { // Use transfer encodings to determine length _response.removeField(HttpFields.__ContentLength); _outputStream.setChunking(); if (!HttpFields.__Chunked.equalsIgnoreCase(transfer_coding)) { // Check against any TE field List te = _request.getAcceptableTransferCodings(); Enumeration enm = _response.getFieldValues(HttpFields.__TransferEncoding, HttpFields.__separators); while (enm.hasMoreElements()) { String coding=(String)enm.nextElement(); if (HttpFields.__Identity.equalsIgnoreCase(coding) || HttpFields.__Chunked.equalsIgnoreCase(coding)) continue; if (te==null || !te.contains(coding)) throw new HttpException(HttpResponse.__501_Not_Implemented,coding); } } } } break; case 0: { // if (can have content and no content length) _response.removeField(HttpFields.__TransferEncoding); if (_keepAlive) { if (status!=HttpResponse.__304_Not_Modified && status!=HttpResponse.__204_No_Content && _response.getField(HttpFields.__ContentLength)==null) { if (_completing) { length=_outputStream.getBytesWritten(); _response.setContentLength(length); _response.setField(HttpFields.__Connection,HttpFields.__KeepAlive); } else { _response.setField(HttpFields.__Connection,HttpFields.__Close); has_close=_close=true; _persistent=false; } } else _response.setField(HttpFields.__Connection,HttpFields.__KeepAlive); } else if (!has_close) _response.setField(HttpFields.__Connection,HttpFields.__Close); break; } default: { _close=true; _persistent=false; _keepAlive=false; } } } // Mark request as handled. _request.setHandled(true); _outputStream.writeHeader(_response); _outputStream.flush(); } /* ------------------------------------------------------------ */ /* Exception reporting policy method. * @param e the Throwable to report. */ private void exception(Throwable e) { try { _persistent=false; int error_code=HttpResponse.__500_Internal_Server_Error; if (e instanceof HttpException) { error_code=((HttpException)e).getCode(); if (_request==null) log.warn(e.toString()); else log.warn(_request.getRequestLine()+" "+e.toString()); log.debug(LogSupport.EXCEPTION,e); } else if (e instanceof EOFException) { LogSupport.ignore(log,e); return; } else { _request.setAttribute("javax.servlet.error.exception_type",e.getClass()); _request.setAttribute("javax.servlet.error.exception",e); if (_request==null) log.warn(LogSupport.EXCEPTION,e); else log.warn(_request.getRequestLine(),e); } if (_response != null && !_response.isCommitted()) { _response.reset(); _response.removeField(HttpFields.__TransferEncoding); _response.setField(HttpFields.__Connection,HttpFields.__Close); _response.sendError(error_code); } } catch(Exception ex) { LogSupport.ignore(log,ex); } } /* ------------------------------------------------------------ */ /** Service a Request. * This implementation passes the request and response to the * service method of the HttpServer for this connections listener. * If no HttpServer has been associated, the 503 is returned. * This method may be specialized to implement other ways of * servicing a request. * @param request The request * @param response The response * @return The HttpContext that completed handling of the request or null. * @exception HttpException * @exception IOException */ protected HttpContext service(HttpRequest request, HttpResponse response) throws HttpException, IOException { if (_httpServer==null) throw new HttpException(HttpResponse.__503_Service_Unavailable); return _httpServer.service(request,response); } /* ------------------------------------------------------------ */ /** Handle the connection. * Once the connection has been created, this method is called * to handle one or more requests that may be received on the * connection. The method only returns once all requests have been * handled, an error has been returned to the requestor or the * connection has been closed. * The handleNext() is called in a loop until it returns false. */ public final void handle() { try { associateThread(); while(_listener.isStarted() && handleNext()) recycle(); } finally { disassociateThread(); destroy(); } } /* ------------------------------------------------------------ */ protected void associateThread() { __threadConnection.set(this); _handlingThread=Thread.currentThread(); } /* ------------------------------------------------------------ */ protected void disassociateThread() { _handlingThread=null; __threadConnection.set(null); } /* ------------------------------------------------------------ */ protected void readRequest() throws IOException { _request.readHeader((LineInput)(_inputStream) .getInputStream()); } /* ------------------------------------------------------------ */ /** Handle next request off the connection. * The service(request,response) method is called by handle to * service each request received on the connection. * If the thread is a PoolThread, the thread is set as inactive * when waiting for a request. * <P> * If a HttpTunnel has been set on this connection, it's handle method is * called and when that completes, false is return from this method. * <P> * The Connection is set as a ThreadLocal of the calling thread and is * available via the getHttpConnection() method. * @return true if the connection is still open and may provide * more requests. */ public boolean handleNext() { // Handle a HTTP tunnel if (_tunnel!=null) { if(log.isDebugEnabled())log.debug("Tunnel: "+_tunnel); _outputStream.resetObservers(); _tunnel.handle(_inputStream.getInputStream(), _outputStream.getOutputStream()); return false; } // Normal handling. HttpContext context=null; boolean stats=false; try { // Assume the connection is not persistent, // unless told otherwise. _persistent=false; _close=false; _keepAlive=false; _firstWrite=false; _completing=false; _dotVersion=0; // Read requests readRequest(); if (_listener==null || !_listener.isStarted()) { // dead connection _response.destroy(); _response=null; _persistent=false; return false; } _listener.customizeRequest(this,_request); if (_request.getState()!=HttpMessage.__MSG_RECEIVED) throw new HttpException(HttpResponse.__400_Bad_Request); // We have a valid request! statsRequestStart(); stats=true; // Pick response version, we assume that _request.getVersion() == 1 _dotVersion=_request.getDotVersion(); if (_dotVersion>1) { _dotVersion=1; } // Common fields on the response _response.setVersion(HttpMessage.__HTTP_1_1); _response.setField(HttpFields.__Date,_request.getTimeStampStr()); if (!Version.isParanoid()) _response.setField(HttpFields.__Server,Version.getDetail()); // Handle Connection header field Enumeration connectionValues = _request.getFieldValues(HttpFields.__Connection, HttpFields.__separators); if (connectionValues!=null) { while (connectionValues.hasMoreElements()) { String token=connectionValues.nextElement().toString(); // handle close token if (token.equalsIgnoreCase(HttpFields.__Close)) { _close=true; _response.setField(HttpFields.__Connection, HttpFields.__Close); } else if (token.equalsIgnoreCase(HttpFields.__KeepAlive) && _dotVersion==0) _keepAlive=true; // Remove headers for HTTP/1.0 requests if (_dotVersion==0) _request.forceRemoveField(token); } } // Handle version specifics if (_dotVersion==1) verifyHTTP_1_1(); else if (_dotVersion==0) verifyHTTP_1_0(); else if (_dotVersion!=-1) throw new HttpException(HttpResponse.__505_HTTP_Version_Not_Supported); if(log.isDebugEnabled())log.debug("REQUEST from "+_listener+":\n"+_request); // handle HttpListener handlers if (!_request.isHandled() && _listener.getHttpHandler()!=null) _listener.getHttpHandler().handle("",null, _request, _response); // service the request if (!_request.isHandled()) context=service(_request,_response); } catch(HttpException e) {exception(e);} catch (IOException e) { if (_request.getState()!=HttpMessage.__MSG_RECEIVED) { if (log.isDebugEnabled()) { if (log.isTraceEnabled()) log.trace(LogSupport.EXCEPTION,e); else if(log.isDebugEnabled())log.debug(e.toString()); } _response.destroy(); _response=null; } else exception(e); } catch (Exception e) {exception(e);} catch (Error e) {exception(e);} finally { int bytes_written=0; int content_length = _response==null ?-1:_response.getIntField(HttpFields.__ContentLength); // Complete the request if (_persistent) { boolean no_continue_sent=false; try{ if (_inputStream.getExpectContinues()!=null) { _inputStream.setExpectContinues(null); no_continue_sent=true; } else { int remaining = _inputStream.getContentLength(); if (remaining!=0) // Read remaining input while(_inputStream.skip(4096)>0 || _inputStream.read()>=0); } } catch(IOException e) { if (_inputStream.getContentLength()>0) _inputStream.setContentLength(0); _persistent=false; LogSupport.ignore(log,e); exception(new HttpException(HttpResponse.__400_Bad_Request, "Missing Content")); } // Check for no more content if (!no_continue_sent && _inputStream.getContentLength()>0) { _inputStream.setContentLength(0); _persistent=false; exception (new HttpException(HttpResponse.__400_Bad_Request,"Missing Content")); } // Commit the response try{ _outputStream.close(); bytes_written=_outputStream.getBytesWritten(); _outputStream.resetStream(); _outputStream.addObserver(this); _inputStream.resetStream(); } catch(IOException e) {exception(e);} } else if (_response!=null) // There was a request { // half hearted attempt to eat any remaining input try{ if (_inputStream.getContentLength()>0) while(_inputStream.skip(4096)>0 || _inputStream.read()>=0); _inputStream.resetStream(); } catch(IOException e){LogSupport.ignore(log,e);} // commit non persistent try{ _outputStream.flush(); _response.commit(); bytes_written=_outputStream.getBytesWritten(); _outputStream.close(); _outputStream.resetStream(); } catch(IOException e) {exception(e);} } // Check response length if (_response!=null) { if(log.isDebugEnabled())log.debug("RESPONSE:\n"+_response); if (_persistent && content_length>=0 && bytes_written>0 && content_length!=bytes_written) { log.warn("Invalid length: Content-Length="+content_length+ " written="+bytes_written+ " for "+_request.getRequestURL()); _persistent=false; try{_outputStream.close();} catch(IOException e) {log.warn(LogSupport.EXCEPTION,e);} } } // stats & logging if (stats) statsRequestEnd(); if (context!=null) context.log(_request,_response,bytes_written); } return (_tunnel!=null) || _persistent; } /* ------------------------------------------------------------ */ protected void statsRequestStart() { if (_statsOn) { if (_reqTime>0) statsRequestEnd(); _requests++; _tmpTime=_request.getTimeStamp(); _reqTime=_tmpTime; _httpServer.statsGotRequest(); } } /* ------------------------------------------------------------ */ protected void statsRequestEnd() { if (_statsOn && _reqTime>0) { _httpServer.statsEndRequest(System.currentTimeMillis()-_reqTime, (_response!=null)); _reqTime=0; } } /* ------------------------------------------------------------ */ /** Recycle the connection. * called by handle when handleNext returns true. */ protected void recycle() { _listener.persistConnection(this); if (_request!=null) _request.recycle(this); if (_response!=null) _response.recycle(this); } /* ------------------------------------------------------------ */ /** Destroy the connection. * called by handle when handleNext returns false. */ protected void destroy() { try{close();} catch (IOException e){LogSupport.ignore(log,e);} catch (Exception e){log.warn(LogSupport.EXCEPTION,e);} // Destroy request and response if (_request!=null) _request.destroy(); if (_response!=null) _response.destroy(); if (_inputStream!=null) _inputStream.destroy(); if (_outputStream!=null) _outputStream.destroy(); _inputStream=null; _outputStream=null; _request=null; _response=null; _handlingThread=null; if (_statsOn) { _tmpTime=System.currentTimeMillis(); if (_reqTime>0) _httpServer.statsEndRequest(_tmpTime-_reqTime,false); _httpServer.statsCloseConnection(_tmpTime-_openTime,_requests); } } }