diff options
author | Greg Wilkins | 2009-03-24 21:07:27 +0000 |
---|---|---|
committer | Greg Wilkins | 2009-03-24 21:07:27 +0000 |
commit | da627b843fe81fa0fe52a046c1be8595630e9ae7 (patch) | |
tree | 5dd3804b874cf01be38575a02b5658a02113f78f /jetty-server/src/main/java/org/eclipse/jetty/server | |
parent | bc1e0bd10201d8a14f20a81e3b93076af6408fe4 (diff) | |
download | org.eclipse.jetty.project-da627b843fe81fa0fe52a046c1be8595630e9ae7.tar.gz org.eclipse.jetty.project-da627b843fe81fa0fe52a046c1be8595630e9ae7.tar.xz org.eclipse.jetty.project-da627b843fe81fa0fe52a046c1be8595630e9ae7.zip |
jetty @ eclipse initial commit
git-svn-id: svn+ssh://dev.eclipse.org/svnroot/rt/org.eclipse.jetty/jetty/trunk@8 7e9141cc-0065-0410-87d8-b60c137991c4
Diffstat (limited to 'jetty-server/src/main/java/org/eclipse/jetty/server')
55 files changed, 21657 insertions, 0 deletions
diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java b/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java new file mode 100644 index 0000000000..1d3430ae2b --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/AbstractConnector.java @@ -0,0 +1,990 @@ +// ======================================================================== +// Copyright (c) 2004-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; + +import javax.servlet.ServletRequest; + +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpHeaders; +import org.eclipse.jetty.http.HttpSchemes; +import org.eclipse.jetty.io.AbstractBuffers; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.EofException; +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.thread.ThreadPool; + + +/** Abstract Connector implementation. + * This abstract implementation of the Connector interface provides:<ul> + * <li>AbstractLifeCycle implementation</li> + * <li>Implementations for connector getters and setters</li> + * <li>Buffer management</li> + * <li>Socket configuration</li> + * <li>Base acceptor thread</li> + * <li>Optional reverse proxy headers checking</li> + * </ul> + * + * + */ +public abstract class AbstractConnector extends AbstractBuffers implements Connector +{ + private String _name; + + private Server _server; + private ThreadPool _threadPool; + private String _host; + private int _port=0; + private String _integralScheme=HttpSchemes.HTTPS; + private int _integralPort=0; + private String _confidentialScheme=HttpSchemes.HTTPS; + private int _confidentialPort=0; + private int _acceptQueueSize=0; + private int _acceptors=1; + private int _acceptorPriorityOffset=0; + private boolean _useDNS; + private boolean _forwarded; + private String _hostHeader; + private String _forwardedHostHeader = "X-Forwarded-Host"; // default to mod_proxy_http header + private String _forwardedServerHeader = "X-Forwarded-Server"; // default to mod_proxy_http header + private String _forwardedForHeader = "X-Forwarded-For"; // default to mod_proxy_http header + private boolean _reuseAddress=true; + + protected int _maxIdleTime=200000; + protected int _lowResourceMaxIdleTime=-1; + protected int _soLingerTime=-1; + + private transient Thread[] _acceptorThread; + + Object _statsLock = new Object(); + transient long _statsStartedAt=-1; + transient int _requests; + transient int _connections; // total number of connections made to server + + transient int _connectionsOpen; // number of connections currently open + transient int _connectionsOpenMin; // min number of connections open simultaneously + transient int _connectionsOpenMax; // max number of connections open simultaneously + + transient long _connectionsDurationMin; // min duration of a connection + transient long _connectionsDurationMax; // max duration of a connection + transient long _connectionsDurationTotal; // total duration of all coneection + + transient int _connectionsRequestsMin; // min requests per connection + transient int _connectionsRequestsMax; // max requests per connection + + + /* ------------------------------------------------------------------------------- */ + /** + */ + public AbstractConnector() + { + } + + /* ------------------------------------------------------------------------------- */ + /* + */ + public Server getServer() + { + return _server; + } + + /* ------------------------------------------------------------------------------- */ + public void setServer(Server server) + { + _server=server; + } + + /* ------------------------------------------------------------------------------- */ + /* + * @see org.eclipse.jetty.http.HttpListener#getHttpServer() + */ + public ThreadPool getThreadPool() + { + return _threadPool; + } + + /* ------------------------------------------------------------------------------- */ + public void setThreadPool(ThreadPool pool) + { + _threadPool=pool; + } + + /* ------------------------------------------------------------------------------- */ + /** + */ + public void setHost(String host) + { + _host=host; + } + + /* ------------------------------------------------------------------------------- */ + /* + */ + public String getHost() + { + return _host; + } + + /* ------------------------------------------------------------------------------- */ + /* + * @see org.eclipse.jetty.server.server.HttpListener#setPort(int) + */ + public void setPort(int port) + { + _port=port; + } + + /* ------------------------------------------------------------------------------- */ + /* + * @see org.eclipse.jetty.server.server.HttpListener#getPort() + */ + public int getPort() + { + return _port; + } + + + /* ------------------------------------------------------------ */ + /** + * @return Returns the maxIdleTime. + */ + public int getMaxIdleTime() + { + return _maxIdleTime; + } + + /* ------------------------------------------------------------ */ + /** + * Set the maximum Idle time for a connection, which roughly translates + * to the {@link Socket#setSoTimeout(int)} call, although with NIO + * implementations other mechanisms may be used to implement the timeout. + * The max idle time is applied:<ul> + * <li>When waiting for a new request to be received on a connection</li> + * <li>When reading the headers and content of a request</li> + * <li>When writing the headers and content of a response</li> + * </ul> + * Jetty interprets this value as the maximum time between some progress being + * made on the connection. So if a single byte is read or written, then the + * timeout (if implemented by jetty) is reset. However, in many instances, + * the reading/writing is delegated to the JVM, and the semantic is more + * strictly enforced as the maximum time a single read/write operation can + * take. Note, that as Jetty supports writes of memory mapped file buffers, + * then a write may take many 10s of seconds for large content written to a + * slow device. + * <p> + * Previously, Jetty supported separate idle timeouts and IO operation timeouts, + * however the expense of changing the value of soTimeout was significant, so + * these timeouts were merged. With the advent of NIO, it may be possible to + * again differentiate these values (if there is demand). + * + * @param maxIdleTime The maxIdleTime to set. + */ + public void setMaxIdleTime(int maxIdleTime) + { + _maxIdleTime = maxIdleTime; + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the maxIdleTime. + */ + public int getLowResourceMaxIdleTime() + { + return _lowResourceMaxIdleTime; + } + + /* ------------------------------------------------------------ */ + /** + * @param maxIdleTime The maxIdleTime to set. + */ + public void setLowResourceMaxIdleTime(int maxIdleTime) + { + _lowResourceMaxIdleTime = maxIdleTime; + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the soLingerTime. + */ + public int getSoLingerTime() + { + return _soLingerTime; + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the acceptQueueSize. + */ + public int getAcceptQueueSize() + { + return _acceptQueueSize; + } + + /* ------------------------------------------------------------ */ + /** + * @param acceptQueueSize The acceptQueueSize to set. + */ + public void setAcceptQueueSize(int acceptQueueSize) + { + _acceptQueueSize = acceptQueueSize; + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the number of acceptor threads. + */ + public int getAcceptors() + { + return _acceptors; + } + + /* ------------------------------------------------------------ */ + /** + * @param acceptors The number of acceptor threads to set. + */ + public void setAcceptors(int acceptors) + { + _acceptors = acceptors; + } + + /* ------------------------------------------------------------ */ + /** + * @param soLingerTime The soLingerTime to set or -1 to disable. + */ + public void setSoLingerTime(int soLingerTime) + { + _soLingerTime = soLingerTime; + } + + /* ------------------------------------------------------------ */ + protected void doStart() throws Exception + { + if (_server==null) + throw new IllegalStateException("No server"); + + // open listener port + open(); + + super.doStart(); + + if (_threadPool==null) + _threadPool=_server.getThreadPool(); + if (_threadPool!=_server.getThreadPool() && (_threadPool instanceof LifeCycle)) + ((LifeCycle)_threadPool).start(); + + // Start selector thread + synchronized(this) + { + _acceptorThread=new Thread[getAcceptors()]; + + for (int i=0;i<_acceptorThread.length;i++) + { + if (!_threadPool.dispatch(new Acceptor(i))) + { + Log.warn("insufficient maxThreads configured for {}",this); + break; + } + } + } + + Log.info("Started {}",this); + } + + /* ------------------------------------------------------------ */ + protected void doStop() throws Exception + { + try{close();} catch(IOException e) {Log.warn(e);} + + if (_threadPool==_server.getThreadPool()) + _threadPool=null; + else if (_threadPool instanceof LifeCycle) + ((LifeCycle)_threadPool).stop(); + + super.doStop(); + + Thread[] acceptors=null; + synchronized(this) + { + acceptors=_acceptorThread; + _acceptorThread=null; + } + if (acceptors != null) + { + for (int i=0;i<acceptors.length;i++) + { + Thread thread=acceptors[i]; + if (thread!=null) + thread.interrupt(); + } + } + } + + /* ------------------------------------------------------------ */ + public void join() throws InterruptedException + { + Thread[] threads=_acceptorThread; + if (threads!=null) + for (int i=0;i<threads.length;i++) + if (threads[i]!=null) + threads[i].join(); + } + + /* ------------------------------------------------------------ */ + protected void configure(Socket socket) + throws IOException + { + try + { + socket.setTcpNoDelay(true); + if (_maxIdleTime >= 0) + socket.setSoTimeout(_maxIdleTime); + if (_soLingerTime >= 0) + socket.setSoLinger(true, _soLingerTime/1000); + else + socket.setSoLinger(false, 0); + } + catch (Exception e) + { + Log.ignore(e); + } + } + + + /* ------------------------------------------------------------ */ + public void customize(EndPoint endpoint, Request request) + throws IOException + { + if (isForwarded()) + checkForwardedHeaders(endpoint, request); + } + + /* ------------------------------------------------------------ */ + protected void checkForwardedHeaders(EndPoint endpoint, Request request) + throws IOException + { + HttpFields httpFields = request.getConnection().getRequestFields(); + + // Retrieving headers from the request + String forwardedHost = getLeftMostValue(httpFields.getStringField(getForwardedHostHeader())); + String forwardedServer = getLeftMostValue(httpFields.getStringField(getForwardedServerHeader())); + String forwardedFor = getLeftMostValue(httpFields.getStringField(getForwardedForHeader())); + + if (_hostHeader!=null) + { + // Update host header + httpFields.put(HttpHeaders.HOST_BUFFER, _hostHeader); + request.setServerName(null); + request.setServerPort(-1); + request.getServerName(); + } + else if (forwardedHost != null) + { + // Update host header + httpFields.put(HttpHeaders.HOST_BUFFER, forwardedHost); + request.setServerName(null); + request.setServerPort(-1); + request.getServerName(); + } + + if (forwardedServer != null) + { + // Use provided server name + request.setServerName(forwardedServer); + } + + if (forwardedFor != null) + { + request.setRemoteAddr(forwardedFor); + InetAddress inetAddress = null; + + if (_useDNS) + { + try + { + inetAddress = InetAddress.getByName(forwardedFor); + } + catch (UnknownHostException e) + { + Log.ignore(e); + } + } + + request.setRemoteHost(inetAddress==null?forwardedFor:inetAddress.getHostName()); + } + } + + /* ------------------------------------------------------------ */ + protected String getLeftMostValue(String headerValue) { + if (headerValue == null) + return null; + + int commaIndex = headerValue.indexOf(','); + + if (commaIndex == -1) + { + // Single value + return headerValue; + } + + // The left-most value is the farthest downstream client + return headerValue.substring(0, commaIndex); + } + + /* ------------------------------------------------------------ */ + public void persist(EndPoint endpoint) + throws IOException + { + } + + + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.jetty.server.Connector#getConfidentialPort() + */ + public int getConfidentialPort() + { + return _confidentialPort; + } + + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.jetty.server.Connector#getConfidentialScheme() + */ + public String getConfidentialScheme() + { + return _confidentialScheme; + } + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.jetty.server.Connector#isConfidential(org.eclipse.jetty.server.Request) + */ + public boolean isIntegral(Request request) + { + return false; + } + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.jetty.server.Connector#getConfidentialPort() + */ + public int getIntegralPort() + { + return _integralPort; + } + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.jetty.server.Connector#getIntegralScheme() + */ + public String getIntegralScheme() + { + return _integralScheme; + } + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.jetty.server.Connector#isConfidential(org.eclipse.jetty.server.Request) + */ + public boolean isConfidential(Request request) + { + return false; + } + + /* ------------------------------------------------------------ */ + /** + * @param confidentialPort The confidentialPort to set. + */ + public void setConfidentialPort(int confidentialPort) + { + _confidentialPort = confidentialPort; + } + + /* ------------------------------------------------------------ */ + /** + * @param confidentialScheme The confidentialScheme to set. + */ + public void setConfidentialScheme(String confidentialScheme) + { + _confidentialScheme = confidentialScheme; + } + + /* ------------------------------------------------------------ */ + /** + * @param integralPort The integralPort to set. + */ + public void setIntegralPort(int integralPort) + { + _integralPort = integralPort; + } + + /* ------------------------------------------------------------ */ + /** + * @param integralScheme The integralScheme to set. + */ + public void setIntegralScheme(String integralScheme) + { + _integralScheme = integralScheme; + } + + /* ------------------------------------------------------------ */ + protected abstract void accept(int acceptorID) throws IOException, InterruptedException; + + /* ------------------------------------------------------------ */ + public void stopAccept(int acceptorID) throws Exception + { + } + + /* ------------------------------------------------------------ */ + public boolean getResolveNames() + { + return _useDNS; + } + + /* ------------------------------------------------------------ */ + public void setResolveNames(boolean resolve) + { + _useDNS=resolve; + } + + /* ------------------------------------------------------------ */ + /** + * Is reverse proxy handling on? + * @return true if this connector is checking the x-forwarded-for/host/server headers + */ + public boolean isForwarded() + { + return _forwarded; + } + + /* ------------------------------------------------------------ */ + /** + * Set reverse proxy handling + * @param check true if this connector is checking the x-forwarded-for/host/server headers + */ + public void setForwarded(boolean check) + { + if (check) + Log.debug(this+" is forwarded"); + _forwarded=check; + } + + /* ------------------------------------------------------------ */ + public String getHostHeader() + { + return _hostHeader; + } + + /* ------------------------------------------------------------ */ + /** + * Set a forced valued for the host header to control what is returned + * by {@link ServletRequest#getServerName()} and {@link ServletRequest#getServerPort()}. + * This value is only used if {@link #isForwarded()} is true. + * @param hostHeader The value of the host header to force. + */ + public void setHostHeader(String hostHeader) + { + _hostHeader=hostHeader; + } + + /* ------------------------------------------------------------ */ + public String getForwardedHostHeader() + { + return _forwardedHostHeader; + } + + /* ------------------------------------------------------------ */ + /** + * @param forwardedHostHeader The header name for forwarded hosts (default x-forwarded-host) + */ + public void setForwardedHostHeader(String forwardedHostHeader) + { + _forwardedHostHeader=forwardedHostHeader; + } + + /* ------------------------------------------------------------ */ + public String getForwardedServerHeader() + { + return _forwardedServerHeader; + } + + /* ------------------------------------------------------------ */ + /** + * @param forwardedHostHeader The header name for forwarded server (default x-forwarded-server) + */ + public void setForwardedServerHeader(String forwardedServerHeader) + { + _forwardedServerHeader=forwardedServerHeader; + } + + /* ------------------------------------------------------------ */ + public String getForwardedForHeader() + { + return _forwardedForHeader; + } + + /* ------------------------------------------------------------ */ + /** + * @param forwardedHostHeader The header name for forwarded for (default x-forwarded-for) + */ + public void setForwardedForHeader(String forwardedRemoteAddressHeade) + { + _forwardedForHeader=forwardedRemoteAddressHeade; + } + + /* ------------------------------------------------------------ */ + public String toString() + { + String name = this.getClass().getName(); + int dot = name.lastIndexOf('.'); + if (dot>0) + name=name.substring(dot+1); + + return name+"@"+(getHost()==null?"0.0.0.0":getHost())+":"+(getLocalPort()<=0?getPort():getLocalPort()); + } + + + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + private class Acceptor implements Runnable + { + int _acceptor=0; + + Acceptor(int id) + { + _acceptor=id; + } + + /* ------------------------------------------------------------ */ + public void run() + { + Thread current = Thread.currentThread(); + synchronized(AbstractConnector.this) + { + if (_acceptorThread==null) + return; + + _acceptorThread[_acceptor]=current; + } + String name =_acceptorThread[_acceptor].getName(); + current.setName(name+" - Acceptor"+_acceptor+" "+AbstractConnector.this); + int old_priority=current.getPriority(); + + try + { + current.setPriority(old_priority-_acceptorPriorityOffset); + while (isRunning() && getConnection()!=null) + { + try + { + accept(_acceptor); + } + catch(EofException e) + { + Log.ignore(e); + } + catch(IOException e) + { + Log.ignore(e); + } + catch(ThreadDeath e) + { + Log.warn(e); + throw e; + } + catch(Throwable e) + { + Log.warn(e); + } + } + } + finally + { + current.setPriority(old_priority); + current.setName(name); + try + { + if (_acceptor==0) + close(); + } + catch (IOException e) + { + Log.warn(e); + } + + synchronized(AbstractConnector.this) + { + if (_acceptorThread!=null) + _acceptorThread[_acceptor]=null; + } + } + } + } + + /* ------------------------------------------------------------ */ + public String getName() + { + if (_name==null) + _name= (getHost()==null?"0.0.0.0":getHost())+":"+(getLocalPort()<=0?getPort():getLocalPort()); + return _name; + } + + /* ------------------------------------------------------------ */ + public void setName(String name) + { + _name = name; + } + + + + /* ------------------------------------------------------------ */ + /** + * @return Get the number of requests handled by this context + * since last call of statsReset(). If setStatsOn(false) then this + * is undefined. + */ + public int getRequests() {return _requests;} + + /* ------------------------------------------------------------ */ + /** + * @return Returns the connectionsDurationMin. + */ + public long getConnectionsDurationMin() + { + return _connectionsDurationMin; + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the connectionsDurationTotal. + */ + public long getConnectionsDurationTotal() + { + return _connectionsDurationTotal; + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the connectionsOpenMin. + */ + public int getConnectionsOpenMin() + { + return _connectionsOpenMin; + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the connectionsRequestsMin. + */ + public int getConnectionsRequestsMin() + { + return _connectionsRequestsMin; + } + + + /* ------------------------------------------------------------ */ + /** + * @return Number of connections accepted by the server since + * statsReset() called. Undefined if setStatsOn(false). + */ + public int getConnections() {return _connections;} + + /* ------------------------------------------------------------ */ + /** + * @return Number of connections currently open that were opened + * since statsReset() called. Undefined if setStatsOn(false). + */ + public int getConnectionsOpen() {return _connectionsOpen;} + + /* ------------------------------------------------------------ */ + /** + * @return Maximum number of connections opened simultaneously + * since statsReset() called. Undefined if setStatsOn(false). + */ + public int getConnectionsOpenMax() {return _connectionsOpenMax;} + + /* ------------------------------------------------------------ */ + /** + * @return Average duration in milliseconds of open connections + * since statsReset() called. Undefined if setStatsOn(false). + */ + public long getConnectionsDurationAve() {return _connections==0?0:(_connectionsDurationTotal/_connections);} + + /* ------------------------------------------------------------ */ + /** + * @return Maximum duration in milliseconds of an open connection + * since statsReset() called. Undefined if setStatsOn(false). + */ + public long getConnectionsDurationMax() {return _connectionsDurationMax;} + + /* ------------------------------------------------------------ */ + /** + * @return Average number of requests per connection + * since statsReset() called. Undefined if setStatsOn(false). + */ + public int getConnectionsRequestsAve() {return _connections==0?0:(_requests/_connections);} + + /* ------------------------------------------------------------ */ + /** + * @return Maximum number of requests per connection + * since statsReset() called. Undefined if setStatsOn(false). + */ + public int getConnectionsRequestsMax() {return _connectionsRequestsMax;} + + + + /* ------------------------------------------------------------ */ + /** Reset statistics. + */ + public void statsReset() + { + _statsStartedAt=_statsStartedAt==-1?-1:System.currentTimeMillis(); + + _connections=0; + + _connectionsOpenMin=_connectionsOpen; + _connectionsOpenMax=_connectionsOpen; + _connectionsOpen=0; + + _connectionsDurationMin=0; + _connectionsDurationMax=0; + _connectionsDurationTotal=0; + + _requests=0; + + _connectionsRequestsMin=0; + _connectionsRequestsMax=0; + } + + /* ------------------------------------------------------------ */ + public void setStatsOn(boolean on) + { + if (on && _statsStartedAt!=-1) + return; + Log.debug("Statistics on = "+on+" for "+this); + statsReset(); + _statsStartedAt=on?System.currentTimeMillis():-1; + } + + /* ------------------------------------------------------------ */ + /** + * @return True if statistics collection is turned on. + */ + public boolean getStatsOn() + { + return _statsStartedAt!=-1; + } + + /* ------------------------------------------------------------ */ + /** + * @return Timestamp stats were started at. + */ + public long getStatsOnMs() + { + return (_statsStartedAt!=-1)?(System.currentTimeMillis()-_statsStartedAt):0; + } + + /* ------------------------------------------------------------ */ + protected void connectionOpened(HttpConnection connection) + { + if (_statsStartedAt==-1) + return; + synchronized(_statsLock) + { + _connectionsOpen++; + if (_connectionsOpen > _connectionsOpenMax) + _connectionsOpenMax=_connectionsOpen; + } + } + + /* ------------------------------------------------------------ */ + protected void connectionClosed(HttpConnection connection) + { + if (_statsStartedAt>=0) + { + long duration=System.currentTimeMillis()-connection.getTimeStamp(); + int requests=connection.getRequests(); + synchronized(_statsLock) + { + _requests+=requests; + _connections++; + _connectionsOpen--; + _connectionsDurationTotal+=duration; + if (_connectionsOpen<0) + _connectionsOpen=0; + if (_connectionsOpen<_connectionsOpenMin) + _connectionsOpenMin=_connectionsOpen; + if (_connectionsDurationMin==0 || duration<_connectionsDurationMin) + _connectionsDurationMin=duration; + if (duration>_connectionsDurationMax) + _connectionsDurationMax=duration; + if (_connectionsRequestsMin==0 || requests<_connectionsRequestsMin) + _connectionsRequestsMin=requests; + if (requests>_connectionsRequestsMax) + _connectionsRequestsMax=requests; + } + } + + if (connection!=null) + connection.destroy(); + } + + /* ------------------------------------------------------------ */ + /** + * @return the acceptorPriority + */ + public int getAcceptorPriorityOffset() + { + return _acceptorPriorityOffset; + } + + /* ------------------------------------------------------------ */ + /** + * Set the priority offset of the acceptor threads. The priority is adjusted by + * this amount (default 0) to either favour the acceptance of new threads and newly active + * connections or to favour the handling of already dispatched connections. + * @param offset the amount to alter the priority of the acceptor threads. + */ + public void setAcceptorPriorityOffset(int offset) + { + _acceptorPriorityOffset=offset; + } + + /* ------------------------------------------------------------ */ + /** + * @return True if the the server socket will be opened in SO_REUSEADDR mode. + */ + public boolean getReuseAddress() + { + return _reuseAddress; + } + + /* ------------------------------------------------------------ */ + /** + * @param reuseAddress True if the the server socket will be opened in SO_REUSEADDR mode. + */ + public void setReuseAddress(boolean reuseAddress) + { + _reuseAddress=reuseAddress; + } + +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncRequest.java b/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncRequest.java new file mode 100644 index 0000000000..4bb375d48d --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/AsyncRequest.java @@ -0,0 +1,708 @@ +// ======================================================================== +// Copyright (c) 2004-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server; + +import javax.servlet.AsyncContext; +import javax.servlet.AsyncEvent; +import javax.servlet.AsyncListener; +import javax.servlet.ServletContext; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import org.eclipse.jetty.io.AsyncEndPoint; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.server.handler.ContextHandler.Context; +import org.eclipse.jetty.util.LazyList; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.thread.Timeout; + +/* ------------------------------------------------------------ */ +/** Asyncrhonous Request. + * + * + * + */ +public class AsyncRequest implements AsyncContext +{ + // STATES: + private static final int __IDLE=0; // Idle request + private static final int __DISPATCHED=1; // Request dispatched to filter/servlet + private static final int __SUSPENDING=2; // Suspend called, but not yet returned to container + private static final int __REDISPATCHING=3;// resumed while dispatched + private static final int __SUSPENDED=4; // Suspended and parked + private static final int __UNSUSPENDING=5; // Has been scheduled + private static final int __REDISPATCHED=6; // Request redispatched to filter/servlet + private static final int __COMPLETING=7; // complete while dispatched + private static final int __UNCOMPLETED=8; // Request is completable + private static final int __COMPLETE=9; // Request is complete + + // State table + // __HANDLE __UNHANDLE __SUSPEND __REDISPATCH + // IDLE */ { __DISPATCHED, __Illegal, __Illegal, __Illegal }, + // DISPATCHED */ { __Illegal, __UNCOMPLETED, __SUSPENDING, __Ignore }, + // SUSPENDING */ { __Illegal, __SUSPENDED, __Illegal,__REDISPATCHING }, + // REDISPATCHING */ { __Illegal, _REDISPATCHED, __Ignored, __Ignore }, + // COMPLETING */ { __Illegal, __UNCOMPLETED, __Illegal, __Illegal }, + // SUSPENDED */ { __REDISPATCHED, __Illegal, __Illegal, __UNSUSPENDING }, + // UNSUSPENDING */ { __REDISPATCHED, __Illegal, __Illegal, __Ignore }, + // REDISPATCHED */ { __Illegal, __UNCOMPLETED, __SUSPENDING, __Ignore }, + + + /* ------------------------------------------------------------ */ + protected HttpConnection _connection; + protected Object _listeners; + + /* ------------------------------------------------------------ */ + private int _state; + private boolean _initial; + private long _timeoutMs; + private AsyncEventState _event; + + /* ------------------------------------------------------------ */ + protected AsyncRequest() + { + _state=__IDLE; + _initial=true; + } + + /* ------------------------------------------------------------ */ + protected AsyncRequest(final HttpConnection connection) + { + this(); + if (connection!=null) + setConnection(connection); + } + + /* ------------------------------------------------------------ */ + protected void setConnection(final HttpConnection connection) + { + _connection=connection; + } + + /* ------------------------------------------------------------ */ + public void setAsyncTimeout(long ms) + { + _timeoutMs=ms; + } + + /* ------------------------------------------------------------ */ + public long getAsyncTimeout() + { + return _timeoutMs; + } + + /* ------------------------------------------------------------ */ + public AsyncEventState getAsyncEventState() + { + return _event; + } + + /* ------------------------------------------------------------ */ + /* (non-Javadoc) + * @see javax.servlet.ServletRequest#isInitial() + */ + public boolean isInitial() + { + synchronized(this) + { + return _initial; + } + } + + /* ------------------------------------------------------------ */ + /* (non-Javadoc) + * @see javax.servlet.ServletRequest#isSuspended() + */ + public boolean isSuspended() + { + synchronized(this) + { + switch(_state) + { + case __SUSPENDING: + case __REDISPATCHING: + case __COMPLETING: + case __SUSPENDED: + return true; + + default: + return false; + } + } + } + + /* ------------------------------------------------------------ */ + public String toString() + { + return getStatusString(); + } + + /* ------------------------------------------------------------ */ + public String getStatusString() + { + synchronized (this) + { + return + ((_state==__IDLE)?"IDLE": + (_state==__DISPATCHED)?"DISPATCHED": + (_state==__SUSPENDING)?"SUSPENDING": + (_state==__SUSPENDED)?"SUSPENDED": + (_state==__REDISPATCHING)?"REDISPATCHING": + (_state==__UNSUSPENDING)?"UNSUSPENDING": + (_state==__REDISPATCHED)?"REDISPATCHED": + (_state==__COMPLETING)?"COMPLETING": + (_state==__UNCOMPLETED)?"UNCOMPLETED": + (_state==__COMPLETE)?"COMPLETE": + ("UNKNOWN?"+_state))+ + (_initial?",initial":""); + } + } + + /* ------------------------------------------------------------ */ + /* (non-Javadoc) + * @see javax.servlet.ServletRequest#resume() + */ + protected boolean handling() + { + synchronized (this) + { + switch(_state) + { + case __DISPATCHED: + case __REDISPATCHED: + case __COMPLETE: + throw new IllegalStateException(this.getStatusString()); + + case __IDLE: + _initial=true; + _state=__DISPATCHED; + return true; + + case __SUSPENDING: + case __REDISPATCHING: + throw new IllegalStateException(this.getStatusString()); + + case __COMPLETING: + _state=__UNCOMPLETED; + return false; + + case __SUSPENDED: + cancelTimeout(); + case __UNSUSPENDING: + _state=__REDISPATCHED; + return true; + + default: + throw new IllegalStateException(""+_state); + } + } + } + + /* ------------------------------------------------------------ */ + /* (non-Javadoc) + * @see javax.servlet.ServletRequest#suspend(long) + */ + protected void suspend(final ServletContext context, + final ServletRequest request, + final ServletResponse response) + { + synchronized (this) + { + _event=new AsyncEventState(context,request,response); + + switch(_state) + { + case __DISPATCHED: + case __REDISPATCHED: + _state=__SUSPENDING; + return; + + case __IDLE: + throw new IllegalStateException(this.getStatusString()); + + case __SUSPENDING: + case __REDISPATCHING: + return; + + case __COMPLETING: + case __SUSPENDED: + case __UNSUSPENDING: + case __COMPLETE: + throw new IllegalStateException(this.getStatusString()); + + default: + throw new IllegalStateException(""+_state); + } + } + } + + /* ------------------------------------------------------------ */ + /** + * Signal that the HttpConnection has finished handling the request. + * For blocking connectors, this call may block if the request has + * been suspended (startAsync called). + * @return true if handling is complete, false if the request should + * be handled again (eg because of a resume that happened before unhandle was called) + */ + protected boolean unhandle() + { + synchronized (this) + { + switch(_state) + { + case __REDISPATCHED: + case __DISPATCHED: + _state=__UNCOMPLETED; + return true; + + case __IDLE: + throw new IllegalStateException(this.getStatusString()); + + case __SUSPENDING: + _initial=false; + _state=__SUSPENDED; + scheduleTimeout(); // could block and change state. + if (_state==__SUSPENDED) + return true; + else if (_state==__COMPLETING) + { + _state=__UNCOMPLETED; + return true; + } + _initial=false; + _state=__REDISPATCHED; + return false; + + case __REDISPATCHING: + _initial=false; + _state=__REDISPATCHED; + return false; + + case __COMPLETING: + _initial=false; + _state=__UNCOMPLETED; + return true; + + case __SUSPENDED: + case __UNSUSPENDING: + default: + throw new IllegalStateException(this.getStatusString()); + } + } + } + + /* ------------------------------------------------------------ */ + public void dispatch() + { + boolean dispatch=false; + synchronized (this) + { + switch(_state) + { + case __REDISPATCHED: + case __DISPATCHED: + case __IDLE: + case __REDISPATCHING: + case __COMPLETING: + case __COMPLETE: + case __UNCOMPLETED: + return; + + case __SUSPENDING: + _state=__REDISPATCHING; + return; + + case __SUSPENDED: + dispatch=true; + _state=__UNSUSPENDING; + break; + + case __UNSUSPENDING: + return; + + default: + throw new IllegalStateException(this.getStatusString()); + } + } + + if (dispatch) + { + cancelTimeout(); + scheduleDispatch(); + } + } + + /* ------------------------------------------------------------ */ + protected void expired() + { + synchronized (this) + { + switch(_state) + { + case __SUSPENDING: + case __SUSPENDED: + break; + default: + return; + } + } + + if (_listeners!=null) + { + for(int i=0;i<LazyList.size(_listeners);i++) + { + try + { + AsyncListener listener=((AsyncListener)LazyList.get(_listeners,i)); + listener.onTimeout(_event); + } + catch(Exception e) + { + Log.warn(e); + } + } + } + + synchronized (this) + { + switch(_state) + { + case __SUSPENDING: + case __SUSPENDED: + if (false) // TODO + dispatch(); + else + complete(); + default: + return; + } + } + } + + /* ------------------------------------------------------------ */ + /* (non-Javadoc) + * @see javax.servlet.ServletRequest#complete() + */ + public void complete() + { + // just like resume, except don't set _resumed=true; + boolean dispatch=false; + synchronized (this) + { + switch(_state) + { + case __IDLE: + case __COMPLETE: + case __REDISPATCHING: + case __COMPLETING: + case __UNSUSPENDING: + return; + + case __DISPATCHED: + case __REDISPATCHED: + throw new IllegalStateException(this.getStatusString()); + + case __SUSPENDING: + _state=__COMPLETING; + return; + + case __SUSPENDED: + _state=__COMPLETING; + dispatch=true; + break; + + default: + throw new IllegalStateException(this.getStatusString()); + } + } + + if (dispatch) + { + cancelTimeout(); + scheduleDispatch(); + } + } + + + /* ------------------------------------------------------------ */ + /* (non-Javadoc) + * @see javax.servlet.ServletRequest#complete() + */ + protected void doComplete() + { + synchronized (this) + { + switch(_state) + { + case __UNCOMPLETED: + _state=__COMPLETE; + break; + + default: + throw new IllegalStateException(this.getStatusString()); + } + } + + if (_listeners!=null) + { + for(int i=0;i<LazyList.size(_listeners);i++) + { + try + { + ((AsyncListener)LazyList.get(_listeners,i)).onComplete(_event); + } + catch(Exception e) + { + Log.warn(e); + } + } + } + } + + /* ------------------------------------------------------------ */ + protected void recycle() + { + synchronized (this) + { + switch(_state) + { + case __DISPATCHED: + case __REDISPATCHED: + throw new IllegalStateException(getStatusString()); + default: + _state=__IDLE; + } + _initial = true; + cancelTimeout(); + _event=null; + _timeoutMs=60000L; // TODO configure + _listeners=null; + } + } + + /* ------------------------------------------------------------ */ + public void cancel() + { + synchronized (this) + { + _state=__COMPLETE; + _initial = false; + cancelTimeout(); + _event=null; + _listeners=null; + } + } + + /* ------------------------------------------------------------ */ + protected void scheduleDispatch() + { + EndPoint endp=_connection.getEndPoint(); + if (!endp.isBlocking()) + { + ((AsyncEndPoint)endp).dispatch(); + } + } + + /* ------------------------------------------------------------ */ + protected void scheduleTimeout() + { + EndPoint endp=_connection.getEndPoint(); + if (endp.isBlocking()) + { + synchronized(this) + { + long expire_at = System.currentTimeMillis()+_timeoutMs; + long wait=_timeoutMs; + while (_timeoutMs>0 && wait>0) + { + try + { + this.wait(wait); + } + catch (InterruptedException e) + { + Log.ignore(e); + } + wait=expire_at-System.currentTimeMillis(); + } + + if (_timeoutMs>0 && wait<=0) + expired(); + } + } + else + _connection.scheduleTimeout(_event._timeout,_timeoutMs); + } + + /* ------------------------------------------------------------ */ + protected void cancelTimeout() + { + EndPoint endp=_connection.getEndPoint(); + if (endp.isBlocking()) + { + synchronized(this) + { + _timeoutMs=0; + this.notifyAll(); + } + } + else if (_event!=null) + _connection.cancelTimeout(_event._timeout); + } + + /* ------------------------------------------------------------ */ + public boolean isCompleting() + { + return _state==__COMPLETING; + } + + /* ------------------------------------------------------------ */ + boolean isUncompleted() + { + return _state==__UNCOMPLETED; + } + + /* ------------------------------------------------------------ */ + public boolean isComplete() + { + return _state==__COMPLETE; + } + + + /* ------------------------------------------------------------ */ + public boolean isAsyncStarted() + { + switch(_state) + { + case __SUSPENDING: + case __REDISPATCHING: + case __UNSUSPENDING: + case __SUSPENDED: + return true; + + default: + return false; + } + } + + + /* ------------------------------------------------------------ */ + public boolean isAsync() + { + switch(_state) + { + case __IDLE: + case __DISPATCHED: + return false; + + default: + return true; + } + } + + /* ------------------------------------------------------------ */ + public void dispatch(ServletContext context, String path) + { + _event._dispatchContext=context; + _event._path=path; + dispatch(); + } + + /* ------------------------------------------------------------ */ + public void dispatch(String path) + { + _event._path=path; + dispatch(); + } + + /* ------------------------------------------------------------ */ + public ServletRequest getRequest() + { + if (_event!=null) + return _event.getRequest(); + return _connection.getRequest(); + } + + /* ------------------------------------------------------------ */ + public ServletResponse getResponse() + { + if (_event!=null) + return _event.getResponse(); + return _connection.getResponse(); + } + + /* ------------------------------------------------------------ */ + public void start(Runnable run) + { + ((Context)_event.getServletContext()).getContextHandler().handle(run); + } + + /* ------------------------------------------------------------ */ + public boolean hasOriginalRequestAndResponse() + { + return (_event!=null && _event.getRequest()==_connection._request && _event.getResponse()==_connection._response); + } + + /* ------------------------------------------------------------ */ + public ContextHandler getContextHandler() + { + if (_event!=null) + return ((Context)_event.getServletContext()).getContextHandler(); + return null; + } + + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + public class AsyncEventState extends AsyncEvent + { + final Timeout.Task _timeout; + final ServletContext _suspendedContext; + ServletContext _dispatchContext; + String _path; + + public AsyncEventState(ServletContext context, ServletRequest request, ServletResponse response) + { + super(request,response); + _suspendedContext=context; + _timeout= new Timeout.Task() + { + public void expired() + { + AsyncRequest.this.expired(); + } + }; + } + + public ServletContext getSuspendedContext() + { + return _suspendedContext; + } + + public ServletContext getDispatchContext() + { + return _dispatchContext; + } + + public ServletContext getServletContext() + { + return _dispatchContext==null?_suspendedContext:_dispatchContext; + } + + public String getPath() + { + return _path; + } + } +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Connector.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Connector.java new file mode 100644 index 0000000000..e5976995e5 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Connector.java @@ -0,0 +1,322 @@ +// ======================================================================== +// Copyright (c) 2004-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server; + +import java.io.IOException; + +import org.eclipse.jetty.io.Buffers; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.util.component.LifeCycle; + +/** HTTP Connector. + * Implementations of this interface provide connectors for the HTTP protocol. + * A connector receives requests (normally from a socket) and calls the + * handle method of the Handler object. These operations are performed using + * threads from the ThreadPool set on the connector. + * + * When a connector is registered with an instance of Server, then the server + * will set itself as both the ThreadPool and the Handler. Note that a connector + * can be used without a Server if a thread pool and handler are directly provided. + * + * + * + */ +public interface Connector extends LifeCycle, Buffers +{ + /* ------------------------------------------------------------ */ + /** + * @return the name of the connector. Defaults to the HostName:port + */ + String getName(); + + /* ------------------------------------------------------------ */ + /** + * Opens the connector + * @throws IOException + */ + void open() throws IOException; + + /* ------------------------------------------------------------ */ + void close() throws IOException; + + /* ------------------------------------------------------------ */ + void setServer(Server server); + + /* ------------------------------------------------------------ */ + Server getServer(); + + /* ------------------------------------------------------------ */ + /** + * @return Returns the headerBufferSize. + */ + int getHeaderBufferSize(); + + /* ------------------------------------------------------------ */ + /** + * Set the size of the buffer to be used for request and response headers. + * An idle connection will at most have one buffer of this size allocated. + * @param headerBufferSize The headerBufferSize to set. + */ + void setHeaderBufferSize(int headerBufferSize); + + + /* ------------------------------------------------------------ */ + /** + * @return Returns the requestBufferSize. + */ + int getRequestBufferSize(); + + /* ------------------------------------------------------------ */ + /** + * Set the size of the content buffer for receiving requests. + * These buffers are only used for active connections that have + * requests with bodies that will not fit within the header buffer. + * @param requestBufferSize The requestBufferSize to set. + */ + void setRequestBufferSize(int requestBufferSize); + + /* ------------------------------------------------------------ */ + /** + * @return Returns the responseBufferSize. + */ + int getResponseBufferSize(); + + /* ------------------------------------------------------------ */ + /** + * Set the size of the content buffer for sending responses. + * These buffers are only used for active connections that are sending + * responses with bodies that will not fit within the header buffer. + * @param responseBufferSize The responseBufferSize to set. + */ + void setResponseBufferSize(int responseBufferSize); + + + /* ------------------------------------------------------------ */ + /** + * @return The port to use when redirecting a request if a data constraint of integral is + * required. See {@link org.eclipse.jetty.server.server.security.Constraint#getDataConstraint()} + */ + int getIntegralPort(); + + /* ------------------------------------------------------------ */ + /** + * @return The schema to use when redirecting a request if a data constraint of integral is + * required. See {@link org.eclipse.jetty.server.server.security.Constraint#getDataConstraint()} + */ + String getIntegralScheme(); + + /* ------------------------------------------------------------ */ + /** + * @param request A request + * @return true if the request is integral. This normally means the https schema has been used. + */ + boolean isIntegral(Request request); + + /* ------------------------------------------------------------ */ + /** + * @return The port to use when redirecting a request if a data constraint of confidential is + * required. See {@link org.eclipse.jetty.server.server.security.Constraint#getDataConstraint()} + */ + int getConfidentialPort(); + + + /* ------------------------------------------------------------ */ + /** + * @return The schema to use when redirecting a request if a data constraint of confidential is + * required. See {@link org.eclipse.jetty.server.server.security.Constraint#getDataConstraint()} + */ + String getConfidentialScheme(); + + /* ------------------------------------------------------------ */ + /** + * @param request A request + * @return true if the request is confidential. This normally means the https schema has been used. + */ + boolean isConfidential(Request request); + + /* ------------------------------------------------------------ */ + /** Customize a request for an endpoint. + * Called on every request to allow customization of the request for + * the particular endpoint (eg security properties from a SSL connection). + * @param endpoint + * @param request + * @throws IOException + */ + void customize(EndPoint endpoint, Request request) throws IOException; + + /* ------------------------------------------------------------ */ + /** Persist an endpoint. + * Called after every request if the connection is to remain open. + * @param endpoint + * @param request + * @throws IOException + */ + void persist(EndPoint endpoint) throws IOException; + + /* ------------------------------------------------------------ */ + String getHost(); + + /* ------------------------------------------------------------ */ + void setHost(String hostname); + + /* ------------------------------------------------------------ */ + /** + * @param port The port fto listen of for connections or 0 if any available + * port may be used. + */ + void setPort(int port); + + /* ------------------------------------------------------------ */ + /** + * @return The configured port for the connector or 0 if any available + * port may be used. + */ + int getPort(); + + /* ------------------------------------------------------------ */ + /** + * @return The actual port the connector is listening on or -1 if there + * is no port or the connector is not open. + */ + int getLocalPort(); + + /* ------------------------------------------------------------ */ + int getMaxIdleTime(); + void setMaxIdleTime(int ms); + + /* ------------------------------------------------------------ */ + int getLowResourceMaxIdleTime(); + void setLowResourceMaxIdleTime(int ms); + + /* ------------------------------------------------------------ */ + /** + * @return the underlying socket, channel, buffer etc. for the connector. + */ + Object getConnection(); + + + /* ------------------------------------------------------------ */ + /** + * @return true if names resolution should be done. + */ + boolean getResolveNames(); + + + + /* ------------------------------------------------------------ */ + /** + * @return Get the number of requests handled by this connector + * since last call of statsReset(). If setStatsOn(false) then this + * is undefined. + */ + public int getRequests(); + + /* ------------------------------------------------------------ */ + /** + * @return Returns the connectionsDurationMin. + */ + public long getConnectionsDurationMin(); + + /* ------------------------------------------------------------ */ + /** + * @return Returns the connectionsDurationTotal. + */ + public long getConnectionsDurationTotal(); + + /* ------------------------------------------------------------ */ + /** + * @return Returns the connectionsOpenMin. + */ + public int getConnectionsOpenMin(); + + /* ------------------------------------------------------------ */ + /** + * @return Returns the connectionsRequestsMin. + */ + public int getConnectionsRequestsMin(); + + + /* ------------------------------------------------------------ */ + /** + * @return Number of connections accepted by the server since + * statsReset() called. Undefined if setStatsOn(false). + */ + public int getConnections() ; + + /* ------------------------------------------------------------ */ + /** + * @return Number of connections currently open that were opened + * since statsReset() called. Undefined if setStatsOn(false). + */ + public int getConnectionsOpen() ; + + /* ------------------------------------------------------------ */ + /** + * @return Maximum number of connections opened simultaneously + * since statsReset() called. Undefined if setStatsOn(false). + */ + public int getConnectionsOpenMax() ; + + /* ------------------------------------------------------------ */ + /** + * @return Average duration in milliseconds of open connections + * since statsReset() called. Undefined if setStatsOn(false). + */ + public long getConnectionsDurationAve() ; + + /* ------------------------------------------------------------ */ + /** + * @return Maximum duration in milliseconds of an open connection + * since statsReset() called. Undefined if setStatsOn(false). + */ + public long getConnectionsDurationMax(); + + /* ------------------------------------------------------------ */ + /** + * @return Average number of requests per connection + * since statsReset() called. Undefined if setStatsOn(false). + */ + public int getConnectionsRequestsAve() ; + + /* ------------------------------------------------------------ */ + /** + * @return Maximum number of requests per connection + * since statsReset() called. Undefined if setStatsOn(false). + */ + public int getConnectionsRequestsMax(); + + + + /* ------------------------------------------------------------ */ + /** Reset statistics. + */ + public void statsReset(); + + /* ------------------------------------------------------------ */ + public void setStatsOn(boolean on); + + /* ------------------------------------------------------------ */ + /** + * @return True if statistics collection is turned on. + */ + public boolean getStatsOn(); + + /* ------------------------------------------------------------ */ + /** + * @return Timestamp stats were started at. + */ + public long getStatsOnMs(); + + +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/CookieCutter.java b/jetty-server/src/main/java/org/eclipse/jetty/server/CookieCutter.java new file mode 100644 index 0000000000..d3e98271ad --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/CookieCutter.java @@ -0,0 +1,296 @@ +// ======================================================================== +// Copyright (c) 2004-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; + +import org.eclipse.jetty.util.LazyList; +import org.eclipse.jetty.util.URIUtil; +import org.eclipse.jetty.util.log.Log; + + +/* ------------------------------------------------------------ */ +/** Cookie parser + * <p>Optimized stateful cookie parser. Cookies fields are added with the + * {@link #addCookieField(String)} method and parsed on the next subsequent + * call to {@link #getCookies()}. + * If the added fields are identical to those last added (as strings), then the + * cookies are not re parsed. + * + * + */ +public class CookieCutter +{ + private static final byte STATE_DELIMITER = 1; + private static final byte STATE_NAME = 2; + private static final byte STATE_VALUE = 4; + private static final byte STATE_QUOTED_VALUE = 8; + private static final byte STATE_UNQUOTED_VALUE = 16; + + private Cookie[] _cookies; + private String[] _fields; + int _added=0; + boolean _dirty; + HttpServletRequest _request; + + public CookieCutter() + { + + } + + public CookieCutter(HttpServletRequest request) + { + _request = request; + } + + public Cookie[] getCookies() + { + if (_added>0) + { + if (!_dirty && _added==_fields.length) + { + // same cookies as last time! + _added=0; + return _cookies; + } + + parseFields(); + } + return _cookies; + } + + public void setCookies(Cookie[] cookies) + { + _dirty=false; + _added=0; + _cookies=cookies; + } + + public void reset() + { + _fields=null; + _cookies=null; + } + + public void addCookieField(String f) + { + if (!_dirty && + _fields!=null && + _fields.length>_added && + _fields[_added].equals(f)) + { + _added++; + return; + } + + if (_dirty) + { + _added++; + _fields=(String[])LazyList.addToArray(_fields,f,String.class); + } + else + { + _dirty=true; + if (_added>0) + { + String[] fields=new String[_added+1]; + System.arraycopy(_fields,0,fields,0,_added); + fields[_added++]=f; + _fields=fields; + } + else + { + _fields = new String[]{f}; + _added=1; + } + + } + } + + protected void parseFields() + { + Object cookies = null; + + int version = 0; + + // For each cookie field + for (int f=0;f<_added;f++) + { + String hdr = _fields[f]; + + // Parse the header + String name = null; + String value = null; + + Cookie cookie = null; + + byte state = STATE_NAME; + for (int i = 0, tokenstart = 0, length = hdr.length(); i < length; i++) + { + char c = hdr.charAt(i); + switch (c) + { + case ',': + case ';': + switch (state) + { + case STATE_DELIMITER: + state = STATE_NAME; + tokenstart = i + 1; + break; + case STATE_UNQUOTED_VALUE: + state = STATE_NAME; + value = hdr.substring(tokenstart, i).trim(); + if(_request!=null && _request.isRequestedSessionIdFromURL()) + value = URIUtil.decodePath(value); + tokenstart = i + 1; + break; + case STATE_NAME: + name = hdr.substring(tokenstart, i); + value = ""; + tokenstart = i + 1; + break; + case STATE_VALUE: + state = STATE_NAME; + value = ""; + tokenstart = i + 1; + break; + } + break; + case '=': + switch (state) + { + case STATE_NAME: + state = STATE_VALUE; + name = hdr.substring(tokenstart, i); + tokenstart = i + 1; + break; + case STATE_VALUE: + state = STATE_UNQUOTED_VALUE; + tokenstart = i; + break; + } + break; + case '"': + switch (state) + { + case STATE_VALUE: + state = STATE_QUOTED_VALUE; + tokenstart = i + 1; + break; + case STATE_QUOTED_VALUE: + state = STATE_DELIMITER; + value = hdr.substring(tokenstart, i); + break; + } + break; + case ' ': + case '\t': + break; + default: + switch (state) + { + case STATE_VALUE: + state = STATE_UNQUOTED_VALUE; + tokenstart = i; + break; + case STATE_DELIMITER: + state = STATE_NAME; + tokenstart = i; + break; + } + } + + if (i + 1 == length) + { + switch (state) + { + case STATE_UNQUOTED_VALUE: + value = hdr.substring(tokenstart).trim(); + if(_request!=null && _request.isRequestedSessionIdFromURL()) + value = URIUtil.decodePath(value); + break; + case STATE_NAME: + name = hdr.substring(tokenstart); + value = ""; + break; + case STATE_VALUE: + value = ""; + break; + } + } + + if (name != null && value != null) + { + name = name.trim(); + + try + { + if (name.startsWith("$")) + { + String lowercaseName = name.toLowerCase(); + if ("$path".equals(lowercaseName)) + { + cookie.setPath(value); + } + else if ("$domain".equals(lowercaseName)) + { + cookie.setDomain(value); + } + else if ("$version".equals(lowercaseName)) + { + version = Integer.parseInt(value); + } + } + else + { + cookie = new Cookie(name, value); + + if (version > 0) + { + cookie.setVersion(version); + } + + cookies = LazyList.add(cookies, cookie); + } + } + catch (Exception e) + { + Log.ignore(e); + } + + name = null; + value = null; + } + } + } + + int l = LazyList.size(cookies); + if (l>0) + { + if (_cookies != null && _cookies.length == l) + { + for (int i = 0; i < l; i++) + _cookies[i] = (Cookie) LazyList.get(cookies, i); + } + else + _cookies = (Cookie[]) LazyList.toArray(cookies,Cookie.class); + } + + _added=0; + _dirty=false; + + } + +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java new file mode 100644 index 0000000000..50e16efbc7 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java @@ -0,0 +1,571 @@ +// ======================================================================== +// Copyright (c) 1999-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server; + +import java.io.IOException; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; + +import javax.servlet.DispatcherType; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.util.Attributes; +import org.eclipse.jetty.util.LazyList; +import org.eclipse.jetty.util.MultiMap; +import org.eclipse.jetty.util.UrlEncoded; + +/* ------------------------------------------------------------ */ +/** Servlet RequestDispatcher. + * + * + */ +public class Dispatcher implements RequestDispatcher +{ + /** Dispatch include attribute names */ + public final static String __INCLUDE_PREFIX="javax.servlet.include."; + public final static String __INCLUDE_REQUEST_URI= INCLUDE_REQUEST_URI; + public final static String __INCLUDE_CONTEXT_PATH= INCLUDE_CONTEXT_PATH; + public final static String __INCLUDE_SERVLET_PATH= INCLUDE_SERVLET_PATH; + public final static String __INCLUDE_PATH_INFO= INCLUDE_PATH_INFO; + public final static String __INCLUDE_QUERY_STRING= INCLUDE_QUERY_STRING; + + /** Dispatch include attribute names */ + public final static String __FORWARD_PREFIX="javax.servlet.forward."; + public final static String __FORWARD_REQUEST_URI= FORWARD_REQUEST_URI; + public final static String __FORWARD_CONTEXT_PATH= FORWARD_CONTEXT_PATH; + public final static String __FORWARD_SERVLET_PATH= FORWARD_SERVLET_PATH; + public final static String __FORWARD_PATH_INFO= FORWARD_PATH_INFO; + public final static String __FORWARD_QUERY_STRING= FORWARD_QUERY_STRING; + + /** JSP attributes */ + public final static String __JSP_FILE="org.apache.catalina.jsp_file"; + + /* ------------------------------------------------------------ */ + private ContextHandler _contextHandler; + private String _uri; + private String _path; + private String _dQuery; + private String _named; + + /* ------------------------------------------------------------ */ + /** + * @param contextHandler + * @param uriInContext + * @param pathInContext + * @param query + */ + public Dispatcher(ContextHandler contextHandler, String uri, String pathInContext, String query) + { + _contextHandler=contextHandler; + _uri=uri; + _path=pathInContext; + _dQuery=query; + } + + + /* ------------------------------------------------------------ */ + /** Constructor. + * @param servletHandler + * @param name + */ + public Dispatcher(ContextHandler contextHandler,String name) + throws IllegalStateException + { + _contextHandler=contextHandler; + _named=name; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.RequestDispatcher#forward(javax.servlet.ServletRequest, javax.servlet.ServletResponse) + */ + public void forward(ServletRequest request, ServletResponse response) throws ServletException, IOException + { + forward(request, response, DispatcherType.FORWARD); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.RequestDispatcher#forward(javax.servlet.ServletRequest, javax.servlet.ServletResponse) + */ + public void error(ServletRequest request, ServletResponse response) throws ServletException, IOException + { + forward(request, response, DispatcherType.ERROR); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.RequestDispatcher#include(javax.servlet.ServletRequest, javax.servlet.ServletResponse) + */ + public void include(ServletRequest request, ServletResponse response) throws ServletException, IOException + { + Request base_request=(request instanceof Request)?((Request)request):HttpConnection.getCurrentConnection().getRequest(); + request.removeAttribute(__JSP_FILE); // TODO remove when glassfish 1044 is fixed + + // TODO - allow stream or writer???? + + DispatcherType old_type = base_request.getDispatcherType(); + Attributes old_attr=base_request.getAttributes(); + MultiMap old_params=base_request.getParameters(); + try + { + base_request.setDispatcherType(DispatcherType.INCLUDE); + base_request.getConnection().include(); + if (_named!=null) + _contextHandler.doHandle(_named, base_request,(HttpServletRequest)request, (HttpServletResponse)response); + else + { + String query=_dQuery; + + if (query!=null) + { + MultiMap parameters=new MultiMap(); + UrlEncoded.decodeTo(query,parameters,request.getCharacterEncoding()); + + if (old_params!=null && old_params.size()>0) + { + // Merge parameters. + Iterator iter = old_params.entrySet().iterator(); + while (iter.hasNext()) + { + Map.Entry entry = (Map.Entry)iter.next(); + String name=(String)entry.getKey(); + Object values=entry.getValue(); + for (int i=0;i<LazyList.size(values);i++) + parameters.add(name, LazyList.get(values, i)); + } + + } + base_request.setParameters(parameters); + } + + IncludeAttributes attr = new IncludeAttributes(old_attr); + + attr._requestURI=_uri; + attr._contextPath=_contextHandler.getContextPath(); + attr._servletPath=null; // set by ServletHandler + attr._pathInfo=_path; + attr._query=query; + + base_request.setAttributes(attr); + + _contextHandler.doHandle(_named==null?_path:_named, base_request,(HttpServletRequest)request, (HttpServletResponse)response); + } + } + finally + { + base_request.setAttributes(old_attr); + base_request.getConnection().included(); + base_request.setParameters(old_params); + base_request.setDispatcherType(old_type); + } + } + + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.RequestDispatcher#forward(javax.servlet.ServletRequest, javax.servlet.ServletResponse) + */ + protected void forward(ServletRequest request, ServletResponse response, DispatcherType dispatch) throws ServletException, IOException + { + Request base_request=(request instanceof Request)?((Request)request):HttpConnection.getCurrentConnection().getRequest(); + Response base_response=(Response)base_request.getResponse(); + base_response.fwdReset(); + request.removeAttribute(__JSP_FILE); // TODO remove when glassfish 1044 is fixed + + String old_uri=base_request.getRequestURI(); + String old_context_path=base_request.getContextPath(); + String old_servlet_path=base_request.getServletPath(); + String old_path_info=base_request.getPathInfo(); + String old_query=base_request.getQueryString(); + Attributes old_attr=base_request.getAttributes(); + MultiMap old_params=base_request.getParameters(); + DispatcherType old_type=base_request.getDispatcherType(); + + try + { + base_request.setDispatcherType(dispatch); + + if (_named!=null) + _contextHandler.doHandle(_named, base_request,(HttpServletRequest)request, (HttpServletResponse)response); + else + { + String query=_dQuery; + + if (query!=null) + { + MultiMap parameters=new MultiMap(); + UrlEncoded.decodeTo(query,parameters,request.getCharacterEncoding()); + + boolean rewrite_old_query = false; + + if( old_params == null ) + { + base_request.getParameterNames(); // force parameters to be evaluated + old_params = base_request.getParameters(); + } + + if (old_params!=null && old_params.size()>0) + { + // Merge parameters; new parameters of the same name take precedence. + Iterator iter = old_params.entrySet().iterator(); + while (iter.hasNext()) + { + Map.Entry entry = (Map.Entry)iter.next(); + String name=(String)entry.getKey(); + + if (parameters.containsKey(name)) + { + rewrite_old_query = true; + } + else + { + Object values=entry.getValue(); + for (int i=0;i<LazyList.size(values);i++) + { + parameters.add(name, LazyList.get(values, i)); + } + } + } + } + + if (old_query != null && old_query.length()>0) + { + if ( rewrite_old_query ) + { + StringBuilder overridden_query_string = new StringBuilder(); + MultiMap overridden_old_query = new MultiMap(); + UrlEncoded.decodeTo(old_query,overridden_old_query,request.getCharacterEncoding()); + + MultiMap overridden_new_query = new MultiMap(); + UrlEncoded.decodeTo(query,overridden_new_query,request.getCharacterEncoding()); + + Iterator iter = overridden_old_query.entrySet().iterator(); + while (iter.hasNext()) + { + Map.Entry entry = (Map.Entry)iter.next(); + String name=(String)entry.getKey(); + if(!overridden_new_query.containsKey(name)) + { + Object values=entry.getValue(); + for (int i=0;i<LazyList.size(values);i++) + { + overridden_query_string.append("&"+name+"="+LazyList.get(values, i)); + } + } + } + + query = query + overridden_query_string; + } + else + { + query=query+"&"+old_query; + } + } + + base_request.setParameters(parameters); + base_request.setQueryString(query); + } + + ForwardAttributes attr = new ForwardAttributes(old_attr); + + //If we have already been forwarded previously, then keep using the established + //original value. Otherwise, this is the first forward and we need to establish the values. + //Note: the established value on the original request for pathInfo and + //for queryString is allowed to be null, but cannot be null for the other values. + if ((String)old_attr.getAttribute(__FORWARD_REQUEST_URI) != null) + { + attr._pathInfo=(String)old_attr.getAttribute(__FORWARD_PATH_INFO); + attr._query=(String)old_attr.getAttribute(__FORWARD_QUERY_STRING); + attr._requestURI=(String)old_attr.getAttribute(__FORWARD_REQUEST_URI); + attr._contextPath=(String)old_attr.getAttribute(__FORWARD_CONTEXT_PATH); + attr._servletPath=(String)old_attr.getAttribute(__FORWARD_SERVLET_PATH); + } + else + { + attr._pathInfo=old_path_info; + attr._query=old_query; + attr._requestURI=old_uri; + attr._contextPath=old_context_path; + attr._servletPath=old_servlet_path; + } + + + + base_request.setRequestURI(_uri); + base_request.setContextPath(_contextHandler.getContextPath()); + base_request.setAttributes(attr); + base_request.setQueryString(query); + + _contextHandler.doHandle(_path, base_request,(HttpServletRequest)request, (HttpServletResponse)response); + + if (base_request.getConnection().getResponse().isWriting()) + { + try {response.getWriter().close();} + catch(IllegalStateException e) { response.getOutputStream().close(); } + } + else + { + try {response.getOutputStream().close();} + catch(IllegalStateException e) { response.getWriter().close(); } + } + } + } + finally + { + base_request.setRequestURI(old_uri); + base_request.setContextPath(old_context_path); + base_request.setServletPath(old_servlet_path); + base_request.setPathInfo(old_path_info); + base_request.setAttributes(old_attr); + base_request.setParameters(old_params); + base_request.setQueryString(old_query); + base_request.setDispatcherType(old_type); + } + } + + + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + private class ForwardAttributes implements Attributes + { + Attributes _attr; + + String _requestURI; + String _contextPath; + String _servletPath; + String _pathInfo; + String _query; + + ForwardAttributes(Attributes attributes) + { + _attr=attributes; + } + + /* ------------------------------------------------------------ */ + public Object getAttribute(String key) + { + if (Dispatcher.this._named==null) + { + if (key.equals(__FORWARD_PATH_INFO)) + return _pathInfo; + if (key.equals(__FORWARD_REQUEST_URI)) + return _requestURI; + if (key.equals(__FORWARD_SERVLET_PATH)) + return _servletPath; + if (key.equals(__FORWARD_CONTEXT_PATH)) + return _contextPath; + if (key.equals(__FORWARD_QUERY_STRING)) + return _query; + } + + if (key.startsWith(__INCLUDE_PREFIX)) + return null; + + return _attr.getAttribute(key); + } + + /* ------------------------------------------------------------ */ + public Enumeration getAttributeNames() + { + HashSet set=new HashSet(); + Enumeration e=_attr.getAttributeNames(); + while(e.hasMoreElements()) + { + String name=(String)e.nextElement(); + if (!name.startsWith(__INCLUDE_PREFIX) && + !name.startsWith(__FORWARD_PREFIX)) + set.add(name); + } + + if (_named==null) + { + if (_pathInfo!=null) + set.add(__FORWARD_PATH_INFO); + else + set.remove(__FORWARD_PATH_INFO); + set.add(__FORWARD_REQUEST_URI); + set.add(__FORWARD_SERVLET_PATH); + set.add(__FORWARD_CONTEXT_PATH); + if (_query!=null) + set.add(__FORWARD_QUERY_STRING); + else + set.remove(__FORWARD_QUERY_STRING); + } + + return Collections.enumeration(set); + } + + /* ------------------------------------------------------------ */ + public void setAttribute(String key, Object value) + { + if (_named==null && key.startsWith("javax.servlet.")) + { + if (key.equals(__FORWARD_PATH_INFO)) + _pathInfo=(String)value; + else if (key.equals(__FORWARD_REQUEST_URI)) + _requestURI=(String)value; + else if (key.equals(__FORWARD_SERVLET_PATH)) + _servletPath=(String)value; + else if (key.equals(__FORWARD_CONTEXT_PATH)) + _contextPath=(String)value; + else if (key.equals(__FORWARD_QUERY_STRING)) + _query=(String)value; + + else if (value==null) + _attr.removeAttribute(key); + else + _attr.setAttribute(key,value); + } + else if (value==null) + _attr.removeAttribute(key); + else + _attr.setAttribute(key,value); + } + + /* ------------------------------------------------------------ */ + public String toString() + { + return "FORWARD+"+_attr.toString(); + } + + /* ------------------------------------------------------------ */ + public void clearAttributes() + { + throw new IllegalStateException(); + } + + /* ------------------------------------------------------------ */ + public void removeAttribute(String name) + { + setAttribute(name,null); + } + } + + /* ------------------------------------------------------------ */ + private class IncludeAttributes implements Attributes + { + Attributes _attr; + + String _requestURI; + String _contextPath; + String _servletPath; + String _pathInfo; + String _query; + + IncludeAttributes(Attributes attributes) + { + _attr=attributes; + } + + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + public Object getAttribute(String key) + { + if (Dispatcher.this._named==null) + { + if (key.equals(__INCLUDE_PATH_INFO)) return _pathInfo; + if (key.equals(__INCLUDE_SERVLET_PATH)) return _servletPath; + if (key.equals(__INCLUDE_CONTEXT_PATH)) return _contextPath; + if (key.equals(__INCLUDE_QUERY_STRING)) return _query; + if (key.equals(__INCLUDE_REQUEST_URI)) return _requestURI; + } + else if (key.startsWith(__INCLUDE_PREFIX)) + return null; + + + return _attr.getAttribute(key); + } + + /* ------------------------------------------------------------ */ + public Enumeration getAttributeNames() + { + HashSet set=new HashSet(); + Enumeration e=_attr.getAttributeNames(); + while(e.hasMoreElements()) + { + String name=(String)e.nextElement(); + if (!name.startsWith(__INCLUDE_PREFIX)) + set.add(name); + } + + if (_named==null) + { + if (_pathInfo!=null) + set.add(__INCLUDE_PATH_INFO); + else + set.remove(__INCLUDE_PATH_INFO); + set.add(__INCLUDE_REQUEST_URI); + set.add(__INCLUDE_SERVLET_PATH); + set.add(__INCLUDE_CONTEXT_PATH); + if (_query!=null) + set.add(__INCLUDE_QUERY_STRING); + else + set.remove(__INCLUDE_QUERY_STRING); + } + + return Collections.enumeration(set); + } + + /* ------------------------------------------------------------ */ + public void setAttribute(String key, Object value) + { + if (_named==null && key.startsWith("javax.servlet.")) + { + if (key.equals(__INCLUDE_PATH_INFO)) _pathInfo=(String)value; + else if (key.equals(__INCLUDE_REQUEST_URI)) _requestURI=(String)value; + else if (key.equals(__INCLUDE_SERVLET_PATH)) _servletPath=(String)value; + else if (key.equals(__INCLUDE_CONTEXT_PATH)) _contextPath=(String)value; + else if (key.equals(__INCLUDE_QUERY_STRING)) _query=(String)value; + else if (value==null) + _attr.removeAttribute(key); + else + _attr.setAttribute(key,value); + } + else if (value==null) + _attr.removeAttribute(key); + else + _attr.setAttribute(key,value); + } + + /* ------------------------------------------------------------ */ + public String toString() + { + return "INCLUDE+"+_attr.toString(); + } + + /* ------------------------------------------------------------ */ + public void clearAttributes() + { + throw new IllegalStateException(); + } + + /* ------------------------------------------------------------ */ + public void removeAttribute(String name) + { + setAttribute(name,null); + } + } +}; diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Handler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Handler.java new file mode 100644 index 0000000000..3e0c3ff5ab --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Handler.java @@ -0,0 +1,48 @@ +// ======================================================================== +// Copyright (c) 2004-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.util.component.LifeCycle; + + +public interface Handler extends LifeCycle +{ + /* ------------------------------------------------------------ */ + /** Handle a request. + * @param target The target of the request - either a URI or a name. + * @param request The request either as the {@link Request} + * object or a wrapper of that request. The {@link HttpConnection#getCurrentConnection()} + * method can be used access the Request object if required. + * @param response The response as the {@link Response} + * object or a wrapper of that request. The {@link HttpConnection#getCurrentConnection()} + * method can be used access the Response object if required. + * @throws IOException + * @throws ServletException + */ + public void handle(String target, HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException; + + public void setServer(Server server); + public Server getServer(); + + public void destroy(); + +} + diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HandlerContainer.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HandlerContainer.java new file mode 100644 index 0000000000..e242f7d30f --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HandlerContainer.java @@ -0,0 +1,33 @@ +// ======================================================================== +// Copyright (c) 2006-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server; + +import org.eclipse.jetty.util.component.LifeCycle; + +/** + * A Handler that contains other Handlers. + * <p> + * The contained handlers may be one (see @{link {@link org.eclipse.jetty.server.server.handler.HandlerWrapper}) + * or many (see {@link org.eclipse.jetty.server.server.handler.HandlerList} or {@link org.eclipse.jetty.server.server.handler.HandlerCollection}. + * + */ +public interface HandlerContainer extends LifeCycle +{ + public void addHandler(Handler handler); + public void removeHandler(Handler handler); + + public Handler[] getChildHandlers(); + public Handler[] getChildHandlersByClass(Class<?> byclass); + public Handler getChildHandlerByClass(Class<?> byclass); +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java new file mode 100644 index 0000000000..3d3584d526 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpConnection.java @@ -0,0 +1,1114 @@ +// ======================================================================== +// Copyright (c) 2004-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; + +import javax.servlet.DispatcherType; +import javax.servlet.ServletInputStream; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.http.AbstractGenerator; +import org.eclipse.jetty.http.EncodedHttpURI; +import org.eclipse.jetty.http.Generator; +import org.eclipse.jetty.http.HttpContent; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpGenerator; +import org.eclipse.jetty.http.HttpHeaderValues; +import org.eclipse.jetty.http.HttpHeaders; +import org.eclipse.jetty.http.HttpMethods; +import org.eclipse.jetty.http.HttpParser; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.http.HttpVersions; +import org.eclipse.jetty.http.MimeTypes; +import org.eclipse.jetty.http.Parser; +import org.eclipse.jetty.io.Buffer; +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.EofException; +import org.eclipse.jetty.io.HttpException; +import org.eclipse.jetty.io.BufferCache.CachedBuffer; +import org.eclipse.jetty.io.nio.SelectChannelEndPoint; +import org.eclipse.jetty.util.QuotedStringTokenizer; +import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.util.URIUtil; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.resource.Resource; +import org.eclipse.jetty.util.thread.Timeout; + +/** + * <p>A HttpConnection represents the connection of a HTTP client to the server + * and is created by an instance of a {@link Connector}. It's prime function is + * to associate {@link Request} and {@link Response} instances with a {@link EndPoint}. + * </p> + * <p> + * A connection is also the prime mechanism used by jetty to recycle objects without + * pooling. The {@link Request}, {@link Response}, {@link HttpParser}, {@link HttpGenerator} + * and {@link HttpFields} instances are all recycled for the duraction of + * a connection. Where appropriate, allocated buffers are also kept associated + * with the connection via the parser and/or generator. + * </p> + * + * + * + * + */ +public class HttpConnection implements Connection +{ + private static int UNKNOWN = -2; + private static ThreadLocal<HttpConnection> __currentConnection = new ThreadLocal<HttpConnection>(); + + private final long _timeStamp=System.currentTimeMillis(); + private int _requests; + private volatile boolean _handling; + + protected final Connector _connector; + protected final EndPoint _endp; + protected final Server _server; + protected final HttpURI _uri; + + protected final Parser _parser; + protected final HttpFields _requestFields; + protected final Request _request; + protected ServletInputStream _in; + + protected final Generator _generator; + protected final HttpFields _responseFields; + protected final Response _response; + protected Output _out; + protected OutputWriter _writer; + protected PrintWriter _printWriter; + + int _include; + + private Object _associatedObject; // associated object + + private transient int _expect = UNKNOWN; + private transient int _version = UNKNOWN; + private transient boolean _head = false; + private transient boolean _host = false; + private transient boolean _delayedHandling=false; + + /* ------------------------------------------------------------ */ + public static HttpConnection getCurrentConnection() + { + return (HttpConnection) __currentConnection.get(); + } + + /* ------------------------------------------------------------ */ + protected static void setCurrentConnection(HttpConnection connection) + { + __currentConnection.set(connection); + } + + /* ------------------------------------------------------------ */ + /** Constructor + * + */ + public HttpConnection(Connector connector, EndPoint endpoint, Server server) + { + _uri = URIUtil.__CHARSET==StringUtil.__UTF8?new HttpURI():new EncodedHttpURI(URIUtil.__CHARSET); + _connector = connector; + _endp = endpoint; + _parser = new HttpParser(_connector, endpoint, new RequestHandler(), _connector.getHeaderBufferSize(), _connector.getRequestBufferSize()); + _requestFields = new HttpFields(); + _responseFields = new HttpFields(); + _request = new Request(this); + _response = new Response(this); + _generator = new HttpGenerator(_connector, _endp, _connector.getHeaderBufferSize(), _connector.getResponseBufferSize()); + _generator.setSendServerVersion(server.getSendServerVersion()); + _server = server; + } + + protected HttpConnection(Connector connector, EndPoint endpoint, Server server, + Parser parser, Generator generator, Request request) + { + _uri = URIUtil.__CHARSET==StringUtil.__UTF8?new HttpURI():new EncodedHttpURI(URIUtil.__CHARSET); + _connector = connector; + _endp = endpoint; + _parser = parser; + _requestFields = new HttpFields(); + _responseFields = new HttpFields(); + _request = request; + _response = new Response(this); + _generator = generator; + _generator.setSendServerVersion(server.getSendServerVersion()); + _server = server; + } + + /* ------------------------------------------------------------ */ + public void destroy() + { + synchronized(this) + { + while(_handling) + Thread.yield(); + + if (_parser!=null) + _parser.reset(true); + + if (_generator!=null) + _generator.reset(true); + + if (_requestFields!=null) + _requestFields.destroy(); + + if (_responseFields!=null) + _responseFields.destroy(); + } + } + + /* ------------------------------------------------------------ */ + /** + * @return the parser used by this connection + */ + public Parser getParser() + { + return _parser; + } + + /* ------------------------------------------------------------ */ + /** + * @return the number of requests handled by this connection + */ + public int getRequests() + { + return _requests; + } + + /* ------------------------------------------------------------ */ + /** + * @return The time this connection was established. + */ + public long getTimeStamp() + { + return _timeStamp; + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the associatedObject. + */ + public Object getAssociatedObject() + { + return _associatedObject; + } + + /* ------------------------------------------------------------ */ + /** + * @param associatedObject The associatedObject to set. + */ + public void setAssociatedObject(Object associatedObject) + { + _associatedObject = associatedObject; + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the connector. + */ + public Connector getConnector() + { + return _connector; + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the requestFields. + */ + public HttpFields getRequestFields() + { + return _requestFields; + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the responseFields. + */ + public HttpFields getResponseFields() + { + return _responseFields; + } + + /* ------------------------------------------------------------ */ + /** + * @return The result of calling {@link #getConnector}.{@link Connector#isConfidential(Request) isCondidential}(request), or false + * if there is no connector. + */ + public boolean isConfidential(Request request) + { + if (_connector!=null) + return _connector.isConfidential(request); + return false; + } + + /* ------------------------------------------------------------ */ + /** + * Find out if the request is INTEGRAL security. + * @param request + * @return <code>true</code> if there is a {@link #getConnector() connector} and it considers <code>request</code> + * to be {@link Connector#isIntegral(Request) integral} + */ + public boolean isIntegral(Request request) + { + if (_connector!=null) + return _connector.isIntegral(request); + return false; + } + + /* ------------------------------------------------------------ */ + /** + * @return The {@link EndPoint} for this connection. + */ + public EndPoint getEndPoint() + { + return _endp; + } + + /* ------------------------------------------------------------ */ + /** + * @return <code>false</code> (this method is not yet implemented) + */ + public boolean getResolveNames() + { + return _connector.getResolveNames(); + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the request. + */ + public Request getRequest() + { + return _request; + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the response. + */ + public Response getResponse() + { + return _response; + } + + /* ------------------------------------------------------------ */ + /** + * @return The input stream for this connection. The stream will be created if it does not already exist. + */ + public ServletInputStream getInputStream() throws IOException + { + // If the client is expecting 100 CONTINUE, then send it now. + if (_expect == HttpHeaderValues.CONTINUE_ORDINAL) + { + if (((HttpParser)_parser).getHeaderBuffer()==null || ((HttpParser)_parser).getHeaderBuffer().length()<2) + { + _generator.setResponse(HttpStatus.CONTINUE_100, null); + _generator.completeHeader(null, true); + _generator.complete(); + _generator.reset(false); + } + _expect = UNKNOWN; + } + + if (_in == null) + _in = new HttpInput(((HttpParser)_parser),_connector.getMaxIdleTime()); + return _in; + } + + /* ------------------------------------------------------------ */ + /** + * @return The output stream for this connection. The stream will be created if it does not already exist. + */ + public ServletOutputStream getOutputStream() + { + if (_out == null) + _out = new Output(); + return _out; + } + + /* ------------------------------------------------------------ */ + /** + * @return A {@link PrintWriter} wrapping the {@link #getOutputStream output stream}. The writer is created if it + * does not already exist. + */ + public PrintWriter getPrintWriter(String encoding) + { + getOutputStream(); + if (_writer==null) + { + _writer=new OutputWriter(); + _printWriter=new PrintWriter(_writer) + { + /* ------------------------------------------------------------ */ + /* + * @see java.io.PrintWriter#close() + */ + public void close() + { + try + { + out.close(); + } + catch(IOException e) + { + Log.debug(e); + setError(); + } + } + + }; + } + _writer.setCharacterEncoding(encoding); + return _printWriter; + } + + /* ------------------------------------------------------------ */ + public boolean isResponseCommitted() + { + return _generator.isCommitted(); + } + + /* ------------------------------------------------------------ */ + public void handle() throws IOException + { + // Loop while more in buffer + boolean more_in_buffer =true; // assume true until proven otherwise + boolean progress=true; + + try + { + _handling=true; + setCurrentConnection(this); + + while (more_in_buffer) + { + try + { + if (_request._async.isAsync()) + { + Log.debug("resume request",_request); + if (!_request._async.isComplete()) + handleRequest(); + else if (!_parser.isComplete()) + progress|=_parser.parseAvailable()>0; + + if (_generator.isCommitted() && !_generator.isComplete()) + _generator.flushBuffer(); + if (_endp.isBufferingOutput()) + _endp.flush(); + } + else + { + // If we are not ended then parse available + if (!_parser.isComplete()) + progress|=_parser.parseAvailable()>0; + + // Do we have more generating to do? + // Loop here because some writes may take multiple steps and + // we need to flush them all before potentially blocking in the + // next loop. + while (_generator.isCommitted() && !_generator.isComplete()) + { + long written=_generator.flushBuffer(); + if (written<=0) + break; + progress=true; + if (_endp.isBufferingOutput()) + _endp.flush(); + } + + // Flush buffers + if (_endp.isBufferingOutput()) + { + _endp.flush(); + if (!_endp.isBufferingOutput()) + progress=true; + } + + if (!progress) + return; + progress=false; + } + } + catch (HttpException e) + { + if (Log.isDebugEnabled()) + { + Log.debug("uri="+_uri); + Log.debug("fields="+_requestFields); + Log.debug(e); + } + _generator.sendError(e.getStatus(), e.getReason(), null, true); + + _parser.reset(true); + _endp.close(); + throw e; + } + finally + { + more_in_buffer = _parser.isMoreInBuffer() || _endp.isBufferingInput(); + + if (_parser.isComplete() && _generator.isComplete() && !_endp.isBufferingOutput()) + { + if (!_generator.isPersistent()) + { + _parser.reset(true); + more_in_buffer=false; + } + + reset(!more_in_buffer); + progress=true; + } + + if (_request.isAsyncStarted()) + { + Log.debug("return with suspended request"); + more_in_buffer=false; + } + else if (_generator.isCommitted() && !_generator.isComplete() && _endp instanceof SelectChannelEndPoint) // TODO remove SelectChannel dependency + ((SelectChannelEndPoint)_endp).setWritable(false); + } + } + } + finally + { + setCurrentConnection(null); + _handling=false; + } + } + + /* ------------------------------------------------------------ */ + public void scheduleTimeout(Timeout.Task task, long timeoutMs) + { + throw new UnsupportedOperationException(); + } + + /* ------------------------------------------------------------ */ + public void cancelTimeout(Timeout.Task task) + { + throw new UnsupportedOperationException(); + } + + /* ------------------------------------------------------------ */ + public void reset(boolean returnBuffers) + { + _parser.reset(returnBuffers); // TODO maybe only release when low on resources + _requestFields.clear(); + _request.recycle(); + + _generator.reset(returnBuffers); // TODO maybe only release when low on resources + _responseFields.clear(); + _response.recycle(); + + _uri.clear(); + } + + /* ------------------------------------------------------------ */ + protected void handleRequest() throws IOException + { + boolean handling=_server.isRunning() && _request._async.handling(); + boolean error = false; + + String threadName=null; + try + { + if (Log.isDebugEnabled()) + { + threadName=Thread.currentThread().getName(); + Thread.currentThread().setName(threadName+" - "+_uri); + } + + // Loop here to handle async request redispatches. + // The loop is controlled by the call to async.unhandle in the + // finally block below. If call is from a non-blocking connector, + // then the unhandle will return false only if an async dispatch has + // already happened when unhandle is called. For a blocking connector, + // the wait for the asynchronous dispatch or timeout actually happens + // within the call to unhandle(). + while (handling) + { + _request.setHandled(false); + try + { + String info=URIUtil.canonicalPath(_uri.getDecodedPath()); + if (info==null) + throw new HttpException(400); + _request.setPathInfo(info); + + if (_out!=null) + _out.reopen(); + + if (_request._async.isInitial()) + { + _request.setDispatcherType(DispatcherType.REQUEST); + _connector.customize(_endp, _request); + _server.handle(this); + } + else + { + _request.setDispatcherType(DispatcherType.ASYNC); + _server.handleAsync(this); + } + + } + catch (RetryRequest r) + { + Log.ignore(r); + } + catch (EofException e) + { + Log.ignore(e); + error=true; + } + catch (HttpException e) + { + Log.debug(e); + _request.setHandled(true); + _response.sendError(e.getStatus(), e.getReason()); + error=true; + } + catch (Exception e) + { + Log.warn(e); + _request.setHandled(true); + _generator.sendError(500, null, null, true); + error=true; + } + catch (Error e) + { + Log.warn(e); + _request.setHandled(true); + _generator.sendError(500, null, null, true); + error=true; + } + finally + { + handling = !_request._async.unhandle() && _server != null; + } + } + } + finally + { + if (threadName!=null) + Thread.currentThread().setName(threadName); + + if (_request._async.isUncompleted()) + { + _request._async.doComplete(); + + if (_expect == HttpHeaderValues.CONTINUE_ORDINAL) + { + // Continue not sent so don't parse any content + _expect = UNKNOWN; + if (_parser instanceof HttpParser) + ((HttpParser)_parser).setState(HttpParser.STATE_END); + } + + if(_endp.isOpen()) + { + if (_generator.isPersistent()) + _connector.persist(_endp); + + if (error) + _endp.close(); + else + { + if (!_response.isCommitted() && !_request.isHandled()) + _response.sendError(HttpServletResponse.SC_NOT_FOUND); + _response.complete(); + } + } + else + { + _response.complete(); + } + + _request.setHandled(true); + } + } + } + + /* ------------------------------------------------------------ */ + public void commitResponse(boolean last) throws IOException + { + if (!_generator.isCommitted()) + { + _generator.setResponse(_response.getStatus(), _response.getReason()); + _generator.completeHeader(_responseFields, last); + } + if (last) + _generator.complete(); + } + + /* ------------------------------------------------------------ */ + public void completeResponse() throws IOException + { + if (!_generator.isCommitted()) + { + _generator.setResponse(_response.getStatus(), _response.getReason()); + _generator.completeHeader(_responseFields, HttpGenerator.LAST); + } + + _generator.complete(); + } + + /* ------------------------------------------------------------ */ + public void flushResponse() throws IOException + { + try + { + commitResponse(HttpGenerator.MORE); + _generator.flushBuffer(); + } + catch(IOException e) + { + throw (e instanceof EofException) ? e:new EofException(e); + } + } + + /* ------------------------------------------------------------ */ + public Generator getGenerator() + { + return _generator; + } + + + /* ------------------------------------------------------------ */ + public boolean isIncluding() + { + return _include>0; + } + + /* ------------------------------------------------------------ */ + public void include() + { + _include++; + } + + /* ------------------------------------------------------------ */ + public void included() + { + _include--; + if (_out!=null) + _out.reopen(); + } + + /* ------------------------------------------------------------ */ + public boolean isIdle() + { + return _generator.isIdle() && (_parser.isIdle() || _delayedHandling); + } + + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + private class RequestHandler extends HttpParser.EventHandler + { + private String _charset; + + /* + * + * @see org.eclipse.jetty.server.server.HttpParser.EventHandler#startRequest(org.eclipse.io.Buffer, + * org.eclipse.io.Buffer, org.eclipse.io.Buffer) + */ + public void startRequest(Buffer method, Buffer uri, Buffer version) throws IOException + { + _host = false; + _expect = UNKNOWN; + _delayedHandling=false; + _charset=null; + + if(_request.getTimeStamp()==0) + _request.setTimeStamp(System.currentTimeMillis()); + _request.setMethod(method.toString()); + + try + { + _uri.parse(uri.array(), uri.getIndex(), uri.length()); + _request.setUri(_uri); + + if (version==null) + { + _request.setProtocol(HttpVersions.HTTP_0_9); + _version=HttpVersions.HTTP_0_9_ORDINAL; + } + else + { + version= HttpVersions.CACHE.get(version); + _version = HttpVersions.CACHE.getOrdinal(version); + if (_version <= 0) _version = HttpVersions.HTTP_1_0_ORDINAL; + _request.setProtocol(version.toString()); + } + + _head = method == HttpMethods.HEAD_BUFFER; // depends on method being decached. + } + catch (Exception e) + { + throw new HttpException(HttpStatus.BAD_REQUEST_400,null,e); + } + } + + /* + * @see org.eclipse.jetty.server.server.HttpParser.EventHandler#parsedHeaderValue(org.eclipse.io.Buffer) + */ + public void parsedHeader(Buffer name, Buffer value) + { + int ho = HttpHeaders.CACHE.getOrdinal(name); + switch (ho) + { + case HttpHeaders.HOST_ORDINAL: + // TODO check if host matched a host in the URI. + _host = true; + break; + + case HttpHeaders.EXPECT_ORDINAL: + value = HttpHeaderValues.CACHE.lookup(value); + _expect = HttpHeaderValues.CACHE.getOrdinal(value); + break; + + case HttpHeaders.ACCEPT_ENCODING_ORDINAL: + case HttpHeaders.USER_AGENT_ORDINAL: + value = HttpHeaderValues.CACHE.lookup(value); + break; + + case HttpHeaders.CONTENT_TYPE_ORDINAL: + value = MimeTypes.CACHE.lookup(value); + _charset=MimeTypes.getCharsetFromContentType(value); + break; + + case HttpHeaders.CONNECTION_ORDINAL: + //looks rather clumsy, but the idea is to optimize for a single valued header + int ordinal = HttpHeaderValues.CACHE.getOrdinal(value); + switch(ordinal) + { + case -1: + { + String[] values = value.toString().split(","); + for (int i=0;values!=null && i<values.length;i++) + { + CachedBuffer cb = HttpHeaderValues.CACHE.get(values[i].trim()); + + if (cb!=null) + { + switch(cb.getOrdinal()) + { + case HttpHeaderValues.CLOSE_ORDINAL: + _responseFields.add(HttpHeaders.CONNECTION_BUFFER,HttpHeaderValues.CLOSE_BUFFER); + _generator.setPersistent(false); + break; + + case HttpHeaderValues.KEEP_ALIVE_ORDINAL: + if (_version==HttpVersions.HTTP_1_0_ORDINAL) + _responseFields.add(HttpHeaders.CONNECTION_BUFFER,HttpHeaderValues.KEEP_ALIVE_BUFFER); + break; + } + } + } + break; + } + case HttpHeaderValues.CLOSE_ORDINAL: + _responseFields.put(HttpHeaders.CONNECTION_BUFFER,HttpHeaderValues.CLOSE_BUFFER); + _generator.setPersistent(false); + break; + + case HttpHeaderValues.KEEP_ALIVE_ORDINAL: + if (_version==HttpVersions.HTTP_1_0_ORDINAL) + _responseFields.put(HttpHeaders.CONNECTION_BUFFER,HttpHeaderValues.KEEP_ALIVE_BUFFER); + break; + } + } + + _requestFields.add(name, value); + } + + /* + * @see org.eclipse.jetty.server.server.HttpParser.EventHandler#headerComplete() + */ + public void headerComplete() throws IOException + { + _requests++; + _generator.setVersion(_version); + switch (_version) + { + case HttpVersions.HTTP_0_9_ORDINAL: + break; + case HttpVersions.HTTP_1_0_ORDINAL: + _generator.setHead(_head); + break; + case HttpVersions.HTTP_1_1_ORDINAL: + _generator.setHead(_head); + + if (_server.getSendDateHeader()) + _responseFields.put(HttpHeaders.DATE_BUFFER, _request.getTimeStampBuffer(),_request.getTimeStamp()); + + if (!_host) + { + _generator.setResponse(HttpStatus.BAD_REQUEST_400, null); + _responseFields.put(HttpHeaders.CONNECTION_BUFFER, HttpHeaderValues.CLOSE_BUFFER); + _generator.completeHeader(_responseFields, true); + _generator.complete(); + return; + } + + if (_expect != UNKNOWN) + { + if (_expect == HttpHeaderValues.CONTINUE_ORDINAL) + { + } + else if (_expect == HttpHeaderValues.PROCESSING_ORDINAL) + { + } + else + { + _generator.sendError(HttpStatus.EXPECTATION_FAILED_417, null, null, true); + return; + } + } + + break; + default: + } + + if(_charset!=null) + _request.setCharacterEncodingUnchecked(_charset); + + // Either handle now or wait for first content + if ((((HttpParser)_parser).getContentLength()<=0 && !((HttpParser)_parser).isChunking())||_expect==HttpHeaderValues.CONTINUE_ORDINAL) + handleRequest(); + else + _delayedHandling=true; + } + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.jetty.server.server.HttpParser.EventHandler#content(int, org.eclipse.io.Buffer) + */ + public void content(Buffer ref) throws IOException + { + if (_delayedHandling) + { + _delayedHandling=false; + handleRequest(); + } + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.jetty.server.server.HttpParser.EventHandler#messageComplete(int) + */ + public void messageComplete(long contentLength) throws IOException + { + if (_delayedHandling) + { + _delayedHandling=false; + handleRequest(); + } + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.jetty.server.server.HttpParser.EventHandler#startResponse(org.eclipse.io.Buffer, int, + * org.eclipse.io.Buffer) + */ + public void startResponse(Buffer version, int status, Buffer reason) + { + Log.debug("Bad request!: "+version+" "+status+" "+reason); + } + + } + + + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + public class Output extends HttpOutput + { + Output() + { + super((AbstractGenerator)HttpConnection.this._generator,_connector.getMaxIdleTime()); + } + + /* ------------------------------------------------------------ */ + /* + * @see java.io.OutputStream#close() + */ + public void close() throws IOException + { + if (_closed) + return; + + if (!isIncluding() && !_generator.isCommitted()) + commitResponse(HttpGenerator.LAST); + else + flushResponse(); + + super.close(); + } + + + /* ------------------------------------------------------------ */ + /* + * @see java.io.OutputStream#flush() + */ + public void flush() throws IOException + { + if (!_generator.isCommitted()) + commitResponse(HttpGenerator.MORE); + super.flush(); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletOutputStream#print(java.lang.String) + */ + public void print(String s) throws IOException + { + if (_closed) + throw new IOException("Closed"); + PrintWriter writer=getPrintWriter(null); + writer.print(s); + } + + /* ------------------------------------------------------------ */ + public void sendResponse(Buffer response) throws IOException + { + ((HttpGenerator)_generator).sendResponse(response); + } + + /* ------------------------------------------------------------ */ + public void sendContent(Object content) throws IOException + { + Resource resource=null; + + if (_closed) + throw new IOException("Closed"); + + if (_generator.getContentWritten() > 0) throw new IllegalStateException("!empty"); + + if (content instanceof HttpContent) + { + HttpContent c = (HttpContent) content; + Buffer contentType = c.getContentType(); + if (contentType != null && !_responseFields.containsKey(HttpHeaders.CONTENT_TYPE_BUFFER)) + { + String enc = _response.getSetCharacterEncoding(); + if(enc==null) + _responseFields.add(HttpHeaders.CONTENT_TYPE_BUFFER, contentType); + else + { + if(contentType instanceof CachedBuffer) + { + CachedBuffer content_type = ((CachedBuffer)contentType).getAssociate(enc); + if(content_type!=null) + _responseFields.put(HttpHeaders.CONTENT_TYPE_BUFFER, content_type); + else + { + _responseFields.put(HttpHeaders.CONTENT_TYPE_BUFFER, + contentType+";charset="+QuotedStringTokenizer.quote(enc,";= ")); + } + } + else + { + _responseFields.put(HttpHeaders.CONTENT_TYPE_BUFFER, + contentType+";charset="+QuotedStringTokenizer.quote(enc,";= ")); + } + } + } + if (c.getContentLength() > 0) + _responseFields.putLongField(HttpHeaders.CONTENT_LENGTH_BUFFER, c.getContentLength()); + Buffer lm = c.getLastModified(); + long lml=c.getResource().lastModified(); + if (lm != null) + _responseFields.put(HttpHeaders.LAST_MODIFIED_BUFFER, lm,lml); + else if (c.getResource()!=null) + { + if (lml!=-1) + _responseFields.putDateField(HttpHeaders.LAST_MODIFIED_BUFFER, lml); + } + + content = c.getBuffer(); + if (content==null) + content=c.getInputStream(); + } + else if (content instanceof Resource) + { + resource=(Resource)content; + _responseFields.putDateField(HttpHeaders.LAST_MODIFIED_BUFFER, resource.lastModified()); + content=resource.getInputStream(); + } + + + if (content instanceof Buffer) + { + _generator.addContent((Buffer) content, HttpGenerator.LAST); + commitResponse(HttpGenerator.LAST); + } + else if (content instanceof InputStream) + { + InputStream in = (InputStream)content; + + try + { + int max = _generator.prepareUncheckedAddContent(); + Buffer buffer = _generator.getUncheckedBuffer(); + + int len=buffer.readFrom(in,max); + + while (len>=0) + { + _generator.completeUncheckedAddContent(); + _out.flush(); + + max = _generator.prepareUncheckedAddContent(); + buffer = _generator.getUncheckedBuffer(); + len=buffer.readFrom(in,max); + } + _generator.completeUncheckedAddContent(); + _out.flush(); + } + finally + { + if (resource!=null) + resource.release(); + else + in.close(); + + } + } + else + throw new IllegalArgumentException("unknown content type?"); + + + } + } + + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + public class OutputWriter extends HttpWriter + { + OutputWriter() + { + super(HttpConnection.this._out); + } + } + +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java new file mode 100644 index 0000000000..d817e55bc8 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpInput.java @@ -0,0 +1,62 @@ +// ======================================================================== +// Copyright (c) 2009-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server; + +import java.io.IOException; + +import javax.servlet.ServletInputStream; + +import org.eclipse.jetty.http.HttpParser; +import org.eclipse.jetty.io.Buffer; + +public class HttpInput extends ServletInputStream +{ + protected final HttpParser _parser; + protected final long _maxIdleTime; + + /* ------------------------------------------------------------ */ + public HttpInput(HttpParser parser, long maxIdleTime) + { + _parser=parser; + _maxIdleTime=maxIdleTime; + } + + /* ------------------------------------------------------------ */ + /* + * @see java.io.InputStream#read() + */ + public int read() throws IOException + { + int c=-1; + Buffer content=_parser.blockForContent(_maxIdleTime); + if (content!=null) + c= 0xff & content.get(); + return c; + } + + /* ------------------------------------------------------------ */ + /* + * @see java.io.InputStream#read(byte[], int, int) + */ + public int read(byte[] b, int off, int len) throws IOException + { + int l=-1; + Buffer content=_parser.blockForContent(_maxIdleTime); + if (content!=null) + l= content.get(b, off, len); + return l; + } + + +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOnlyCookie.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOnlyCookie.java new file mode 100644 index 0000000000..6d21c11807 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOnlyCookie.java @@ -0,0 +1,45 @@ +// ======================================================================== +// Copyright (c) 2004-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server; + +import javax.servlet.http.Cookie; + +/* ------------------------------------------------------------ */ +/** HttpOnlyCookie. + * + * <p> + * Implements {@link javax.servlet.Cookie} from the {@link javax.servlet} package. + * </p> + * This derivation of javax.servlet.http.Cookie can be used to indicate + * that the microsoft httponly extension should be used. + * The addSetCookie method on HttpFields checks for this type. + * @deprecated use {@link javax.servlet.Cookie#setHttpOnly(boolean)} + * + * + */ +public class HttpOnlyCookie extends Cookie +{ + + /* ------------------------------------------------------------ */ + /** + * @param name + * @param value + */ + public HttpOnlyCookie(String name, String value) + { + super(name, value); + setHttpOnly(true); + } + +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java new file mode 100644 index 0000000000..75a0a53d18 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java @@ -0,0 +1,173 @@ +// ======================================================================== +// Copyright (c) 2008-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server; + +import java.io.IOException; +import java.io.Writer; + +import javax.servlet.ServletOutputStream; + +import org.eclipse.jetty.http.AbstractGenerator; +import org.eclipse.jetty.http.Generator; +import org.eclipse.jetty.io.Buffer; +import org.eclipse.jetty.io.ByteArrayBuffer; +import org.eclipse.jetty.io.EofException; +import org.eclipse.jetty.util.ByteArrayOutputStream2; + +/** Output. + * + * <p> + * Implements {@link javax.servlet.ServletOutputStream} from the {@link javax.servlet} package. + * </p> + * A {@link ServletOutputStream} implementation that writes content + * to a {@link AbstractGenerator}. The class is designed to be reused + * and can be reopened after a close. + */ +public class HttpOutput extends ServletOutputStream +{ + protected final AbstractGenerator _generator; + protected final long _maxIdleTime; + protected final ByteArrayBuffer _buf = new ByteArrayBuffer(AbstractGenerator.NO_BYTES); + protected boolean _closed; + + // These are held here for reuse by Writer + String _characterEncoding; + Writer _converter; + char[] _chars; + ByteArrayOutputStream2 _bytes; + + + /* ------------------------------------------------------------ */ + public HttpOutput(AbstractGenerator generator, long maxIdleTime) + { + _generator=generator; + _maxIdleTime=maxIdleTime; + } + + /* ------------------------------------------------------------ */ + /* + * @see java.io.OutputStream#close() + */ + public void close() throws IOException + { + _closed=true; + } + + /* ------------------------------------------------------------ */ + public void reopen() + { + _closed=false; + } + + /* ------------------------------------------------------------ */ + public void flush() throws IOException + { + _generator.flush(_maxIdleTime); + } + + /* ------------------------------------------------------------ */ + public void write(byte[] b, int off, int len) throws IOException + { + _buf.wrap(b, off, len); + write(_buf); + } + + /* ------------------------------------------------------------ */ + /* + * @see java.io.OutputStream#write(byte[]) + */ + public void write(byte[] b) throws IOException + { + _buf.wrap(b); + write(_buf); + } + + /* ------------------------------------------------------------ */ + /* + * @see java.io.OutputStream#write(int) + */ + public void write(int b) throws IOException + { + if (_closed) + throw new IOException("Closed"); + if (!_generator.isOpen()) + throw new EofException(); + + // Block until we can add _content. + while (_generator.isBufferFull()) + { + _generator.blockForOutput(_maxIdleTime); + if (_closed) + throw new IOException("Closed"); + if (!_generator.isOpen()) + throw new EofException(); + } + + // Add the _content + if (_generator.addContent((byte)b)) + // Buffers are full so flush. + flush(); + + if (_generator.isContentWritten()) + { + flush(); + close(); + } + } + + /* ------------------------------------------------------------ */ + private void write(Buffer buffer) throws IOException + { + if (_closed) + throw new IOException("Closed"); + if (!_generator.isOpen()) + throw new EofException(); + + // Block until we can add _content. + while (_generator.isBufferFull()) + { + _generator.blockForOutput(_maxIdleTime); + if (_closed) + throw new IOException("Closed"); + if (!_generator.isOpen()) + throw new EofException(); + } + + // Add the _content + _generator.addContent(buffer, Generator.MORE); + + // Have to flush and complete headers? + if (_generator.isBufferFull()) + flush(); + + if (_generator.isContentWritten()) + { + flush(); + close(); + } + + // Block until our buffer is free + while (buffer.length() > 0 && _generator.isOpen()) + _generator.blockForOutput(_maxIdleTime); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletOutputStream#print(java.lang.String) + */ + public void print(String s) throws IOException + { + write(s.getBytes()); + } +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/HttpWriter.java b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpWriter.java new file mode 100644 index 0000000000..723802d91e --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/HttpWriter.java @@ -0,0 +1,266 @@ +// ======================================================================== +// Copyright (c) 2008-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server; + +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; + +import org.eclipse.jetty.http.AbstractGenerator; +import org.eclipse.jetty.util.ByteArrayOutputStream2; +import org.eclipse.jetty.util.StringUtil; + +/** OutputWriter. + * A writer that can wrap a {@link HttpOutput} stream and provide + * character encodings. + * + * The UTF-8 encoding is done by this class and no additional + * buffers or Writers are used. + * The UTF-8 code was inspired by http://javolution.org + */ +public class HttpWriter extends Writer +{ + private static final int WRITE_CONV = 0; + private static final int WRITE_ISO1 = 1; + private static final int WRITE_UTF8 = 2; + + HttpOutput _out; + AbstractGenerator _generator; + int _writeMode; + int _surrogate; + + /* ------------------------------------------------------------ */ + public HttpWriter(HttpOutput out) + { + _out=out; + _generator=_out._generator; + + } + + /* ------------------------------------------------------------ */ + public void setCharacterEncoding(String encoding) + { + if (encoding == null || StringUtil.__ISO_8859_1.equalsIgnoreCase(encoding)) + { + _writeMode = WRITE_ISO1; + } + else if (StringUtil.__UTF8.equalsIgnoreCase(encoding)) + { + _writeMode = WRITE_UTF8; + } + else + { + _writeMode = WRITE_CONV; + if (_out._characterEncoding == null || !_out._characterEncoding.equalsIgnoreCase(encoding)) + _out._converter = null; // Set lazily in getConverter() + } + + _out._characterEncoding = encoding; + if (_out._bytes==null) + _out._bytes = new ByteArrayOutputStream2(AbstractGenerator.MAX_OUTPUT_CHARS); + } + + /* ------------------------------------------------------------ */ + public void close() throws IOException + { + _out.close(); + } + + /* ------------------------------------------------------------ */ + public void flush() throws IOException + { + _out.flush(); + } + + /* ------------------------------------------------------------ */ + public void write (String s,int offset, int length) throws IOException + { + while (length > AbstractGenerator.MAX_OUTPUT_CHARS) + { + write(s, offset, AbstractGenerator.MAX_OUTPUT_CHARS); + offset += AbstractGenerator.MAX_OUTPUT_CHARS; + length -= AbstractGenerator.MAX_OUTPUT_CHARS; + } + + if (_out._chars==null) + { + _out._chars = new char[AbstractGenerator.MAX_OUTPUT_CHARS]; + } + char[] chars = _out._chars; + s.getChars(offset, offset + length, chars, 0); + write(chars, 0, length); + } + + /* ------------------------------------------------------------ */ + public void write (char[] s,int offset, int length) throws IOException + { + HttpOutput out = _out; + + while (length > 0) + { + out._bytes.reset(); + int chars = length>AbstractGenerator.MAX_OUTPUT_CHARS?AbstractGenerator.MAX_OUTPUT_CHARS:length; + + switch (_writeMode) + { + case WRITE_CONV: + { + Writer converter=getConverter(); + converter.write(s, offset, chars); + converter.flush(); + } + break; + + case WRITE_ISO1: + { + byte[] buffer=out._bytes.getBuf(); + int bytes=out._bytes.getCount(); + + if (chars>buffer.length-bytes) + chars=buffer.length-bytes; + + for (int i = 0; i < chars; i++) + { + int c = s[offset+i]; + buffer[bytes++]=(byte)(c<256?c:'?'); // ISO-1 and UTF-8 match for 0 - 255 + } + if (bytes>=0) + out._bytes.setCount(bytes); + + break; + } + + case WRITE_UTF8: + { + byte[] buffer=out._bytes.getBuf(); + int bytes=out._bytes.getCount(); + + if (bytes+chars>buffer.length) + chars=buffer.length-bytes; + + for (int i = 0; i < chars; i++) + { + int code = s[offset+i]; + + if ((code & 0xffffff80) == 0) + { + // 1b + buffer[bytes++]=(byte)(code); + } + else if((code&0xfffff800)==0) + { + // 2b + if (bytes+2>buffer.length) + { + chars=i; + break; + } + buffer[bytes++]=(byte)(0xc0|(code>>6)); + buffer[bytes++]=(byte)(0x80|(code&0x3f)); + + if (bytes+chars-i-1>buffer.length) + chars-=1; + } + else if((code&0xffff0000)==0) + { + // 3b + if (bytes+3>buffer.length) + { + chars=i; + break; + } + buffer[bytes++]=(byte)(0xe0|(code>>12)); + buffer[bytes++]=(byte)(0x80|((code>>6)&0x3f)); + buffer[bytes++]=(byte)(0x80|(code&0x3f)); + + if (bytes+chars-i-1>buffer.length) + chars-=2; + } + else if((code&0xff200000)==0) + { + // 4b + if (bytes+4>buffer.length) + { + chars=i; + break; + } + buffer[bytes++]=(byte)(0xf0|(code>>18)); + buffer[bytes++]=(byte)(0x80|((code>>12)&0x3f)); + buffer[bytes++]=(byte)(0x80|((code>>6)&0x3f)); + buffer[bytes++]=(byte)(0x80|(code&0x3f)); + + if (bytes+chars-i-1>buffer.length) + chars-=3; + } + else if((code&0xf4000000)==0) + { + // 5b + if (bytes+5>buffer.length) + { + chars=i; + break; + } + buffer[bytes++]=(byte)(0xf8|(code>>24)); + buffer[bytes++]=(byte)(0x80|((code>>18)&0x3f)); + buffer[bytes++]=(byte)(0x80|((code>>12)&0x3f)); + buffer[bytes++]=(byte)(0x80|((code>>6)&0x3f)); + buffer[bytes++]=(byte)(0x80|(code&0x3f)); + + if (bytes+chars-i-1>buffer.length) + chars-=4; + } + else if((code&0x80000000)==0) + { + // 6b + if (bytes+6>buffer.length) + { + chars=i; + break; + } + buffer[bytes++]=(byte)(0xfc|(code>>30)); + buffer[bytes++]=(byte)(0x80|((code>>24)&0x3f)); + buffer[bytes++]=(byte)(0x80|((code>>18)&0x3f)); + buffer[bytes++]=(byte)(0x80|((code>>12)&0x3f)); + buffer[bytes++]=(byte)(0x80|((code>>6)&0x3f)); + buffer[bytes++]=(byte)(0x80|(code&0x3f)); + + if (bytes+chars-i-1>buffer.length) + chars-=5; + } + else + { + buffer[bytes++]=(byte)('?'); + } + } + out._bytes.setCount(bytes); + break; + } + default: + throw new IllegalStateException(); + } + + out._bytes.writeTo(out); + length-=chars; + offset+=chars; + } + } + + /* ------------------------------------------------------------ */ + private Writer getConverter() throws IOException + { + if (_out._converter == null) + _out._converter = new OutputStreamWriter(_out._bytes, _out._characterEncoding); + return _out._converter; + } +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/InclusiveByteRange.java b/jetty-server/src/main/java/org/eclipse/jetty/server/InclusiveByteRange.java new file mode 100644 index 0000000000..e99850c117 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/InclusiveByteRange.java @@ -0,0 +1,211 @@ +// ======================================================================== +// Copyright (c) 2002-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server; + +import java.util.Enumeration; +import java.util.List; +import java.util.StringTokenizer; + +import org.eclipse.jetty.util.LazyList; +import org.eclipse.jetty.util.log.Log; + +/* ------------------------------------------------------------ */ +/** Byte range inclusive of end points. + * <PRE> + * + * parses the following types of byte ranges: + * + * bytes=100-499 + * bytes=-300 + * bytes=100- + * bytes=1-2,2-3,6-,-2 + * + * given an entity length, converts range to string + * + * bytes 100-499/500 + * + * </PRE> + * + * Based on RFC2616 3.12, 14.16, 14.35.1, 14.35.2 + * @version $version$ + * + */ +public class InclusiveByteRange +{ + long first = 0; + long last = 0; + + public InclusiveByteRange(long first, long last) + { + this.first = first; + this.last = last; + } + + public long getFirst() + { + return first; + } + + public long getLast() + { + return last; + } + + + + /* ------------------------------------------------------------ */ + /** + * @param headers Enumeration of Range header fields. + * @param size Size of the resource. + * @return LazyList of satisfiable ranges + */ + public static List satisfiableRanges(Enumeration headers,long size) + { + Object satRanges=null; + + // walk through all Range headers + headers: + while (headers.hasMoreElements()) + { + String header = (String) headers.nextElement(); + StringTokenizer tok = new StringTokenizer(header,"=,",false); + String t=null; + try + { + // read all byte ranges for this header + while (tok.hasMoreTokens()) + { + t=tok.nextToken().trim(); + + long first = -1; + long last = -1; + int d=t.indexOf('-'); + if (d<0 || t.indexOf("-",d+1)>=0) + { + if ("bytes".equals(t)) + continue; + Log.warn("Bad range format: {}",t); + continue headers; + } + else if (d==0) + { + if (d+1<t.length()) + last = Long.parseLong(t.substring(d+1).trim()); + else + { + Log.warn("Bad range format: {}",t); + continue headers; + } + } + else if (d+1<t.length()) + { + first = Long.parseLong(t.substring(0,d).trim()); + last = Long.parseLong(t.substring(d+1).trim()); + } + else + first = Long.parseLong(t.substring(0,d).trim()); + + + if (first == -1 && last == -1) + continue headers; + + if (first != -1 && last != -1 && (first > last)) + continue headers; + + if (first<size) + { + InclusiveByteRange range = new + InclusiveByteRange(first, last); + satRanges=LazyList.add(satRanges,range); + } + } + } + catch(Exception e) + { + Log.warn("Bad range format: "+t); + Log.ignore(e); + } + } + return LazyList.getList(satRanges,true); + } + + /* ------------------------------------------------------------ */ + public long getFirst(long size) + { + if (first<0) + { + long tf=size-last; + if (tf<0) + tf=0; + return tf; + } + return first; + } + + /* ------------------------------------------------------------ */ + public long getLast(long size) + { + if (first<0) + return size-1; + + if (last<0 ||last>=size) + return size-1; + return last; + } + + /* ------------------------------------------------------------ */ + public long getSize(long size) + { + return getLast(size)-getFirst(size)+1; + } + + + /* ------------------------------------------------------------ */ + public String toHeaderRangeString(long size) + { + StringBuilder sb = new StringBuilder(40); + sb.append("bytes "); + sb.append(getFirst(size)); + sb.append('-'); + sb.append(getLast(size)); + sb.append("/"); + sb.append(size); + return sb.toString(); + } + + /* ------------------------------------------------------------ */ + public static String to416HeaderRangeString(long size) + { + StringBuilder sb = new StringBuilder(40); + sb.append("bytes */"); + sb.append(size); + return sb.toString(); + } + + + /* ------------------------------------------------------------ */ + public String toString() + { + StringBuilder sb = new StringBuilder(60); + sb.append(Long.toString(first)); + sb.append(":"); + sb.append(Long.toString(last)); + return sb.toString(); + } + + +} + + + diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/LocalConnector.java b/jetty-server/src/main/java/org/eclipse/jetty/server/LocalConnector.java new file mode 100644 index 0000000000..1eed8de27d --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/LocalConnector.java @@ -0,0 +1,221 @@ +// ======================================================================== +// Copyright (c) 2006-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server; + +import java.io.IOException; + +import org.eclipse.jetty.io.Buffer; +import org.eclipse.jetty.io.ByteArrayBuffer; +import org.eclipse.jetty.io.ByteArrayEndPoint; +import org.eclipse.jetty.util.StringUtil; + +public class LocalConnector extends AbstractConnector +{ + ByteArrayEndPoint _endp; + ByteArrayBuffer _in; + ByteArrayBuffer _out; + + Server _server; + boolean _accepting; + boolean _keepOpen; + + public LocalConnector() + { + setPort(1); + } + + /* ------------------------------------------------------------ */ + public Object getConnection() + { + return _endp; + } + + + /* ------------------------------------------------------------ */ + public void setServer(Server server) + { + super.setServer(server); + this._server=server; + } + + /* ------------------------------------------------------------ */ + public void clear() + { + _in.clear(); + _out.clear(); + } + + /* ------------------------------------------------------------ */ + public void reopen() + { + _in.clear(); + _out.clear(); + _endp = new ByteArrayEndPoint(); + _endp.setIn(_in); + _endp.setOut(_out); + _endp.setGrowOutput(true); + _accepting=false; + } + + /* ------------------------------------------------------------ */ + public void doStart() + throws Exception + { + _in=new ByteArrayBuffer(8192); + _out=new ByteArrayBuffer(8192); + _endp = new ByteArrayEndPoint(); + _endp.setIn(_in); + _endp.setOut(_out); + _endp.setGrowOutput(true); + _accepting=false; + + super.doStart(); + } + + /* ------------------------------------------------------------ */ + public String getResponses(String requests) + throws Exception + { + return getResponses(requests,false); + } + + /* ------------------------------------------------------------ */ + public String getResponses(String requests, boolean keepOpen) + throws Exception + { + // System.out.println("\nREQUESTS :\n"+requests); + // System.out.flush(); + ByteArrayBuffer buf=new ByteArrayBuffer(requests,StringUtil.__ISO_8859_1); + if (_in.space()<buf.length()) + { + ByteArrayBuffer n = new ByteArrayBuffer(_in.length()+buf.length()); + n.put(_in); + _in=n; + _endp.setIn(_in); + } + _in.put(buf); + + synchronized (this) + { + _keepOpen=keepOpen; + _accepting=true; + this.notify(); + + while(_accepting) + this.wait(); + } + + // System.err.println("\nRESPONSES:\n"+out); + _out=_endp.getOut(); + return _out.toString(StringUtil.__ISO_8859_1); + } + + /* ------------------------------------------------------------ */ + public ByteArrayBuffer getResponses(ByteArrayBuffer buf, boolean keepOpen) + throws Exception + { + if (_in.space()<buf.length()) + { + ByteArrayBuffer n = new ByteArrayBuffer(_in.length()+buf.length()); + n.put(_in); + _in=n; + _endp.setIn(_in); + } + _in.put(buf); + + synchronized (this) + { + _keepOpen=keepOpen; + _accepting=true; + this.notify(); + + while(_accepting) + this.wait(); + } + + // System.err.println("\nRESPONSES:\n"+out); + _out=_endp.getOut(); + return _out; + } + + /* ------------------------------------------------------------ */ + public Buffer newBuffer(int size) + { + return new ByteArrayBuffer(size); + } + + /* ------------------------------------------------------------ */ + protected void accept(int acceptorID) throws IOException, InterruptedException + { + HttpConnection connection=null; + + while (isRunning()) + { + synchronized (this) + { + try + { + while(!_accepting) + this.wait(); + } + catch(InterruptedException e) + { + return; + } + } + + try + { + if (connection==null) + { + connection=new HttpConnection(this,_endp,getServer()); + connectionOpened(connection); + } + while (_in.length()>0) + connection.handle(); + } + finally + { + if (!_keepOpen) + { + connectionClosed(connection); + connection.destroy(); + connection=null; + } + synchronized (this) + { + _accepting=false; + this.notify(); + } + } + } + } + + + public void open() throws IOException + { + } + + public void close() throws IOException + { + } + + /* ------------------------------------------------------------------------------- */ + public int getLocalPort() + { + return -1; + } + + +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/NCSARequestLog.java b/jetty-server/src/main/java/org/eclipse/jetty/server/NCSARequestLog.java new file mode 100644 index 0000000000..6a7587de84 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/NCSARequestLog.java @@ -0,0 +1,513 @@ +// ======================================================================== +// Copyright (c) 1997-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Locale; +import java.util.TimeZone; + +import javax.servlet.http.Cookie; + +import org.eclipse.jetty.http.HttpHeaders; +import org.eclipse.jetty.http.PathMap; +import org.eclipse.jetty.util.DateCache; +import org.eclipse.jetty.util.RolloverFileOutputStream; +import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.util.TypeUtil; +import org.eclipse.jetty.util.Utf8StringBuilder; +import org.eclipse.jetty.util.component.AbstractLifeCycle; +import org.eclipse.jetty.util.log.Log; + +/** + * This {@link RequestLog} implementation outputs logs in the pseudo-standard + * NCSA common log format. Configuration options allow a choice between the + * standard Common Log Format (as used in the 3 log format) and the Combined Log + * Format (single log format). This log format can be output by most web + * servers, and almost all web log analysis software can understand these + * formats. + * + * + * + * + * @org.apache.xbean.XBean element="ncsaLog" + */ +public class NCSARequestLog extends AbstractLifeCycle implements RequestLog +{ + private String _filename; + private boolean _extended; + private boolean _append; + private int _retainDays; + private boolean _closeOut; + private boolean _preferProxiedForAddress; + private String _logDateFormat = "dd/MMM/yyyy:HH:mm:ss Z"; + private String _filenameDateFormat = null; + private Locale _logLocale = Locale.getDefault(); + private String _logTimeZone = "GMT"; + private String[] _ignorePaths; + private boolean _logLatency = false; + private boolean _logCookies = false; + private boolean _logServer = false; + + private transient OutputStream _out; + private transient OutputStream _fileOut; + private transient DateCache _logDateCache; + private transient PathMap _ignorePathMap; + private transient Writer _writer; + private transient ArrayList _buffers; + private transient char[] _copy; + + public NCSARequestLog() + { + _extended = true; + _append = true; + _retainDays = 31; + } + + /* ------------------------------------------------------------ */ + /** + * @param filename + * The filename for the request log. This may be in the + * format expected by {@link RolloverFileOutputStream} + */ + public NCSARequestLog(String filename) + { + _extended = true; + _append = true; + _retainDays = 31; + setFilename(filename); + } + + /* ------------------------------------------------------------ */ + /** + * @param filename + * The filename for the request log. This may be in the + * format expected by {@link RolloverFileOutputStream} + */ + public void setFilename(String filename) + { + if (filename != null) + { + filename = filename.trim(); + if (filename.length() == 0) + filename = null; + } + _filename = filename; + } + + public String getFilename() + { + return _filename; + } + + public String getDatedFilename() + { + if (_fileOut instanceof RolloverFileOutputStream) + return ((RolloverFileOutputStream)_fileOut).getDatedFilename(); + return null; + } + + /* ------------------------------------------------------------ */ + /** + * @param format + * Format for the timestamps in the log file. If not set, the + * pre-formated request timestamp is used. + */ + public void setLogDateFormat(String format) + { + _logDateFormat = format; + } + + public String getLogDateFormat() + { + return _logDateFormat; + } + + public void setLogLocale(Locale logLocale) + { + _logLocale = logLocale; + } + + public Locale getLogLocale() + { + return _logLocale; + } + + public void setLogTimeZone(String tz) + { + _logTimeZone = tz; + } + + public String getLogTimeZone() + { + return _logTimeZone; + } + + public void setRetainDays(int retainDays) + { + _retainDays = retainDays; + } + + public int getRetainDays() + { + return _retainDays; + } + + public void setExtended(boolean extended) + { + _extended = extended; + } + + public boolean isExtended() + { + return _extended; + } + + public void setAppend(boolean append) + { + _append = append; + } + + public boolean isAppend() + { + return _append; + } + + public void setIgnorePaths(String[] ignorePaths) + { + _ignorePaths = ignorePaths; + } + + public String[] getIgnorePaths() + { + return _ignorePaths; + } + + public void setLogCookies(boolean logCookies) + { + _logCookies = logCookies; + } + + public boolean getLogCookies() + { + return _logCookies; + } + + public boolean getLogServer() + { + return _logServer; + } + + public void setLogServer(boolean logServer) + { + _logServer = logServer; + } + + public void setLogLatency(boolean logLatency) + { + _logLatency = logLatency; + } + + public boolean getLogLatency() + { + return _logLatency; + } + + public void setPreferProxiedForAddress(boolean preferProxiedForAddress) + { + _preferProxiedForAddress = preferProxiedForAddress; + } + + /* ------------------------------------------------------------ */ + public void log(Request request, Response response) + { + if (!isStarted()) + return; + + try + { + if (_ignorePathMap != null && _ignorePathMap.getMatch(request.getRequestURI()) != null) + return; + + if (_fileOut == null) + return; + + Utf8StringBuilder u8buf; + StringBuilder buf; + synchronized(_writer) + { + int size=_buffers.size(); + u8buf = size==0?new Utf8StringBuilder(160):(Utf8StringBuilder)_buffers.remove(size-1); + buf = u8buf.getStringBuilder(); + } + + if (_logServer) + { + buf.append(request.getServerName()); + buf.append(' '); + } + + String addr = null; + if (_preferProxiedForAddress) + { + addr = request.getHeader(HttpHeaders.X_FORWARDED_FOR); + } + + if (addr == null) + addr = request.getRemoteAddr(); + + buf.append(addr); + buf.append(" - "); + String user = request.getRemoteUser(); + buf.append((user == null)?" - ":user); + buf.append(" ["); + if (_logDateCache != null) + buf.append(_logDateCache.format(request.getTimeStamp())); + else + buf.append(request.getTimeStampBuffer().toString()); + + buf.append("] \""); + buf.append(request.getMethod()); + buf.append(' '); + + request.getUri().writeTo(u8buf); + + buf.append(' '); + buf.append(request.getProtocol()); + buf.append("\" "); + if (request.getAsyncRequest().isInitial()) + { + int status = response.getStatus(); + if (status <= 0) + status = 404; + buf.append((char)('0' + ((status / 100) % 10))); + buf.append((char)('0' + ((status / 10) % 10))); + buf.append((char)('0' + (status % 10))); + } + else + buf.append("Async"); + + long responseLength = response.getContentCount(); + if (responseLength >= 0) + { + buf.append(' '); + if (responseLength > 99999) + buf.append(responseLength); + else + { + if (responseLength > 9999) + buf.append((char)('0' + ((responseLength / 10000) % 10))); + if (responseLength > 999) + buf.append((char)('0' + ((responseLength / 1000) % 10))); + if (responseLength > 99) + buf.append((char)('0' + ((responseLength / 100) % 10))); + if (responseLength > 9) + buf.append((char)('0' + ((responseLength / 10) % 10))); + buf.append((char)('0' + (responseLength) % 10)); + } + buf.append(' '); + } + else + buf.append(" - "); + + if (!_extended && !_logCookies && !_logLatency) + { + synchronized(_writer) + { + buf.append(StringUtil.__LINE_SEPARATOR); + int l=buf.length(); + if (l>_copy.length) + l=_copy.length; + buf.getChars(0,l,_copy,0); + _writer.write(_copy,0,l); + _writer.flush(); + u8buf.reset(); + _buffers.add(u8buf); + } + } + else + { + synchronized(_writer) + { + int l=buf.length(); + if (l>_copy.length) + l=_copy.length; + buf.getChars(0,l,_copy,0); + _writer.write(_copy,0,l); + u8buf.reset(); + _buffers.add(u8buf); + + // TODO do outside synchronized scope + if (_extended) + logExtended(request, response, _writer); + + // TODO do outside synchronized scope + if (_logCookies) + { + Cookie[] cookies = request.getCookies(); + if (cookies == null || cookies.length == 0) + _writer.write(" -"); + else + { + _writer.write(" \""); + for (int i = 0; i < cookies.length; i++) + { + if (i != 0) + _writer.write(';'); + _writer.write(cookies[i].getName()); + _writer.write('='); + _writer.write(cookies[i].getValue()); + } + _writer.write('\"'); + } + } + + if (_logLatency) + { + _writer.write(' '); + _writer.write(TypeUtil.toString(System.currentTimeMillis() - request.getTimeStamp())); + } + + _writer.write(StringUtil.__LINE_SEPARATOR); + _writer.flush(); + } + } + } + catch (IOException e) + { + Log.warn(e); + } + + } + + /* ------------------------------------------------------------ */ + protected void logExtended(Request request, + Response response, + Writer writer) throws IOException + { + String referer = request.getHeader(HttpHeaders.REFERER); + if (referer == null) + writer.write("\"-\" "); + else + { + writer.write('"'); + writer.write(referer); + writer.write("\" "); + } + + String agent = request.getHeader(HttpHeaders.USER_AGENT); + if (agent == null) + writer.write("\"-\" "); + else + { + writer.write('"'); + writer.write(agent); + writer.write('"'); + } + } + + /* ------------------------------------------------------------ */ + protected void doStart() throws Exception + { + if (_logDateFormat != null) + { + _logDateCache = new DateCache(_logDateFormat,_logLocale); + _logDateCache.setTimeZoneID(_logTimeZone); + } + + if (_filename != null) + { + _fileOut = new RolloverFileOutputStream(_filename,_append,_retainDays,TimeZone.getTimeZone(_logTimeZone),_filenameDateFormat,null); + _closeOut = true; + Log.info("Opened " + getDatedFilename()); + } + else + _fileOut = System.err; + + _out = _fileOut; + + if (_ignorePaths != null && _ignorePaths.length > 0) + { + _ignorePathMap = new PathMap(); + for (int i = 0; i < _ignorePaths.length; i++) + _ignorePathMap.put(_ignorePaths[i],_ignorePaths[i]); + } + else + _ignorePathMap = null; + + _writer = new OutputStreamWriter(_out); + _buffers = new ArrayList(); + _copy = new char[1024]; + super.doStart(); + } + + /* ------------------------------------------------------------ */ + protected void doStop() throws Exception + { + super.doStop(); + try + { + if (_writer != null) + _writer.flush(); + } + catch (IOException e) + { + Log.ignore(e); + } + if (_out != null && _closeOut) + try + { + _out.close(); + } + catch (IOException e) + { + Log.ignore(e); + } + + _out = null; + _fileOut = null; + _closeOut = false; + _logDateCache = null; + _writer = null; + _buffers = null; + _copy = null; + } + + /* ------------------------------------------------------------ */ + /** + * @return the log File Date Format + */ + public String getFilenameDateFormat() + { + return _filenameDateFormat; + } + + /* ------------------------------------------------------------ */ + /** + * Set the log file date format. + * + * @see {@link RolloverFileOutputStream#RolloverFileOutputStream(String, boolean, int, TimeZone, String, String)} + * @param logFileDateFormat + * the logFileDateFormat to pass to + * {@link RolloverFileOutputStream} + */ + public void setFilenameDateFormat(String logFileDateFormat) + { + _filenameDateFormat = logFileDateFormat; + } + +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java new file mode 100644 index 0000000000..431ae744c8 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java @@ -0,0 +1,1866 @@ +// ======================================================================== +// Copyright (c) 2004-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.security.Principal; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.EventListener; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import javax.security.auth.login.LoginException; +import javax.servlet.AsyncContext; +import javax.servlet.AsyncEvent; +import javax.servlet.AsyncListener; +import javax.servlet.DispatcherType; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletContext; +import javax.servlet.ServletInputStream; +import javax.servlet.ServletRequest; +import javax.servlet.ServletRequestAttributeEvent; +import javax.servlet.ServletRequestAttributeListener; +import javax.servlet.ServletRequestListener; +import javax.servlet.ServletResponse; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpHeaders; +import org.eclipse.jetty.http.HttpMethods; +import org.eclipse.jetty.http.HttpParser; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.http.HttpVersions; +import org.eclipse.jetty.http.MimeTypes; +import org.eclipse.jetty.io.Buffer; +import org.eclipse.jetty.io.BufferUtil; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.nio.DirectNIOBuffer; +import org.eclipse.jetty.io.nio.IndirectNIOBuffer; +import org.eclipse.jetty.io.nio.NIOBuffer; +import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.server.handler.ContextHandler.Context; +import org.eclipse.jetty.util.Attributes; +import org.eclipse.jetty.util.AttributesMap; +import org.eclipse.jetty.util.LazyList; +import org.eclipse.jetty.util.MultiMap; +import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.util.URIUtil; +import org.eclipse.jetty.util.UrlEncoded; +import org.eclipse.jetty.util.ajax.Continuation; +import org.eclipse.jetty.util.log.Log; + +/* ------------------------------------------------------------ */ +/** Jetty Request. + * <p> + * Implements {@link javax.servlet.http.HttpServletRequest} from the {@link javax.servlet.http} package. + * </p> + * <p> + * The standard interface of mostly getters, + * is extended with setters so that the request is mutable by the handlers that it is + * passed to. This allows the request object to be as lightweight as possible and not + * actually implement any significant behaviour. For example<ul> + * + * <li>The {@link Request#getContextPath} method will return null, until the requeset has been + * passed to a {@link ContextHandler} which matches the {@link Request#getPathInfo} with a context + * path and calls {@link Request#setContextPath} as a result.</li> + * + * <li>the HTTP session methods + * will all return null sessions until such time as a request has been passed to + * a {@link org.eclipse.jetty.servlet.SessionHandler} which checks for session cookies + * and enables the ability to create new sessions.</li> + * + * <li>The {@link Request#getServletPath} method will return null until the request has been + * passed to a {@link org.eclipse.jetty.servlet.ServletHandler} and the pathInfo matched + * against the servlet URL patterns and {@link Request#setServletPath} called as a result.</li> + * </ul> + * + * A request instance is created for each {@link HttpConnection} accepted by the server + * and recycled for each HTTP request received via that connection. An effort is made + * to avoid reparsing headers and cookies that are likely to be the same for + * requests from the same connection. + * + * + * + */ +public class Request implements HttpServletRequest +{ + private static final String __ASYNC_FWD="org.eclipse.asyncfwd"; + private static final Collection __defaultLocale = Collections.singleton(Locale.getDefault()); + private static final int __NONE=0, _STREAM=1, __READER=2; + + /* ------------------------------------------------------------ */ + public static Request getRequest(HttpServletRequest request) + { + if (request instanceof Request) + return (Request) request; + + return HttpConnection.getCurrentConnection().getRequest(); + } + protected final AsyncRequest _async = new AsyncRequest(); + private boolean _asyncSupported=true; + private Attributes _attributes; + private String _authType; + private MultiMap<String> _baseParameters; + private String _characterEncoding; + protected HttpConnection _connection; + private ContextHandler.Context _context; + private String _contextPath; + private Continuation _continuation; + private CookieCutter _cookies; + private boolean _cookiesExtracted=false; + private DispatcherType _dispatcherType; + private boolean _dns=false; + private EndPoint _endp; + private boolean _handled =false; + private int _inputState=__NONE; + private String _method; + private MultiMap<String> _parameters; + private boolean _paramsExtracted; + private String _pathInfo; + private int _port; + private String _protocol=HttpVersions.HTTP_1_1; + private String _queryEncoding; + private String _queryString; + private BufferedReader _reader; + private String _readerEncoding; + private String _remoteAddr; + private String _remoteHost; + private Object _requestAttributeListeners; + private String _requestedSessionId; + private boolean _requestedSessionIdFromCookie=false; + private Object _requestListeners; + private String _requestURI; + private Map<Object,HttpSession> _savedNewSessions; + private String _scheme=URIUtil.HTTP; + private String _serverName; + private String _servletName; + private String _servletPath; + private HttpSession _session; + private SessionManager _sessionManager; + private long _timeStamp; + private Buffer _timeStampBuffer; + private HttpURI _uri; + + private UserIdentity _userIdentity = UserIdentity.UNAUTHENTICATED_IDENTITY; + + /* ------------------------------------------------------------ */ + public Request() + { + } + + /* ------------------------------------------------------------ */ + public Request(HttpConnection connection) + { + setConnection(connection); + } + + /* ------------------------------------------------------------ */ + public void addAsyncListener(AsyncListener listener) + { + _async._listeners=LazyList.add(_async._listeners,listener); + } + + /* ------------------------------------------------------------ */ + public void addAsyncListener(final AsyncListener listener, ServletRequest servletRequest, ServletResponse servletResponse) + { + final AsyncEvent event = new AsyncEvent(servletRequest,servletResponse); + + _async._listeners=LazyList.add(_async._listeners,new AsyncListener() + { + public void onComplete(AsyncEvent ev) throws IOException + { + listener.onComplete(event); + } + + public void onTimeout(AsyncEvent ev) throws IOException + { + listener.onComplete(event); + } + }); + } + + /* ------------------------------------------------------------ */ + public void addEventListener(final EventListener listener) + { + if (listener instanceof ServletRequestAttributeListener) + _requestAttributeListeners= LazyList.add(_requestAttributeListeners, listener); + } + + /* ------------------------------------------------------------ */ + /* + * Extract Paramters from query string and/or form _content. + */ + private void extractParameters() + { + if (_baseParameters == null) + _baseParameters = new MultiMap(16); + + if (_paramsExtracted) + { + if (_parameters==null) + _parameters=_baseParameters; + return; + } + + _paramsExtracted = true; + + // Handle query string + if (_uri!=null && _uri.hasQuery()) + { + if (_queryEncoding==null) + _uri.decodeQueryTo(_baseParameters); + else + { + try + { + _uri.decodeQueryTo(_baseParameters,_queryEncoding); + + } + catch (UnsupportedEncodingException e) + { + if (Log.isDebugEnabled()) + Log.warn(e); + else + Log.warn(e.toString()); + } + } + + } + + // handle any _content. + String encoding = getCharacterEncoding(); + String content_type = getContentType(); + if (content_type != null && content_type.length() > 0) + { + content_type = HttpFields.valueParameters(content_type, null); + + if (MimeTypes.FORM_ENCODED.equalsIgnoreCase(content_type) && + (HttpMethods.POST.equals(getMethod()) || HttpMethods.PUT.equals(getMethod()))) + { + int content_length = getContentLength(); + if (content_length != 0) + { + try + { + int maxFormContentSize=-1; + + if (_context!=null) + maxFormContentSize=_context.getContextHandler().getMaxFormContentSize(); + else + { + Integer size = (Integer)_connection.getConnector().getServer().getAttribute("org.eclipse.jetty.server.Request.maxFormContentSize"); + if (size!=null) + maxFormContentSize =size.intValue(); + } + + if (content_length>maxFormContentSize && maxFormContentSize > 0) + { + throw new IllegalStateException("Form too large"+content_length+">"+maxFormContentSize); + } + InputStream in = getInputStream(); + + // Add form params to query params + UrlEncoded.decodeTo(in, _baseParameters, encoding,content_length<0?maxFormContentSize:-1); + } + catch (IOException e) + { + if (Log.isDebugEnabled()) + Log.warn(e); + else + Log.warn(e.toString()); + } + } + } + } + + if (_parameters==null) + _parameters=_baseParameters; + else if (_parameters!=_baseParameters) + { + // Merge parameters (needed if parameters extracted after a forward). + Iterator iter = _baseParameters.entrySet().iterator(); + while (iter.hasNext()) + { + Map.Entry entry = (Map.Entry)iter.next(); + String name=(String)entry.getKey(); + Object values=entry.getValue(); + for (int i=0;i<LazyList.size(values);i++) + _parameters.add(name, LazyList.get(values, i)); + } + } + } + + /* ------------------------------------------------------------ */ + public AsyncContext getAsyncContext() + { + if (_async.isInitial() && !isAsyncStarted()) + throw new IllegalStateException(_async.getStatusString()); + return _async; + } + + /* ------------------------------------------------------------ */ + public AsyncRequest getAsyncRequest() + { + return _async; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletRequest#getAttribute(java.lang.String) + */ + public Object getAttribute(String name) + { + if ("org.eclipse.jetty.util.ajax.Continuation".equals(name)) + return getContinuation(true); + + if (DispatcherType.ASYNC.equals(_dispatcherType)) + { + // TODO handle forwards(path!) + if (name.equals(Dispatcher.__FORWARD_PATH_INFO)) return getPathInfo(); + if (name.equals(Dispatcher.__FORWARD_REQUEST_URI)) return getRequestURI(); + if (name.equals(Dispatcher.__FORWARD_SERVLET_PATH)) return getServletPath(); + if (name.equals(Dispatcher.__FORWARD_CONTEXT_PATH)) return getContextPath(); + if (name.equals(Dispatcher.__FORWARD_QUERY_STRING)) return getQueryString(); + } + + if (_attributes==null) + return null; + return _attributes.getAttribute(name); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletRequest#getAttributeNames() + */ + public Enumeration getAttributeNames() + { + if (_attributes==null) + return Collections.enumeration(Collections.EMPTY_LIST); + return AttributesMap.getAttributeNamesCopy(_attributes); + } + + /* ------------------------------------------------------------ */ + /* + */ + public Attributes getAttributes() + { + if (_attributes==null) + _attributes=new AttributesMap(); + return _attributes; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletRequest#getAuthType() + */ + public String getAuthType() + { + return _authType; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletRequest#getCharacterEncoding() + */ + public String getCharacterEncoding() + { + return _characterEncoding; + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the connection. + */ + public HttpConnection getConnection() + { + return _connection; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletRequest#getContentLength() + */ + public int getContentLength() + { + return (int)_connection.getRequestFields().getLongField(HttpHeaders.CONTENT_LENGTH_BUFFER); + } + + public long getContentRead() + { + if (_connection==null || _connection.getParser()==null) + return -1; + + return ((HttpParser)_connection.getParser()).getContentRead(); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletRequest#getContentType() + */ + public String getContentType() + { + return _connection.getRequestFields().getStringField(HttpHeaders.CONTENT_TYPE_BUFFER); + } + + /* ------------------------------------------------------------ */ + /** + * @return The current {@link Context context} used for this request, or <code>null</code> if {@link #setContext} has not yet + * been called. + */ + public Context getContext() + { + return _context; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletRequest#getContextPath() + */ + public String getContextPath() + { + return _contextPath; + } + + /* ------------------------------------------------------------ */ + /** + * @deprecated + */ + public Continuation getContinuation() + { + return _continuation; + } + + /* ------------------------------------------------------------ */ + /** + * @deprecated + */ + public Continuation getContinuation(boolean create) + { + if (_continuation==null && create) + _continuation=new Servlet3Continuation(this); + return _continuation; + } + + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletRequest#getCookies() + */ + public Cookie[] getCookies() + { + if (_cookiesExtracted) + return _cookies==null?null:_cookies.getCookies(); + + // Handle no cookies + if (!_connection.getRequestFields().containsKey(HttpHeaders.COOKIE_BUFFER)) + { + _cookiesExtracted = true; + if (_cookies!=null) + _cookies.reset(); + return null; + } + + if (_cookies==null) + _cookies=new CookieCutter(this); + + Enumeration enm = _connection.getRequestFields().getValues(HttpHeaders.COOKIE_BUFFER); + while (enm.hasMoreElements()) + { + String c = (String)enm.nextElement(); + _cookies.addCookieField(c); + } + _cookiesExtracted=true; + + return _cookies.getCookies(); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletRequest#getDateHeader(java.lang.String) + */ + public long getDateHeader(String name) + { + return _connection.getRequestFields().getDateField(name); + } + + /* ------------------------------------------------------------ */ + public DispatcherType getDispatcherType() + { + return _dispatcherType; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletRequest#getHeader(java.lang.String) + */ + public String getHeader(String name) + { + return _connection.getRequestFields().getStringField(name); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletRequest#getHeaderNames() + */ + public Enumeration getHeaderNames() + { + return _connection.getRequestFields().getFieldNames(); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletRequest#getHeaders(java.lang.String) + */ + public Enumeration getHeaders(String name) + { + Enumeration e = _connection.getRequestFields().getValues(name); + if (e==null) + return Collections.enumeration(Collections.EMPTY_LIST); + return e; + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the inputState. + */ + public int getInputState() + { + return _inputState; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletRequest#getInputStream() + */ + public ServletInputStream getInputStream() throws IOException + { + if (_inputState!=__NONE && _inputState!=_STREAM) + throw new IllegalStateException("READER"); + _inputState=_STREAM; + return _connection.getInputStream(); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletRequest#getIntHeader(java.lang.String) + */ + public int getIntHeader(String name) + { + return (int)_connection.getRequestFields().getLongField(name); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletRequest#getLocalAddr() + */ + public String getLocalAddr() + { + return _endp==null?null:_endp.getLocalAddr(); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletRequest#getLocale() + */ + public Locale getLocale() + { + Enumeration enm = _connection.getRequestFields().getValues(HttpHeaders.ACCEPT_LANGUAGE, HttpFields.__separators); + + // handle no locale + if (enm == null || !enm.hasMoreElements()) + return Locale.getDefault(); + + // sort the list in quality order + List acceptLanguage = HttpFields.qualityList(enm); + if (acceptLanguage.size()==0) + return Locale.getDefault(); + + int size=acceptLanguage.size(); + + // convert to locals + for (int i=0; i<size; i++) + { + String language = (String)acceptLanguage.get(i); + language=HttpFields.valueParameters(language,null); + String country = ""; + int dash = language.indexOf('-'); + if (dash > -1) + { + country = language.substring(dash + 1).trim(); + language = language.substring(0,dash).trim(); + } + return new Locale(language,country); + } + + return Locale.getDefault(); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletRequest#getLocales() + */ + public Enumeration getLocales() + { + + Enumeration enm = _connection.getRequestFields().getValues(HttpHeaders.ACCEPT_LANGUAGE, HttpFields.__separators); + + // handle no locale + if (enm == null || !enm.hasMoreElements()) + return Collections.enumeration(__defaultLocale); + + // sort the list in quality order + List acceptLanguage = HttpFields.qualityList(enm); + + if (acceptLanguage.size()==0) + return + Collections.enumeration(__defaultLocale); + + Object langs = null; + int size=acceptLanguage.size(); + + // convert to locals + for (int i=0; i<size; i++) + { + String language = (String)acceptLanguage.get(i); + language=HttpFields.valueParameters(language,null); + String country = ""; + int dash = language.indexOf('-'); + if (dash > -1) + { + country = language.substring(dash + 1).trim(); + language = language.substring(0,dash).trim(); + } + langs=LazyList.ensureSize(langs,size); + langs=LazyList.add(langs,new Locale(language,country)); + } + + if (LazyList.size(langs)==0) + return Collections.enumeration(__defaultLocale); + + return Collections.enumeration(LazyList.getList(langs)); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletRequest#getLocalName() + */ + public String getLocalName() + { + if (_dns) + return _endp==null?null:_endp.getLocalHost(); + return _endp==null?null:_endp.getLocalAddr(); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletRequest#getLocalPort() + */ + public int getLocalPort() + { + return _endp==null?0:_endp.getLocalPort(); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletRequest#getMethod() + */ + public String getMethod() + { + return _method; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletRequest#getParameter(java.lang.String) + */ + public String getParameter(String name) + { + if (!_paramsExtracted) + extractParameters(); + return (String) _parameters.getValue(name, 0); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletRequest#getParameterMap() + */ + public Map getParameterMap() + { + if (!_paramsExtracted) + extractParameters(); + + return Collections.unmodifiableMap(_parameters.toStringArrayMap()); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletRequest#getParameterNames() + */ + public Enumeration getParameterNames() + { + if (!_paramsExtracted) + extractParameters(); + return Collections.enumeration(_parameters.keySet()); + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the parameters. + */ + public MultiMap getParameters() + { + return _parameters; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletRequest#getParameterValues(java.lang.String) + */ + public String[] getParameterValues(String name) + { + if (!_paramsExtracted) + extractParameters(); + List vals = _parameters.getValues(name); + if (vals==null) + return null; + return (String[])vals.toArray(new String[vals.size()]); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletRequest#getPathInfo() + */ + public String getPathInfo() + { + return _pathInfo; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletRequest#getPathTranslated() + */ + public String getPathTranslated() + { + if (_pathInfo==null || _context==null) + return null; + return _context.getRealPath(_pathInfo); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletRequest#getProtocol() + */ + public String getProtocol() + { + return _protocol; + } + + /* ------------------------------------------------------------ */ + public String getQueryEncoding() + { + return _queryEncoding; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletRequest#getQueryString() + */ + public String getQueryString() + { + if (_queryString==null && _uri!=null) + { + if (_queryEncoding==null) + _queryString=_uri.getQuery(); + else + _queryString=_uri.getQuery(_queryEncoding); + } + return _queryString; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletRequest#getReader() + */ + public BufferedReader getReader() throws IOException + { + if (_inputState!=__NONE && _inputState!=__READER) + throw new IllegalStateException("STREAMED"); + + if (_inputState==__READER) + return _reader; + + String encoding=getCharacterEncoding(); + if (encoding==null) + encoding=StringUtil.__ISO_8859_1; + + if (_reader==null || !encoding.equalsIgnoreCase(_readerEncoding)) + { + final ServletInputStream in = getInputStream(); + _readerEncoding=encoding; + _reader=new BufferedReader(new InputStreamReader(in,encoding)) + { + public void close() throws IOException + { + in.close(); + } + }; + } + _inputState=__READER; + return _reader; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletRequest#getRealPath(java.lang.String) + */ + public String getRealPath(String path) + { + if (_context==null) + return null; + return _context.getRealPath(path); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletRequest#getRemoteAddr() + */ + public String getRemoteAddr() + { + if (_remoteAddr != null) + return _remoteAddr; + return _endp==null?null:_endp.getRemoteAddr(); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletRequest#getRemoteHost() + */ + public String getRemoteHost() + { + if (_dns) + { + if (_remoteHost != null) + { + return _remoteHost; + } + return _endp==null?null:_endp.getRemoteHost(); + } + return getRemoteAddr(); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletRequest#getRemotePort() + */ + public int getRemotePort() + { + return _endp==null?0:_endp.getRemotePort(); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletRequest#getRemoteUser() + */ + public String getRemoteUser() + { + Principal p = getUserPrincipal(); + if (p==null) + return null; + return p.getName(); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletRequest#getRequestDispatcher(java.lang.String) + */ + public RequestDispatcher getRequestDispatcher(String path) + { + if (path == null || _context==null) + return null; + + // handle relative path + if (!path.startsWith("/")) + { + String relTo=URIUtil.addPaths(_servletPath,_pathInfo); + int slash=relTo.lastIndexOf("/"); + if (slash>1) + relTo=relTo.substring(0,slash+1); + else + relTo="/"; + path=URIUtil.addPaths(relTo,path); + } + + return _context.getRequestDispatcher(path); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletRequest#getRequestedSessionId() + */ + public String getRequestedSessionId() + { + return _requestedSessionId; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletRequest#getRequestURI() + */ + public String getRequestURI() + { + if (_requestURI==null && _uri!=null) + _requestURI=_uri.getPathAndParam(); + return _requestURI; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletRequest#getRequestURL() + */ + public StringBuffer getRequestURL() + { + StringBuffer url = new StringBuffer(48); + synchronized (url) + { + String scheme = getScheme(); + int port = getServerPort(); + + url.append(scheme); + url.append("://"); + url.append(getServerName()); + if (_port>0 && + ((scheme.equalsIgnoreCase(URIUtil.HTTP) && port != 80) || + (scheme.equalsIgnoreCase(URIUtil.HTTPS) && port != 443))) + { + url.append(':'); + url.append(_port); + } + + url.append(getRequestURI()); + return url; + } + } + + /* ------------------------------------------------------------ */ + public Response getResponse() + { + return _connection._response; + } + + /* ------------------------------------------------------------ */ + /** + * Reconstructs the URL the client used to make the request. The returned URL contains a + * protocol, server name, port number, and, but it does not include a path. + * <p> + * Because this method returns a <code>StringBuffer</code>, not a string, you can modify the + * URL easily, for example, to append path and query parameters. + * + * This method is useful for creating redirect messages and for reporting errors. + * + * @return "scheme://host:port" + */ + public StringBuilder getRootURL() + { + StringBuilder url = new StringBuilder(48); + String scheme = getScheme(); + int port = getServerPort(); + + url.append(scheme); + url.append("://"); + url.append(getServerName()); + + if (port > 0 && ((scheme.equalsIgnoreCase("http") && port != 80) || (scheme.equalsIgnoreCase("https") && port != 443))) + { + url.append(':'); + url.append(port); + } + return url; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletRequest#getScheme() + */ + public String getScheme() + { + return _scheme; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletRequest#getServerName() + */ + public String getServerName() + { + // Return already determined host + if (_serverName != null) + return _serverName; + + // Return host from absolute URI + _serverName = _uri.getHost(); + _port = _uri.getPort(); + if (_serverName != null) + return _serverName; + + // Return host from header field + Buffer hostPort = _connection.getRequestFields().get(HttpHeaders.HOST_BUFFER); + if (hostPort!=null) + { + for (int i=hostPort.length();i-->0;) + { + if (hostPort.peek(hostPort.getIndex()+i)==':') + { + _serverName=BufferUtil.to8859_1_String(hostPort.peek(hostPort.getIndex(), i)); + _port=BufferUtil.toInt(hostPort.peek(hostPort.getIndex()+i+1, hostPort.length()-i-1)); + return _serverName; + } + } + if (_serverName==null || _port<0) + { + _serverName=BufferUtil.to8859_1_String(hostPort); + _port = 0; + } + + return _serverName; + } + + // Return host from connection + if (_connection != null) + { + _serverName = getLocalName(); + _port = getLocalPort(); + if (_serverName != null && !StringUtil.ALL_INTERFACES.equals(_serverName)) + return _serverName; + } + + // Return the local host + try + { + _serverName = InetAddress.getLocalHost().getHostAddress(); + } + catch (java.net.UnknownHostException e) + { + Log.ignore(e); + } + return _serverName; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletRequest#getServerPort() + */ + public int getServerPort() + { + if (_port<=0) + { + if (_serverName==null) + getServerName(); + + if (_port<=0) + { + if (_serverName!=null && _uri!=null) + _port = _uri.getPort(); + else + _port = _endp==null?0:_endp.getLocalPort(); + } + } + + if (_port<=0) + { + if (getScheme().equalsIgnoreCase(URIUtil.HTTPS)) + return 443; + return 80; + } + return _port; + } + + /* ------------------------------------------------------------ */ + public ServletContext getServletContext() + { + return _context; + } + + /* ------------------------------------------------------------ */ + /* + */ + public String getServletName() + { + return _servletName; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletRequest#getServletPath() + */ + public String getServletPath() + { + if (_servletPath==null) + _servletPath=""; + return _servletPath; + } + + /* ------------------------------------------------------------ */ + public ServletResponse getServletResponse() + { + return _connection.getResponse(); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletRequest#getSession() + */ + public HttpSession getSession() + { + return getSession(true); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletRequest#getSession(boolean) + */ + public HttpSession getSession(boolean create) + { + if (_sessionManager==null && create) + throw new IllegalStateException("No SessionHandler or SessionManager"); + + if (_session != null && _sessionManager!=null && _sessionManager.isValid(_session)) + return _session; + + _session=null; + + String id=getRequestedSessionId(); + + if (id != null && _sessionManager!=null) + { + _session=_sessionManager.getHttpSession(id); + if (_session == null && !create) + return null; + } + + if (_session == null && _sessionManager!=null && create ) + { + _session=_sessionManager.newHttpSession(this); + Cookie cookie=_sessionManager.getSessionCookie(_session,getContextPath(),isSecure()); + if (cookie!=null) + _connection.getResponse().addCookie(cookie); + } + + return _session; + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the sessionManager. + */ + public SessionManager getSessionManager() + { + return _sessionManager; + } + + + /* ------------------------------------------------------------ */ + /** + * Get Request TimeStamp + * + * @return The time that the request was received. + */ + public long getTimeStamp() + { + return _timeStamp; + } + + /* ------------------------------------------------------------ */ + /** + * Get Request TimeStamp + * + * @return The time that the request was received. + */ + public Buffer getTimeStampBuffer() + { + if (_timeStampBuffer == null && _timeStamp > 0) + _timeStampBuffer = HttpFields.__dateCache.formatBuffer(_timeStamp); + return _timeStampBuffer; + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the uri. + */ + public HttpURI getUri() + { + return _uri; + } + + public UserIdentity getUserIdentity() + { + return _userIdentity; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletRequest#getUserPrincipal() + */ + public Principal getUserPrincipal() + { + return _userIdentity.getUserPrincipal(); + } + + /* ------------------------------------------------------------ */ + public boolean isAsyncStarted() + { + return _async.isAsyncStarted(); + } + + /* ------------------------------------------------------------ */ + public boolean isAsyncSupported() + { + return _asyncSupported; + } + + /* ------------------------------------------------------------ */ + public boolean isHandled() + { + return _handled; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletRequest#isRequestedSessionIdFromCookie() + */ + public boolean isRequestedSessionIdFromCookie() + { + return _requestedSessionId!=null && _requestedSessionIdFromCookie; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletRequest#isRequestedSessionIdFromUrl() + */ + public boolean isRequestedSessionIdFromUrl() + { + return _requestedSessionId!=null && !_requestedSessionIdFromCookie; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletRequest#isRequestedSessionIdFromURL() + */ + public boolean isRequestedSessionIdFromURL() + { + return _requestedSessionId!=null && !_requestedSessionIdFromCookie; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletRequest#isRequestedSessionIdValid() + */ + public boolean isRequestedSessionIdValid() + { + if (_requestedSessionId==null) + return false; + + HttpSession session=getSession(false); + return (session==null?false:_sessionManager.getIdManager().getClusterId(_requestedSessionId).equals(_sessionManager.getClusterId(session))); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletRequest#isSecure() + */ + public boolean isSecure() + { + return _connection.isConfidential(this); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletRequest#isUserInRole(java.lang.String) + */ + public boolean isUserInRole(String role) + { + return _userIdentity!=null && _userIdentity.isUserInRole(role); + } + + /* ------------------------------------------------------------ */ + public HttpSession recoverNewSession(Object key) + { + if (_savedNewSessions==null) + return null; + return (HttpSession) _savedNewSessions.get(key); + } + + /* ------------------------------------------------------------ */ + protected void recycle() + { + _authType=null; + _async.recycle(); + _asyncSupported=true; + _handled=false; + if (_context!=null) + throw new IllegalStateException("Request in context!"); + if(_attributes!=null) + _attributes.clearAttributes(); + _characterEncoding=null; + _queryEncoding=null; + _context=null; + _serverName=null; + _method=null; + _pathInfo=null; + _port=0; + _protocol=HttpVersions.HTTP_1_1; + _queryString=null; + _requestedSessionId=null; + _requestedSessionIdFromCookie=false; + _session=null; + _requestURI=null; + _scheme=URIUtil.HTTP; + _servletPath=null; + _timeStamp=0; + _timeStampBuffer=null; + _uri=null; + _userIdentity=UserIdentity.UNAUTHENTICATED_IDENTITY; + if (_baseParameters!=null) + _baseParameters.clear(); + _parameters=null; + _paramsExtracted=false; + _inputState=__NONE; + + _cookiesExtracted=false; + if (_savedNewSessions!=null) + _savedNewSessions.clear(); + _savedNewSessions=null; + if (_continuation!=null && _continuation.isPending()) + _continuation.reset(); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletRequest#removeAttribute(java.lang.String) + */ + public void removeAttribute(String name) + { + Object old_value=_attributes==null?null:_attributes.getAttribute(name); + + if (_attributes!=null) + _attributes.removeAttribute(name); + + if (old_value!=null) + { + if (_requestAttributeListeners!=null) + { + final ServletRequestAttributeEvent event = + new ServletRequestAttributeEvent(_context,this,name, old_value); + final int size=LazyList.size(_requestAttributeListeners); + for(int i=0;i<size;i++) + { + final EventListener listener = (ServletRequestAttributeListener)LazyList.get(_requestAttributeListeners,i); + if (listener instanceof ServletRequestAttributeListener) + { + final ServletRequestAttributeListener l = (ServletRequestAttributeListener)listener; + ((ServletRequestAttributeListener)l).attributeRemoved(event); + } + } + } + } + } + + /* ------------------------------------------------------------ */ + public void removeEventListener(final EventListener listener) + { + _requestAttributeListeners= LazyList.remove(_requestAttributeListeners, listener); + } + + /* ------------------------------------------------------------ */ + public void saveNewSession(Object key,HttpSession session) + { + if (_savedNewSessions==null) + _savedNewSessions=new HashMap<Object,HttpSession>(); + _savedNewSessions.put(key,session); + } + + /* ------------------------------------------------------------ */ + public void setAsyncSupported(boolean supported) + { + _asyncSupported=supported; + } + /* ------------------------------------------------------------ */ + public void setAsyncTimeout(long timeout) + { + _async.setAsyncTimeout(timeout); + } + /* ------------------------------------------------------------ */ + /* + * Set a request attribute. + * if the attribute name is "org.eclipse.jetty.server.server.Request.queryEncoding" then + * the value is also passed in a call to {@link #setQueryEncoding}. + * + * if the attribute name is "org.eclipse.jetty.server.server.ResponseBuffer", then + * the response buffer is flushed with @{link #flushResponseBuffer} + * + * @see javax.servlet.ServletRequest#setAttribute(java.lang.String, java.lang.Object) + */ + public void setAttribute(String name, Object value) + { + Object old_value=_attributes==null?null:_attributes.getAttribute(name); + + if ("org.eclipse.jetty.server.Request.queryEncoding".equals(name)) + setQueryEncoding(value==null?null:value.toString()); + else if("org.eclipse.jetty.server.sendContent".equals(name)) + { + try + { + ((HttpConnection.Output)getServletResponse().getOutputStream()).sendContent(value); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + else if("org.eclipse.jetty.server.ResponseBuffer".equals(name)) + { + try + { + ByteBuffer byteBuffer=(ByteBuffer)value; + synchronized (byteBuffer) + { + NIOBuffer buffer = byteBuffer.isDirect() + ?(NIOBuffer)new DirectNIOBuffer(byteBuffer,true) + :(NIOBuffer)new IndirectNIOBuffer(byteBuffer,true); + ((HttpConnection.Output)getServletResponse().getOutputStream()).sendResponse(buffer); + } + } + catch (IOException e) + { + throw new RuntimeException(e); + } + } + + if (_attributes==null) + _attributes=new AttributesMap(); + _attributes.setAttribute(name, value); + + if (_requestAttributeListeners!=null) + { + final ServletRequestAttributeEvent event = + new ServletRequestAttributeEvent(_context,this,name, old_value==null?value:old_value); + final int size=LazyList.size(_requestAttributeListeners); + for(int i=0;i<size;i++) + { + final EventListener listener = (ServletRequestAttributeListener)LazyList.get(_requestAttributeListeners,i); + if (listener instanceof ServletRequestAttributeListener) + { + final ServletRequestAttributeListener l = (ServletRequestAttributeListener)listener; + + if (old_value==null) + l.attributeAdded(event); + else if (value==null) + l.attributeRemoved(event); + else + l.attributeReplaced(event); + } + } + } + } + + /* ------------------------------------------------------------ */ + /* + */ + public void setAttributes(Attributes attributes) + { + _attributes=attributes; + } + + /* ------------------------------------------------------------ */ + public void setAuthType(String authType) + { + _authType=authType; + } + + /* ------------------------------------------------------------ */ + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletRequest#setCharacterEncoding(java.lang.String) + */ + public void setCharacterEncoding(String encoding) throws UnsupportedEncodingException + { + if (_inputState!=__NONE) + return; + + _characterEncoding=encoding; + + // check encoding is supported + if (!StringUtil.isUTF8(encoding)) + "".getBytes(encoding); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletRequest#setCharacterEncoding(java.lang.String) + */ + public void setCharacterEncodingUnchecked(String encoding) + { + _characterEncoding=encoding; + } + + /* ------------------------------------------------------------ */ + //final so we can safely call this from constructor + protected final void setConnection(HttpConnection connection) + { + _connection=connection; + _async.setConnection(connection); + _endp=connection.getEndPoint(); + _dns=connection.getResolveNames(); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletRequest#getContentType() + */ + public void setContentType(String contentType) + { + _connection.getRequestFields().put(HttpHeaders.CONTENT_TYPE_BUFFER,contentType); + + } + + /* ------------------------------------------------------------ */ + /** + * @param context + */ + public void setContext(Context context) + { + _context=context; + } + + /* ------------------------------------------------------------ */ + /** + * Sets the "context path" for this request + * @see HttpServletRequest#getContextPath + */ + public void setContextPath(String contextPath) + { + _contextPath = contextPath; + } + + /* ------------------------------------------------------------ */ + void setContinuation(Continuation cont) + { + _continuation=cont; + } + + /* ------------------------------------------------------------ */ + /** + * @param cookies The cookies to set. + */ + public void setCookies(Cookie[] cookies) + { + if (_cookies==null) + _cookies=new CookieCutter(this); + _cookies.setCookies(cookies); + } + + /* ------------------------------------------------------------ */ + public void setDispatcherType(DispatcherType type) + { + _dispatcherType=type; + } + + /* ------------------------------------------------------------ */ + public void setHandled(boolean h) + { + _handled=h; + } + + /* ------------------------------------------------------------ */ + /** + * @param method The method to set. + */ + public void setMethod(String method) + { + _method = method; + } + + /* ------------------------------------------------------------ */ + /** + * @param parameters The parameters to set. + */ + public void setParameters(MultiMap parameters) + { + _parameters= (parameters==null)?_baseParameters:parameters; + if (_paramsExtracted && _parameters==null) + throw new IllegalStateException(); + } + + /* ------------------------------------------------------------ */ + /** + * @param pathInfo The pathInfo to set. + */ + public void setPathInfo(String pathInfo) + { + _pathInfo = pathInfo; + } + + /* ------------------------------------------------------------ */ + /** + * @param protocol The protocol to set. + */ + public void setProtocol(String protocol) + { + _protocol = protocol; + } + + /* ------------------------------------------------------------ */ + /** Set the character encoding used for the query string. + * This call will effect the return of getQueryString and getParamaters. + * It must be called before any geParameter methods. + * + * The request attribute "org.eclipse.jetty.server.server.Request.queryEncoding" + * may be set as an alternate method of calling setQueryEncoding. + * + * @param queryEncoding + */ + public void setQueryEncoding(String queryEncoding) + { + _queryEncoding=queryEncoding; + _queryString=null; + } + + /* ------------------------------------------------------------ */ + /** + * @param queryString The queryString to set. + */ + public void setQueryString(String queryString) + { + _queryString = queryString; + } + + /* ------------------------------------------------------------ */ + /** + * @param addr The address to set. + */ + public void setRemoteAddr(String addr) + { + _remoteAddr = addr; + } + + /* ------------------------------------------------------------ */ + /** + * @param host The host to set. + */ + public void setRemoteHost(String host) + { + _remoteHost = host; + } + + /* ------------------------------------------------------------ */ + /** + * @param requestedSessionId The requestedSessionId to set. + */ + public void setRequestedSessionId(String requestedSessionId) + { + _requestedSessionId = requestedSessionId; + } + /* ------------------------------------------------------------ */ + /** + * @param requestedSessionIdCookie The requestedSessionIdCookie to set. + */ + public void setRequestedSessionIdFromCookie(boolean requestedSessionIdCookie) + { + _requestedSessionIdFromCookie = requestedSessionIdCookie; + } + + /* ------------------------------------------------------------ */ + /** + * @param requestListeners {@link LazyList} of {@link ServletRequestListener}s + */ + public void setRequestListeners(Object requestListeners) + { + _requestListeners=requestListeners; + } + + /* ------------------------------------------------------------ */ + /** + * @param requestURI The requestURI to set. + */ + public void setRequestURI(String requestURI) + { + _requestURI = requestURI; + } + + /* ------------------------------------------------------------ */ + /** + * @param scheme The scheme to set. + */ + public void setScheme(String scheme) + { + _scheme = scheme; + } + + /* ------------------------------------------------------------ */ + /** + * @param host The host to set. + */ + public void setServerName(String host) + { + _serverName = host; + } + + /* ------------------------------------------------------------ */ + /** + * @param port The port to set. + */ + public void setServerPort(int port) + { + _port = port; + } + + /* ------------------------------------------------------------ */ + /** + * @param name The servletName to set. + */ + public void setServletName(String name) + { + _servletName = name; + } + + /* ------------------------------------------------------------ */ + /** + * @param servletPath The servletPath to set. + */ + public void setServletPath(String servletPath) + { + _servletPath = servletPath; + } + + /* ------------------------------------------------------------ */ + /** + * @param session The session to set. + */ + public void setSession(HttpSession session) + { + _session = session; + } + + /* ------------------------------------------------------------ */ + /** + * @param sessionManager The sessionManager to set. + */ + public void setSessionManager(SessionManager sessionManager) + { + _sessionManager = sessionManager; + } + + /* ------------------------------------------------------------ */ + public void setTimeStamp(long ts) + { + _timeStamp = ts; + } + + /* ------------------------------------------------------------ */ + /** + * @param uri The uri to set. + */ + public void setUri(HttpURI uri) + { + _uri = uri; + } + + public void setUserIdentity(UserIdentity userIdentity) + { + if (userIdentity == null) + throw new NullPointerException("No UserIdentity"); + _userIdentity = userIdentity; + } + + /* ------------------------------------------------------------ */ + public AsyncContext startAsync() throws IllegalStateException + { + if (!_asyncSupported) + throw new IllegalStateException("!asyncSupported"); + _async.suspend(_context,this,_connection._response); + return _async; + } + + /* ------------------------------------------------------------ */ + public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) throws IllegalStateException + { + if (!_asyncSupported) + throw new IllegalStateException("!asyncSupported"); + _async.suspend(_context,servletRequest,servletResponse); + return _async; + } + + /* ------------------------------------------------------------ */ + /** + * @return {@link LazyList} of {@link ServletRequestListener}s + */ + public Object takeRequestListeners() + { + final Object listeners=_requestListeners; + _requestListeners=null; + return listeners; + } + + /* ------------------------------------------------------------ */ + public String toString() + { + return (_handled?"[":"(")+getMethod()+" "+_uri+(_handled?"]@":")@")+hashCode()+" "+super.toString(); + } + + /* ------------------------------------------------------------ */ + /** + * @see javax.servlet.http.HttpServletRequest#login(javax.servlet.http.HttpServletResponse) + */ + public boolean login(HttpServletResponse response) throws IOException, LoginException + { + // TODO Auto-generated method stub + return false; + } + + /* ------------------------------------------------------------ */ + /** + * @see javax.servlet.http.HttpServletRequest#login(java.lang.String, java.lang.String) + */ + public void login(String username, String password) throws LoginException + { + // TODO Auto-generated method stub + + } + + /* ------------------------------------------------------------ */ + /** + * @see javax.servlet.http.HttpServletRequest#logout() + */ + public void logout() throws LoginException + { + // TODO Auto-generated method stub + + } + +} + diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/RequestLog.java b/jetty-server/src/main/java/org/eclipse/jetty/server/RequestLog.java new file mode 100644 index 0000000000..2e95bbc0e0 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/RequestLog.java @@ -0,0 +1,26 @@ +// ======================================================================== +// Copyright (c) 2006-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server; + +import org.eclipse.jetty.util.component.LifeCycle; + +/** + * A <code>RequestLog</code> can be attached to a {@link org.eclipse.jetty.server.server.handler.RequestLogHandler} to enable logging of requests/responses. + * + * @see Server#setRequestLog + */ +public interface RequestLog extends LifeCycle +{ + public void log(Request request, Response response); +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceCache.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceCache.java new file mode 100644 index 0000000000..09b3626015 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceCache.java @@ -0,0 +1,555 @@ +// ======================================================================== +// Copyright (c) 2000-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jetty.http.HttpContent; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.MimeTypes; +import org.eclipse.jetty.io.Buffer; +import org.eclipse.jetty.io.ByteArrayBuffer; +import org.eclipse.jetty.io.View; +import org.eclipse.jetty.util.component.AbstractLifeCycle; +import org.eclipse.jetty.util.resource.Resource; +import org.eclipse.jetty.util.resource.ResourceFactory; + + +/* ------------------------------------------------------------ */ +/** + * + */ +public class ResourceCache extends AbstractLifeCycle implements Serializable +{ + private int _maxCachedFileSize =1024*1024; + private int _maxCachedFiles=2048; + private int _maxCacheSize =16*1024*1024; + private MimeTypes _mimeTypes; + + protected transient Map _cache; + protected transient int _cachedSize; + protected transient int _cachedFiles; + protected transient Content _mostRecentlyUsed; + protected transient Content _leastRecentlyUsed; + + /* ------------------------------------------------------------ */ + /** Constructor. + */ + public ResourceCache(MimeTypes mimeTypes) + { + _mimeTypes=mimeTypes; + } + + /* ------------------------------------------------------------ */ + public int getCachedSize() + { + return _cachedSize; + } + + /* ------------------------------------------------------------ */ + public int getCachedFiles() + { + return _cachedFiles; + } + + + /* ------------------------------------------------------------ */ + public int getMaxCachedFileSize() + { + return _maxCachedFileSize; + } + + /* ------------------------------------------------------------ */ + public void setMaxCachedFileSize(int maxCachedFileSize) + { + _maxCachedFileSize = maxCachedFileSize; + flushCache(); + } + + /* ------------------------------------------------------------ */ + public int getMaxCacheSize() + { + return _maxCacheSize; + } + + /* ------------------------------------------------------------ */ + public void setMaxCacheSize(int maxCacheSize) + { + _maxCacheSize = maxCacheSize; + flushCache(); + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the maxCachedFiles. + */ + public int getMaxCachedFiles() + { + return _maxCachedFiles; + } + + /* ------------------------------------------------------------ */ + /** + * @param maxCachedFiles The maxCachedFiles to set. + */ + public void setMaxCachedFiles(int maxCachedFiles) + { + _maxCachedFiles = maxCachedFiles; + } + + /* ------------------------------------------------------------ */ + public void flushCache() + { + if (_cache!=null) + { + synchronized(this) + { + ArrayList<Content> values=new ArrayList<Content>(_cache.values()); + for (Content content : values) + content.invalidate(); + + _cache.clear(); + + _cachedSize=0; + _cachedFiles=0; + _mostRecentlyUsed=null; + _leastRecentlyUsed=null; + } + } + } + + /* ------------------------------------------------------------ */ + /** Get a Entry from the cache. + * Get either a valid entry object or create a new one if possible. + * + * @param pathInContext The key into the cache + * @param factory If no matching entry is found, this {@link ResourceFactory} will be used to create the {@link Resource} + * for the new enry that is created. + * @return The entry matching <code>pathInContext</code>, or a new entry if no matching entry was found + */ + public Content lookup(String pathInContext, ResourceFactory factory) + throws IOException + { + Content content=null; + + // Look up cache operations + synchronized(_cache) + { + // Look for it in the cache + content = (Content)_cache.get(pathInContext); + + if (content!=null && content.isValid()) + { + return content; + } + } + Resource resource=factory.getResource(pathInContext); + return load(pathInContext,resource); + } + + /* ------------------------------------------------------------ */ + public Content lookup(String pathInContext, Resource resource) + throws IOException + { + Content content=null; + + // Look up cache operations + synchronized(_cache) + { + // Look for it in the cache + content = (Content)_cache.get(pathInContext); + + if (content!=null && content.isValid()) + { + return content; + } + } + return load(pathInContext,resource); + } + + /* ------------------------------------------------------------ */ + private Content load(String pathInContext, Resource resource) + throws IOException + { + Content content=null; + if (resource!=null && resource.exists() && !resource.isDirectory()) + { + long len = resource.length(); + if (len>0 && len<_maxCachedFileSize && len<_maxCacheSize) + { + int must_be_smaller_than=_maxCacheSize-(int)len; + + synchronized(_cache) + { + // check the cache is not full of locked content before loading content + + while(_leastRecentlyUsed!=null && (_cachedSize>must_be_smaller_than || (_maxCachedFiles>0 && _cachedFiles>=_maxCachedFiles))) + _leastRecentlyUsed.invalidate(); + + if(_cachedSize>must_be_smaller_than || (_maxCachedFiles>0 && _cachedFiles>=_maxCachedFiles)) + return null; + } + + content = new Content(resource); + fill(content); + + synchronized(_cache) + { + // check that somebody else did not fill this spot. + Content content2 =(Content)_cache.get(pathInContext); + if (content2!=null) + { + content.release(); + return content2; + } + + while(_leastRecentlyUsed!=null && (_cachedSize>must_be_smaller_than || (_maxCachedFiles>0 && _cachedFiles>=_maxCachedFiles))) + _leastRecentlyUsed.invalidate(); + + if(_cachedSize>must_be_smaller_than || (_maxCachedFiles>0 && _cachedFiles>=_maxCachedFiles)) + return null; // this could waste an allocated File or DirectBuffer + + content.cache(pathInContext); + + return content; + } + } + } + + return null; + } + + /* ------------------------------------------------------------ */ + /** Remember a Resource Miss! + * @param pathInContext + * @param resource + * @throws IOException + */ + public void miss(String pathInContext, Resource resource) + throws IOException + { + synchronized(_cache) + { + while(_maxCachedFiles>0 && _cachedFiles>=_maxCachedFiles && _leastRecentlyUsed!=null) + _leastRecentlyUsed.invalidate(); + if (_maxCachedFiles>0 && _cachedFiles>=_maxCachedFiles) + return; + + // check that somebody else did not fill this spot. + Miss miss = new Miss(resource); + Content content2 =(Content)_cache.get(pathInContext); + if (content2!=null) + { + miss.release(); + return; + } + + miss.cache(pathInContext); + } + } + + /* ------------------------------------------------------------ */ + public synchronized void doStart() + throws Exception + { + _cache=new HashMap(); + _cachedSize=0; + _cachedFiles=0; + } + + /* ------------------------------------------------------------ */ + /** Stop the context. + */ + public void doStop() + throws InterruptedException + { + flushCache(); + } + + /* ------------------------------------------------------------ */ + protected void fill(Content content) + throws IOException + { + try + { + InputStream in = content.getResource().getInputStream(); + int len=(int)content.getResource().length(); + Buffer buffer = new ByteArrayBuffer(len); + buffer.readFrom(in,len); + in.close(); + content.setBuffer(buffer); + } + finally + { + content.getResource().release(); + } + } + + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + /** MetaData associated with a context Resource. + */ + public class Content implements HttpContent + { + boolean _locked; + String _key; + Resource _resource; + long _lastModified; + Content _prev; + Content _next; + + Buffer _lastModifiedBytes; + Buffer _contentType; + Buffer _buffer; + + /* ------------------------------------------------------------ */ + Content(Resource resource) + { + _resource=resource; + + _next=this; + _prev=this; + _contentType=_mimeTypes.getMimeByExtension(_resource.toString()); + + _lastModified=resource.lastModified(); + } + + + /* ------------------------------------------------------------ */ + /** + * @return true if the content is locked in the cache + */ + public boolean isLocked() + { + return _locked; + } + + + /* ------------------------------------------------------------ */ + /** + * @param locked true if the content is locked in the cache + */ + public void setLocked(boolean locked) + { + synchronized (_cache) + { + if (_locked && !locked) + { + _locked = locked; + _next=_mostRecentlyUsed; + _mostRecentlyUsed=this; + if (_next!=null) + _next._prev=this; + _prev=null; + if (_leastRecentlyUsed==null) + _leastRecentlyUsed=this; + } + else if (!_locked && locked) + { + if (_prev!=null) + _prev._next=_next; + if (_next!=null) + _next._prev=_prev; + _next=_prev=null; + } + else + _locked = locked; + } + } + + + /* ------------------------------------------------------------ */ + void cache(String pathInContext) + { + _key=pathInContext; + + if (!_locked) + { + _next=_mostRecentlyUsed; + _mostRecentlyUsed=this; + if (_next!=null) + _next._prev=this; + _prev=null; + if (_leastRecentlyUsed==null) + _leastRecentlyUsed=this; + } + _cache.put(_key,this); + if (_buffer!=null) + _cachedSize+=_buffer.length(); + _cachedFiles++; + if (_lastModified!=-1) + _lastModifiedBytes=new ByteArrayBuffer(HttpFields.formatDate(_lastModified)); + } + + /* ------------------------------------------------------------ */ + public String getKey() + { + return _key; + } + + /* ------------------------------------------------------------ */ + public boolean isCached() + { + return _key!=null; + } + + /* ------------------------------------------------------------ */ + public Resource getResource() + { + return _resource; + } + + /* ------------------------------------------------------------ */ + boolean isValid() + { + if (_lastModified==_resource.lastModified()) + { + if (!_locked && _mostRecentlyUsed!=this) + { + Content tp = _prev; + Content tn = _next; + + _next=_mostRecentlyUsed; + _mostRecentlyUsed=this; + if (_next!=null) + _next._prev=this; + _prev=null; + + if (tp!=null) + tp._next=tn; + if (tn!=null) + tn._prev=tp; + + if (_leastRecentlyUsed==this && tp!=null) + _leastRecentlyUsed=tp; + } + return true; + } + + invalidate(); + return false; + } + + /* ------------------------------------------------------------ */ + public void invalidate() + { + synchronized(this) + { + // Invalidate it + _cache.remove(_key); + _key=null; + if (_buffer!=null) + _cachedSize=_cachedSize-(int)_buffer.length(); + _cachedFiles--; + + if (_mostRecentlyUsed==this) + _mostRecentlyUsed=_next; + else + _prev._next=_next; + + if (_leastRecentlyUsed==this) + _leastRecentlyUsed=_prev; + else + _next._prev=_prev; + + _prev=null; + _next=null; + if (_resource!=null) + _resource.release(); + _resource=null; + + } + } + + /* ------------------------------------------------------------ */ + public Buffer getLastModified() + { + return _lastModifiedBytes; + } + + /* ------------------------------------------------------------ */ + public Buffer getContentType() + { + return _contentType; + } + + /* ------------------------------------------------------------ */ + public void setContentType(Buffer type) + { + _contentType=type; + } + + /* ------------------------------------------------------------ */ + public void release() + { + } + + /* ------------------------------------------------------------ */ + public Buffer getBuffer() + { + if (_buffer==null) + return null; + return new View(_buffer); + } + + /* ------------------------------------------------------------ */ + public void setBuffer(Buffer buffer) + { + _buffer=buffer; + } + + /* ------------------------------------------------------------ */ + public long getContentLength() + { + if (_buffer==null) + return -1; + return _buffer.length(); + } + + /* ------------------------------------------------------------ */ + public InputStream getInputStream() throws IOException + { + return _resource.getInputStream(); + } + } + + + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + /** MetaData associated with a context Resource. + */ + public class Miss extends Content + { + Miss(Resource resource) + { + super(resource); + } + + /* ------------------------------------------------------------ */ + boolean isValid() + { + if (_resource.exists()) + { + invalidate(); + return false; + } + return true; + } + } +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java new file mode 100644 index 0000000000..8fc497162f --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java @@ -0,0 +1,1191 @@ +// ======================================================================== +// Copyright (c) 2004-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Locale; + +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.eclipse.jetty.http.Generator; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpGenerator; +import org.eclipse.jetty.http.HttpHeaderValues; +import org.eclipse.jetty.http.HttpHeaders; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.HttpVersions; +import org.eclipse.jetty.http.MimeTypes; +import org.eclipse.jetty.io.BufferCache.CachedBuffer; +import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.server.handler.ErrorHandler; +import org.eclipse.jetty.util.ByteArrayISO8859Writer; +import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.util.QuotedStringTokenizer; +import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.util.URIUtil; +import org.eclipse.jetty.util.log.Log; + +/* ------------------------------------------------------------ */ +/** Response. + * <p> + * Implements {@link javax.servlet.HttpServletResponse} from the {@link javax.servlet} package. + * </p> + * + * + * + */ +public class Response implements HttpServletResponse +{ + public static final int + NONE=0, + STREAM=1, + WRITER=2; + + /** + * If a header name starts with this string, the header (stripped of the prefix) + * can be set during include using only {@link #setHeader(String, String)} or + * {@link #addHeader(String, String)}. + */ + public static String SET_INCLUDE_HEADER_PREFIX = "org.eclipse.jetty.server.include."; + + private static PrintWriter __nullPrintWriter; + private static ServletOutputStream __nullServletOut; + + static + { + try{ + __nullPrintWriter = new PrintWriter(IO.getNullWriter()); + __nullServletOut = new NullOutput(); + } + catch (Exception e) + { + Log.warn(e); + } + } + + private HttpConnection _connection; + private int _status=SC_OK; + private String _reason; + private Locale _locale; + private String _mimeType; + private CachedBuffer _cachedMimeType; + private String _characterEncoding; + private boolean _explicitEncoding; + private String _contentType; + private int _outputState; + private PrintWriter _writer; + + /* ------------------------------------------------------------ */ + /** + * + */ + public Response(HttpConnection connection) + { + _connection=connection; + } + + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletResponse#reset() + */ + protected void recycle() + { + _status=SC_OK; + _reason=null; + _locale=null; + _mimeType=null; + _cachedMimeType=null; + _characterEncoding=null; + _explicitEncoding=false; + _contentType=null; + _outputState=NONE; + _writer=null; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletResponse#addCookie(javax.servlet.http.Cookie) + */ + public void addCookie(Cookie cookie) + { + _connection.getResponseFields().addSetCookie(cookie.getName(), + cookie.getValue(), + cookie.getDomain(), + cookie.getPath(), + cookie.getMaxAge(), + cookie.getComment(), + cookie.getSecure(), + cookie.isHttpOnly(), + cookie.getVersion()); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletResponse#containsHeader(java.lang.String) + */ + public boolean containsHeader(String name) + { + return _connection.getResponseFields().containsKey(name); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletResponse#encodeURL(java.lang.String) + */ + public String encodeURL(String url) + { + Request request=_connection.getRequest(); + SessionManager sessionManager = request.getSessionManager(); + if (sessionManager==null) + return url; + String sessionURLPrefix = sessionManager.getSessionIdPathParameterNamePrefix(); + if (sessionURLPrefix==null) + return url; + + // should not encode if cookies in evidence + if (url==null || request==null || request.isRequestedSessionIdFromCookie()) + { + int prefix=url.indexOf(sessionURLPrefix); + if (prefix!=-1) + { + int suffix=url.indexOf("?",prefix); + if (suffix<0) + suffix=url.indexOf("#",prefix); + + if (suffix<=prefix) + return url.substring(0,prefix); + return url.substring(0,prefix)+url.substring(suffix); + } + return url; + } + + // get session; + HttpSession session=request.getSession(false); + + // no session + if (session == null) + return url; + + + // invalid session + if (!sessionManager.isValid(session)) + return url; + + String id=sessionManager.getNodeId(session); + + + // TODO Check host and port are for this server + // Already encoded + int prefix=url.indexOf(sessionURLPrefix); + if (prefix!=-1) + { + int suffix=url.indexOf("?",prefix); + if (suffix<0) + suffix=url.indexOf("#",prefix); + + if (suffix<=prefix) + return url.substring(0,prefix+sessionURLPrefix.length())+id; + return url.substring(0,prefix+sessionURLPrefix.length())+id+ + url.substring(suffix); + } + + // edit the session + int suffix=url.indexOf('?'); + if (suffix<0) + suffix=url.indexOf('#'); + if (suffix<0) + return url+sessionURLPrefix+id; + return url.substring(0,suffix)+ + sessionURLPrefix+id+url.substring(suffix); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletResponse#encodeRedirectURL(java.lang.String) + */ + public String encodeRedirectURL(String url) + { + return encodeURL(url); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletResponse#encodeUrl(java.lang.String) + */ + public String encodeUrl(String url) + { + return encodeURL(url); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletResponse#encodeRedirectUrl(java.lang.String) + */ + public String encodeRedirectUrl(String url) + { + return encodeURL(url); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletResponse#sendError(int, java.lang.String) + */ + public void sendError(int code, String message) throws IOException + { + if (_connection.isIncluding()) + return; + + if (isCommitted()) + Log.warn("Committed before "+code+" "+message); + + resetBuffer(); + _characterEncoding=null; + setHeader(HttpHeaders.EXPIRES,null); + setHeader(HttpHeaders.LAST_MODIFIED,null); + setHeader(HttpHeaders.CACHE_CONTROL,null); + setHeader(HttpHeaders.CONTENT_TYPE,null); + setHeader(HttpHeaders.CONTENT_LENGTH,null); + + _outputState=NONE; + setStatus(code,message); + + if (message==null) + message=HttpStatus.getCode(code).getMessage(); + + // If we are allowed to have a body + if (code!=SC_NO_CONTENT && + code!=SC_NOT_MODIFIED && + code!=SC_PARTIAL_CONTENT && + code>=SC_OK) + { + Request request = _connection.getRequest(); + + ErrorHandler error_handler = null; + ContextHandler.Context context = request.getContext(); + if (context!=null) + error_handler=context.getContextHandler().getErrorHandler(); + if (error_handler!=null) + { + // TODO - probably should reset these after the request? + request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE,new Integer(code)); + request.setAttribute(RequestDispatcher.ERROR_MESSAGE, message); + request.setAttribute(RequestDispatcher.ERROR_REQUEST_URI, request.getRequestURI()); + request.setAttribute(RequestDispatcher.ERROR_SERVLET_NAME,request.getServletName()); + + error_handler.handle(null,_connection.getRequest(),this); + } + else + { + setHeader(HttpHeaders.CACHE_CONTROL, "must-revalidate,no-cache,no-store"); + setContentType(MimeTypes.TEXT_HTML_8859_1); + ByteArrayISO8859Writer writer= new ByteArrayISO8859Writer(2048); + if (message != null) + { + message= StringUtil.replace(message, "&", "&"); + message= StringUtil.replace(message, "<", "<"); + message= StringUtil.replace(message, ">", ">"); + } + String uri= request.getRequestURI(); + if (uri!=null) + { + uri= StringUtil.replace(uri, "&", "&"); + uri= StringUtil.replace(uri, "<", "<"); + uri= StringUtil.replace(uri, ">", ">"); + } + + writer.write("<html>\n<head>\n<meta http-equiv=\"Content-Type\" content=\"text/html;charset=ISO-8859-1\"/>\n"); + writer.write("<title>Error "); + writer.write(Integer.toString(code)); + writer.write(' '); + if (message==null) + message=HttpStatus.getCode(code).getMessage(); + writer.write(message); + writer.write("</title>\n</head>\n<body>\n<h2>HTTP ERROR: "); + writer.write(Integer.toString(code)); + writer.write("</h2>\n<p>Problem accessing "); + writer.write(uri); + writer.write(". Reason:\n<pre> "); + writer.write(message); + writer.write("</pre>"); + writer.write("</p>\n<hr /><i><small>Powered by Jetty://</small></i>"); + + for (int i= 0; i < 20; i++) + writer.write("\n "); + writer.write("\n</body>\n</html>\n"); + + writer.flush(); + setContentLength(writer.size()); + writer.writeTo(getOutputStream()); + writer.destroy(); + } + } + else if (code!=SC_PARTIAL_CONTENT) + { + _connection.getRequestFields().remove(HttpHeaders.CONTENT_TYPE_BUFFER); + _connection.getRequestFields().remove(HttpHeaders.CONTENT_LENGTH_BUFFER); + _characterEncoding=null; + _mimeType=null; + _cachedMimeType=null; + } + + complete(); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletResponse#sendError(int) + */ + public void sendError(int sc) throws IOException + { + if (sc==102) + sendProcessing(); + else + sendError(sc,null); + } + + /* ------------------------------------------------------------ */ + /* Send a 102-Processing response. + * If the connection is a HTTP connection, the version is 1.1 and the + * request has a Expect header starting with 102, then a 102 response is + * sent. This indicates that the request still be processed and real response + * can still be sent. This method is called by sendError if it is passed 102. + * @see javax.servlet.http.HttpServletResponse#sendError(int) + */ + public void sendProcessing() throws IOException + { + Generator g = _connection.getGenerator(); + if (g instanceof HttpGenerator) + { + HttpGenerator generator = (HttpGenerator)g; + + String expect = _connection.getRequest().getHeader(HttpHeaders.EXPECT); + + if (expect!=null && expect.startsWith("102") && generator.getVersion()>=HttpVersions.HTTP_1_1_ORDINAL) + { + boolean was_persistent=generator.isPersistent(); + generator.setResponse(102,null); + generator.completeHeader(null,true); + generator.setPersistent(true); + generator.complete(); + generator.flushBuffer(); + generator.reset(false); + generator.setPersistent(was_persistent); + } + } + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletResponse#sendRedirect(java.lang.String) + */ + public void sendRedirect(String location) throws IOException + { + if (_connection.isIncluding()) + return; + + if (location==null) + throw new IllegalArgumentException(); + + if (!URIUtil.hasScheme(location)) + { + StringBuilder buf = _connection.getRequest().getRootURL(); + if (location.startsWith("/")) + buf.append(URIUtil.canonicalPath(location)); + else + { + String path=_connection.getRequest().getRequestURI(); + String parent=(path.endsWith("/"))?path:URIUtil.parentPath(path); + location=URIUtil.canonicalPath(URIUtil.addPaths(parent,location)); + if(location==null) + throw new IllegalStateException("path cannot be above root"); + if (!location.startsWith("/")) + buf.append('/'); + buf.append(location); + } + + location=buf.toString(); + } + resetBuffer(); + + setHeader(HttpHeaders.LOCATION,location); + setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY); + complete(); + + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletResponse#setDateHeader(java.lang.String, long) + */ + public void setDateHeader(String name, long date) + { + if (!_connection.isIncluding()) + _connection.getResponseFields().putDateField(name, date); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletResponse#addDateHeader(java.lang.String, long) + */ + public void addDateHeader(String name, long date) + { + if (!_connection.isIncluding()) + _connection.getResponseFields().addDateField(name, date); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletResponse#setHeader(java.lang.String, java.lang.String) + */ + public void setHeader(String name, String value) + { + if (_connection.isIncluding()) + { + if (name.startsWith(SET_INCLUDE_HEADER_PREFIX)) + name=name.substring(SET_INCLUDE_HEADER_PREFIX.length()); + else + return; + } + _connection.getResponseFields().put(name, value); + if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) + { + if (value==null) + _connection._generator.setContentLength(-1); + else + _connection._generator.setContentLength(Long.parseLong(value)); + } + } + + /* ------------------------------------------------------------ */ + /* + */ + public String getHeader(String name) + { + return _connection.getResponseFields().getStringField(name); + } + + /* ------------------------------------------------------------ */ + /* + */ + public Enumeration getHeaders(String name) + { + Enumeration e = _connection.getResponseFields().getValues(name); + if (e==null) + return Collections.enumeration(Collections.EMPTY_LIST); + return e; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletResponse#addHeader(java.lang.String, java.lang.String) + */ + public void addHeader(String name, String value) + { + if (_connection.isIncluding()) + { + if (name.startsWith(SET_INCLUDE_HEADER_PREFIX)) + name=name.substring(SET_INCLUDE_HEADER_PREFIX.length()); + else + return; + } + + _connection.getResponseFields().add(name, value); + if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) + _connection._generator.setContentLength(Long.parseLong(value)); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletResponse#setIntHeader(java.lang.String, int) + */ + public void setIntHeader(String name, int value) + { + if (!_connection.isIncluding()) + { + _connection.getResponseFields().putLongField(name, value); + if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) + _connection._generator.setContentLength(value); + } + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletResponse#addIntHeader(java.lang.String, int) + */ + public void addIntHeader(String name, int value) + { + if (!_connection.isIncluding()) + { + _connection.getResponseFields().addLongField(name, value); + if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) + _connection._generator.setContentLength(value); + } + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletResponse#setStatus(int) + */ + public void setStatus(int sc) + { + setStatus(sc,null); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpServletResponse#setStatus(int, java.lang.String) + */ + public void setStatus(int sc, String sm) + { + if (sc<=0) + throw new IllegalArgumentException(); + if (!_connection.isIncluding()) + { + _status=sc; + _reason=sm; + } + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletResponse#getCharacterEncoding() + */ + public String getCharacterEncoding() + { + if (_characterEncoding==null) + _characterEncoding=StringUtil.__ISO_8859_1; + return _characterEncoding; + } + + /* ------------------------------------------------------------ */ + String getSetCharacterEncoding() + { + return _characterEncoding; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletResponse#getContentType() + */ + public String getContentType() + { + return _contentType; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletResponse#getOutputStream() + */ + public ServletOutputStream getOutputStream() throws IOException + { + if (_outputState!=NONE && _outputState!=STREAM) + throw new IllegalStateException("WRITER"); + + _outputState=STREAM; + return _connection.getOutputStream(); + } + + /* ------------------------------------------------------------ */ + public boolean isWriting() + { + return _outputState==WRITER; + } + + /* ------------------------------------------------------------ */ + public boolean isOutputing() + { + return _outputState!=NONE; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletResponse#getWriter() + */ + public PrintWriter getWriter() throws IOException + { + if (_outputState!=NONE && _outputState!=WRITER) + throw new IllegalStateException("STREAM"); + + /* if there is no writer yet */ + if (_writer==null) + { + /* get encoding from Content-Type header */ + String encoding = _characterEncoding; + + if (encoding==null) + { + /* implementation of educated defaults */ + if(_mimeType!=null) + encoding = null; // TODO getHttpContext().getEncodingByMimeType(_mimeType); + + if (encoding==null) + encoding = StringUtil.__ISO_8859_1; + + setCharacterEncoding(encoding); + } + + /* construct Writer using correct encoding */ + _writer = _connection.getPrintWriter(encoding); + } + _outputState=WRITER; + return _writer; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletResponse#setCharacterEncoding(java.lang.String) + */ + public void setCharacterEncoding(String encoding) + { + if (_connection.isIncluding()) + return; + + if (this._outputState==0 && !isCommitted()) + { + _explicitEncoding=true; + + if (encoding==null) + { + // Clear any encoding. + if (_characterEncoding!=null) + { + _characterEncoding=null; + if (_cachedMimeType!=null) + _connection.getResponseFields().put(HttpHeaders.CONTENT_TYPE_BUFFER,_cachedMimeType); + else + _connection.getResponseFields().put(HttpHeaders.CONTENT_TYPE_BUFFER,_mimeType); + } + } + else + { + // No, so just add this one to the mimetype + _characterEncoding=encoding; + if (_contentType!=null) + { + int i0=_contentType.indexOf(';'); + if (i0<0) + { + _contentType=null; + if(_cachedMimeType!=null) + { + CachedBuffer content_type = _cachedMimeType.getAssociate(_characterEncoding); + if (content_type!=null) + { + _contentType=content_type.toString(); + _connection.getResponseFields().put(HttpHeaders.CONTENT_TYPE_BUFFER,content_type); + } + } + + if (_contentType==null) + { + _contentType = _mimeType+";charset="+QuotedStringTokenizer.quote(_characterEncoding,";= "); + _connection.getResponseFields().put(HttpHeaders.CONTENT_TYPE_BUFFER,_contentType); + } + } + else + { + int i1=_contentType.indexOf("charset=",i0); + if (i1<0) + { + _contentType = _contentType+" charset="+QuotedStringTokenizer.quote(_characterEncoding,";= "); + } + else + { + int i8=i1+8; + int i2=_contentType.indexOf(" ",i8); + if (i2<0) + _contentType=_contentType.substring(0,i8)+QuotedStringTokenizer.quote(_characterEncoding,";= "); + else + _contentType=_contentType.substring(0,i8)+QuotedStringTokenizer.quote(_characterEncoding,";= ")+_contentType.substring(i2); + } + _connection.getResponseFields().put(HttpHeaders.CONTENT_TYPE_BUFFER,_contentType); + } + } + } + } + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletResponse#setContentLength(int) + */ + public void setContentLength(int len) + { + // Protect from setting after committed as default handling + // of a servlet HEAD request ALWAYS sets _content length, even + // if the getHandling committed the response! + if (isCommitted() || _connection.isIncluding()) + return; + _connection._generator.setContentLength(len); + if (len>=0) + { + _connection.getResponseFields().putLongField(HttpHeaders.CONTENT_LENGTH, len); + if (_connection._generator.isContentWritten()) + { + if (_outputState==WRITER) + _writer.close(); + else if (_outputState==STREAM) + { + try + { + getOutputStream().close(); + } + catch(IOException e) + { + throw new RuntimeException(e); + } + } + } + } + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletResponse#setContentLength(int) + */ + public void setLongContentLength(long len) + { + // Protect from setting after committed as default handling + // of a servlet HEAD request ALWAYS sets _content length, even + // if the getHandling committed the response! + if (isCommitted() || _connection.isIncluding()) + return; + _connection._generator.setContentLength(len); + _connection.getResponseFields().putLongField(HttpHeaders.CONTENT_LENGTH, len); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletResponse#setContentType(java.lang.String) + */ + public void setContentType(String contentType) + { + if (isCommitted() || _connection.isIncluding()) + return; + + // Yes this method is horribly complex.... but there are lots of special cases and + // as this method is called on every request, it is worth trying to save string creation. + // + + if (contentType==null) + { + if (_locale==null) + _characterEncoding=null; + _mimeType=null; + _cachedMimeType=null; + _contentType=null; + _connection.getResponseFields().remove(HttpHeaders.CONTENT_TYPE_BUFFER); + } + else + { + // Look for encoding in contentType + int i0=contentType.indexOf(';'); + + if (i0>0) + { + // we have content type parameters + + // Extract params off mimetype + _mimeType=contentType.substring(0,i0).trim(); + _cachedMimeType=MimeTypes.CACHE.get(_mimeType); + + // Look for charset + int i1=contentType.indexOf("charset=",i0+1); + if (i1>=0) + { + _explicitEncoding=true; + int i8=i1+8; + int i2 = contentType.indexOf(' ',i8); + + if (_outputState==WRITER) + { + // strip the charset and ignore; + if ((i1==i0+1 && i2<0) || (i1==i0+2 && i2<0 && contentType.charAt(i0+1)==' ')) + { + if (_cachedMimeType!=null) + { + CachedBuffer content_type = _cachedMimeType.getAssociate(_characterEncoding); + if (content_type!=null) + { + _contentType=content_type.toString(); + _connection.getResponseFields().put(HttpHeaders.CONTENT_TYPE_BUFFER,content_type); + } + else + { + _contentType=_mimeType+";charset="+_characterEncoding; + _connection.getResponseFields().put(HttpHeaders.CONTENT_TYPE_BUFFER,_contentType); + } + } + else + { + _contentType=_mimeType+";charset="+_characterEncoding; + _connection.getResponseFields().put(HttpHeaders.CONTENT_TYPE_BUFFER,_contentType); + } + } + else if (i2<0) + { + _contentType=contentType.substring(0,i1)+" charset="+QuotedStringTokenizer.quote(_characterEncoding,";= "); + _connection.getResponseFields().put(HttpHeaders.CONTENT_TYPE_BUFFER,_contentType); + } + else + { + _contentType=contentType.substring(0,i1)+contentType.substring(i2)+" charset="+QuotedStringTokenizer.quote(_characterEncoding,";= "); + _connection.getResponseFields().put(HttpHeaders.CONTENT_TYPE_BUFFER,_contentType); + } + } + else if ((i1==i0+1 && i2<0) || (i1==i0+2 && i2<0 && contentType.charAt(i0+1)==' ')) + { + // The params are just the char encoding + _cachedMimeType=MimeTypes.CACHE.get(_mimeType); + _characterEncoding = QuotedStringTokenizer.unquote(contentType.substring(i8)); + + if (_cachedMimeType!=null) + { + CachedBuffer content_type = _cachedMimeType.getAssociate(_characterEncoding); + if (content_type!=null) + { + _contentType=content_type.toString(); + _connection.getResponseFields().put(HttpHeaders.CONTENT_TYPE_BUFFER,content_type); + } + else + { + _contentType=contentType; + _connection.getResponseFields().put(HttpHeaders.CONTENT_TYPE_BUFFER,_contentType); + } + } + else + { + _contentType=contentType; + _connection.getResponseFields().put(HttpHeaders.CONTENT_TYPE_BUFFER,_contentType); + } + } + else if (i2>0) + { + _characterEncoding = QuotedStringTokenizer.unquote(contentType.substring(i8,i2)); + _contentType=contentType; + _connection.getResponseFields().put(HttpHeaders.CONTENT_TYPE_BUFFER,_contentType); + } + else + { + _characterEncoding = QuotedStringTokenizer.unquote(contentType.substring(i8)); + _contentType=contentType; + _connection.getResponseFields().put(HttpHeaders.CONTENT_TYPE_BUFFER,_contentType); + } + } + else // No encoding in the params. + { + _cachedMimeType=null; + _contentType=_characterEncoding==null?contentType:contentType+" charset="+QuotedStringTokenizer.quote(_characterEncoding,";= "); + _connection.getResponseFields().put(HttpHeaders.CONTENT_TYPE_BUFFER,_contentType); + } + } + else // No params at all + { + _mimeType=contentType; + _cachedMimeType=MimeTypes.CACHE.get(_mimeType); + + if (_characterEncoding!=null) + { + if (_cachedMimeType!=null) + { + CachedBuffer content_type = _cachedMimeType.getAssociate(_characterEncoding); + if (content_type!=null) + { + _contentType=content_type.toString(); + _connection.getResponseFields().put(HttpHeaders.CONTENT_TYPE_BUFFER,content_type); + } + else + { + _contentType=_mimeType+";charset="+QuotedStringTokenizer.quote(_characterEncoding,";= "); + _connection.getResponseFields().put(HttpHeaders.CONTENT_TYPE_BUFFER,_contentType); + } + } + else + { + _contentType=contentType+";charset="+QuotedStringTokenizer.quote(_characterEncoding,";= "); + _connection.getResponseFields().put(HttpHeaders.CONTENT_TYPE_BUFFER,_contentType); + } + } + else if (_cachedMimeType!=null) + { + _contentType=_cachedMimeType.toString(); + _connection.getResponseFields().put(HttpHeaders.CONTENT_TYPE_BUFFER,_cachedMimeType); + } + else + { + _contentType=contentType; + _connection.getResponseFields().put(HttpHeaders.CONTENT_TYPE_BUFFER,_contentType); + } + } + } + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletResponse#setBufferSize(int) + */ + public void setBufferSize(int size) + { + if (isCommitted() || getContentCount()>0) + throw new IllegalStateException("Committed or content written"); + _connection.getGenerator().increaseContentBufferSize(size); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletResponse#getBufferSize() + */ + public int getBufferSize() + { + return _connection.getGenerator().getContentBufferSize(); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletResponse#flushBuffer() + */ + public void flushBuffer() throws IOException + { + _connection.flushResponse(); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletResponse#reset() + */ + public void reset() + { + fwdReset(); + _status=200; + _reason=null; + + HttpFields response_fields=_connection.getResponseFields(); + + response_fields.clear(); + String connection=_connection.getRequestFields().getStringField(HttpHeaders.CONNECTION_BUFFER); + if (connection!=null) + { + String[] values = connection.split(","); + for (int i=0;values!=null && i<values.length;i++) + { + CachedBuffer cb = HttpHeaderValues.CACHE.get(values[0].trim()); + + if (cb!=null) + { + switch(cb.getOrdinal()) + { + case HttpHeaderValues.CLOSE_ORDINAL: + response_fields.put(HttpHeaders.CONNECTION_BUFFER,HttpHeaderValues.CLOSE_BUFFER); + break; + + case HttpHeaderValues.KEEP_ALIVE_ORDINAL: + if (HttpVersions.HTTP_1_0.equalsIgnoreCase(_connection.getRequest().getProtocol())) + response_fields.put(HttpHeaders.CONNECTION_BUFFER,HttpHeaderValues.KEEP_ALIVE); + break; + case HttpHeaderValues.TE_ORDINAL: + response_fields.put(HttpHeaders.CONNECTION_BUFFER,HttpHeaderValues.TE); + break; + } + } + } + } + + if (_connection.getConnector().getServer().getSendDateHeader()) + { + Request request=_connection.getRequest(); + response_fields.put(HttpHeaders.DATE_BUFFER, request.getTimeStampBuffer(),request.getTimeStamp()); + } + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletResponse#reset() + */ + public void fwdReset() + { + resetBuffer(); + _mimeType=null; + _cachedMimeType=null; + _contentType=null; + _characterEncoding=null; + _explicitEncoding=false; + _locale=null; + _outputState=NONE; + _writer=null; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletResponse#resetBuffer() + */ + public void resetBuffer() + { + if (isCommitted()) + throw new IllegalStateException("Committed"); + _connection.getGenerator().resetBuffer(); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletResponse#isCommitted() + */ + public boolean isCommitted() + { + return _connection.isResponseCommitted(); + } + + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletResponse#setLocale(java.util.Locale) + */ + public void setLocale(Locale locale) + { + if (locale == null || isCommitted() ||_connection.isIncluding()) + return; + + _locale = locale; + _connection.getResponseFields().put(HttpHeaders.CONTENT_LANGUAGE_BUFFER,locale.toString().replace('_','-')); + + if (_explicitEncoding || _outputState!=0 ) + return; + + if (_connection.getRequest().getContext()==null) + return; + + String charset = _connection.getRequest().getContext().getContextHandler().getLocaleEncoding(locale); + + if (charset!=null && charset.length()>0) + { + _characterEncoding=charset; + + /* get current MIME type from Content-Type header */ + String type=getContentType(); + if (type!=null) + { + _characterEncoding=charset; + int semi=type.indexOf(';'); + if (semi<0) + { + _mimeType=type; + _contentType= type += ";charset="+charset; + } + else + { + _mimeType=type.substring(0,semi); + _contentType= _mimeType += ";charset="+charset; + } + + _cachedMimeType=MimeTypes.CACHE.get(_mimeType); + _connection.getResponseFields().put(HttpHeaders.CONTENT_TYPE_BUFFER,_contentType); + } + } + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletResponse#getLocale() + */ + public Locale getLocale() + { + if (_locale==null) + return Locale.getDefault(); + return _locale; + } + + /* ------------------------------------------------------------ */ + /** + * @return The HTTP status code that has been set for this request. This will be <code>200<code> + * ({@link HttpServletResponse#SC_OK}), unless explicitly set through one of the <code>setStatus</code> methods. + */ + public int getStatus() + { + return _status; + } + + /* ------------------------------------------------------------ */ + /** + * @return The reason associated with the current {@link #getStatus() status}. This will be <code>null</code>, + * unless one of the <code>setStatus</code> methods have been called. + */ + public String getReason() + { + return _reason; + } + + /* ------------------------------------------------------------ */ + /** + */ + public void complete() + throws IOException + { + _connection.completeResponse(); + } + + /* ------------------------------------------------------------- */ + /** + * @return the number of bytes actually written in response body + */ + public long getContentCount() + { + if (_connection==null || _connection.getGenerator()==null) + return -1; + return _connection.getGenerator().getContentWritten(); + } + + /* ------------------------------------------------------------ */ + public HttpFields getHttpFields() + { + return _connection.getResponseFields(); + } + + /* ------------------------------------------------------------ */ + public String toString() + { + return "HTTP/1.1 "+_status+" "+ (_reason==null?"":_reason) +System.getProperty("line.separator")+ + _connection.getResponseFields().toString(); + } + + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + private static class NullOutput extends ServletOutputStream + { + public void write(int b) throws IOException + { + } + + public void print(String s) throws IOException + { + } + + public void println(String s) throws IOException + { + } + + public void write(byte[] b, int off, int len) throws IOException + { + } + + } + +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/RetryRequest.java b/jetty-server/src/main/java/org/eclipse/jetty/server/RetryRequest.java new file mode 100644 index 0000000000..e3546c376e --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/RetryRequest.java @@ -0,0 +1,29 @@ +// ======================================================================== +// Copyright (c) 2004-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server; + +/* ------------------------------------------------------------ */ +/** Retry Request + * This is thrown by a non-blocking {@link Continuation} such as + * {@link SuspendableSelectChannelEndPoint}. While it + * extends ThreadDeath, it does not actually stop the thread calling it. + * It extends ThreadDeath so as to be an Error that will not be caught + * by most frameworks. + * + * + * + */ +public class RetryRequest extends ThreadDeath +{ +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Server.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Server.java new file mode 100644 index 0000000000..2deaf4be13 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Server.java @@ -0,0 +1,779 @@ +// ======================================================================== +// Copyright (c) 2004-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +import javax.servlet.AsyncContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.http.HttpGenerator; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.server.bio.SocketConnector; +import org.eclipse.jetty.server.handler.HandlerCollection; +import org.eclipse.jetty.server.handler.HandlerWrapper; +import org.eclipse.jetty.server.nio.SelectChannelConnector; +import org.eclipse.jetty.util.Attributes; +import org.eclipse.jetty.util.AttributesMap; +import org.eclipse.jetty.util.LazyList; +import org.eclipse.jetty.util.MultiException; +import org.eclipse.jetty.util.URIUtil; +import org.eclipse.jetty.util.component.Container; +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.eclipse.jetty.util.thread.ThreadPool; + +/* ------------------------------------------------------------ */ +/** Jetty HTTP Servlet Server. + * This class is the main class for the Jetty HTTP Servlet server. + * It aggregates Connectors (HTTP request receivers) and request Handlers. + * The server is itself a handler and a ThreadPool. Connectors use the ThreadPool methods + * to run jobs that will eventually call the handle method. + * + * @org.apache.xbean.XBean description="Creates an embedded Jetty web server" + */ +public class Server extends HandlerWrapper implements Attributes +{ + private static ShutdownHookThread hookThread = new ShutdownHookThread(); + private static String _version = (Server.class.getPackage()!=null && Server.class.getPackage().getImplementationVersion()!=null) + ?Server.class.getPackage().getImplementationVersion() + :"7.0.x"; + + private ThreadPool _threadPool; + private Connector[] _connectors; + private Container _container=new Container(); + private SessionIdManager _sessionIdManager; + private boolean _sendServerVersion = true; //send Server: header + private boolean _sendDateHeader = false; //send Date: header + private AttributesMap _attributes = new AttributesMap(); + private List<Object> _dependentBeans=new ArrayList<Object>(); + private int _graceful=0; + + /* ------------------------------------------------------------ */ + public Server() + { + setServer(this); + } + + /* ------------------------------------------------------------ */ + /** Convenience constructor + * Creates server and a {@link SocketConnector} at the passed port. + */ + public Server(int port) + { + setServer(this); + + Connector connector=new SelectChannelConnector(); + connector.setPort(port); + setConnectors(new Connector[]{connector}); + } + + + /* ------------------------------------------------------------ */ + public static String getVersion() + { + return _version; + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the container. + */ + public Container getContainer() + { + return _container; + } + + /* ------------------------------------------------------------ */ + public boolean getStopAtShutdown() + { + return hookThread.contains(this); + } + + /* ------------------------------------------------------------ */ + public void setStopAtShutdown(boolean stop) + { + if (stop) + hookThread.add(this); + else + hookThread.remove(this); + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the connectors. + */ + public Connector[] getConnectors() + { + return _connectors; + } + + + /* ------------------------------------------------------------ */ + public void addConnector(Connector connector) + { + setConnectors((Connector[])LazyList.addToArray(getConnectors(), connector, Connector.class)); + } + + /* ------------------------------------------------------------ */ + /** + * Conveniance method which calls {@link #getConnectors()} and {@link #setConnectors(Connector[])} to + * remove a connector. + * @param connector The connector to remove. + */ + public void removeConnector(Connector connector) { + setConnectors((Connector[])LazyList.removeFromArray (getConnectors(), connector)); + } + + /* ------------------------------------------------------------ */ + /** Set the connectors for this server. + * Each connector has this server set as it's ThreadPool and its Handler. + * @param connectors The connectors to set. + */ + public void setConnectors(Connector[] connectors) + { + if (connectors!=null) + { + for (int i=0;i<connectors.length;i++) + connectors[i].setServer(this); + } + + _container.update(this, _connectors, connectors, "connector"); + _connectors = connectors; + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the threadPool. + */ + public ThreadPool getThreadPool() + { + return _threadPool; + } + + /* ------------------------------------------------------------ */ + /** + * @param threadPool The threadPool to set. + */ + public void setThreadPool(ThreadPool threadPool) + { + _container.update(this,_threadPool,threadPool, "threadpool",true); + _threadPool = threadPool; + } + + /* ------------------------------------------------------------ */ + protected void doStart() throws Exception + { + Log.info("jetty-"+_version); + HttpGenerator.setServerVersion(_version); + MultiException mex=new MultiException(); + + Iterator itor = _dependentBeans.iterator(); + while (itor.hasNext()) + { + try + { + Object o=itor.next(); + if (o instanceof LifeCycle) + ((LifeCycle)o).start(); + } + catch (Throwable e) {mex.add(e);} + } + + if (_threadPool==null) + { + QueuedThreadPool tp=new QueuedThreadPool(); + setThreadPool(tp); + } + + if (_sessionIdManager!=null) + _sessionIdManager.start(); + + try + { + if (_threadPool instanceof LifeCycle) + ((LifeCycle)_threadPool).start(); + } + catch(Throwable e) { mex.add(e);} + + try + { + super.doStart(); + } + catch(Throwable e) + { + Log.warn("Error starting handlers",e); + } + + if (_connectors!=null) + { + for (int i=0;i<_connectors.length;i++) + { + try{_connectors[i].start();} + catch(Throwable e) + { + mex.add(e); + } + } + } + mex.ifExceptionThrow(); + } + + /* ------------------------------------------------------------ */ + protected void doStop() throws Exception + { + MultiException mex=new MultiException(); + + if (_graceful>0) + { + if (_connectors!=null) + { + for (int i=_connectors.length;i-->0;) + { + Log.info("Graceful shutdown {}",_connectors[i]); + try{_connectors[i].close();}catch(Throwable e){mex.add(e);} + } + } + + Handler[] contexts = getChildHandlersByClass(Graceful.class); + for (int c=0;c<contexts.length;c++) + { + Graceful context=(Graceful)contexts[c]; + Log.info("Graceful shutdown {}",context); + context.setShutdown(true); + } + Thread.sleep(_graceful); + } + + if (_connectors!=null) + { + for (int i=_connectors.length;i-->0;) + try{_connectors[i].stop();}catch(Throwable e){mex.add(e);} + } + + try {super.doStop(); } catch(Throwable e) { mex.add(e);} + + if (_sessionIdManager!=null) + _sessionIdManager.stop(); + + try + { + if (_threadPool instanceof LifeCycle) + ((LifeCycle)_threadPool).stop(); + } + catch(Throwable e){mex.add(e);} + + if (!_dependentBeans.isEmpty()) + { + ListIterator itor = _dependentBeans.listIterator(_dependentBeans.size()); + while (itor.hasPrevious()) + { + try + { + Object o =itor.previous(); + if (o instanceof LifeCycle) + ((LifeCycle)o).stop(); + } + catch (Throwable e) {mex.add(e);} + } + } + + mex.ifExceptionThrow(); + } + + /* ------------------------------------------------------------ */ + /* Handle a request from a connection. + * Called to handle a request on the connection when either the header has been received, + * or after the entire request has been received (for short requests of known length), or + * on the dispatch of an async request. + */ + public void handle(HttpConnection connection) throws IOException, ServletException + { + final String target=connection.getRequest().getPathInfo(); + final HttpServletRequest request=connection.getRequest(); + final HttpServletResponse response=connection.getResponse(); + + if (Log.isDebugEnabled()) + { + Log.debug("REQUEST "+target+" on "+connection); + handle(target, request, response); + Log.debug("RESPONSE "+target+" "+connection.getResponse().getStatus()); + } + else + handle(target, request, response); + } + + /* ------------------------------------------------------------ */ + /* Handle a request from a connection. + * Called to handle a request on the connection when either the header has been received, + * or after the entire request has been received (for short requests of known length), or + * on the dispatch of an async request. + */ + public void handleAsync(HttpConnection connection) throws IOException, ServletException + { + final AsyncRequest async = connection.getRequest().getAsyncRequest(); + final AsyncRequest.AsyncEventState state = async.getAsyncEventState(); + + final Request base_request=connection.getRequest(); + final String path=state.getPath(); + if (path!=null) + { + // this is a dispatch with a path + base_request.setAttribute(AsyncContext.ASYNC_REQUEST_URI,base_request.getRequestURI()); + base_request.setAttribute(AsyncContext.ASYNC_QUERY_STRING,base_request.getQueryString()); + + base_request.setAttribute(AsyncContext.ASYNC_CONTEXT_PATH,state.getSuspendedContext().getContextPath()); + + final String contextPath=state.getServletContext().getContextPath(); + HttpURI uri = new HttpURI(URIUtil.addPaths(contextPath,path)); + base_request.setUri(uri); + base_request.setRequestURI(null); + base_request.setPathInfo(base_request.getRequestURI()); + base_request.setQueryString(uri.getQuery()); + } + + final String target=base_request.getPathInfo(); + final HttpServletRequest request=(HttpServletRequest)async.getRequest(); + final HttpServletResponse response=(HttpServletResponse)async.getResponse(); + + if (Log.isDebugEnabled()) + { + Log.debug("REQUEST "+target+" on "+connection); + handle(target, request, response); + Log.debug("RESPONSE "+target+" "+connection.getResponse().getStatus()); + } + else + handle(target, request, response); + } + + + + /* ------------------------------------------------------------ */ + public void join() throws InterruptedException + { + getThreadPool().join(); + } + + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + /** + * @return Returns the sessionIdManager. + */ + public SessionIdManager getSessionIdManager() + { + return _sessionIdManager; + } + + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + /** + * @param sessionIdManager The sessionIdManager to set. + */ + public void setSessionIdManager(SessionIdManager sessionIdManager) + { + _container.update(this,_sessionIdManager,sessionIdManager, "sessionIdManager",true); + _sessionIdManager = sessionIdManager; + } + + /* ------------------------------------------------------------ */ + public void setSendServerVersion (boolean sendServerVersion) + { + _sendServerVersion = sendServerVersion; + } + + /* ------------------------------------------------------------ */ + public boolean getSendServerVersion() + { + return _sendServerVersion; + } + + /* ------------------------------------------------------------ */ + /** + * @param sendDateHeader + */ + public void setSendDateHeader(boolean sendDateHeader) + { + _sendDateHeader = sendDateHeader; + } + + /* ------------------------------------------------------------ */ + public boolean getSendDateHeader() + { + return _sendDateHeader; + } + + + /* ------------------------------------------------------------ */ + /** + * Add a LifeCycle object to be started/stopped + * along with the Server. + * @deprecated Use {@link #addBean(LifeCycle)} + * @param c + */ + public void addLifeCycle (LifeCycle c) + { + addBean(c); + } + + /* ------------------------------------------------------------ */ + /** + * Add an associated bean. + * The bean will be added to the servers {@link Container} + * and if it is a {@link LifeCycle} instance, it will be + * started/stopped along with the Server. + * @param c + */ + public void addBean(Object o) + { + if (o == null) + return; + + if (!_dependentBeans.contains(o)) + { + _dependentBeans.add(o); + _container.addBean(o); + } + + try + { + if (isStarted() && o instanceof LifeCycle) + ((LifeCycle)o).start(); + } + catch (Exception e) + { + throw new RuntimeException (e); + } + } + + /* ------------------------------------------------------------ */ + /** Get dependent beans of a specific class + * @see #addBean(Object) + * @param clazz + * @return List of beans. + */ + public <T> List<T> getBeans(Class<T> clazz) + { + ArrayList<T> beans = new ArrayList<T>(); + Iterator<?> iter = _dependentBeans.iterator(); + while (iter.hasNext()) + { + Object o = iter.next(); + if (clazz.isInstance(o)) + beans.add((T)o); + } + return beans; + } + + /** + * Remove a LifeCycle object to be started/stopped + * along with the Server + * @deprecated Use {@link #removeBean(Object)} + */ + public void removeLifeCycle (LifeCycle c) + { + removeBean(c); + } + + /** + * Remove an associated bean. + */ + public void removeBean (Object o) + { + if (o == null) + return; + _dependentBeans.remove(o); + _container.removeBean(o); + } + + + + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + /** + * ShutdownHook thread for stopping all servers. + * + * Thread is hooked first time list of servers is changed. + */ + private static class ShutdownHookThread extends Thread + { + private boolean hooked = false; + private ArrayList servers = new ArrayList(); + + /** + * Hooks this thread for shutdown. + * + * @see java.lang.Runtime#addShutdownHook(java.lang.Thread) + */ + private void createShutdownHook() + { + if (!Boolean.getBoolean("JETTY_NO_SHUTDOWN_HOOK") && !hooked) + { + try + { + Method shutdownHook = java.lang.Runtime.class.getMethod("addShutdownHook", new Class[] + { java.lang.Thread.class}); + shutdownHook.invoke(Runtime.getRuntime(), new Object[] + { this}); + this.hooked = true; + } + catch (Exception e) + { + if (Log.isDebugEnabled()) + Log.debug("No shutdown hook in JVM ", e); + } + } + } + + /** + * Add Server to servers list. + */ + public boolean add(Server server) + { + createShutdownHook(); + return this.servers.add(server); + } + + /** + * Contains Server in servers list? + */ + public boolean contains(Server server) + { + return this.servers.contains(server); + } + + /** + * Append all Servers from Collection + */ + public boolean addAll(Collection c) + { + createShutdownHook(); + return this.servers.addAll(c); + } + + /** + * Clear list of Servers. + */ + public void clear() + { + createShutdownHook(); + this.servers.clear(); + } + + /** + * Remove Server from list. + */ + public boolean remove(Server server) + { + createShutdownHook(); + return this.servers.remove(server); + } + + /** + * Remove all Servers in Collection from list. + */ + public boolean removeAll(Collection c) + { + createShutdownHook(); + return this.servers.removeAll(c); + } + + /** + * Stop all Servers in list. + */ + public void run() + { + setName("Shutdown"); + Log.info("Shutdown hook executing"); + Iterator it = servers.iterator(); + while (it.hasNext()) + { + Server svr = (Server) it.next(); + if (svr == null) + continue; + try + { + svr.stop(); + } + catch (Exception e) + { + Log.warn(e); + } + Log.info("Shutdown hook complete"); + + // Try to avoid JVM crash + try + { + Thread.sleep(1000); + } + catch (Exception e) + { + Log.warn(e); + } + } + } + } + + + + + /* ------------------------------------------------------------ */ + /** + */ + public void addHandler(Handler handler) + { + if (getHandler() == null) + setHandler(handler); + else if (getHandler() instanceof HandlerCollection) + ((HandlerCollection)getHandler()).addHandler(handler); + else + { + HandlerCollection collection=new HandlerCollection(); + collection.setHandlers(new Handler[]{getHandler(),handler}); + setHandler(collection); + } + } + + /* ------------------------------------------------------------ */ + /** + */ + public void removeHandler(Handler handler) + { + if (getHandler() instanceof HandlerCollection) + ((HandlerCollection)getHandler()).removeHandler(handler); + } + + /* ------------------------------------------------------------ */ + /** + */ + public Handler[] getHandlers() + { + if (getHandler() instanceof HandlerCollection) + return ((HandlerCollection)getHandler()).getHandlers(); + + return null; + } + + /* ------------------------------------------------------------ */ + /** + */ + public void setHandlers(Handler[] handlers) + { + HandlerCollection collection; + if (getHandler() instanceof HandlerCollection) + collection=(HandlerCollection)getHandler(); + else + { + collection=new HandlerCollection(); + setHandler(collection); + } + + collection.setHandlers(handlers); + } + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.util.AttributesMap#clearAttributes() + */ + public void clearAttributes() + { + _attributes.clearAttributes(); + } + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.util.AttributesMap#getAttribute(java.lang.String) + */ + public Object getAttribute(String name) + { + return _attributes.getAttribute(name); + } + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.util.AttributesMap#getAttributeNames() + */ + public Enumeration getAttributeNames() + { + return AttributesMap.getAttributeNamesCopy(_attributes); + } + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.util.AttributesMap#removeAttribute(java.lang.String) + */ + public void removeAttribute(String name) + { + _attributes.removeAttribute(name); + } + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.util.AttributesMap#setAttribute(java.lang.String, java.lang.Object) + */ + public void setAttribute(String name, Object attribute) + { + _attributes.setAttribute(name, attribute); + } + + /* ------------------------------------------------------------ */ + /** + * @return the graceful + */ + public int getGracefulShutdown() + { + return _graceful; + } + + /* ------------------------------------------------------------ */ + /** + * Set graceful shutdown timeout. If set, the {@link #doStop()} method will not immediately stop the + * server. Instead, all {@link Connector}s will be closed so that new connections will not be accepted + * and all handlers that implement {@link Graceful} will be put into the shutdown mode so that no new requests + * will be accepted, but existing requests can complete. The server will then wait the configured timeout + * before stopping. + * @param timeoutMS the milliseconds to wait for existing request to complete before stopping the server. + * + */ + public void setGracefulShutdown(int timeoutMS) + { + _graceful=timeoutMS; + } + + public String toString() + { + return this.getClass().getName()+"@"+Integer.toHexString(hashCode()); + } + + + /* ------------------------------------------------------------ */ + /* A handler that can be gracefully shutdown. + * Called by doStop if a {@link #setGracefulShutdown} period is set. + * TODO move this somewhere better + */ + public interface Graceful extends Handler + { + public void setShutdown(boolean shutdown); + } +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Servlet3Continuation.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Servlet3Continuation.java new file mode 100644 index 0000000000..08703c229d --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Servlet3Continuation.java @@ -0,0 +1,115 @@ +// ======================================================================== +// Copyright (c) 2004-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server; + +import java.io.IOException; + +import javax.servlet.AsyncContext; +import javax.servlet.AsyncEvent; +import javax.servlet.AsyncListener; + +import org.eclipse.jetty.util.ajax.Continuation; + +public class Servlet3Continuation implements Continuation, AsyncListener +{ + AsyncContext _asyncContext; + Request _request; + Object _object; + RetryRequest _retry; + boolean _resumed=false; + boolean _timeout=false; + + Servlet3Continuation(Request request) + { + _request=request; + } + + public Object getObject() + { + return _object; + } + + public boolean isExpired() + { + return _asyncContext!=null && _timeout; + } + + public boolean isNew() + { + return _retry==null; + } + + public boolean isPending() + { + return _asyncContext!=null && (_request.getAsyncRequest().isSuspended() || !_request.getAsyncRequest().isInitial()); + } + + public boolean isResumed() + { + return _asyncContext!=null && _resumed; + } + + public void reset() + { + _resumed=false; + _timeout=false; + } + + public void resume() + { + if (_asyncContext==null) + throw new IllegalStateException(); + _resumed=true; + _asyncContext.dispatch(); + } + + public void setMutex(Object mutex) + { + } + + public void setObject(Object o) + { + _object=o; + } + + public boolean suspend(long timeout) + { + _asyncContext=_request.startAsync();; + if (!_request.getAsyncRequest().isInitial()||_resumed||_timeout) + { + _resumed=false; + _timeout=false; + return _resumed; + } + + _request.setAsyncTimeout(timeout); + _request.addAsyncListener(this); + if (_retry==null) + _retry=new RetryRequest(); + throw _retry; + + } + + public void onComplete(AsyncEvent event) throws IOException + { + + } + + public void onTimeout(AsyncEvent event) throws IOException + { + _timeout=true; + _request.getAsyncRequest().dispatch(); + } + +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/SessionIdManager.java b/jetty-server/src/main/java/org/eclipse/jetty/server/SessionIdManager.java new file mode 100644 index 0000000000..1d76581822 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/SessionIdManager.java @@ -0,0 +1,83 @@ +// ======================================================================== +// Copyright (c) 2004-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; + +import org.eclipse.jetty.util.component.LifeCycle; + +/** Session ID Manager. + * Manages session IDs across multiple contexts. + * + * + */ +/* ------------------------------------------------------------ */ +/** + * + * + */ +public interface SessionIdManager extends LifeCycle +{ + /** + * @param id The session ID without any cluster node extension + * @return True if the session ID is in use by at least one context. + */ + public boolean idInUse(String id); + + /** + * Add a session to the list of known sessions for a given ID. + * @param session The session + */ + public void addSession(HttpSession session); + + /** + * Remove session from the list of known sessions for a given ID. + * @param session + */ + public void removeSession(HttpSession session); + + /** + * Call {@link HttpSession#invalidate()} on all known sessions for the given id. + * @param id The session ID without any cluster node extension + */ + public void invalidateAll(String id); + + /** + * @param request + * @param created + * @return + */ + public String newSessionId(HttpServletRequest request,long created); + + public String getWorkerName(); + + + /* ------------------------------------------------------------ */ + /** Get a cluster ID from a node ID. + * Strip node identifier from a located session ID. + * @param nodeId + * @return + */ + public String getClusterId(String nodeId); + + /* ------------------------------------------------------------ */ + /** Get a node ID from a cluster ID and a request + * @param clusterId The ID of the session + * @param request The request that for the session (or null) + * @return The session ID qualified with the node ID. + */ + public String getNodeId(String clusterId,HttpServletRequest request); + +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/SessionManager.java b/jetty-server/src/main/java/org/eclipse/jetty/server/SessionManager.java new file mode 100644 index 0000000000..6546bf182d --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/SessionManager.java @@ -0,0 +1,327 @@ +// ======================================================================== +// Copyright (c) 1996-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server; + +import java.util.EventListener; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; + +import org.eclipse.jetty.server.session.SessionHandler; +import org.eclipse.jetty.util.component.LifeCycle; + +/* --------------------------------------------------------------------- */ +/** + * Session Manager. + * The API required to manage sessions for a servlet context. + * + * + */ +public interface SessionManager extends LifeCycle +{ + /* ------------------------------------------------------------ */ + /** + * Session cookie name. + * Defaults to JSESSIONID, but can be set with the + * org.eclipse.jetty.servlet.SessionCookie context init parameter. + */ + public final static String __SessionCookieProperty = "org.eclipse.jetty.servlet.SessionCookie"; + public final static String __DefaultSessionCookie = "JSESSIONID"; + + + /* ------------------------------------------------------------ */ + /** + * Session id path parameter name. + * Defaults to jsessionid, but can be set with the + * org.eclipse.jetty.servlet.SessionIdPathParameterName context init parameter. + * If set to null or "none" no URL rewriting will be done. + */ + public final static String __SessionIdPathParameterNameProperty = "org.eclipse.jetty.servlet.SessionIdPathParameterName"; + public final static String __DefaultSessionIdPathParameterName = "jsessionid"; + + + /* ------------------------------------------------------------ */ + /** + * Session Domain. + * If this property is set as a ServletContext InitParam, then it is + * used as the domain for session cookies. If it is not set, then + * no domain is specified for the session cookie. + */ + public final static String __SessionDomainProperty = "org.eclipse.jetty.servlet.SessionDomain"; + public final static String __DefaultSessionDomain = null; + + + /* ------------------------------------------------------------ */ + /** + * Session Path. + * If this property is set as a ServletContext InitParam, then it is + * used as the path for the session cookie. If it is not set, then + * the context path is used as the path for the cookie. + */ + public final static String __SessionPathProperty = "org.eclipse.jetty.servlet.SessionPath"; + + /* ------------------------------------------------------------ */ + /** + * Session Max Age. + * If this property is set as a ServletContext InitParam, then it is + * used as the max age for the session cookie. If it is not set, then + * a max age of -1 is used. + */ + public final static String __MaxAgeProperty = "org.eclipse.jetty.servlet.MaxAge"; + + /* ------------------------------------------------------------ */ + /** + * Returns the <code>HttpSession</code> with the given session id + * + * @param id the session id + * @return the <code>HttpSession</code> with the corresponding id or null if no session with the given id exists + */ + public HttpSession getHttpSession(String id); + + /* ------------------------------------------------------------ */ + /** + * Creates a new <code>HttpSession</code>. + * + * @param request the HttpServletRequest containing the requested session id + * @return the new <code>HttpSession</code> + */ + public HttpSession newHttpSession(HttpServletRequest request); + + /* ------------------------------------------------------------ */ + /** + * @return true if session cookies should be secure + */ + public boolean getSecureCookies(); + + /* ------------------------------------------------------------ */ + /** + * @return true if session cookies should be HTTP-only (Microsoft extension) + * @see Cookie#isHttpOnly() + */ + public boolean getHttpOnly(); + + /* ------------------------------------------------------------ */ + /** + * @return the max period of inactivity, after which the session is invalidated, in seconds. + * @see #setMaxInactiveInterval(int) + */ + public int getMaxInactiveInterval(); + + /* ------------------------------------------------------------ */ + /** + * Sets the max period of inactivity, after which the session is invalidated, in seconds. + * + * @param seconds the max inactivity period, in seconds. + * @see #getMaxInactiveInterval() + */ + public void setMaxInactiveInterval(int seconds); + + /* ------------------------------------------------------------ */ + /** + * Sets the {@link SessionHandler}. + * + * @param handler the <code>SessionHandler</code> object + */ + public void setSessionHandler(SessionHandler handler); + + /* ------------------------------------------------------------ */ + /** + * Adds an event listener for session-related events. + * + * @param listener the session event listener to add + * Individual SessionManagers implementations may accept arbitrary listener types, + * but they are expected to at least handle HttpSessionActivationListener, + * HttpSessionAttributeListener, HttpSessionBindingListener and HttpSessionListener. + * @see #removeEventListener(EventListener) + */ + public void addEventListener(EventListener listener); + + /* ------------------------------------------------------------ */ + /** + * Removes an event listener for for session-related events. + * + * @param listener the session event listener to remove + * @see #addEventListener(EventListener) + */ + public void removeEventListener(EventListener listener); + + /* ------------------------------------------------------------ */ + /** + * Removes all event listeners for session-related events. + * + * @see #removeEventListener(EventListener) + */ + public void clearEventListeners(); + + /* ------------------------------------------------------------ */ + /** + * Gets a Cookie for a session. + * + * @param session the session to which the cookie should refer. + * @param contextPath the context to which the cookie should be linked. + * The client will only send the cookie value when requesting resources under this path. + * @param requestIsSecure whether the client is accessing the server over a secure protocol (i.e. HTTPS). + * @return if this <code>SessionManager</code> uses cookies, then this method will return a new + * {@link Cookie cookie object} that should be set on the client in order to link future HTTP requests + * with the <code>session</code>. If cookies are not in use, this method returns <code>null</code>. + */ + public Cookie getSessionCookie(HttpSession session, String contextPath, boolean requestIsSecure); + + /* ------------------------------------------------------------ */ + /** + * @return the cross context session id manager. + * @see #setIdManager(SessionIdManager) + */ + public SessionIdManager getIdManager(); + + /* ------------------------------------------------------------ */ + /** + * @return the cross context session id manager. + * @deprecated use {@link #getIdManager()} + */ + public SessionIdManager getMetaManager(); + + /* ------------------------------------------------------------ */ + /** + * Sets the cross context session id manager + * + * @param idManager the cross context session id manager. + * @see #getIdManager() + */ + public void setIdManager(SessionIdManager idManager); + + /* ------------------------------------------------------------ */ + /** + * @param session the session to test for validity + * @return whether the given session is valid, that is, it has not been invalidated. + */ + public boolean isValid(HttpSession session); + + /* ------------------------------------------------------------ */ + /** + * @param session the session object + * @return the unique id of the session within the cluster, extended with an optional node id. + * @see #getClusterId(HttpSession) + */ + public String getNodeId(HttpSession session); + + /* ------------------------------------------------------------ */ + /** + * @param session the session object + * @return the unique id of the session within the cluster (without a node id extension) + * @see #getNodeId(HttpSession) + */ + public String getClusterId(HttpSession session); + + /* ------------------------------------------------------------ */ + /** + * Called by the {@link SessionHandler} when a session is first accessed by a request. + * + * @param session the session object + * @param secure whether the request is secure or not + * @return the session cookie. If not null, this cookie should be set on the response to either migrate + * the session or to refresh a session cookie that may expire. + * @see #complete(HttpSession) + */ + public Cookie access(HttpSession session, boolean secure); + + /* ------------------------------------------------------------ */ + /** + * Called by the {@link SessionHandler} when a session is last accessed by a request. + * + * @param session the session object + * @see #access(HttpSession, boolean) + */ + public void complete(HttpSession session); + + /** + * Sets the session cookie name. + * @param cookieName the session cookie name + * @see #getSessionCookie() + */ + public void setSessionCookie(String cookieName); + + /** + * @return the session cookie name, by default "JSESSIONID". + * @see #setSessionCookie(String) + */ + public String getSessionCookie(); + + /** + * Sets the session id URL path parameter name. + * + * @param parameterName the URL path parameter name for session id URL rewriting (null or "none" for no rewriting). + * @see #getSessionIdPathParameterName() + * @see #getSessionIdPathParameterNamePrefix() + */ + public void setSessionIdPathParameterName(String parameterName); + + /** + * @return the URL path parameter name for session id URL rewriting, by default "jsessionid". + * @see #setSessionIdPathParameterName(String) + */ + public String getSessionIdPathParameterName(); + + /** + * @return a formatted version of {@link #getSessionIdPathParameterName()}, by default + * ";" + sessionIdParameterName + "=", for easier lookup in URL strings. + * @see #getSessionIdPathParameterName() + */ + public String getSessionIdPathParameterNamePrefix(); + + /** + * Sets the domain to set on the session cookie + * @param domain the domain to set on the session cookie + * @see #getSessionDomain() + */ + public void setSessionDomain(String domain); + + /** + * @return the domain to set on the session cookie + * @see #setSessionDomain(String) + */ + public String getSessionDomain(); + + /** + * Sets the path to set on the session cookie + * @param path the path to set on the session cookie + * @see #getSessionPath() + */ + public void setSessionPath(String path); + + /** + * @return the path to set on the session cookie + * @see #setSessionPath(String) + */ + public String getSessionPath(); + + /** + * Sets the max age to set on the session cookie, in seconds + * @param maxCookieAge the max age to set on the session cookie, in seconds + * @see #getMaxCookieAge() + */ + public void setMaxCookieAge(int maxCookieAge); + + /** + * @return the max age to set on the session cookie, in seconds + * @see #setMaxCookieAge(int) + */ + public int getMaxCookieAge(); + + /** + * @return whether the session management is handled via cookies. + */ + public boolean isUsingCookies(); +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/UserIdentity.java b/jetty-server/src/main/java/org/eclipse/jetty/server/UserIdentity.java new file mode 100644 index 0000000000..314c6c06fd --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/UserIdentity.java @@ -0,0 +1,136 @@ +// ======================================================================== +// Copyright (c) 1996-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server; +import java.security.Principal; +import java.util.Map; + +import javax.security.auth.Subject; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +/* ------------------------------------------------------------ */ +/** User object that encapsulates user identity and operations such as run-as-role actions, + * checking isUserInRole and getUserPrincipal. + * + * Implementations of UserIdentity should be immutable so that they may be + * cached by Authenticators and LoginServices. + * + */ +public interface UserIdentity +{ + final static String[] NO_ROLES = new String[]{}; + + /* ------------------------------------------------------------ */ + /** + * @return The user subject + */ + Subject getSubject(); + + /* ------------------------------------------------------------ */ + /** + * @return The user principal + */ + Principal getUserPrincipal(); + + /* ------------------------------------------------------------ */ + /** + * @return The users roles + */ + String[] getRoles(); + + /* ------------------------------------------------------------ */ + /** Check if the user is in a role. + * This call is used to satisfy authorization calls from + * container code which will be using translated role names. + * @param role A role name. + * @return True if the user can act in that role. + */ + boolean isUserInRole(String role); + + + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + /** + * A UserIdentity Scope. + * A scope is the environment in which a User Identity is to + * be interpreted. Typically it is set by the target servlet of + * a request. + * @see org.eclipse.jetty.servlet.ServletHolder + */ + interface Scope + { + /* ------------------------------------------------------------ */ + /** + * @return The context path that the identity is being considered within + */ + String getContextPath(); + + /* ------------------------------------------------------------ */ + /** + * @return The name of the identity context. Typically this is the servlet name. + */ + String getName(); + + /* ------------------------------------------------------------ */ + /** + * @return The name of a runAs entity. Typically this is a runAs role applied to a servlet. + */ + String getRunAsRole(); + + /* ------------------------------------------------------------ */ + /** + * @return A map of role reference names that converts from names used by application code + * to names used by the context deployment. + */ + Map<String,String> getRoleRefMap(); + } + + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + public interface UnauthticatedUserIdentity extends UserIdentity + { + UserIdentity login(ServletRequest request, ServletResponse response); + UserIdentity login(String username, String password); + }; + + public static final UserIdentity UNAUTHENTICATED_IDENTITY = new UserIdentity() + { + public Subject getSubject() + { + return null; + } + + public Principal getUserPrincipal() + { + return null; + } + + public String[] getRoles() + { + return NO_ROLES; + } + + public boolean isUserInRole(String role) + { + return false; + } + + public String toString() + { + return "UNAUTHENTICATED"; + } + }; +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/bio/SocketConnector.java b/jetty-server/src/main/java/org/eclipse/jetty/server/bio/SocketConnector.java new file mode 100644 index 0000000000..f89a08c020 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/bio/SocketConnector.java @@ -0,0 +1,264 @@ +// ======================================================================== +// Copyright (c) 2003-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server.bio; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import org.eclipse.jetty.io.Buffer; +import org.eclipse.jetty.io.ByteArrayBuffer; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.EofException; +import org.eclipse.jetty.io.HttpException; +import org.eclipse.jetty.io.bio.SocketEndPoint; +import org.eclipse.jetty.server.AbstractConnector; +import org.eclipse.jetty.server.HttpConnection; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.util.log.Log; + + +/* ------------------------------------------------------------------------------- */ +/** Socket Connector. + * This connector implements a traditional blocking IO and threading model. + * Normal JRE sockets are used and a thread is allocated per connection. + * Buffers are managed so that large buffers are only allocated to active connections. + * + * This Connector should only be used if NIO is not available. + * + * @org.apache.xbean.XBean element="bioConnector" description="Creates a BIO based socket connector" + * + * + */ +public class SocketConnector extends AbstractConnector +{ + protected ServerSocket _serverSocket; + protected Set _connections; + + /* ------------------------------------------------------------ */ + /** Constructor. + * + */ + public SocketConnector() + { + } + + /* ------------------------------------------------------------ */ + public Object getConnection() + { + return _serverSocket; + } + + /* ------------------------------------------------------------ */ + public void open() throws IOException + { + // Create a new server socket and set to non blocking mode + if (_serverSocket==null || _serverSocket.isClosed()) + _serverSocket= newServerSocket(getHost(),getPort(),getAcceptQueueSize()); + _serverSocket.setReuseAddress(getReuseAddress()); + } + + /* ------------------------------------------------------------ */ + protected ServerSocket newServerSocket(String host, int port,int backlog) throws IOException + { + ServerSocket ss= host==null? + new ServerSocket(port,backlog): + new ServerSocket(port,backlog,InetAddress.getByName(host)); + + return ss; + } + + /* ------------------------------------------------------------ */ + public void close() throws IOException + { + if (_serverSocket!=null) + _serverSocket.close(); + _serverSocket=null; + } + + /* ------------------------------------------------------------ */ + public void accept(int acceptorID) + throws IOException, InterruptedException + { + Socket socket = _serverSocket.accept(); + configure(socket); + + Connection connection=new Connection(socket); + connection.dispatch(); + } + + /* ------------------------------------------------------------------------------- */ + /** + * Allows subclass to override Conection if required. + */ + protected HttpConnection newHttpConnection(EndPoint endpoint) + { + return new HttpConnection(this, endpoint, getServer()); + } + + /* ------------------------------------------------------------------------------- */ + public Buffer newBuffer(int size) + { + return new ByteArrayBuffer(size); + } + + /* ------------------------------------------------------------------------------- */ + public void customize(EndPoint endpoint, Request request) + throws IOException + { + Connection connection = (Connection)endpoint; + if (connection._sotimeout!=_maxIdleTime) + { + connection._sotimeout=_maxIdleTime; + ((Socket)endpoint.getTransport()).setSoTimeout(_maxIdleTime); + } + + super.customize(endpoint, request); + } + + /* ------------------------------------------------------------------------------- */ + public int getLocalPort() + { + if (_serverSocket==null || _serverSocket.isClosed()) + return -1; + return _serverSocket.getLocalPort(); + } + + /* ------------------------------------------------------------------------------- */ + protected void doStart() throws Exception + { + _connections=new HashSet(); + super.doStart(); + } + + /* ------------------------------------------------------------------------------- */ + protected void doStop() throws Exception + { + super.doStop(); + Set set=null; + + synchronized(_connections) + { + set= new HashSet(_connections); + } + + Iterator iter=set.iterator(); + while(iter.hasNext()) + { + Connection connection = (Connection)iter.next(); + connection.close(); + } + } + + /* ------------------------------------------------------------------------------- */ + /* ------------------------------------------------------------------------------- */ + /* ------------------------------------------------------------------------------- */ + protected class Connection extends SocketEndPoint implements Runnable + { + boolean _dispatched=false; + HttpConnection _connection; + int _sotimeout; + protected Socket _socket; + + public Connection(Socket socket) throws IOException + { + super(socket); + _connection = newHttpConnection(this); + _sotimeout=socket.getSoTimeout(); + _socket=socket; + } + + public void dispatch() throws InterruptedException, IOException + { + if (getThreadPool()==null || !getThreadPool().dispatch(this)) + { + Log.warn("dispatch failed for {}",_connection); + close(); + } + } + + public int fill(Buffer buffer) throws IOException + { + int l = super.fill(buffer); + if (l<0) + close(); + return l; + } + + public void close() throws IOException + { + _connection.getRequest().getAsyncRequest().cancel(); + super.close(); + } + + public void run() + { + try + { + connectionOpened(_connection); + synchronized(_connections) + { + _connections.add(this); + } + + while (isStarted() && !isClosed()) + { + if (_connection.isIdle()) + { + if (getServer().getThreadPool().isLowOnThreads()) + { + int lrmit = getLowResourceMaxIdleTime(); + if (lrmit>=0 && _sotimeout!= lrmit) + { + _sotimeout=lrmit; + _socket.setSoTimeout(_sotimeout); + } + } + } + _connection.handle(); + } + } + catch (EofException e) + { + Log.debug("EOF", e); + try{close();} + catch(IOException e2){Log.ignore(e2);} + } + catch (HttpException e) + { + Log.debug("BAD", e); + try{close();} + catch(IOException e2){Log.ignore(e2);} + } + catch(Exception e) + { + Log.warn("handle failed?",e); + try{close();} + catch(IOException e2){Log.ignore(e2);} + } + finally + { + connectionClosed(_connection); + synchronized(_connections) + { + _connections.remove(this); + } + } + } + } +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AbstractHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AbstractHandler.java new file mode 100644 index 0000000000..a09c5989ec --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AbstractHandler.java @@ -0,0 +1,97 @@ +// ======================================================================== +// Copyright (c) 2004-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server.handler; + + +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.component.AbstractLifeCycle; +import org.eclipse.jetty.util.log.Log; + + +/* ------------------------------------------------------------ */ +/** AbstractHandler. + * + * + */ +public abstract class AbstractHandler extends AbstractLifeCycle implements Handler +{ + protected String _string; + private Server _server; + + /* ------------------------------------------------------------ */ + /** + * + */ + public AbstractHandler() + { + } + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.thread.LifeCycle#start() + */ + protected void doStart() throws Exception + { + Log.debug("starting {}",this); + } + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.thread.LifeCycle#stop() + */ + protected void doStop() throws Exception + { + Log.debug("stopping {}",this); + } + + /* ------------------------------------------------------------ */ + public String toString() + { + if (_string==null) + { + _string=super.toString(); + _string=_string.substring(_string.lastIndexOf('.')+1); + } + return _string; + } + + /* ------------------------------------------------------------ */ + public void setServer(Server server) + { + Server old_server=_server; + if (old_server!=null && old_server!=server) + old_server.getContainer().removeBean(this); + _server=server; + if (_server!=null && _server!=old_server) + _server.getContainer().addBean(this); + } + + /* ------------------------------------------------------------ */ + public Server getServer() + { + return _server; + } + + + /* ------------------------------------------------------------ */ + public void destroy() + { + if (!isStopped()) + throw new IllegalStateException("!STOPPED"); + if (_server!=null) + _server.getContainer().removeBean(this); + } + +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AbstractHandlerContainer.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AbstractHandlerContainer.java new file mode 100644 index 0000000000..b5eeb780bb --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AbstractHandlerContainer.java @@ -0,0 +1,88 @@ +// ======================================================================== +// Copyright (c) 2004-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server.handler; + + +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.HandlerContainer; +import org.eclipse.jetty.util.LazyList; + + +/* ------------------------------------------------------------ */ +/** Abstract Handler Container. + * This is the base class for handlers that may contain other handlers. + * + * + * + */ +public abstract class AbstractHandlerContainer extends AbstractHandler implements HandlerContainer +{ + /* ------------------------------------------------------------ */ + public AbstractHandlerContainer() + { + } + + /* ------------------------------------------------------------ */ + public Handler[] getChildHandlers() + { + Object list = expandChildren(null,null); + return (Handler[])LazyList.toArray(list, Handler.class); + } + + /* ------------------------------------------------------------ */ + public Handler[] getChildHandlersByClass(Class<?> byclass) + { + Object list = expandChildren(null,byclass); + return (Handler[])LazyList.toArray(list, byclass); + } + + /* ------------------------------------------------------------ */ + public Handler getChildHandlerByClass(Class<?> byclass) + { + // TODO this can be more efficient? + Object list = expandChildren(null,byclass); + if (list==null) + return null; + return LazyList.get(list, 0); + } + + /* ------------------------------------------------------------ */ + protected Object expandChildren(Object list, Class<?> byClass) + { + return list; + } + + /* ------------------------------------------------------------ */ + protected Object expandHandler(Handler handler, Object list, Class<Handler> byClass) + { + if (handler==null) + return list; + + if (handler!=null && (byClass==null || byClass.isAssignableFrom(handler.getClass()))) + list=LazyList.add(list, handler); + + if (handler instanceof AbstractHandlerContainer) + list=((AbstractHandlerContainer)handler).expandChildren(list, byClass); + else if (handler instanceof HandlerContainer) + { + HandlerContainer container = (HandlerContainer)handler; + Handler[] handlers=byClass==null?container.getChildHandlers():container.getChildHandlersByClass(byClass); + list=LazyList.addArray(list, handlers); + } + + return list; + } + + +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/CompleteHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/CompleteHandler.java new file mode 100644 index 0000000000..3311b73c8e --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/CompleteHandler.java @@ -0,0 +1,37 @@ +// ======================================================================== +// Copyright (c) 2004-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server.handler; + +import java.util.List; + +import javax.servlet.ServletRequest; + +import org.eclipse.jetty.server.Request; + +/** + * An interface for handlers that wish to be notified of request completion. + * + * If the request attribute COMPLETE_HANDLER_ATTR is set as either a single + * CompleteHandler instance or a {@link List} of CompleteHandler instances, + * then when the {@link ServletRequest#complete()} method is called, then + * the {@link #complete(Request)} method is called for each CompleteHandler. + * + * + * + */ +public interface CompleteHandler +{ + public final static String COMPLETE_HANDLER_ATTR = "org.eclipse.jetty.server.handler.CompleteHandlers"; + void complete(Request request); +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java new file mode 100644 index 0000000000..d764f1a602 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java @@ -0,0 +1,1903 @@ +// ======================================================================== +// Copyright (c) 2004-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server.handler; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Enumeration; +import java.util.EventListener; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import javax.servlet.DispatcherType; +import javax.servlet.Filter; +import javax.servlet.FilterRegistration; +import javax.servlet.RequestDispatcher; +import javax.servlet.Servlet; +import javax.servlet.ServletContext; +import javax.servlet.ServletContextAttributeEvent; +import javax.servlet.ServletContextAttributeListener; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import javax.servlet.ServletException; +import javax.servlet.ServletRegistration; +import javax.servlet.ServletRequestAttributeListener; +import javax.servlet.ServletRequestEvent; +import javax.servlet.ServletRequestListener; +import javax.servlet.SessionCookieConfig; +import javax.servlet.SessionTrackingMode; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.http.MimeTypes; +import org.eclipse.jetty.io.Buffer; +import org.eclipse.jetty.io.HttpException; +import org.eclipse.jetty.server.Dispatcher; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.HandlerContainer; +import org.eclipse.jetty.server.HttpConnection; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.Attributes; +import org.eclipse.jetty.util.AttributesMap; +import org.eclipse.jetty.util.LazyList; +import org.eclipse.jetty.util.Loader; +import org.eclipse.jetty.util.URIUtil; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.resource.Resource; + +/* ------------------------------------------------------------ */ +/** ContextHandler. + * + * This handler wraps a call to handle by setting the context and + * servlet path, plus setting the context classloader. + * + * <p> + * If the context init parameter "org.eclipse.jetty.servlet.ManagedAttributes" + * is set to a coma separated list of names, then they are treated as context + * attribute names, which if set as attributes are passed to the servers Container + * so that they may be managed with JMX. + * + * @org.apache.xbean.XBean description="Creates a basic HTTP context" + * + * + * + */ +public class ContextHandler extends HandlerWrapper implements Attributes, Server.Graceful, CompleteHandler +{ + private static ThreadLocal<Context> __context=new ThreadLocal<Context>(); + public static final String MANAGED_ATTRIBUTES = "org.eclipse.jetty.server.servlet.ManagedAttributes"; + + /* ------------------------------------------------------------ */ + /** Get the current ServletContext implementation. + * This call is only valid during a call to doStart and is available to + * nested handlers to access the context. + * + * @return ServletContext implementation + */ + public static Context getCurrentContext() + { + Context context = __context.get(); + return context; + } + + protected Context _scontext; + + private AttributesMap _attributes; + private AttributesMap _contextAttributes; + private ClassLoader _classLoader; + private String _contextPath="/"; + private Map<String,String> _initParams; + private String _displayName; + private Resource _baseResource; + private MimeTypes _mimeTypes; + private Map<String,String> _localeEncodingMap; + private String[] _welcomeFiles; + private ErrorHandler _errorHandler; + private String[] _vhosts; + private Set<String> _connectors; + private EventListener[] _eventListeners; + private Logger _logger; + private boolean _shutdown; + private boolean _allowNullPathInfo; + private int _maxFormContentSize=Integer.getInteger("org.eclipse.jetty.server.Request.maxFormContentSize",200000).intValue(); + private boolean _compactPath=false; + private boolean _aliases=true; + + private Object _contextListeners; + private Object _contextAttributeListeners; + private Object _requestListeners; + private Object _asyncListeners; + private Object _requestAttributeListeners; + private Set<String> _managedAttributes; + + /* ------------------------------------------------------------ */ + /** + * + */ + public ContextHandler() + { + super(); + _scontext=new Context(); + _attributes=new AttributesMap(); + _initParams=new HashMap<String,String>(); + } + + /* ------------------------------------------------------------ */ + /** + * + */ + protected ContextHandler(Context context) + { + super(); + _scontext=context; + _attributes=new AttributesMap(); + _initParams=new HashMap<String,String>(); + } + + /* ------------------------------------------------------------ */ + /** + * + */ + public ContextHandler(String contextPath) + { + this(); + setContextPath(contextPath); + } + + /* ------------------------------------------------------------ */ + /** + * + */ + public ContextHandler(HandlerContainer parent, String contextPath) + { + this(); + setContextPath(contextPath); + parent.addHandler(this); + } + + /* ------------------------------------------------------------ */ + public Context getServletContext() + { + return _scontext; + } + + /* ------------------------------------------------------------ */ + /** + * @return the allowNullPathInfo true if /context is not redirected to /context/ + */ + public boolean getAllowNullPathInfo() + { + return _allowNullPathInfo; + } + + /* ------------------------------------------------------------ */ + /** + * @param allowNullPathInfo true if /context is not redirected to /context/ + */ + public void setAllowNullPathInfo(boolean allowNullPathInfo) + { + _allowNullPathInfo=allowNullPathInfo; + } + + /* ------------------------------------------------------------ */ + public void setServer(Server server) + { + if (_errorHandler!=null) + { + Server old_server=getServer(); + if (old_server!=null && old_server!=server) + old_server.getContainer().update(this, _errorHandler, null, "error",true); + super.setServer(server); + if (server!=null && server!=old_server) + server.getContainer().update(this, null, _errorHandler, "error",true); + _errorHandler.setServer(server); + } + else + super.setServer(server); + } + + /* ------------------------------------------------------------ */ + /** Set the virtual hosts for the context. + * Only requests that have a matching host header or fully qualified + * URL will be passed to that context with a virtual host name. + * A context with no virtual host names or a null virtual host name is + * available to all requests that are not served by a context with a + * matching virtual host name. + * @param vhosts Array of virtual hosts that this context responds to. A + * null host name or null/empty array means any hostname is acceptable. + * Host names may be String representation of IP addresses. Host names may + * start with '*.' to wildcard one level of names. + */ + public void setVirtualHosts( String[] vhosts ) + { + if ( vhosts == null ) + { + _vhosts = vhosts; + } + else + { + _vhosts = new String[vhosts.length]; + for ( int i = 0; i < vhosts.length; i++ ) + _vhosts[i] = normalizeHostname( vhosts[i]); + } + } + + /* ------------------------------------------------------------ */ + /** Get the virtual hosts for the context. + * Only requests that have a matching host header or fully qualified + * URL will be passed to that context with a virtual host name. + * A context with no virtual host names or a null virtual host name is + * available to all requests that are not served by a context with a + * matching virtual host name. + * @return Array of virtual hosts that this context responds to. A + * null host name or empty array means any hostname is acceptable. + * Host names may be String representation of IP addresses. + * Host names may start with '*.' to wildcard one level of names. + */ + public String[] getVirtualHosts() + { + return _vhosts; + } + + /* ------------------------------------------------------------ */ + /** + * @deprecated use {@link #setConnectorNames(String[])} + */ + public void setHosts(String[] hosts) + { + setConnectorNames(hosts); + } + + /* ------------------------------------------------------------ */ + /** Get the hosts for the context. + * @deprecated + */ + public String[] getHosts() + { + return getConnectorNames(); + } + + /* ------------------------------------------------------------ */ + /** + * @return an array of connector names that this context + * will accept a request from. + */ + public String[] getConnectorNames() + { + if (_connectors==null || _connectors.size()==0) + return null; + + return (String[])_connectors.toArray(new String[_connectors.size()]); + } + + /* ------------------------------------------------------------ */ + /** Set the names of accepted connectors. + * + * Names are either "host:port" or a specific configured name for a connector. + * + * @param connectors If non null, an array of connector names that this context + * will accept a request from. + */ + public void setConnectorNames(String[] connectors) + { + if (connectors==null || connectors.length==0) + _connectors=null; + else + _connectors= new HashSet<String>(Arrays.asList(connectors)); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletContext#getAttribute(java.lang.String) + */ + public Object getAttribute(String name) + { + return _attributes.getAttribute(name); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletContext#getAttributeNames() + */ + @SuppressWarnings("unchecked") + public Enumeration getAttributeNames() + { + return AttributesMap.getAttributeNamesCopy(_attributes); + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the attributes. + */ + public Attributes getAttributes() + { + return _attributes; + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the classLoader. + */ + public ClassLoader getClassLoader() + { + return _classLoader; + } + + /* ------------------------------------------------------------ */ + /** + * Make best effort to extract a file classpath from the context classloader + * @return Returns the classLoader. + */ + public String getClassPath() + { + if ( _classLoader==null || !(_classLoader instanceof URLClassLoader)) + return null; + URLClassLoader loader = (URLClassLoader)_classLoader; + URL[] urls =loader.getURLs(); + StringBuilder classpath=new StringBuilder(); + for (int i=0;i<urls.length;i++) + { + try + { + Resource resource = newResource(urls[i]); + File file=resource.getFile(); + if (file.exists()) + { + if (classpath.length()>0) + classpath.append(File.pathSeparatorChar); + classpath.append(file.getAbsolutePath()); + } + } + catch (IOException e) + { + Log.debug(e); + } + } + if (classpath.length()==0) + return null; + return classpath.toString(); + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the _contextPath. + */ + public String getContextPath() + { + return _contextPath; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletContext#getInitParameter(java.lang.String) + */ + public String getInitParameter(String name) + { + return (String)_initParams.get(name); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletContext#getInitParameterNames() + */ + @SuppressWarnings("unchecked") + public Enumeration getInitParameterNames() + { + return Collections.enumeration(_initParams.keySet()); + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the initParams. + */ + public Map<String,String> getInitParams() + { + return _initParams; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletContext#getServletContextName() + */ + public String getDisplayName() + { + return _displayName; + } + + /* ------------------------------------------------------------ */ + public EventListener[] getEventListeners() + { + return _eventListeners; + } + + /* ------------------------------------------------------------ */ + /** + * Set the context event listeners. + * @see ServletContextListener + * @see ServletContextAttributeListener + * @see ServletRequestListener + * @see ServletRequestAttributeListener + */ + public void setEventListeners(EventListener[] eventListeners) + { + _contextListeners=null; + _contextAttributeListeners=null; + _requestListeners=null; + _requestAttributeListeners=null; + + _eventListeners=eventListeners; + + for (int i=0; eventListeners!=null && i<eventListeners.length;i ++) + { + EventListener listener = _eventListeners[i]; + + if (listener instanceof ServletContextListener) + _contextListeners= LazyList.add(_contextListeners, listener); + + if (listener instanceof ServletContextAttributeListener) + _contextAttributeListeners= LazyList.add(_contextAttributeListeners, listener); + + if (listener instanceof ServletRequestListener) + _requestListeners= LazyList.add(_requestListeners, listener); + + if (listener instanceof ServletRequestAttributeListener) + _requestAttributeListeners= LazyList.add(_requestAttributeListeners, listener); + } + } + + /* ------------------------------------------------------------ */ + /** + * Add a context event listeners. + * @see ServletContextListener + * @see ServletContextAttributeListener + * @see ServletRequestListener + * @see ServletRequestAttributeListener + */ + public void addEventListener(EventListener listener) + { + setEventListeners((EventListener[])LazyList.addToArray(getEventListeners(), listener, EventListener.class)); + } + + /* ------------------------------------------------------------ */ + /** + * @return true if this context is accepting new requests + */ + public boolean isShutdown() + { + return !_shutdown; + } + + /* ------------------------------------------------------------ */ + /** Set shutdown status. + * This field allows for graceful shutdown of a context. A started context may be put into non accepting state so + * that existing requests can complete, but no new requests are accepted. + * @param shutdown true if this context is (not?) accepting new requests + */ + public void setShutdown(boolean shutdown) + { + _shutdown = shutdown; + } + + /* ------------------------------------------------------------ */ + public Logger getLogger() + { + return _logger; + } + + /* ------------------------------------------------------------ */ + public void setLogger(Logger logger) + { + _logger=logger; + } + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.thread.AbstractLifeCycle#doStart() + */ + protected void doStart() throws Exception + { + if (_contextPath==null) + throw new IllegalStateException("Null contextPath"); + + _logger=Log.getLogger(getDisplayName()==null?getContextPath():getDisplayName()); + ClassLoader old_classloader=null; + Thread current_thread=null; + Context old_context=null; + + _contextAttributes=new AttributesMap(); + try + { + + // Set the classloader + if (_classLoader!=null) + { + current_thread=Thread.currentThread(); + old_classloader=current_thread.getContextClassLoader(); + current_thread.setContextClassLoader(_classLoader); + } + + + if (_mimeTypes==null) + _mimeTypes=new MimeTypes(); + + old_context=__context.get(); + __context.set(_scontext); + + if (_errorHandler==null) + setErrorHandler(new ErrorHandler()); + + // defers the calling of super.doStart() + startContext(); + + + } + finally + { + __context.set(old_context); + + // reset the classloader + if (_classLoader!=null) + { + current_thread.setContextClassLoader(old_classloader); + } + } + } + + /* ------------------------------------------------------------ */ + /** + * Extensible startContext. + * this method is called from {@link ContextHandler#doStart()} instead of a + * call to super.doStart(). This allows derived classes to insert additional + * handling (Eg configuration) before the call to super.doStart by this method + * will start contained handlers. + * @see org.eclipse.jetty.Scope.Context + * @see org.eclipse.jetty.webapp.WebAppContext + */ + protected void startContext() + throws Exception + { + super.doStart(); + + if (_errorHandler!=null) + _errorHandler.start(); + + // Context listeners + if (_contextListeners != null ) + { + ServletContextEvent event= new ServletContextEvent(_scontext); + for (int i= 0; i < LazyList.size(_contextListeners); i++) + { + ((ServletContextListener)LazyList.get(_contextListeners, i)).contextInitialized(event); + } + } + + String managedAttributes = _initParams.get(MANAGED_ATTRIBUTES); + if (managedAttributes!=null) + { + _managedAttributes=new HashSet<String>(); + String[] attributes = managedAttributes.toString().split(","); + for (String s : attributes) + _managedAttributes.add(s); + + Enumeration e = _scontext.getAttributeNames(); + while(e.hasMoreElements()) + { + String name = (String)e.nextElement(); + Object value = _scontext.getAttribute(name); + setManagedAttribute(name,value); + } + } + } + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.thread.AbstractLifeCycle#doStop() + */ + protected void doStop() throws Exception + { + ClassLoader old_classloader=null; + Thread current_thread=null; + + Context old_context=__context.get(); + __context.set(_scontext); + try + { + // Set the classloader + if (_classLoader!=null) + { + current_thread=Thread.currentThread(); + old_classloader=current_thread.getContextClassLoader(); + current_thread.setContextClassLoader(_classLoader); + } + + super.doStop(); + + // Context listeners + if (_contextListeners != null ) + { + ServletContextEvent event= new ServletContextEvent(_scontext); + for (int i=LazyList.size(_contextListeners); i-->0;) + { + ((ServletContextListener)LazyList.get(_contextListeners, i)).contextDestroyed(event); + } + } + + if (_errorHandler!=null) + _errorHandler.stop(); + + Enumeration e = _scontext.getAttributeNames(); + while(e.hasMoreElements()) + { + String name = (String)e.nextElement(); + setManagedAttribute(name,null); + } + } + finally + { + __context.set(old_context); + // reset the classloader + if (_classLoader!=null) + current_thread.setContextClassLoader(old_classloader); + } + + if (_contextAttributes!=null) + _contextAttributes.clearAttributes(); + _contextAttributes=null; + } + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.jetty.server.Handler#handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) + */ + public void handle(String target, HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException + { + Request baseRequest=(request instanceof Request)?(Request)request:HttpConnection.getCurrentConnection().getRequest(); + DispatcherType dispatch=request.getDispatcherType(); + + if( !isStarted() || _shutdown || (DispatcherType.REQUEST.equals(dispatch) && baseRequest.isHandled())) + return; + + // Check the vhosts + if (_vhosts!=null && _vhosts.length>0) + { + String vhost = normalizeHostname( request.getServerName()); + + boolean match=false; + + // TODO non-linear lookup + for (int i=0;!match && i<_vhosts.length;i++) + { + String contextVhost = _vhosts[i]; + if(contextVhost==null) continue; + if(contextVhost.startsWith("*.")) { + // wildcard only at the beginning, and only for one additional subdomain level + match=contextVhost.regionMatches(true,2,vhost,vhost.indexOf(".")+1,contextVhost.length()-2); + } else + match=contextVhost.equalsIgnoreCase(vhost); + } + if (!match) + return; + } + + // Check the connector + if (_connectors!=null && _connectors.size()>0) + { + String connector=HttpConnection.getCurrentConnection().getConnector().getName(); + if (connector==null || !_connectors.contains(connector)) + return; + } + + if (_compactPath) + target=URIUtil.compactPath(target); + + if (target.startsWith(_contextPath)) + { + if (_contextPath.length()==target.length() && _contextPath.length()>1 &&!_allowNullPathInfo) + { + // context request must end with / + baseRequest.setHandled(true); + if (request.getQueryString()!=null) + response.sendRedirect(URIUtil.addPaths(request.getRequestURI(),URIUtil.SLASH)+"?"+request.getQueryString()); + else + response.sendRedirect(URIUtil.addPaths(request.getRequestURI(),URIUtil.SLASH)); + return; + } + } + else + { + // Not for this context! + return; + } + + doHandle(target,baseRequest,request,response); + } + + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.jetty.server.Handler#handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) + */ + public void doHandle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException + { + boolean new_context=false; + Context old_context=null; + String old_context_path=null; + String old_servlet_path=null; + String old_path_info=null; + ClassLoader old_classloader=null; + Thread current_thread=null; + String pathInfo=null; + + DispatcherType dispatch=request.getDispatcherType(); + + old_context=baseRequest.getContext(); + + // Are we already in this context? + if (old_context!=_scontext) + { + new_context=true; + + // check the target. + if (DispatcherType.REQUEST.equals(dispatch) || DispatcherType.ASYNC.equals(dispatch)) + { + if (target.length()>_contextPath.length()) + { + if (_contextPath.length()>1) + target=target.substring(_contextPath.length()); + pathInfo=target; + } + else if (_contextPath.length()==1) + { + target=URIUtil.SLASH; + pathInfo=URIUtil.SLASH; + } + else + { + target=URIUtil.SLASH; + pathInfo=null; + } + } + } + + try + { + old_context_path=baseRequest.getContextPath(); + old_servlet_path=baseRequest.getServletPath(); + old_path_info=baseRequest.getPathInfo(); + + // Update the paths + baseRequest.setContext(_scontext); + if (!DispatcherType.INCLUDE.equals(dispatch) && target.startsWith("/")) + { + if (_contextPath.length()==1) + baseRequest.setContextPath(""); + else + baseRequest.setContextPath(_contextPath); + baseRequest.setServletPath(null); + baseRequest.setPathInfo(pathInfo); + } + + ServletRequestEvent event=null; + if (new_context) + { + // Set the classloader + if (_classLoader!=null) + { + current_thread=Thread.currentThread(); + old_classloader=current_thread.getContextClassLoader(); + current_thread.setContextClassLoader(_classLoader); + } + + // Handle the REALLY SILLY request events! + baseRequest.setRequestListeners(_requestListeners); + if (_requestAttributeListeners!=null) + { + final int s=LazyList.size(_requestAttributeListeners); + for(int i=0;i<s;i++) + baseRequest.addEventListener(((EventListener)LazyList.get(_requestAttributeListeners,i))); + } + } + + // Handle the request + try + { + if (DispatcherType.REQUEST.equals(dispatch) && isProtectedTarget(target)) + throw new HttpException(HttpServletResponse.SC_NOT_FOUND); + + Handler handler = getHandler(); + if (handler!=null) + handler.handle(target, request, response); + } + catch(HttpException e) + { + Log.debug(e); + response.sendError(e.getStatus(), e.getReason()); + } + finally + { + // Handle more REALLY SILLY request events! + if (new_context) + { + baseRequest.takeRequestListeners(); + if (_requestAttributeListeners!=null) + { + for(int i=LazyList.size(_requestAttributeListeners);i-->0;) + baseRequest.removeEventListener(((EventListener)LazyList.get(_requestAttributeListeners,i))); + } + } + } + } + finally + { + if (old_context!=_scontext) + { + // reset the classloader + if (_classLoader!=null) + { + current_thread.setContextClassLoader(old_classloader); + } + + // reset the context and servlet path. + baseRequest.setContext(old_context); + baseRequest.setContextPath(old_context_path); + baseRequest.setServletPath(old_servlet_path); + baseRequest.setPathInfo(old_path_info); + } + } + } + + /* ------------------------------------------------------------ */ + /* Handle a runnable in this context + */ + public void handle(Runnable runnable) + { + ClassLoader old_classloader=null; + Thread current_thread=null; + try + { + // Set the classloader + if (_classLoader!=null) + { + current_thread=Thread.currentThread(); + old_classloader=current_thread.getContextClassLoader(); + current_thread.setContextClassLoader(_classLoader); + } + + runnable.run(); + } + finally + { + if (old_classloader!=null) + { + current_thread.setContextClassLoader(old_classloader); + } + } + } + + /* ------------------------------------------------------------ */ + /** Check the target. + * Called by {@link #handle(String, HttpServletRequest, HttpServletResponse)} when a + * target within a context is determined. If the target is protected, 404 is returned. + * The default implementation always returns false. + * @see org.eclipse.jetty.webapp.WebAppContext#isProtectedTarget(String) + */ + /* ------------------------------------------------------------ */ + protected boolean isProtectedTarget(String target) + { + return false; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletContext#removeAttribute(java.lang.String) + */ + public void removeAttribute(String name) + { + setManagedAttribute(name,null); + _attributes.removeAttribute(name); + } + + /* ------------------------------------------------------------ */ + /* Set a context attribute. + * Attributes set via this API cannot be overriden by the ServletContext.setAttribute API. + * Their lifecycle spans the stop/start of a context. No attribute listener events are + * triggered by this API. + * @see javax.servlet.ServletContext#setAttribute(java.lang.String, java.lang.Object) + */ + public void setAttribute(String name, Object value) + { + setManagedAttribute(name,value); + _attributes.setAttribute(name,value); + } + + /* ------------------------------------------------------------ */ + /** + * @param attributes The attributes to set. + */ + public void setAttributes(Attributes attributes) + { + if (attributes instanceof AttributesMap) + { + _attributes = (AttributesMap)attributes; + Enumeration e = _attributes.getAttributeNames(); + while (e.hasMoreElements()) + { + String name = (String)e.nextElement(); + setManagedAttribute(name,attributes.getAttribute(name)); + } + } + else + { + _attributes=new AttributesMap(); + Enumeration e = attributes.getAttributeNames(); + while (e.hasMoreElements()) + { + String name = (String)e.nextElement(); + Object value=attributes.getAttribute(name); + setManagedAttribute(name,value); + _attributes.setAttribute(name,value); + } + } + } + + /* ------------------------------------------------------------ */ + public void clearAttributes() + { + Enumeration e = _attributes.getAttributeNames(); + while (e.hasMoreElements()) + { + String name = (String)e.nextElement(); + setManagedAttribute(name,null); + } + _attributes.clearAttributes(); + } + + /* ------------------------------------------------------------ */ + private void setManagedAttribute(String name, Object value) + { + if (_managedAttributes!=null && _managedAttributes.contains(name)) + { + Object o =_scontext.getAttribute(name); + if (o!=null) + getServer().getContainer().removeBean(o); + if (value!=null) + getServer().getContainer().addBean(value); + } + } + + /* ------------------------------------------------------------ */ + /** + * @param classLoader The classLoader to set. + */ + public void setClassLoader(ClassLoader classLoader) + { + _classLoader = classLoader; + } + + /* ------------------------------------------------------------ */ + /** + * @param contextPath The _contextPath to set. + */ + public void setContextPath(String contextPath) + { + if (contextPath!=null && contextPath.length()>1 && contextPath.endsWith("/")) + throw new IllegalArgumentException("ends with /"); + _contextPath = contextPath; + + if (getServer()!=null && (getServer().isStarting() || getServer().isStarted())) + { + Handler[] contextCollections = getServer().getChildHandlersByClass(ContextHandlerCollection.class); + for (int h=0;contextCollections!=null&& h<contextCollections.length;h++) + ((ContextHandlerCollection)contextCollections[h]).mapContexts(); + } + } + + /* ------------------------------------------------------------ */ + /** + * @param initParams The initParams to set. + */ + public void setInitParams(Map<String,String> initParams) + { + if (initParams == null) + return; + _initParams = new HashMap<String,String>(initParams); + } + + /* ------------------------------------------------------------ */ + /** + * @param servletContextName The servletContextName to set. + */ + public void setDisplayName(String servletContextName) + { + _displayName = servletContextName; + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the resourceBase. + */ + public Resource getBaseResource() + { + if (_baseResource==null) + return null; + return _baseResource; + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the base resource as a string. + */ + public String getResourceBase() + { + if (_baseResource==null) + return null; + return _baseResource.toString(); + } + + /* ------------------------------------------------------------ */ + /** + * @param base The resourceBase to set. + */ + public void setBaseResource(Resource base) + { + _baseResource=base; + } + + /* ------------------------------------------------------------ */ + /** + * @param resourceBase The base resource as a string. + */ + public void setResourceBase(String resourceBase) + { + try + { + setBaseResource(newResource(resourceBase)); + } + catch (Exception e) + { + Log.warn(e); + throw new IllegalArgumentException(resourceBase); + } + } + /* ------------------------------------------------------------ */ + /** + * @return True if alias checking is performed on resources. + */ + public boolean isAliases() + { + return _aliases; + } + + /* ------------------------------------------------------------ */ + /** + * @param aliases alias checking performed on resources. + */ + public void setAliases(boolean aliases) + { + _aliases = aliases; + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the mimeTypes. + */ + public MimeTypes getMimeTypes() + { + return _mimeTypes; + } + + /* ------------------------------------------------------------ */ + /** + * @param mimeTypes The mimeTypes to set. + */ + public void setMimeTypes(MimeTypes mimeTypes) + { + _mimeTypes = mimeTypes; + } + + /* ------------------------------------------------------------ */ + /** + */ + public void setWelcomeFiles(String[] files) + { + _welcomeFiles=files; + } + + /* ------------------------------------------------------------ */ + /** + * @return The names of the files which the server should consider to be welcome files in this context. + * @see <a href="http://jcp.org/aboutJava/communityprocess/final/jsr154/index.html">The Servlet Specification</a> + * @see #setWelcomeFiles + */ + public String[] getWelcomeFiles() + { + return _welcomeFiles; + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the errorHandler. + */ + public ErrorHandler getErrorHandler() + { + return _errorHandler; + } + + /* ------------------------------------------------------------ */ + /** + * @param errorHandler The errorHandler to set. + */ + public void setErrorHandler(ErrorHandler errorHandler) + { + if (errorHandler!=null) + errorHandler.setServer(getServer()); + if (getServer()!=null) + getServer().getContainer().update(this, _errorHandler, errorHandler, "errorHandler",true); + _errorHandler = errorHandler; + } + + /* ------------------------------------------------------------ */ + public int getMaxFormContentSize() + { + return _maxFormContentSize; + } + + /* ------------------------------------------------------------ */ + public void setMaxFormContentSize(int maxSize) + { + _maxFormContentSize=maxSize; + } + + + /* ------------------------------------------------------------ */ + /** + * @return True if URLs are compacted to replace multiple '/'s with a single '/' + */ + public boolean isCompactPath() + { + return _compactPath; + } + + /* ------------------------------------------------------------ */ + /** + * @param compactPath True if URLs are compacted to replace multiple '/'s with a single '/' + */ + public void setCompactPath(boolean compactPath) + { + _compactPath=compactPath; + } + + /* ------------------------------------------------------------ */ + public String toString() + { + + return this.getClass().getName()+"@"+Integer.toHexString(hashCode())+"{"+getContextPath()+","+getBaseResource()+"}"; + } + + /* ------------------------------------------------------------ */ + public synchronized Class<?> loadClass(String className) + throws ClassNotFoundException + { + if (className==null) + return null; + + if (_classLoader==null) + return Loader.loadClass(this.getClass(), className); + + return _classLoader.loadClass(className); + } + + + /* ------------------------------------------------------------ */ + public void addLocaleEncoding(String locale,String encoding) + { + if (_localeEncodingMap==null) + _localeEncodingMap=new HashMap<String,String>(); + _localeEncodingMap.put(locale, encoding); + } + + /* ------------------------------------------------------------ */ + /** + * Get the character encoding for a locale. The full locale name is first + * looked up in the map of encodings. If no encoding is found, then the + * locale language is looked up. + * + * @param locale a <code>Locale</code> value + * @return a <code>String</code> representing the character encoding for + * the locale or null if none found. + */ + public String getLocaleEncoding(Locale locale) + { + if (_localeEncodingMap==null) + return null; + String encoding = (String)_localeEncodingMap.get(locale.toString()); + if (encoding==null) + encoding = (String)_localeEncodingMap.get(locale.getLanguage()); + return encoding; + } + + /* ------------------------------------------------------------ */ + /* + */ + public Resource getResource(String path) throws MalformedURLException + { + if (path==null || !path.startsWith(URIUtil.SLASH)) + throw new MalformedURLException(path); + + if (_baseResource==null) + return null; + + try + { + path=URIUtil.canonicalPath(path); + Resource resource=_baseResource.addPath(path); + + if (_aliases && resource.getAlias()!=null) + { + if (resource.exists()) + Log.warn("Aliased resource: "+resource+"~="+resource.getAlias()); + else if (Log.isDebugEnabled()) + Log.debug("Aliased resource: "+resource+"~="+resource.getAlias()); + return null; + } + + return resource; + } + catch(Exception e) + { + Log.ignore(e); + } + + return null; + } + + /* ------------------------------------------------------------ */ + /** Convert URL to Resource + * wrapper for {@link Resource#newResource(URL)} enables extensions to + * provide alternate resource implementations. + */ + public Resource newResource(URL url) throws IOException + { + return Resource.newResource(url); + } + + /* ------------------------------------------------------------ */ + /** Convert URL to Resource + * wrapper for {@link Resource#newResource(String)} enables extensions to + * provide alternate resource implementations. + */ + public Resource newResource(String url) throws IOException + { + return Resource.newResource(url); + } + + /* ------------------------------------------------------------ */ + /* + */ + public Set<String> getResourcePaths(String path) + { + try + { + path=URIUtil.canonicalPath(path); + Resource resource=getResource(path); + + if (resource!=null && resource.exists()) + { + if (!path.endsWith(URIUtil.SLASH)) + path=path+URIUtil.SLASH; + + String[] l=resource.list(); + if (l!=null) + { + HashSet<String> set = new HashSet<String>(); + for(int i=0;i<l.length;i++) + set.add(path+l[i]); + return set; + } + } + } + catch(Exception e) + { + Log.ignore(e); + } + return Collections.emptySet(); + } + + + + /* ------------------------------------------------------------ */ + private String normalizeHostname( String host ) + { + if ( host == null ) + return null; + + if ( host.endsWith( "." ) ) + return host.substring( 0, host.length() -1); + + return host; + } + + public void complete(Request request) + { + // TODO Auto-generated method stub + + } + + /* ------------------------------------------------------------ */ + /** Context. + * <p> + * A partial implementation of {@link javax.servlet.ServletContext}. + * A complete implementation is provided by the derived {@link org.eclipse.jetty.servlet.ServletContextHandler.Context}. + * </p> + * + * + */ + public class Context implements ServletContext + { + /* ------------------------------------------------------------ */ + protected Context() + { + } + + /* ------------------------------------------------------------ */ + public ContextHandler getContextHandler() + { + // TODO reduce visibility of this method + return ContextHandler.this; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletContext#getContext(java.lang.String) + */ + public ServletContext getContext(String uripath) + { + // TODO this is a very poor implementation! + // TODO move this to Server + ContextHandler context=null; + Handler[] handlers = getServer().getChildHandlersByClass(ContextHandler.class); + for (int i=0;i<handlers.length;i++) + { + if (handlers[i]==null || !handlers[i].isStarted()) + continue; + ContextHandler ch = (ContextHandler)handlers[i]; + String context_path=ch.getContextPath(); + if (uripath.equals(context_path) || (uripath.startsWith(context_path)&&uripath.charAt(context_path.length())=='/')) + { + if (context==null || context_path.length()>context.getContextPath().length()) + context=ch; + } + } + + if (context!=null) + return context._scontext; + return null; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletContext#getMajorVersion() + */ + public int getMajorVersion() + { + return 3; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletContext#getMimeType(java.lang.String) + */ + public String getMimeType(String file) + { + if (_mimeTypes==null) + return null; + Buffer mime = _mimeTypes.getMimeByExtension(file); + if (mime!=null) + return mime.toString(); + return null; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletContext#getMinorVersion() + */ + public int getMinorVersion() + { + return 0; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletContext#getNamedDispatcher(java.lang.String) + */ + public RequestDispatcher getNamedDispatcher(String name) + { + return null; + } + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletContext#getRequestDispatcher(java.lang.String) + */ + public RequestDispatcher getRequestDispatcher(String uriInContext) + { + if (uriInContext == null) + return null; + + if (!uriInContext.startsWith("/")) + return null; + + try + { + String query=null; + int q=0; + if ((q=uriInContext.indexOf('?'))>0) + { + query=uriInContext.substring(q+1); + uriInContext=uriInContext.substring(0,q); + } + if ((q=uriInContext.indexOf(';'))>0) + uriInContext=uriInContext.substring(0,q); + + String pathInContext=URIUtil.canonicalPath(URIUtil.decodePath(uriInContext)); + String uri=URIUtil.addPaths(getContextPath(), uriInContext); + ContextHandler context=ContextHandler.this; + return new Dispatcher(context,uri, pathInContext, query); + } + catch(Exception e) + { + Log.ignore(e); + } + return null; + } + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletContext#getRealPath(java.lang.String) + */ + public String getRealPath(String path) + { + if(path==null) + return null; + if(path.length()==0) + path = URIUtil.SLASH; + else if(path.charAt(0)!='/') + path = URIUtil.SLASH + path; + + try + { + Resource resource=ContextHandler.this.getResource(path); + if(resource!=null) + { + File file = resource.getFile(); + if (file!=null) + return file.getCanonicalPath(); + } + } + catch (Exception e) + { + Log.ignore(e); + } + + return null; + } + + /* ------------------------------------------------------------ */ + public URL getResource(String path) throws MalformedURLException + { + Resource resource=ContextHandler.this.getResource(path); + if (resource!=null && resource.exists()) + return resource.getURL(); + return null; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletContext#getResourceAsStream(java.lang.String) + */ + public InputStream getResourceAsStream(String path) + { + try + { + URL url=getResource(path); + if (url==null) + return null; + return url.openStream(); + } + catch(Exception e) + { + Log.ignore(e); + return null; + } + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletContext#getResourcePaths(java.lang.String) + */ + public Set getResourcePaths(String path) + { + return ContextHandler.this.getResourcePaths(path); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletContext#getServerInfo() + */ + public String getServerInfo() + { + return "jetty/"+Server.getVersion(); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletContext#getServlet(java.lang.String) + */ + public Servlet getServlet(String name) throws ServletException + { + return null; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletContext#getServletNames() + */ + @SuppressWarnings("unchecked") + public Enumeration getServletNames() + { + return Collections.enumeration(Collections.EMPTY_LIST); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletContext#getServlets() + */ + @SuppressWarnings("unchecked") + public Enumeration getServlets() + { + return Collections.enumeration(Collections.EMPTY_LIST); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletContext#log(java.lang.Exception, java.lang.String) + */ + public void log(Exception exception, String msg) + { + _logger.warn(msg,exception); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletContext#log(java.lang.String) + */ + public void log(String msg) + { + _logger.info(msg, null, null); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletContext#log(java.lang.String, java.lang.Throwable) + */ + public void log(String message, Throwable throwable) + { + _logger.warn(message,throwable); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletContext#getInitParameter(java.lang.String) + */ + public String getInitParameter(String name) + { + return ContextHandler.this.getInitParameter(name); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletContext#getInitParameterNames() + */ + @SuppressWarnings("unchecked") + public Enumeration getInitParameterNames() + { + return ContextHandler.this.getInitParameterNames(); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletContext#getAttribute(java.lang.String) + */ + public synchronized Object getAttribute(String name) + { + Object o = ContextHandler.this.getAttribute(name); + if (o==null && _contextAttributes!=null) + o=_contextAttributes.getAttribute(name); + return o; + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletContext#getAttributeNames() + */ + @SuppressWarnings("unchecked") + public synchronized Enumeration getAttributeNames() + { + HashSet<String> set = new HashSet<String>(); + if (_contextAttributes!=null) + { + Enumeration<String> e = _contextAttributes.getAttributeNames(); + while(e.hasMoreElements()) + set.add(e.nextElement()); + } + Enumeration<String> e = _attributes.getAttributeNames(); + while(e.hasMoreElements()) + set.add(e.nextElement()); + + return Collections.enumeration(set); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletContext#setAttribute(java.lang.String, java.lang.Object) + */ + public synchronized void setAttribute(String name, Object value) + { + + if (_contextAttributes==null) + { + // Set it on the handler + ContextHandler.this.setAttribute(name, value); + return; + } + + setManagedAttribute(name,value); + Object old_value=_contextAttributes==null?null:_contextAttributes.getAttribute(name); + + if (value==null) + _contextAttributes.removeAttribute(name); + else + _contextAttributes.setAttribute(name,value); + + if (_contextAttributeListeners!=null) + { + ServletContextAttributeEvent event = + new ServletContextAttributeEvent(_scontext,name, old_value==null?value:old_value); + + for(int i=0;i<LazyList.size(_contextAttributeListeners);i++) + { + ServletContextAttributeListener l = (ServletContextAttributeListener)LazyList.get(_contextAttributeListeners,i); + + if (old_value==null) + l.attributeAdded(event); + else if (value==null) + l.attributeRemoved(event); + else + l.attributeReplaced(event); + } + } + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletContext#removeAttribute(java.lang.String) + */ + public synchronized void removeAttribute(String name) + { + setManagedAttribute(name,null); + + if (_contextAttributes==null) + { + // Set it on the handler + _attributes.removeAttribute(name); + return; + } + + Object old_value=_contextAttributes.getAttribute(name); + _contextAttributes.removeAttribute(name); + if (old_value!=null) + { + if (_contextAttributeListeners!=null) + { + ServletContextAttributeEvent event = + new ServletContextAttributeEvent(_scontext,name, old_value); + + for(int i=0;i<LazyList.size(_contextAttributeListeners);i++) + ((ServletContextAttributeListener)LazyList.get(_contextAttributeListeners,i)).attributeRemoved(event); + } + } + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.ServletContext#getServletContextName() + */ + public String getServletContextName() + { + String name = ContextHandler.this.getDisplayName(); + if (name==null) + name=ContextHandler.this.getContextPath(); + return name; + } + + /* ------------------------------------------------------------ */ + public String getContextPath() + { + if ((_contextPath != null) && _contextPath.equals(URIUtil.SLASH)) + return ""; + + return _contextPath; + } + + /* ------------------------------------------------------------ */ + public String toString() + { + return "ServletContext@"+Integer.toHexString(hashCode())+"{"+(getContextPath().equals("")?URIUtil.SLASH:getContextPath())+","+getBaseResource()+"}"; + } + + /* ------------------------------------------------------------ */ + public boolean setInitParameter(String name, String value) + { + if (ContextHandler.this.getInitParameter(name)!=null) + return false; + ContextHandler.this.getInitParams().put(name,value); + return true; + } + + /* ------------------------------------------------------------ */ + /** + * @see javax.servlet.ServletContext#addFilter(java.lang.String, java.lang.Class) + */ + public FilterRegistration addFilter(String filterName, Class<? extends Filter> filterClass) + { + Log.warn("Use servlet Context"); + return null; + } + + /* ------------------------------------------------------------ */ + /** + * @see javax.servlet.ServletContext#addFilter(java.lang.String, java.lang.String) + */ + public FilterRegistration addFilter(String filterName, String className) + { + Log.warn("Use servlet Context"); + return null; + } + + /* ------------------------------------------------------------ */ + /** + * @see javax.servlet.ServletContext#addServlet(java.lang.String, java.lang.Class) + */ + public ServletRegistration addServlet(String servletName, Class<? extends Servlet> servletClass) + { + Log.warn("Use servlet Context"); + return null; + } + + /* ------------------------------------------------------------ */ + /** + * @see javax.servlet.ServletContext#addServlet(java.lang.String, java.lang.String) + */ + public ServletRegistration addServlet(String servletName, String className) + { + Log.warn("Use servlet Context"); + return null; + } + + /* ------------------------------------------------------------ */ + /** + * @see javax.servlet.ServletContext#findFilterRegistration(java.lang.String) + */ + public FilterRegistration findFilterRegistration(String filterName) + { + Log.warn("Use servlet Context"); + return null; + } + + /* ------------------------------------------------------------ */ + /** + * @see javax.servlet.ServletContext#findServletRegistration(java.lang.String) + */ + public ServletRegistration findServletRegistration(String servletName) + { + Log.warn("Use servlet Context"); + return null; + } + + /* ------------------------------------------------------------ */ + /** + * @see javax.servlet.ServletContext#getDefaultSessionTrackingModes() + */ + public EnumSet<SessionTrackingMode> getDefaultSessionTrackingModes() + { + Log.warn("Use servlet Context"); + return null; + } + + /* ------------------------------------------------------------ */ + /** + * @see javax.servlet.ServletContext#getEffectiveSessionTrackingModes() + */ + public EnumSet<SessionTrackingMode> getEffectiveSessionTrackingModes() + { + Log.warn("Use servlet Context"); + return null; + } + + /* ------------------------------------------------------------ */ + /** + * @see javax.servlet.ServletContext#getSessionCookieConfig() + */ + public SessionCookieConfig getSessionCookieConfig() + { + Log.warn("Use servlet Context"); + return null; + } + + /* ------------------------------------------------------------ */ + /** + * @see javax.servlet.ServletContext#setSessionTrackingModes(java.util.EnumSet) + */ + public void setSessionTrackingModes(EnumSet<SessionTrackingMode> sessionTrackingModes) + { + Log.warn("Use servlet Context"); + } + + /* ------------------------------------------------------------ */ + /** + * @see javax.servlet.ServletContext#addFilter(java.lang.String, javax.servlet.Filter) + */ + public FilterRegistration addFilter(String filterName, Filter filter) + { + Log.warn("Use servlet Context"); + return null; + } + + /* ------------------------------------------------------------ */ + /** + * @see javax.servlet.ServletContext#addServlet(java.lang.String, javax.servlet.Servlet) + */ + public ServletRegistration addServlet(String servletName, Servlet servlet) + { + Log.warn("Use servlet Context"); + return null; + } + + /* ------------------------------------------------------------ */ + /** + * @see javax.servlet.ServletContext#setSessionTrackingModes(java.util.Set) + */ + public void setSessionTrackingModes(Set<SessionTrackingMode> sessionTrackingModes) + { + // TODO Auto-generated method stub + Log.warn("Not implemented"); + + } + + + } + +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandlerCollection.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandlerCollection.java new file mode 100644 index 0000000000..4c9a3464b4 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandlerCollection.java @@ -0,0 +1,319 @@ +// ======================================================================== +// Copyright (c) 2006-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server.handler; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.http.PathMap; +import org.eclipse.jetty.server.AsyncRequest; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.HandlerContainer; +import org.eclipse.jetty.server.HttpConnection; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.util.LazyList; +import org.eclipse.jetty.util.log.Log; + +/* ------------------------------------------------------------ */ +/** ContextHandlerCollection. + * + * This {@link org.eclipse.jetty.server.handler.HandlerCollection} is creates a + * {@link org.eclipse.jetty.http.servlet.PathMap} to it's contained handlers based + * on the context path and virtual hosts of any contained {@link org.eclipse.jetty.server.handler.ContextHandler}s. + * The contexts do not need to be directly contained, only children of the contained handlers. + * Multiple contexts may have the same context path and they are called in order until one + * handles the request. + * + * @org.apache.xbean.XBean element="contexts" + */ +public class ContextHandlerCollection extends HandlerCollection +{ + private PathMap _contextMap; + private Class _contextClass = ContextHandler.class; + + /* ------------------------------------------------------------ */ + /** + * Remap the context paths. + */ + public void mapContexts() + { + PathMap contextMap = new PathMap(); + Handler[] branches = getHandlers(); + + + for (int b=0;branches!=null && b<branches.length;b++) + { + Handler[] handlers=null; + + if (branches[b] instanceof ContextHandler) + { + handlers = new Handler[]{ branches[b] }; + } + else if (branches[b] instanceof HandlerContainer) + { + handlers = ((HandlerContainer)branches[b]).getChildHandlersByClass(ContextHandler.class); + } + else + continue; + + for (int i=0;i<handlers.length;i++) + { + ContextHandler handler=(ContextHandler)handlers[i]; + + String contextPath=handler.getContextPath(); + + if (contextPath==null || contextPath.indexOf(',')>=0 || contextPath.startsWith("*")) + throw new IllegalArgumentException ("Illegal context spec:"+contextPath); + + if(!contextPath.startsWith("/")) + contextPath='/'+contextPath; + + if (contextPath.length()>1) + { + if (contextPath.endsWith("/")) + contextPath+="*"; + else if (!contextPath.endsWith("/*")) + contextPath+="/*"; + } + + Object contexts=contextMap.get(contextPath); + String[] vhosts=handler.getVirtualHosts(); + + + if (vhosts!=null && vhosts.length>0) + { + Map hosts; + + if (contexts instanceof Map) + hosts=(Map)contexts; + else + { + hosts=new HashMap(); + hosts.put("*",contexts); + contextMap.put(contextPath, hosts); + } + + for (int j=0;j<vhosts.length;j++) + { + String vhost=vhosts[j]; + contexts=hosts.get(vhost); + contexts=LazyList.add(contexts,branches[b]); + hosts.put(vhost,contexts); + } + } + else if (contexts instanceof Map) + { + Map hosts=(Map)contexts; + contexts=hosts.get("*"); + contexts= LazyList.add(contexts, branches[b]); + hosts.put("*",contexts); + } + else + { + contexts= LazyList.add(contexts, branches[b]); + contextMap.put(contextPath, contexts); + } + } + } + _contextMap=contextMap; + + } + + + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.jetty.server.server.handler.HandlerCollection#setHandlers(org.eclipse.jetty.server.server.Handler[]) + */ + public void setHandlers(Handler[] handlers) + { + _contextMap=null; + super.setHandlers(handlers); + if (isStarted()) + mapContexts(); + } + + /* ------------------------------------------------------------ */ + protected void doStart() throws Exception + { + mapContexts(); + super.doStart(); + } + + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.jetty.server.server.Handler#handle(java.lang.String, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, int) + */ + public void handle(String target, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + Handler[] handlers = getHandlers(); + if (handlers==null || handlers.length==0) + return; + + Request base_request = HttpConnection.getCurrentConnection().getRequest(); + + AsyncRequest async = base_request.getAsyncRequest(); + if (async!=null) + { + ContextHandler context=async.getContextHandler(); + if (context!=null) + { + context.doHandle(target,base_request,request,response); + return; + } + } + + // data structure which maps a request to a context; first-best match wins + // { context path => + // { virtual host => context } + // } + PathMap map = _contextMap; + if (map!=null && target!=null && target.startsWith("/")) + { + // first, get all contexts matched by context path + Object contexts = map.getLazyMatches(target); + + for (int i=0; i<LazyList.size(contexts); i++) + { + // then, match against the virtualhost of each context + Map.Entry entry = (Map.Entry)LazyList.get(contexts, i); + Object list = entry.getValue(); + + if (list instanceof Map) + { + Map hosts = (Map)list; + String host = normalizeHostname(request.getServerName()); + + // explicitly-defined virtual hosts, most specific + list=hosts.get(host); + for (int j=0; j<LazyList.size(list); j++) + { + Handler handler = (Handler)LazyList.get(list,j); + handler.handle(target,request, response); + if (base_request.isHandled()) + return; + } + + // wildcard for one level of names + list=hosts.get("*."+host.substring(host.indexOf(".")+1)); + for (int j=0; j<LazyList.size(list); j++) + { + Handler handler = (Handler)LazyList.get(list,j); + handler.handle(target,request, response); + if (base_request.isHandled()) + return; + } + + // no virtualhosts defined for the context, least specific + // will handle any request that does not match to a specific virtual host above + list=hosts.get("*"); + for (int j=0; j<LazyList.size(list); j++) + { + Handler handler = (Handler)LazyList.get(list,j); + handler.handle(target,request, response); + if (base_request.isHandled()) + return; + } + } + else + { + for (int j=0; j<LazyList.size(list); j++) + { + Handler handler = (Handler)LazyList.get(list,j); + handler.handle(target,request, response); + if (base_request.isHandled()) + return; + } + } + } + } + else + { + // This may not work in all circumstances... but then I think it should never be called + for (int i=0;i<handlers.length;i++) + { + handlers[i].handle(target,request, response); + if ( base_request.isHandled()) + return; + } + } + } + + + /* ------------------------------------------------------------ */ + /** Add a context handler. + * @param contextPath The context path to add + * @return + * @throws IllegalAccessException + * @throws InstantiationException + */ + public ContextHandler addContext(String contextPath,String resourceBase) + { + try + { + ContextHandler context = (ContextHandler)_contextClass.newInstance(); + context.setContextPath(contextPath); + context.setResourceBase(resourceBase); + addHandler(context); + return context; + } + catch (Exception e) + { + Log.warn(e); + throw new Error(e); + } + } + + + + /* ------------------------------------------------------------ */ + /** + * @return The class to use to add new Contexts + */ + public Class getContextClass() + { + return _contextClass; + } + + + /* ------------------------------------------------------------ */ + /** + * @param contextClass The class to use to add new Contexts + */ + public void setContextClass(Class contextClass) + { + if (contextClass ==null || !(ContextHandler.class.isAssignableFrom(contextClass))) + throw new IllegalArgumentException(); + _contextClass = contextClass; + } + + /* ------------------------------------------------------------ */ + private String normalizeHostname( String host ) + { + if ( host == null ) + return null; + + if ( host.endsWith( "." ) ) + return host.substring( 0, host.length() -1); + + return host; + } + +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DefaultHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DefaultHandler.java new file mode 100644 index 0000000000..4077111992 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/DefaultHandler.java @@ -0,0 +1,193 @@ +// ======================================================================== +// Copyright (c) 1999-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server.handler; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.URL; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.http.HttpHeaders; +import org.eclipse.jetty.http.HttpMethods; +import org.eclipse.jetty.http.MimeTypes; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.HttpConnection; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.ByteArrayISO8859Writer; +import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.util.log.Log; + + +/* ------------------------------------------------------------ */ +/** Default Handler. + * + * This handle will deal with unhandled requests in the server. + * For requests for favicon.ico, the Jetty icon is served. + * For reqests to '/' a 404 with a list of known contexts is served. + * For all other requests a normal 404 is served. + * TODO Implement OPTIONS and TRACE methods for the server. + * + * + * @org.apache.xbean.XBean + */ +public class DefaultHandler extends AbstractHandler +{ + long _faviconModified=(System.currentTimeMillis()/1000)*1000; + byte[] _favicon; + boolean _serveIcon=true; + + public DefaultHandler() + { + try + { + URL fav = this.getClass().getClassLoader().getResource("org/eclipse/jetty/favicon.ico"); + if (fav!=null) + _favicon=IO.readBytes(fav.openStream()); + } + catch(Exception e) + { + Log.warn(e); + } + } + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.jetty.server.server.Handler#handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, int) + */ + public void handle(String target, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + Request base_request = request instanceof Request?(Request)request:HttpConnection.getCurrentConnection().getRequest(); + + if (response.isCommitted() || base_request.isHandled()) + return; + + base_request.setHandled(true); + + String method=request.getMethod(); + + // little cheat for common request + if (_serveIcon && _favicon!=null && method.equals(HttpMethods.GET) && request.getRequestURI().equals("/favicon.ico")) + { + if (request.getDateHeader(HttpHeaders.IF_MODIFIED_SINCE)==_faviconModified) + response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + else + { + response.setStatus(HttpServletResponse.SC_OK); + response.setContentType("image/x-icon"); + response.setContentLength(_favicon.length); + response.setDateHeader(HttpHeaders.LAST_MODIFIED, _faviconModified); + response.setHeader(HttpHeaders.CACHE_CONTROL,"max-age=360000,public"); + response.getOutputStream().write(_favicon); + } + return; + } + + + if (!method.equals(HttpMethods.GET) || !request.getRequestURI().equals("/")) + { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + response.setContentType(MimeTypes.TEXT_HTML); + + ByteArrayISO8859Writer writer = new ByteArrayISO8859Writer(1500); + + String uri=request.getRequestURI(); + uri=StringUtil.replace(uri,"<","<"); + uri=StringUtil.replace(uri,">",">"); + + writer.write("<HTML>\n<HEAD>\n<TITLE>Error 404 - Not Found"); + writer.write("</TITLE>\n<BODY>\n<H2>Error 404 - Not Found.</H2>\n"); + writer.write("No context on this server matched or handled this request.<BR>"); + writer.write("Contexts known to this server are: <ul>"); + + + Server server = getServer(); + Handler[] handlers = server==null?null:server.getChildHandlersByClass(ContextHandler.class); + + for (int i=0;handlers!=null && i<handlers.length;i++) + { + ContextHandler context = (ContextHandler)handlers[i]; + if (context.isRunning()) + { + writer.write("<li><a href=\""); + if (context.getVirtualHosts()!=null && context.getVirtualHosts().length>0) + writer.write("http://"+context.getVirtualHosts()[0]+":"+request.getLocalPort()); + writer.write(context.getContextPath()); + if (context.getContextPath().length()>1 && context.getContextPath().endsWith("/")) + writer.write("/"); + writer.write("\">"); + writer.write(context.getContextPath()); + if (context.getVirtualHosts()!=null && context.getVirtualHosts().length>0) + writer.write(" @ "+context.getVirtualHosts()[0]+":"+request.getLocalPort()); + writer.write(" ---> "); + writer.write(context.toString()); + writer.write("</a></li>\n"); + } + else + { + writer.write("<li>"); + writer.write(context.getContextPath()); + if (context.getVirtualHosts()!=null && context.getVirtualHosts().length>0) + writer.write(" @ "+context.getVirtualHosts()[0]+":"+request.getLocalPort()); + writer.write(" ---> "); + writer.write(context.toString()); + if (context.isFailed()) + writer.write(" [failed]"); + if (context.isStopped()) + writer.write(" [stopped]"); + writer.write("</li>\n"); + } + } + + for (int i=0;i<10;i++) + writer.write("\n<!-- Padding for IE -->"); + + writer.write("\n</BODY>\n</HTML>\n"); + writer.flush(); + response.setContentLength(writer.size()); + OutputStream out=response.getOutputStream(); + writer.writeTo(out); + out.close(); + + return; + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns true if the handle can server the jetty favicon.ico + */ + public boolean getServeIcon() + { + return _serveIcon; + } + + /* ------------------------------------------------------------ */ + /** + * @param serveIcon true if the handle can server the jetty favicon.ico + */ + public void setServeIcon(boolean serveIcon) + { + _serveIcon = serveIcon; + } + + +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java new file mode 100644 index 0000000000..cdbd35f5c2 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ErrorHandler.java @@ -0,0 +1,175 @@ +// ======================================================================== +// Copyright (c) 1999-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +package org.eclipse.jetty.server.handler; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.http.HttpHeaders; +import org.eclipse.jetty.http.HttpMethods; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.http.MimeTypes; +import org.eclipse.jetty.server.HttpConnection; +import org.eclipse.jetty.util.ByteArrayISO8859Writer; +import org.eclipse.jetty.util.StringUtil; + + +/* ------------------------------------------------------------ */ +/** Handler for Error pages + * A handler that is registered at the org.eclipse.http.ErrorHandler + * context attributed and called by the HttpResponse.sendError method to write a + * error page. + * + * + */ +public class ErrorHandler extends AbstractHandler +{ + boolean _showStacks=true; + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.jetty.server.server.Handler#handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, int) + */ + public void handle(String target, HttpServletRequest request, HttpServletResponse response) throws IOException + { + HttpConnection connection = HttpConnection.getCurrentConnection(); + connection.getRequest().setHandled(true); + String method = request.getMethod(); + if(!method.equals(HttpMethods.GET) && !method.equals(HttpMethods.POST)) + return; + response.setContentType(MimeTypes.TEXT_HTML_8859_1); + response.setHeader(HttpHeaders.CACHE_CONTROL, "must-revalidate,no-cache,no-store"); + ByteArrayISO8859Writer writer= new ByteArrayISO8859Writer(4096); + handleErrorPage(request, writer, connection.getResponse().getStatus(), connection.getResponse().getReason()); + writer.flush(); + response.setContentLength(writer.size()); + writer.writeTo(response.getOutputStream()); + writer.destroy(); + } + + /* ------------------------------------------------------------ */ + protected void handleErrorPage(HttpServletRequest request, Writer writer, int code, String message) + throws IOException + { + writeErrorPage(request, writer, code, message, _showStacks); + } + + /* ------------------------------------------------------------ */ + protected void writeErrorPage(HttpServletRequest request, Writer writer, int code, String message, boolean showStacks) + throws IOException + { + if (message == null) + message=HttpStatus.getCode(code).getMessage(); + else + { + message= StringUtil.replace(message, "&", "&"); + message= StringUtil.replace(message, "<", "<"); + message= StringUtil.replace(message, ">", ">"); + } + + writer.write("<html>\n<head>\n"); + writeErrorPageHead(request,writer,code,message); + writer.write("</head>\n<body>"); + writeErrorPageBody(request,writer,code,message,showStacks); + writer.write("\n</body>\n</html>\n"); + } + + /* ------------------------------------------------------------ */ + protected void writeErrorPageHead(HttpServletRequest request, Writer writer, int code, String message) + throws IOException + { + writer.write("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=ISO-8859-1\"/>\n"); + writer.write("<title>Error "); + writer.write(Integer.toString(code)); + writer.write(' '); + if (message!=null) + writer.write(message); + writer.write("</title>\n"); + } + + /* ------------------------------------------------------------ */ + protected void writeErrorPageBody(HttpServletRequest request, Writer writer, int code, String message, boolean showStacks) + throws IOException + { + String uri= request.getRequestURI(); + if (uri!=null) + { + uri= StringUtil.replace(uri, "&", "&"); + uri= StringUtil.replace(uri, "<", "<"); + uri= StringUtil.replace(uri, ">", ">"); + } + + writeErrorPageMessage(request,writer,code,message,uri); + if (showStacks) + writeErrorPageStacks(request,writer); + writer.write("<hr /><i><small>Powered by Jetty://</small></i>"); + for (int i= 0; i < 20; i++) + writer.write("<br/> \n"); + } + + /* ------------------------------------------------------------ */ + protected void writeErrorPageMessage(HttpServletRequest request, Writer writer, int code, String message,String uri) + throws IOException + { + writer.write("<h2>HTTP ERROR "); + writer.write(Integer.toString(code)); + writer.write("</h2>\n<p>Problem accessing "); + writer.write(uri); + writer.write(". Reason:\n<pre> "); + writer.write(message); + writer.write("</pre></p>"); + } + + /* ------------------------------------------------------------ */ + protected void writeErrorPageStacks(HttpServletRequest request, Writer writer) + throws IOException + { + Throwable th = (Throwable)request.getAttribute("javax.servlet.error.exception"); + while(th!=null) + { + writer.write("<h3>Caused by:</h3><pre>"); + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + th.printStackTrace(pw); + pw.flush(); + writer.write(sw.getBuffer().toString()); + writer.write("</pre>\n"); + + th =th.getCause(); + } + } + + + /* ------------------------------------------------------------ */ + /** + * @return True if stack traces are shown in the error pages + */ + public boolean isShowStacks() + { + return _showStacks; + } + + /* ------------------------------------------------------------ */ + /** + * @param showStacks True if stack traces are shown in the error pages + */ + public void setShowStacks(boolean showStacks) + { + _showStacks = showStacks; + } + +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/HandlerCollection.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/HandlerCollection.java new file mode 100644 index 0000000000..51fefd5fb6 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/HandlerCollection.java @@ -0,0 +1,220 @@ +// ======================================================================== +// Copyright (c) 2004-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server.handler; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.LazyList; +import org.eclipse.jetty.util.MultiException; + +/* ------------------------------------------------------------ */ +/** A collection of handlers. + * <p> + * For derived HandlerCollections, the order or manner of calling + * the contained handlers is not defined. The default implementations + * calls all handlers in list order, regardless of + * the response status or exceptions. + * <p> + * + * + * @org.apache.xbean.XBean + */ +public class HandlerCollection extends AbstractHandlerContainer +{ + private Handler[] _handlers; + + /* ------------------------------------------------------------ */ + public HandlerCollection() + { + super(); + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the handlers. + */ + public Handler[] getHandlers() + { + return _handlers; + } + + /* ------------------------------------------------------------ */ + /** + * + * @param handlers The handlers to set. + */ + public void setHandlers(Handler[] handlers) + { + Handler [] old_handlers = _handlers==null?null:(Handler[])_handlers.clone(); + + if (getServer()!=null) + getServer().getContainer().update(this, old_handlers, handlers, "handler"); + + Server server = getServer(); + MultiException mex = new MultiException(); + for (int i=0;handlers!=null && i<handlers.length;i++) + { + if (handlers[i].getServer()!=server) + handlers[i].setServer(server); + } + + // quasi atomic.... so don't go doing this under load on a SMP system. + _handlers = handlers; + + for (int i=0;old_handlers!=null && i<old_handlers.length;i++) + { + if (old_handlers[i]!=null) + { + try + { + if (old_handlers[i].isStarted()) + old_handlers[i].stop(); + } + catch (Throwable e) + { + mex.add(e); + } + } + } + + mex.ifExceptionThrowRuntime(); + } + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.jetty.server.server.EventHandler#handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) + */ + public void handle(String target, HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException + { + if (_handlers!=null && isStarted()) + { + MultiException mex=null; + + for (int i=0;i<_handlers.length;i++) + { + try + { + _handlers[i].handle(target,request, response); + } + catch(IOException e) + { + throw e; + } + catch(RuntimeException e) + { + throw e; + } + catch(Exception e) + { + if (mex==null) + mex=new MultiException(); + mex.add(e); + } + } + if (mex!=null) + { + if (mex.size()==1) + throw new ServletException(mex.getThrowable(0)); + else + throw new ServletException(mex); + } + + } + } + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.jetty.server.server.handler.AbstractHandler#doStart() + */ + protected void doStart() throws Exception + { + MultiException mex=new MultiException(); + if (_handlers!=null) + { + for (int i=0;i<_handlers.length;i++) + try{_handlers[i].start();}catch(Throwable e){mex.add(e);} + } + super.doStart(); + mex.ifExceptionThrow(); + } + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.jetty.server.server.handler.AbstractHandler#doStop() + */ + protected void doStop() throws Exception + { + MultiException mex=new MultiException(); + try { super.doStop(); } catch(Throwable e){mex.add(e);} + if (_handlers!=null) + { + for (int i=_handlers.length;i-->0;) + try{_handlers[i].stop();}catch(Throwable e){mex.add(e);} + } + mex.ifExceptionThrow(); + } + + /* ------------------------------------------------------------ */ + public void setServer(Server server) + { + Server old_server=getServer(); + + super.setServer(server); + + Handler[] h=getHandlers(); + for (int i=0;h!=null && i<h.length;i++) + h[i].setServer(server); + + if (server!=null && server!=old_server) + server.getContainer().update(this, null,_handlers, "handler"); + + } + + /* ------------------------------------------------------------ */ + /* Add a handler. + * This implementation adds the passed handler to the end of the existing collection of handlers. + * @see org.eclipse.jetty.server.server.HandlerContainer#addHandler(org.eclipse.jetty.server.server.Handler) + */ + public void addHandler(Handler handler) + { + setHandlers((Handler[])LazyList.addToArray(getHandlers(), handler, Handler.class)); + } + + /* ------------------------------------------------------------ */ + public void removeHandler(Handler handler) + { + Handler[] handlers = getHandlers(); + + if (handlers!=null && handlers.length>0 ) + setHandlers((Handler[])LazyList.removeFromArray(handlers, handler)); + } + + /* ------------------------------------------------------------ */ + protected Object expandChildren(Object list, Class byClass) + { + Handler[] handlers = getHandlers(); + for (int i=0;handlers!=null && i<handlers.length;i++) + list=expandHandler(handlers[i], list, byClass); + return list; + } + + +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/HandlerList.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/HandlerList.java new file mode 100644 index 0000000000..e8c792400c --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/HandlerList.java @@ -0,0 +1,54 @@ +// ======================================================================== +// Copyright (c) 2006-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server.handler; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.HttpConnection; +import org.eclipse.jetty.server.Request; + +/* ------------------------------------------------------------ */ +/** HandlerList. + * This extension of {@link org.eclipse.jetty.server.server.handler.HandlerCollection} will call + * each contained handler in turn until either an exception is thrown, the response + * is committed or a positive response status is set. + */ +public class HandlerList extends HandlerCollection +{ + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.jetty.server.server.EventHandler#handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) + */ + public void handle(String target, HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException + { + Handler[] handlers = getHandlers(); + + if (handlers!=null && isStarted()) + { + Request base_request = HttpConnection.getCurrentConnection().getRequest(); + for (int i=0;i<handlers.length;i++) + { + handlers[i].handle(target,request, response); + if ( base_request.isHandled()) + return; + } + } + } +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/HandlerWrapper.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/HandlerWrapper.java new file mode 100644 index 0000000000..63021d814d --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/HandlerWrapper.java @@ -0,0 +1,179 @@ +// ======================================================================== +// Copyright (c) 2004-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server.handler; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.HandlerContainer; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.component.LifeCycle; + +/* ------------------------------------------------------------ */ +/** A <code>HandlerWrapper</code> acts as a {@link Handler} but delegates the {@link Handler#handle handle} method and + * {@link LifeCycle life cycle} events to a delegate. This is primarily used to implement the <i>Decorator</i> pattern. + * + */ +public class HandlerWrapper extends AbstractHandlerContainer +{ + private Handler _handler; + + /* ------------------------------------------------------------ */ + /** + * + */ + public HandlerWrapper() + { + super(); + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the handlers. + */ + public Handler getHandler() + { + return _handler; + } + + /* ------------------------------------------------------------ */ + /** + * @param handler Set the {@link Handler} which should be wrapped. + */ + public void setHandler(Handler handler) + { + try + { + Handler old_handler = _handler; + + if (getServer()!=null) + getServer().getContainer().update(this, old_handler, handler, "handler"); + + if (handler!=null) + { + handler.setServer(getServer()); + } + + _handler = handler; + + if (old_handler!=null) + { + if (old_handler.isStarted()) + old_handler.stop(); + } + } + catch(Exception e) + { + IllegalStateException ise= new IllegalStateException(); + ise.initCause(e); + throw ise; + } + } + + /* ------------------------------------------------------------ */ + /** Add a handler. + * This implementation of addHandler calls setHandler with the + * passed handler. If this HandlerWrapper had a previous wrapped + * handler, then it is passed to a call to addHandler on the passed + * handler. Thus this call can add a handler in a chain of + * wrapped handlers. + * + * @param handler + */ + public void addHandler(Handler handler) + { + Handler old = getHandler(); + if (old!=null && !(handler instanceof HandlerContainer)) + throw new IllegalArgumentException("Cannot add"); + setHandler(handler); + if (old!=null) + ((HandlerContainer)handler).addHandler(old); + } + + + public void removeHandler (Handler handler) + { + Handler old = getHandler(); + if (old!=null && (old instanceof HandlerContainer)) + ((HandlerContainer)old).removeHandler(handler); + else if (old!=null && handler==old) + setHandler(null); + else + throw new IllegalStateException("Cannot remove"); + } + + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.thread.AbstractLifeCycle#doStart() + */ + protected void doStart() throws Exception + { + if (_handler!=null) + _handler.start(); + super.doStart(); + } + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.thread.AbstractLifeCycle#doStop() + */ + protected void doStop() throws Exception + { + super.doStop(); + if (_handler!=null) + _handler.stop(); + } + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.jetty.server.server.EventHandler#handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) + */ + public void handle(String target, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + if (_handler!=null && isStarted()) + { + _handler.handle(target,request, response); + } + } + + + /* ------------------------------------------------------------ */ + public void setServer(Server server) + { + Server old_server=getServer(); + + super.setServer(server); + + Handler h=getHandler(); + if (h!=null) + h.setServer(server); + + if (server!=null && server!=old_server) + server.getContainer().update(this, null,_handler, "handler"); + } + + + /* ------------------------------------------------------------ */ + protected Object expandChildren(Object list, Class byClass) + { + return expandHandler(_handler,list,byClass); + } + + +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/MovedContextHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/MovedContextHandler.java new file mode 100644 index 0000000000..8854a5c6e5 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/MovedContextHandler.java @@ -0,0 +1,158 @@ +// ======================================================================== +// Copyright (c) 2006-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server.handler; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.http.HttpHeaders; +import org.eclipse.jetty.server.HandlerContainer; +import org.eclipse.jetty.server.HttpConnection; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.util.URIUtil; + +/* ------------------------------------------------------------ */ +/** Moved ContextHandler. + * This context can be used to replace a context that has changed + * location. Requests are redirected (either to a fixed URL or to a + * new context base). + */ +public class MovedContextHandler extends ContextHandler +{ + String _newContextURL; + boolean _discardPathInfo; + boolean _discardQuery; + boolean _permanent; + Redirector _redirector; + String _expires; + + public MovedContextHandler() + { + _redirector=new Redirector(); + addHandler(_redirector); + setAllowNullPathInfo(true); + } + + public MovedContextHandler(HandlerContainer parent, String contextPath, String newContextURL) + { + super(parent,contextPath); + _newContextURL=newContextURL; + _redirector=new Redirector(); + addHandler(_redirector); + } + + public boolean isDiscardPathInfo() + { + return _discardPathInfo; + } + + public void setDiscardPathInfo(boolean discardPathInfo) + { + _discardPathInfo = discardPathInfo; + } + + public String getNewContextURL() + { + return _newContextURL; + } + + public void setNewContextURL(String newContextURL) + { + _newContextURL = newContextURL; + } + + public boolean isPermanent() + { + return _permanent; + } + + public void setPermanent(boolean permanent) + { + _permanent = permanent; + } + + public boolean isDiscardQuery() + { + return _discardQuery; + } + + public void setDiscardQuery(boolean discardQuery) + { + _discardQuery = discardQuery; + } + + private class Redirector extends AbstractHandler + { + public void handle(String target, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + if (_newContextURL==null) + return; + + Request base_request=(request instanceof Request)?(Request)request:HttpConnection.getCurrentConnection().getRequest(); + + String url = _newContextURL; + if (!_discardPathInfo && request.getPathInfo()!=null) + url=URIUtil.addPaths(url, request.getPathInfo()); + if (!_discardQuery && request.getQueryString()!=null) + url+="?"+request.getQueryString(); + + response.sendRedirect(url); + + String path=_newContextURL; + if (!_discardPathInfo && request.getPathInfo()!=null) + path=URIUtil.addPaths(path, request.getPathInfo()); + + StringBuilder location = URIUtil.hasScheme(path)?new StringBuilder():base_request.getRootURL(); + + location.append(path); + if (!_discardQuery && request.getQueryString()!=null) + { + location.append('?'); + location.append(request.getQueryString()); + } + + response.setHeader(HttpHeaders.LOCATION,location.toString()); + + if (_expires!=null) + response.setHeader(HttpHeaders.EXPIRES,_expires); + + response.setStatus(_permanent?HttpServletResponse.SC_MOVED_PERMANENTLY:HttpServletResponse.SC_FOUND); + response.setContentLength(0); + base_request.setHandled(true); + } + + } + + /* ------------------------------------------------------------ */ + /** + * @return the expires header value or null if no expires header + */ + public String getExpires() + { + return _expires; + } + + /* ------------------------------------------------------------ */ + /** + * @param expires the expires header value or null if no expires header + */ + public void setExpires(String expires) + { + _expires = expires; + } + +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/RequestLogHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/RequestLogHandler.java new file mode 100644 index 0000000000..bdfebd379a --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/RequestLogHandler.java @@ -0,0 +1,132 @@ +// ======================================================================== +// Copyright (c) 2006-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server.handler; + +import java.io.IOException; + +import javax.servlet.DispatcherType; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.RequestLog; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.log.Log; + + + +/** + * RequestLogHandler. + * This handler can be used to wrap an individual context for context logging. + * + * + * @org.apache.xbean.XBean + */ +public class RequestLogHandler extends HandlerWrapper +{ + private RequestLog _requestLog; + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.jetty.server.server.Handler#handle(java.lang.String, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, int) + */ + public void handle(String target, HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException + { + super.handle(target, request, response); + if (DispatcherType.REQUEST.equals(request.getDispatcherType()) && _requestLog!=null) + _requestLog.log((Request)request, (Response)response); + } + + /* ------------------------------------------------------------ */ + public void setRequestLog(RequestLog requestLog) + { + //are we changing the request log impl? + try + { + if (_requestLog != null) + _requestLog.stop(); + } + catch (Exception e) + { + Log.warn (e); + } + + if (getServer()!=null) + getServer().getContainer().update(this, _requestLog, requestLog, "logimpl",true); + + _requestLog = requestLog; + + //if we're already started, then start our request log + try + { + if (isStarted() && (_requestLog != null)) + _requestLog.start(); + } + catch (Exception e) + { + throw new RuntimeException (e); + } + } + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.jetty.server.server.handler.HandlerWrapper#setServer(org.eclipse.jetty.server.server.Server) + */ + public void setServer(Server server) + { + if (_requestLog!=null) + { + if (getServer()!=null && getServer()!=server) + getServer().getContainer().update(this, _requestLog, null, "logimpl",true); + super.setServer(server); + if (server!=null && server!=getServer()) + server.getContainer().update(this, null,_requestLog, "logimpl",true); + } + else + super.setServer(server); + } + + /* ------------------------------------------------------------ */ + public RequestLog getRequestLog() + { + return _requestLog; + } + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.jetty.server.server.handler.HandlerWrapper#doStart() + */ + protected void doStart() throws Exception + { + super.doStart(); + if (_requestLog!=null) + _requestLog.start(); + } + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.jetty.server.server.handler.HandlerWrapper#doStop() + */ + protected void doStop() throws Exception + { + super.doStop(); + if (_requestLog!=null) + _requestLog.stop(); + } + + +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ResourceHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ResourceHandler.java new file mode 100644 index 0000000000..73744ccd11 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ResourceHandler.java @@ -0,0 +1,335 @@ +// ======================================================================== +// Copyright (c) 1999-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server.handler; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.MalformedURLException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpHeaders; +import org.eclipse.jetty.http.HttpMethods; +import org.eclipse.jetty.http.MimeTypes; +import org.eclipse.jetty.io.Buffer; +import org.eclipse.jetty.io.ByteArrayBuffer; +import org.eclipse.jetty.io.WriterOutputStream; +import org.eclipse.jetty.server.HttpConnection; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.handler.ContextHandler.Context; +import org.eclipse.jetty.util.TypeUtil; +import org.eclipse.jetty.util.URIUtil; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.resource.Resource; + + +/* ------------------------------------------------------------ */ +/** Resource Handler. + * + * This handle will serve static content and handle If-Modified-Since headers. + * No caching is done. + * Requests that cannot be handled are let pass (Eg no 404's) + * + * + * @org.apache.xbean.XBean + */ +public class ResourceHandler extends AbstractHandler +{ + ContextHandler _context; + Resource _baseResource; + String[] _welcomeFiles={"index.html"}; + MimeTypes _mimeTypes = new MimeTypes(); + ByteArrayBuffer _cacheControl; + + /* ------------------------------------------------------------ */ + public ResourceHandler() + { + } + + /* ------------------------------------------------------------ */ + public MimeTypes getMimeTypes() + { + return _mimeTypes; + } + + /* ------------------------------------------------------------ */ + public void setMimeTypes(MimeTypes mimeTypes) + { + _mimeTypes = mimeTypes; + } + + /* ------------------------------------------------------------ */ + public void doStart() + throws Exception + { + Context scontext = ContextHandler.getCurrentContext(); + _context = (scontext==null?null:scontext.getContextHandler()); + super.doStart(); + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the resourceBase. + */ + public Resource getBaseResource() + { + if (_baseResource==null) + return null; + return _baseResource; + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the base resource as a string. + */ + public String getResourceBase() + { + if (_baseResource==null) + return null; + return _baseResource.toString(); + } + + + /* ------------------------------------------------------------ */ + /** + * @param base The resourceBase to set. + */ + public void setBaseResource(Resource base) + { + _baseResource=base; + } + + /* ------------------------------------------------------------ */ + /** + * @param resourceBase The base resource as a string. + */ + public void setResourceBase(String resourceBase) + { + try + { + setBaseResource(Resource.newResource(resourceBase)); + } + catch (Exception e) + { + Log.warn(e); + throw new IllegalArgumentException(resourceBase); + } + } + + /* ------------------------------------------------------------ */ + /** + * @return the cacheControl header to set on all static content. + */ + public String getCacheControl() + { + return _cacheControl.toString(); + } + + /* ------------------------------------------------------------ */ + /** + * @param cacheControl the cacheControl header to set on all static content. + */ + public void setCacheControl(String cacheControl) + { + _cacheControl=cacheControl==null?null:new ByteArrayBuffer(cacheControl); + } + + /* ------------------------------------------------------------ */ + /* + */ + public Resource getResource(String path) throws MalformedURLException + { + if (path==null || !path.startsWith("/")) + throw new MalformedURLException(path); + + Resource base = _baseResource; + if (base==null) + { + if (_context==null) + return null; + base=_context.getBaseResource(); + if (base==null) + return null; + } + + try + { + path=URIUtil.canonicalPath(path); + Resource resource=base.addPath(path); + return resource; + } + catch(Exception e) + { + Log.ignore(e); + } + + return null; + } + + /* ------------------------------------------------------------ */ + protected Resource getResource(HttpServletRequest request) throws MalformedURLException + { + String path_info=request.getPathInfo(); + if (path_info==null) + return null; + return getResource(path_info); + } + + + /* ------------------------------------------------------------ */ + public String[] getWelcomeFiles() + { + return _welcomeFiles; + } + + /* ------------------------------------------------------------ */ + public void setWelcomeFiles(String[] welcomeFiles) + { + _welcomeFiles=welcomeFiles; + } + + /* ------------------------------------------------------------ */ + protected Resource getWelcome(Resource directory) throws MalformedURLException, IOException + { + for (int i=0;i<_welcomeFiles.length;i++) + { + Resource welcome=directory.addPath(_welcomeFiles[i]); + if (welcome.exists() && !welcome.isDirectory()) + return welcome; + } + + return null; + } + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.jetty.server.server.Handler#handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, int) + */ + public void handle(String target, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + Request base_request = request instanceof Request?(Request)request:HttpConnection.getCurrentConnection().getRequest(); + if (base_request.isHandled()) + return; + + boolean skipContentBody = false; + if(!HttpMethods.GET.equals(request.getMethod())) + { + if(!HttpMethods.HEAD.equals(request.getMethod())) + return; + skipContentBody = true; + } + + Resource resource=getResource(request); + + if (resource==null || !resource.exists()) + return; + + // We are going to server something + base_request.setHandled(true); + + if (resource.isDirectory()) + { + if (!request.getPathInfo().endsWith(URIUtil.SLASH)) + { + response.sendRedirect(URIUtil.addPaths(request.getRequestURI(),URIUtil.SLASH)); + return; + } + resource=getWelcome(resource); + + if (resource==null || !resource.exists() || resource.isDirectory()) + { + response.sendError(HttpServletResponse.SC_FORBIDDEN); + return; + } + } + + // set some headers + long last_modified=resource.lastModified(); + if (last_modified>0) + { + long if_modified=request.getDateHeader(HttpHeaders.IF_MODIFIED_SINCE); + if (if_modified>0 && last_modified/1000<=if_modified/1000) + { + response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + return; + } + } + + Buffer mime=_mimeTypes.getMimeByExtension(resource.toString()); + if (mime==null) + mime=_mimeTypes.getMimeByExtension(request.getPathInfo()); + + // set the headers + doResponseHeaders(response,resource,mime!=null?mime.toString():null); + response.setDateHeader(HttpHeaders.LAST_MODIFIED,last_modified); + if(skipContentBody) + return; + // Send the content + OutputStream out =null; + try {out = response.getOutputStream();} + catch(IllegalStateException e) {out = new WriterOutputStream(response.getWriter());} + + // See if a short direct method can be used? + if (out instanceof HttpConnection.Output) + { + // TODO file mapped buffers + ((HttpConnection.Output)out).sendContent(resource.getInputStream()); + } + else + { + // Write content normally + resource.writeTo(out,0,resource.length()); + } + } + + /* ------------------------------------------------------------ */ + /** Set the response headers. + * This method is called to set the response headers such as content type and content length. + * May be extended to add additional headers. + * @param response + * @param resource + * @param mimeType + */ + protected void doResponseHeaders(HttpServletResponse response, Resource resource, String mimeType) + { + if (mimeType!=null) + response.setContentType(mimeType); + + long length=resource.length(); + + if (response instanceof Response) + { + HttpFields fields = ((Response)response).getHttpFields(); + + if (length>0) + fields.putLongField(HttpHeaders.CONTENT_LENGTH_BUFFER,length); + + if (_cacheControl!=null) + fields.put(HttpHeaders.CACHE_CONTROL_BUFFER,_cacheControl); + } + else + { + if (length>0) + response.setHeader(HttpHeaders.CONTENT_LENGTH,TypeUtil.toString(length)); + + if (_cacheControl!=null) + response.setHeader(HttpHeaders.CACHE_CONTROL,_cacheControl.toString()); + } + + } +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/StatisticsHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/StatisticsHandler.java new file mode 100644 index 0000000000..1aa6c6b00a --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/StatisticsHandler.java @@ -0,0 +1,369 @@ +// ======================================================================== +// Copyright (c) 2006-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server.handler; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.server.AsyncRequest; +import org.eclipse.jetty.server.HttpConnection; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.LazyList; + +public class StatisticsHandler extends HandlerWrapper implements CompleteHandler +{ + transient long _statsStartedAt; + + transient int _requests; + + transient long _requestsDurationMin; // min request duration + transient long _requestsDurationMax; // max request duration + transient long _requestsDurationTotal; // total request duration + transient long _requestsActiveDurationMin; // min request active duration + transient long _requestsActiveDurationMax; // max request active duration + transient long _requestsActiveDurationTotal; // total request active duration + + transient int _requestsActive; + transient int _requestsActiveMin; // min number of connections handled simultaneously + transient int _requestsActiveMax; + transient int _requestsResumed; + transient int _requestsTimedout; // requests that timed out while suspended + transient int _responses1xx; // Informal + transient int _responses2xx; // Success + transient int _responses3xx; // Redirection + transient int _responses4xx; // Client Error + transient int _responses5xx; // Server Error + + transient long _responsesBytesTotal; + + /* ------------------------------------------------------------ */ + public void statsReset() + { + synchronized(this) + { + if (isStarted()) + _statsStartedAt=System.currentTimeMillis(); + _requests=0; + _responses1xx=0; + _responses2xx=0; + _responses3xx=0; + _responses4xx=0; + _responses5xx=0; + + _requestsActiveMin=_requestsActive; + _requestsActiveMax=_requestsActive; + + _requestsDurationMin=0; + _requestsDurationMax=0; + _requestsDurationTotal=0; + + _requestsActiveDurationMin=0; + _requestsActiveDurationMax=0; + _requestsActiveDurationTotal=0; + } + } + + + /* ------------------------------------------------------------ */ + public void handle(String target, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException + { + final Request base_request=(request instanceof Request)?((Request)request):HttpConnection.getCurrentConnection().getRequest(); + final Response base_response=(response instanceof Response)?((Response)response):HttpConnection.getCurrentConnection().getResponse(); + + long timestamp0=base_request.getTimeStamp(); + long timestamp1=timestamp0; + try + { + synchronized(this) + { + AsyncRequest asyncContextState=base_request.getAsyncRequest(); + + if(asyncContextState==null) + { + _requests++; + } + else + { + if(asyncContextState.isInitial()) + _requests++; + else + { + timestamp1=System.currentTimeMillis(); + /* + if (asyncContextState.isTimeout()) + _requestsTimedout++; + if(asyncContextState.isResumed()) + _requestsResumed++; + */ + } + } + + _requestsActive++; + if (_requestsActive>_requestsActiveMax) + _requestsActiveMax=_requestsActive; + } + + super.handle(target, request, response); + } + finally + { + synchronized(this) + { + _requestsActive--; + if (_requestsActive<0) + _requestsActive=0; + if (_requestsActive < _requestsActiveMin) + _requestsActiveMin=_requestsActive; + + long duration = System.currentTimeMillis()-timestamp1; + _requestsActiveDurationTotal+=duration; + if (_requestsActiveDurationMin==0 || duration<_requestsActiveDurationMin) + _requestsActiveDurationMin=duration; + if (duration>_requestsActiveDurationMax) + _requestsActiveDurationMax=duration; + + + if(request.isAsyncStarted()) + { + Object list = base_request.getAttribute(COMPLETE_HANDLER_ATTR); + base_request.setAttribute(COMPLETE_HANDLER_ATTR, LazyList.add(list, this)); + } + else + { + duration = System.currentTimeMillis()-timestamp0; + addRequestsDurationTotal(duration); + + switch(base_response.getStatus()/100) + { + case 1: _responses1xx++;break; + case 2: _responses2xx++;break; + case 3: _responses3xx++;break; + case 4: _responses4xx++;break; + case 5: _responses5xx++;break; + } + + _responsesBytesTotal += base_response.getContentCount(); + } + } + } + } + + /* ------------------------------------------------------------ */ + protected void doStart() throws Exception + { + super.doStart(); + _statsStartedAt=System.currentTimeMillis(); + } + + /* ------------------------------------------------------------ */ + protected void doStop() throws Exception + { + super.doStop(); + } + + /* ------------------------------------------------------------ */ + /** + * @return Get the number of requests handled by this context + * since last call of statsReset(), not counting resumed requests. + * If setStatsOn(false) then this is undefined. + */ + public int getRequests() {return _requests;} + + /* ------------------------------------------------------------ */ + /** + * @return Number of requests currently active. + * Undefined if setStatsOn(false). + */ + public int getRequestsActive() {return _requestsActive;} + + /* ------------------------------------------------------------ */ + /** + * @return Number of requests that have been resumed. + * Undefined if setStatsOn(false). + */ + public int getRequestsResumed() {return _requestsResumed;} + + /* ------------------------------------------------------------ */ + /** + * @return Number of requests that timed out while suspended. + * Undefined if setStatsOn(false). + */ + public int getRequestsTimedout() {return _requestsTimedout;} + + /* ------------------------------------------------------------ */ + /** + * @return Maximum number of active requests + * since statsReset() called. Undefined if setStatsOn(false). + */ + public int getRequestsActiveMax() {return _requestsActiveMax;} + + /* ------------------------------------------------------------ */ + /** + * @return Get the number of responses with a 2xx status returned + * by this context since last call of statsReset(). Undefined if + * if setStatsOn(false). + */ + public int getResponses1xx() {return _responses1xx;} + + /* ------------------------------------------------------------ */ + /** + * @return Get the number of responses with a 100 status returned + * by this context since last call of statsReset(). Undefined if + * if setStatsOn(false). + */ + public int getResponses2xx() {return _responses2xx;} + + /* ------------------------------------------------------------ */ + /** + * @return Get the number of responses with a 3xx status returned + * by this context since last call of statsReset(). Undefined if + * if setStatsOn(false). + */ + public int getResponses3xx() {return _responses3xx;} + + /* ------------------------------------------------------------ */ + /** + * @return Get the number of responses with a 4xx status returned + * by this context since last call of statsReset(). Undefined if + * if setStatsOn(false). + */ + public int getResponses4xx() {return _responses4xx;} + + /* ------------------------------------------------------------ */ + /** + * @return Get the number of responses with a 5xx status returned + * by this context since last call of statsReset(). Undefined if + * if setStatsOn(false). + */ + public int getResponses5xx() {return _responses5xx;} + + /* ------------------------------------------------------------ */ + /** + * @return Timestamp stats were started at. + */ + public long getStatsOnMs() + { + return System.currentTimeMillis()-_statsStartedAt; + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the requestsActiveMin. + */ + public int getRequestsActiveMin() + { + return _requestsActiveMin; + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the requestsDurationMin. + */ + public long getRequestsDurationMin() + { + return _requestsDurationMin; + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the requestsDurationTotal. + */ + public long getRequestsDurationTotal() + { + return _requestsDurationTotal; + } + + /* ------------------------------------------------------------ */ + /** + * @return Average duration of request handling in milliseconds + * since statsReset() called. Undefined if setStatsOn(false). + */ + public long getRequestsDurationAve() {return _requests==0?0:(_requestsDurationTotal/_requests);} + + /* ------------------------------------------------------------ */ + /** + * @return Get maximum duration in milliseconds of request handling + * since statsReset() called. Undefined if setStatsOn(false). + */ + public long getRequestsDurationMax() {return _requestsDurationMax;} + + /* ------------------------------------------------------------ */ + /** + * @return Returns the requestsActiveDurationMin. + */ + public long getRequestsActiveDurationMin() + { + return _requestsActiveDurationMin; + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the requestsActiveDurationTotal. + */ + public long getRequestsActiveDurationTotal() + { + return _requestsActiveDurationTotal; + } + + /* ------------------------------------------------------------ */ + /** + * @return Average duration of request handling in milliseconds + * since statsReset() called. Undefined if setStatsOn(false). + */ + public long getRequestsActiveDurationAve() {return _requests==0?0:(_requestsActiveDurationTotal/_requests);} + + /* ------------------------------------------------------------ */ + /** + * @return Get maximum duration in milliseconds of request handling + * since statsReset() called. Undefined if setStatsOn(false). + */ + public long getRequestsActiveDurationMax() {return _requestsActiveDurationMax;} + + /* ------------------------------------------------------------ */ + /** + * @return Total bytes of content sent in responses + */ + public long getResponsesBytesTotal() {return _responsesBytesTotal; } + + private void addRequestsDurationTotal(long duration) + { + synchronized(this) + { + _requestsDurationTotal+=duration; + if (_requestsDurationMin==0 || duration<_requestsDurationMin) + _requestsDurationMin=duration; + if (duration>_requestsDurationMax) + _requestsDurationMax=duration; + } + } + + + /* ------------------------------------------------------------ */ + /** + * Handle completed requests. + * + * @param request + * the request which has just completed + */ + public void complete(Request request) + { + long duration = System.currentTimeMillis() - request.getTimeStamp(); + addRequestsDurationTotal(duration); + } + +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/nio/AbstractNIOConnector.java b/jetty-server/src/main/java/org/eclipse/jetty/server/nio/AbstractNIOConnector.java new file mode 100644 index 0000000000..6876144b71 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/nio/AbstractNIOConnector.java @@ -0,0 +1,77 @@ +// ======================================================================== +// Copyright (c) 2004-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +/** + * + */ +package org.eclipse.jetty.server.nio; + +import org.eclipse.jetty.io.Buffer; +import org.eclipse.jetty.io.nio.DirectNIOBuffer; +import org.eclipse.jetty.io.nio.IndirectNIOBuffer; +import org.eclipse.jetty.io.nio.NIOBuffer; +import org.eclipse.jetty.server.AbstractConnector; + +/* ------------------------------------------------------------ */ +/** + * + * + */ +public abstract class AbstractNIOConnector extends AbstractConnector implements NIOConnector +{ + private boolean _useDirectBuffers=true; + + /* ------------------------------------------------------------------------------- */ + public boolean getUseDirectBuffers() + { + return _useDirectBuffers; + } + + /* ------------------------------------------------------------------------------- */ + /** + * @param direct If True (the default), the connector can use NIO direct buffers. + * Some JVMs have memory management issues (bugs) with direct buffers. + */ + public void setUseDirectBuffers(boolean direct) + { + _useDirectBuffers=direct; + } + + /* ------------------------------------------------------------------------------- */ + public Buffer newBuffer(int size) + { + // TODO + // Header buffers always byte array buffers (efficiency of random access) + // There are lots of things to consider here... DIRECT buffers are faster to + // send but more expensive to build and access! so we have choices to make... + // + headers are constructed bit by bit and parsed bit by bit, so INDiRECT looks + // good for them. + // + but will a gather write of an INDIRECT header with a DIRECT body be any good? + // this needs to be benchmarked. + // + Will it be possible to get a DIRECT header buffer just for the gather writes of + // content from file mapped buffers? + // + Are gather writes worth the effort? Maybe they will work well with two INDIRECT + // buffers being copied into a single kernel buffer? + // + Buffer buf = null; + if (size==getHeaderBufferSize()) + buf= new IndirectNIOBuffer(size); + else + buf = _useDirectBuffers + ?(NIOBuffer)new DirectNIOBuffer(size) + :(NIOBuffer)new IndirectNIOBuffer(size); + return buf; + } + + +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/nio/BlockingChannelConnector.java b/jetty-server/src/main/java/org/eclipse/jetty/server/nio/BlockingChannelConnector.java new file mode 100644 index 0000000000..393bfef14d --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/nio/BlockingChannelConnector.java @@ -0,0 +1,189 @@ +// ======================================================================== +// Copyright (c) 2003-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server.nio; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.nio.channels.ByteChannel; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; + +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.EofException; +import org.eclipse.jetty.io.HttpException; +import org.eclipse.jetty.io.nio.ChannelEndPoint; +import org.eclipse.jetty.server.HttpConnection; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.util.log.Log; + + +/* ------------------------------------------------------------------------------- */ +/** Blocking NIO connector. + * This connector uses efficient NIO buffers with a traditional blocking thread model. + * Direct NIO buffers are used and a thread is allocated per connections. + * + * This connector is best used when there are a few very active connections. + * + * @org.apache.xbean.XBean element="blockingNioConnector" description="Creates a blocking NIO based socket connector" + * + * + * + */ +public class BlockingChannelConnector extends AbstractNIOConnector +{ + private transient ServerSocketChannel _acceptChannel; + + /* ------------------------------------------------------------ */ + /** Constructor. + * + */ + public BlockingChannelConnector() + { + } + + /* ------------------------------------------------------------ */ + public Object getConnection() + { + return _acceptChannel; + } + + /* ------------------------------------------------------------ */ + public void open() throws IOException + { + // Create a new server socket and set to non blocking mode + _acceptChannel= ServerSocketChannel.open(); + _acceptChannel.configureBlocking(true); + + // Bind the server socket to the local host and port + InetSocketAddress addr = getHost()==null?new InetSocketAddress(getPort()):new InetSocketAddress(getHost(),getPort()); + _acceptChannel.socket().bind(addr,getAcceptQueueSize()); + } + + /* ------------------------------------------------------------ */ + public void close() throws IOException + { + if (_acceptChannel != null) + _acceptChannel.close(); + _acceptChannel=null; + } + + /* ------------------------------------------------------------ */ + public void accept(int acceptorID) + throws IOException, InterruptedException + { + SocketChannel channel = _acceptChannel.accept(); + channel.configureBlocking(true); + Socket socket=channel.socket(); + configure(socket); + + Connection connection=new Connection(channel); + connection.dispatch(); + } + + /* ------------------------------------------------------------------------------- */ + public void customize(EndPoint endpoint, Request request) + throws IOException + { + Connection connection = (Connection)endpoint; + if (connection._sotimeout!=_maxIdleTime) + { + connection._sotimeout=_maxIdleTime; + ((SocketChannel)endpoint.getTransport()).socket().setSoTimeout(_maxIdleTime); + } + + super.customize(endpoint, request); + configure(((SocketChannel)endpoint.getTransport()).socket()); + } + + + /* ------------------------------------------------------------------------------- */ + public int getLocalPort() + { + if (_acceptChannel==null || !_acceptChannel.isOpen()) + return -1; + return _acceptChannel.socket().getLocalPort(); + } + + /* ------------------------------------------------------------------------------- */ + /* ------------------------------------------------------------------------------- */ + /* ------------------------------------------------------------------------------- */ + private class Connection extends ChannelEndPoint implements Runnable + { + boolean _dispatched=false; + HttpConnection _connection; + int _sotimeout; + + Connection(ByteChannel channel) + { + super(channel); + _connection = new HttpConnection(BlockingChannelConnector.this,this,getServer()); + } + + void dispatch() throws IOException + { + if (!getThreadPool().dispatch(this)) + { + Log.warn("dispatch failed for {}",_connection); + close(); + } + } + + public void run() + { + try + { + connectionOpened(_connection); + + while (isOpen()) + { + if (_connection.isIdle()) + { + if (getServer().getThreadPool().isLowOnThreads()) + { + if (_sotimeout!=getLowResourceMaxIdleTime()) + { + _sotimeout=getLowResourceMaxIdleTime(); + ((SocketChannel)getTransport()).socket().setSoTimeout(_sotimeout); + } + } + } + _connection.handle(); + } + } + catch (EofException e) + { + Log.debug("EOF", e); + try{close();} + catch(IOException e2){Log.ignore(e2);} + } + catch (HttpException e) + { + Log.debug("BAD", e); + try{close();} + catch(IOException e2){Log.ignore(e2);} + } + catch(Throwable e) + { + Log.warn("handle failed",e); + try{close();} + catch(IOException e2){Log.ignore(e2);} + } + finally + { + connectionClosed(_connection); + } + } + } +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/nio/InheritedChannelConnector.java b/jetty-server/src/main/java/org/eclipse/jetty/server/nio/InheritedChannelConnector.java new file mode 100644 index 0000000000..fca291ba7e --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/nio/InheritedChannelConnector.java @@ -0,0 +1,66 @@ +// ======================================================================== +// Copyright (c) 2008-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server.nio; + +import java.io.IOException; +import java.nio.channels.Channel; +import java.nio.channels.ServerSocketChannel; + +import org.eclipse.jetty.util.log.Log; + +/** + * An implementation of the SelectChannelConnector which first tries to + * inherit from a channel provided by the system. If there is no inherited + * channel available, or if the inherited channel provided not usable, then + * it will fall back upon normal ServerSocketChannel creation. + * <p> + * Note that System.inheritedChannel() is only available from Java 1.5 onwards. + * Trying to use this class under Java 1.4 will be the same as using a normal + * SelectChannelConnector. + * <p> + * Use it with xinetd/inetd, to launch an instance of Jetty on demand. The port + * used to access pages on the Jetty instance is the same as the port used to + * launch Jetty. + * + * @author athena + */ +public class InheritedChannelConnector extends SelectChannelConnector +{ + /* ------------------------------------------------------------ */ + public void open() throws IOException + { + synchronized(this) + { + try + { + Channel channel = System.inheritedChannel(); + if ( channel instanceof ServerSocketChannel ) + _acceptChannel = (ServerSocketChannel)channel; + else + Log.warn("Unable to use System.inheritedChannel() [" +channel+ "]. Trying a new ServerSocketChannel at " + getHost() + ":" + getPort()); + + if ( _acceptChannel != null ) + _acceptChannel.configureBlocking(false); + } + catch(NoSuchMethodError e) + { + Log.warn("Need at least Java 5 to use socket inherited from xinetd/inetd."); + } + + if (_acceptChannel == null) + super.open(); + } + } + +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/nio/NIOConnector.java b/jetty-server/src/main/java/org/eclipse/jetty/server/nio/NIOConnector.java new file mode 100644 index 0000000000..16d27566fa --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/nio/NIOConnector.java @@ -0,0 +1,26 @@ +// ======================================================================== +// Copyright (c) 2006-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server.nio; + +/** + * NIOConnector. + * A marker interface that indicates that NIOBuffers can be handled (efficiently) by this Connector. + * + * + * + */ +public interface NIOConnector +{ + boolean getUseDirectBuffers(); +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/nio/SelectChannelConnector.java b/jetty-server/src/main/java/org/eclipse/jetty/server/nio/SelectChannelConnector.java new file mode 100644 index 0000000000..14ad86adee --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/nio/SelectChannelConnector.java @@ -0,0 +1,328 @@ +// ======================================================================== +// Copyright (c) 2003-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server.nio; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.nio.channels.SelectionKey; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; + +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.nio.SelectChannelEndPoint; +import org.eclipse.jetty.io.nio.SelectorManager; +import org.eclipse.jetty.io.nio.SelectorManager.SelectSet; +import org.eclipse.jetty.server.HttpConnection; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.thread.Timeout.Task; + +/* ------------------------------------------------------------------------------- */ +/** + * Selecting NIO connector. + * <p> + * This connector uses efficient NIO buffers with a non blocking threading model. Direct NIO buffers + * are used and threads are only allocated to connections with requests. Synchronization is used to + * simulate blocking for the servlet API, and any unflushed content at the end of request handling + * is written asynchronously. + * </p> + * <p> + * This connector is best used when there are a many connections that have idle periods. + * </p> + * <p> + * When used with {@link org.eclipse.jetty.util.ajax.Continuation}, threadless waits are supported. When + * a filter or servlet calls getEvent on a Continuation, a {@link org.eclipse.jetty.server.RetryRequest} + * runtime exception is thrown to allow the thread to exit the current request handling. Jetty will + * catch this exception and will not send a response to the client. Instead the thread is released + * and the Continuation is placed on the timer queue. If the Continuation timeout expires, or it's + * resume method is called, then the request is again allocated a thread and the request is retried. + * The limitation of this approach is that request content is not available on the retried request, + * thus if possible it should be read after the continuation or saved as a request attribute or as the + * associated object of the Continuation instance. + * </p> + * + * @org.apache.xbean.XBean element="nioConnector" description="Creates an NIO based socket connector" + * + * + * + */ +public class SelectChannelConnector extends AbstractNIOConnector +{ + protected transient ServerSocketChannel _acceptChannel; + private long _lowResourcesConnections; + private long _lowResourcesMaxIdleTime; + + private SelectorManager _manager = new SelectorManager() + { + protected SocketChannel acceptChannel(SelectionKey key) throws IOException + { + // TODO handle max connections + SocketChannel channel = ((ServerSocketChannel)key.channel()).accept(); + if (channel==null) + return null; + channel.configureBlocking(false); + Socket socket = channel.socket(); + configure(socket); + return channel; + } + + public boolean dispatch(Runnable task) + { + return getThreadPool().dispatch(task); + } + + protected void endPointClosed(SelectChannelEndPoint endpoint) + { + // TODO handle max connections and low resources + connectionClosed((HttpConnection)endpoint.getConnection()); + } + + protected void endPointOpened(SelectChannelEndPoint endpoint) + { + // TODO handle max connections and low resources + connectionOpened((HttpConnection)endpoint.getConnection()); + } + + protected Connection newConnection(SocketChannel channel,SelectChannelEndPoint endpoint) + { + return SelectChannelConnector.this.newConnection(channel,endpoint); + } + + protected SelectChannelEndPoint newEndPoint(SocketChannel channel, SelectSet selectSet, SelectionKey sKey) throws IOException + { + return SelectChannelConnector.this.newEndPoint(channel,selectSet,sKey); + } + }; + + /* ------------------------------------------------------------------------------- */ + /** + * Constructor. + * + */ + public SelectChannelConnector() + { + } + + /* ------------------------------------------------------------ */ + public void accept(int acceptorID) throws IOException + { + _manager.doSelect(acceptorID); + } + + /* ------------------------------------------------------------ */ + public void close() throws IOException + { + synchronized(this) + { + if(_manager.isRunning()) + { + try + { + _manager.stop(); + } + catch (Exception e) + { + Log.warn(e); + } + } + if (_acceptChannel != null) + _acceptChannel.close(); + _acceptChannel = null; + } + } + + /* ------------------------------------------------------------------------------- */ + @Override + public void customize(EndPoint endpoint, Request request) throws IOException + { + SelectChannelEndPoint cep = ((SelectChannelEndPoint)endpoint); + cep.cancelIdle(); + request.setTimeStamp(cep.getSelectSet().getNow()); + super.customize(endpoint, request); + } + + /* ------------------------------------------------------------------------------- */ + @Override + public void persist(EndPoint endpoint) throws IOException + { + ((SelectChannelEndPoint)endpoint).scheduleIdle(); + super.persist(endpoint); + } + + /* ------------------------------------------------------------ */ + public Object getConnection() + { + return _acceptChannel; + } + + /* ------------------------------------------------------------------------------- */ + public int getLocalPort() + { + synchronized(this) + { + if (_acceptChannel==null || !_acceptChannel.isOpen()) + return -1; + return _acceptChannel.socket().getLocalPort(); + } + } + + /* ------------------------------------------------------------ */ + public void open() throws IOException + { + synchronized(this) + { + if (_acceptChannel == null) + { + // Create a new server socket + _acceptChannel = ServerSocketChannel.open(); + + // Bind the server socket to the local host and port + _acceptChannel.socket().setReuseAddress(getReuseAddress()); + InetSocketAddress addr = getHost()==null?new InetSocketAddress(getPort()):new InetSocketAddress(getHost(),getPort()); + _acceptChannel.socket().bind(addr,getAcceptQueueSize()); + + // Set to non blocking mode + _acceptChannel.configureBlocking(false); + + } + } + } + + /* ------------------------------------------------------------ */ + public void setMaxIdleTime(int maxIdleTime) + { + _manager.setMaxIdleTime(maxIdleTime); + super.setMaxIdleTime(maxIdleTime); + } + + /* ------------------------------------------------------------ */ + /** + * @return the lowResourcesConnections + */ + public long getLowResourcesConnections() + { + return _lowResourcesConnections; + } + + /* ------------------------------------------------------------ */ + /** + * Set the number of connections, which if exceeded places this manager in low resources state. + * This is not an exact measure as the connection count is averaged over the select sets. + * @param lowResourcesConnections the number of connections + * @see {@link #setLowResourcesMaxIdleTime(long)} + */ + public void setLowResourcesConnections(long lowResourcesConnections) + { + _lowResourcesConnections=lowResourcesConnections; + } + + /* ------------------------------------------------------------ */ + /** + * @return the lowResourcesMaxIdleTime + */ + public long getLowResourcesMaxIdleTime() + { + return _lowResourcesMaxIdleTime; + } + + /* ------------------------------------------------------------ */ + /** + * Set the period in ms that a connection is allowed to be idle when this there are more + * than {@link #getLowResourcesConnections()} connections. This allows the server to rapidly close idle connections + * in order to gracefully handle high load situations. + * @param lowResourcesMaxIdleTime the period in ms that a connection is allowed to be idle when resources are low. + * @see {@link #setMaxIdleTime(long)} + * @deprecated use {@link #setLowResourceMaxIdleTime(int)} + */ + public void setLowResourcesMaxIdleTime(long lowResourcesMaxIdleTime) + { + _lowResourcesMaxIdleTime=lowResourcesMaxIdleTime; + super.setLowResourceMaxIdleTime((int)lowResourcesMaxIdleTime); // TODO fix the name duplications + } + + /* ------------------------------------------------------------ */ + /** + * Set the period in ms that a connection is allowed to be idle when this there are more + * than {@link #getLowResourcesConnections()} connections. This allows the server to rapidly close idle connections + * in order to gracefully handle high load situations. + * @param lowResourcesMaxIdleTime the period in ms that a connection is allowed to be idle when resources are low. + * @see {@link #setMaxIdleTime(long)} + */ + public void setLowResourceMaxIdleTime(int lowResourcesMaxIdleTime) + { + _lowResourcesMaxIdleTime=lowResourcesMaxIdleTime; + super.setLowResourceMaxIdleTime(lowResourcesMaxIdleTime); + } + + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.jetty.server.server.AbstractConnector#doStart() + */ + protected void doStart() throws Exception + { + _manager.setSelectSets(getAcceptors()); + _manager.setMaxIdleTime(getMaxIdleTime()); + _manager.setLowResourcesConnections(getLowResourcesConnections()); + _manager.setLowResourcesMaxIdleTime(getLowResourcesMaxIdleTime()); + _manager.start(); + open(); + _manager.register(_acceptChannel); + super.doStart(); + } + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.jetty.server.server.AbstractConnector#doStop() + */ + protected void doStop() throws Exception + { + super.doStop(); + } + + /* ------------------------------------------------------------ */ + protected SelectChannelEndPoint newEndPoint(SocketChannel channel, SelectSet selectSet, SelectionKey key) throws IOException + { + return new SelectChannelEndPoint(channel,selectSet,key) + { + // TODO remove this hack + public boolean isReadyForDispatch() + { + Request request = ((HttpConnection)getConnection()).getRequest(); + return super.isReadyForDispatch() && !(request.getAsyncRequest().isSuspended()); + } + }; + } + + /* ------------------------------------------------------------------------------- */ + protected Connection newConnection(SocketChannel channel,final SelectChannelEndPoint endpoint) + { + return new HttpConnection(SelectChannelConnector.this,endpoint,getServer()) + { + /* ------------------------------------------------------------ */ + public void cancelTimeout(Task task) + { + endpoint.getSelectSet().cancelTimeout(task); + } + + /* ------------------------------------------------------------ */ + public void scheduleTimeout(Task task, long timeoutMs) + { + endpoint.getSelectSet().scheduleTimeout(task,timeoutMs); + } + }; + } +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionIdManager.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionIdManager.java new file mode 100644 index 0000000000..f02388c7dc --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionIdManager.java @@ -0,0 +1,161 @@ +// ======================================================================== +// Copyright (c) 2004-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server.session; + +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Random; + +import javax.servlet.http.HttpServletRequest; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.SessionIdManager; +import org.eclipse.jetty.util.component.AbstractLifeCycle; +import org.eclipse.jetty.util.log.Log; + +public abstract class AbstractSessionIdManager extends AbstractLifeCycle implements SessionIdManager +{ + private final static String __NEW_SESSION_ID="org.eclipse.jetty.server.newSessionId"; + protected final static String SESSION_ID_RANDOM_ALGORITHM = "SHA1PRNG"; + protected final static String SESSION_ID_RANDOM_ALGORITHM_ALT = "IBMSecureRandom"; + + protected Random _random; + protected boolean _weakRandom; + protected String _workerName; + protected Server _server; + + + public AbstractSessionIdManager(Server server) + { + _server=server; + } + + + public AbstractSessionIdManager(Server server, Random random) + { + _random=random; + _server=server; + } + + public String getWorkerName() + { + return _workerName; + } + + public void setWorkerName (String name) + { + _workerName=name; + } + + /* ------------------------------------------------------------ */ + public Random getRandom() + { + return _random; + } + + /* ------------------------------------------------------------ */ + public void setRandom(Random random) + { + _random=random; + _weakRandom=false; + } + /** + * Create a new session id if necessary. + * + * @see org.eclipse.jetty.server.SessionIdManager#newSessionId(javax.servlet.http.HttpServletRequest, long) + */ + public String newSessionId(HttpServletRequest request, long created) + { + synchronized (this) + { + // A requested session ID can only be used if it is in use already. + String requested_id=request.getRequestedSessionId(); + if (requested_id!=null) + { + String cluster_id=getClusterId(requested_id); + if (idInUse(cluster_id)) + return cluster_id; + } + + // Else reuse any new session ID already defined for this request. + String new_id=(String)request.getAttribute(__NEW_SESSION_ID); + if (new_id!=null&&idInUse(new_id)) + return new_id; + + + + // pick a new unique ID! + String id=null; + while (id==null||id.length()==0||idInUse(id)) + { + long r=_weakRandom + ?(hashCode()^Runtime.getRuntime().freeMemory()^_random.nextInt()^(((long)request.hashCode())<<32)) + :_random.nextLong(); + r^=created; + if (request!=null && request.getRemoteAddr()!=null) + r^=request.getRemoteAddr().hashCode(); + if (r<0) + r=-r; + id=Long.toString(r,36); + + //add in the id of the node to ensure unique id across cluster + //NOTE this is different to the node suffix which denotes which node the request was received on + id=_workerName + id; + } + + request.setAttribute(__NEW_SESSION_ID,id); + return id; + } + } + + + public void doStart() + { + initRandom(); + } + + + + + /** + * Set up a random number generator for the sessionids. + * + * By preference, use a SecureRandom but allow to be injected. + */ + public void initRandom () + { + if (_random==null) + { + try + { + _random=SecureRandom.getInstance(SESSION_ID_RANDOM_ALGORITHM); + } + catch (NoSuchAlgorithmException e) + { + try + { + _random=SecureRandom.getInstance(SESSION_ID_RANDOM_ALGORITHM_ALT); + _weakRandom=false; + } + catch (NoSuchAlgorithmException e_alt) + { + Log.warn("Could not generate SecureRandom for session-id randomness",e); + _random=new Random(); + _weakRandom=true; + } + } + } + _random.setSeed(_random.nextLong()^System.currentTimeMillis()^hashCode()^Runtime.getRuntime().freeMemory()); + } +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionManager.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionManager.java new file mode 100644 index 0000000000..d346aa78e6 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/AbstractSessionManager.java @@ -0,0 +1,1182 @@ +// ======================================================================== +// Copyright (c) 1999-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server.session; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.EventListener; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import javax.servlet.ServletContext; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; +import javax.servlet.http.HttpSessionActivationListener; +import javax.servlet.http.HttpSessionAttributeListener; +import javax.servlet.http.HttpSessionBindingEvent; +import javax.servlet.http.HttpSessionBindingListener; +import javax.servlet.http.HttpSessionContext; +import javax.servlet.http.HttpSessionEvent; +import javax.servlet.http.HttpSessionListener; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.SessionIdManager; +import org.eclipse.jetty.server.SessionManager; +import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.util.LazyList; +import org.eclipse.jetty.util.component.AbstractLifeCycle; + +/* ------------------------------------------------------------ */ +/** + * An Abstract implementation of SessionManager. The partial implementation of + * SessionManager interface provides the majority of the handling required to + * implement a SessionManager. Concrete implementations of SessionManager based + * on AbstractSessionManager need only implement the newSession method to return + * a specialized version of the Session inner class that provides an attribute + * Map. + * <p> + * If the property + * org.eclipse.jetty.servlet.AbstractSessionManager.23Notifications is set to + * true, the 2.3 servlet spec notification style will be used. + * <p> + * + * + */ +public abstract class AbstractSessionManager extends AbstractLifeCycle implements SessionManager +{ + /* ------------------------------------------------------------ */ + public final static int __distantFuture=60*60*24*7*52*20; + + private static final HttpSessionContext __nullSessionContext=new NullSessionContext(); + + private boolean _usingCookies=true; + + /* ------------------------------------------------------------ */ + // Setting of max inactive interval for new sessions + // -1 means no timeout + protected int _dftMaxIdleSecs=-1; + protected SessionHandler _sessionHandler; + protected boolean _httpOnly=false; + protected int _maxSessions=0; + + protected int _minSessions=0; + protected SessionIdManager _sessionIdManager; + protected boolean _secureCookies=false; + protected Object _sessionAttributeListeners; + protected Object _sessionListeners; + + protected ClassLoader _loader; + protected ContextHandler.Context _context; + protected String _sessionCookie=__DefaultSessionCookie; + protected String _sessionIdPathParameterName = __DefaultSessionIdPathParameterName; + protected String _sessionIdPathParameterNamePrefix =";"+ _sessionIdPathParameterName +"="; + protected String _sessionDomain; + protected String _sessionPath; + protected int _maxCookieAge=-1; + protected int _refreshCookieAge; + protected boolean _nodeIdInSessionId; + + /* ------------------------------------------------------------ */ + public AbstractSessionManager() + { + } + + /* ------------------------------------------------------------ */ + public Cookie access(HttpSession session,boolean secure) + { + long now=System.currentTimeMillis(); + + Session s = ((SessionIf)session).getSession(); + s.access(now); + + // Do we need to refresh the cookie? + if (isUsingCookies() && + (s.isIdChanged() || + (getMaxCookieAge()>0 && getRefreshCookieAge()>0 && ((now-s.getCookieSetTime())/1000>getRefreshCookieAge())) + ) + ) + { + Cookie cookie=getSessionCookie(session,_context.getContextPath(),secure); + s.cookieSet(); + s.setIdChanged(false); + return cookie; + } + + return null; + } + + /* ------------------------------------------------------------ */ + public void addEventListener(EventListener listener) + { + if (listener instanceof HttpSessionAttributeListener) + _sessionAttributeListeners=LazyList.add(_sessionAttributeListeners,listener); + if (listener instanceof HttpSessionListener) + _sessionListeners=LazyList.add(_sessionListeners,listener); + } + + /* ------------------------------------------------------------ */ + public void clearEventListeners() + { + _sessionAttributeListeners=null; + _sessionListeners=null; + } + + /* ------------------------------------------------------------ */ + public void complete(HttpSession session) + { + Session s = ((SessionIf)session).getSession(); + s.complete(); + } + + /* ------------------------------------------------------------ */ + public void doStart() throws Exception + { + _context=ContextHandler.getCurrentContext(); + _loader=Thread.currentThread().getContextClassLoader(); + + if (_sessionIdManager==null) + { + Server server=getSessionHandler().getServer(); + synchronized (server) + { + _sessionIdManager=server.getSessionIdManager(); + if (_sessionIdManager==null) + { + _sessionIdManager=new HashSessionIdManager(); + server.setSessionIdManager(_sessionIdManager); + } + } + } + if (!_sessionIdManager.isStarted()) + _sessionIdManager.start(); + + // Look for a session cookie name + String tmp=_context.getInitParameter(SessionManager.__SessionCookieProperty); + if (tmp!=null) + _sessionCookie=tmp; + + tmp=_context.getInitParameter(SessionManager.__SessionIdPathParameterNameProperty); + if (tmp!=null) + { + setSessionIdPathParameterName(tmp); + } + + // set up the max session cookie age if it isn't already + if (_maxCookieAge==-1) + { + if (_context!=null) + { + String str=_context.getInitParameter(SessionManager.__MaxAgeProperty); + if (str!=null) + _maxCookieAge=Integer.parseInt(str.trim()); + } + } + // set up the session domain if it isn't already + if (_sessionDomain==null) + { + // only try the context initParams + if (_context!=null) + _sessionDomain=_context.getInitParameter(SessionManager.__SessionDomainProperty); + } + + // set up the sessionPath if it isn't already + if (_sessionPath==null) + { + // only the context initParams + if (_context!=null) + _sessionPath=_context.getInitParameter(SessionManager.__SessionPathProperty); + } + + super.doStart(); + } + + /* ------------------------------------------------------------ */ + public void doStop() throws Exception + { + super.doStop(); + + invalidateSessions(); + + _loader=null; + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the httpOnly. + */ + public boolean getHttpOnly() + { + return _httpOnly; + } + + /* ------------------------------------------------------------ */ + public HttpSession getHttpSession(String nodeId) + { + String cluster_id = getIdManager().getClusterId(nodeId); + + synchronized (this) + { + Session session = getSession(cluster_id); + + if (session!=null && !session.getNodeId().equals(nodeId)) + session.setIdChanged(true); + return session; + } + } + + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + /** + * @return Returns the metaManager used for cross context session management + */ + public SessionIdManager getIdManager() + { + return _sessionIdManager; + } + + /* ------------------------------------------------------------ */ + public int getMaxCookieAge() + { + return _maxCookieAge; + } + + /* ------------------------------------------------------------ */ + /** + * @return seconds + */ + public int getMaxInactiveInterval() + { + return _dftMaxIdleSecs; + } + + /* ------------------------------------------------------------ */ + public int getMaxSessions() + { + return _maxSessions; + } + + /* ------------------------------------------------------------ */ + /** + * @deprecated use {@link #getIdManager()} + */ + public SessionIdManager getMetaManager() + { + return getIdManager(); + } + + /* ------------------------------------------------------------ */ + public int getMinSessions() + { + return _minSessions; + } + + /* ------------------------------------------------------------ */ + public int getRefreshCookieAge() + { + return _refreshCookieAge; + } + + + /* ------------------------------------------------------------ */ + /** + * @return Returns the secureCookies. + */ + public boolean getSecureCookies() + { + return _secureCookies; + } + + /* ------------------------------------------------------------ */ + public String getSessionCookie() + { + return _sessionCookie; + } + + /* ------------------------------------------------------------ */ + public Cookie getSessionCookie(HttpSession session, String contextPath, boolean requestIsSecure) + { + if (isUsingCookies()) + { + String id = getNodeId(session); + Cookie cookie=new Cookie(_sessionCookie,id); + cookie.setHttpOnly(getHttpOnly()); + + cookie.setPath((contextPath==null||contextPath.length()==0)?"/":contextPath); + cookie.setMaxAge(getMaxCookieAge()); + cookie.setSecure(requestIsSecure&&getSecureCookies()); + + // set up the overrides + if (_sessionDomain!=null) + cookie.setDomain(_sessionDomain); + if (_sessionPath!=null) + cookie.setPath(_sessionPath); + + return cookie; + } + return null; + } + + public String getSessionDomain() + { + return _sessionDomain; + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the sessionHandler. + */ + public SessionHandler getSessionHandler() + { + return _sessionHandler; + } + + /* ------------------------------------------------------------ */ + /** + * @deprecated. Need to review if it is needed. + */ + public abstract Map getSessionMap(); + + /* ------------------------------------------------------------ */ + public String getSessionPath() + { + return _sessionPath; + } + + /* ------------------------------------------------------------ */ + public abstract int getSessions(); + + /* ------------------------------------------------------------ */ + public String getSessionIdPathParameterName() + { + return _sessionIdPathParameterName; + } + + /* ------------------------------------------------------------ */ + public String getSessionIdPathParameterNamePrefix() + { + return _sessionIdPathParameterNamePrefix; + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the usingCookies. + */ + public boolean isUsingCookies() + { + return _usingCookies; + } + + /* ------------------------------------------------------------ */ + public boolean isValid(HttpSession session) + { + Session s = ((SessionIf)session).getSession(); + return s.isValid(); + } + + /* ------------------------------------------------------------ */ + public String getClusterId(HttpSession session) + { + Session s = ((SessionIf)session).getSession(); + return s.getClusterId(); + } + + /* ------------------------------------------------------------ */ + public String getNodeId(HttpSession session) + { + Session s = ((SessionIf)session).getSession(); + return s.getNodeId(); + } + + /* ------------------------------------------------------------ */ + /** + * Create a new HttpSession for a request + */ + public HttpSession newHttpSession(HttpServletRequest request) + { + Session session=newSession(request); + session.setMaxInactiveInterval(_dftMaxIdleSecs); + addSession(session,true); + return session; + } + + /* ------------------------------------------------------------ */ + public void removeEventListener(EventListener listener) + { + if (listener instanceof HttpSessionAttributeListener) + _sessionAttributeListeners=LazyList.remove(_sessionAttributeListeners,listener); + if (listener instanceof HttpSessionListener) + _sessionListeners=LazyList.remove(_sessionListeners,listener); + } + + /* ------------------------------------------------------------ */ + public void resetStats() + { + _minSessions=getSessions(); + _maxSessions=getSessions(); + } + + /* ------------------------------------------------------------ */ + /** + * @param httpOnly + * The httpOnly to set. + */ + public void setHttpOnly(boolean httpOnly) + { + _httpOnly=httpOnly; + } + + + /* ------------------------------------------------------------ */ + /** + * @param metaManager The metaManager used for cross context session management. + */ + public void setIdManager(SessionIdManager metaManager) + { + _sessionIdManager=metaManager; + } + + /* ------------------------------------------------------------ */ + public void setMaxCookieAge(int maxCookieAgeInSeconds) + { + _maxCookieAge=maxCookieAgeInSeconds; + + if (_maxCookieAge>0 && _refreshCookieAge==0) + _refreshCookieAge=_maxCookieAge/3; + + } + + /* ------------------------------------------------------------ */ + /** + * @param seconds + */ + public void setMaxInactiveInterval(int seconds) + { + _dftMaxIdleSecs=seconds; + } + + /* ------------------------------------------------------------ */ + /** + * @deprecated use {@link #setIdManager(SessionIdManager)} + */ + public void setMetaManager(SessionIdManager metaManager) + { + setIdManager(metaManager); + } + + /* ------------------------------------------------------------ */ + public void setRefreshCookieAge(int ageInSeconds) + { + _refreshCookieAge=ageInSeconds; + } + + + /* ------------------------------------------------------------ */ + /** + * @param secureCookies + * The secureCookies to set. + */ + public void setSecureCookies(boolean secureCookies) + { + _secureCookies=secureCookies; + } + + public void setSessionCookie(String cookieName) + { + _sessionCookie=cookieName; + } + + public void setSessionDomain(String domain) + { + _sessionDomain=domain; + } + + /* ------------------------------------------------------------ */ + /** + * @param sessionHandler + * The sessionHandler to set. + */ + public void setSessionHandler(SessionHandler sessionHandler) + { + _sessionHandler=sessionHandler; + } + + /* ------------------------------------------------------------ */ + public void setSessionPath(String path) + { + _sessionPath=path; + } + + /* ------------------------------------------------------------ */ + public void setSessionIdPathParameterName(String param) + { + _sessionIdPathParameterName =(param==null||"none".equals(param))?null:param; + _sessionIdPathParameterNamePrefix =(param==null||"none".equals(param))?null:(";"+ _sessionIdPathParameterName +"="); + } + /* ------------------------------------------------------------ */ + /** + * @param usingCookies + * The usingCookies to set. + */ + public void setUsingCookies(boolean usingCookies) + { + _usingCookies=usingCookies; + } + + + protected abstract void addSession(Session session); + + /* ------------------------------------------------------------ */ + /** + * Add the session Registers the session with this manager and registers the + * session ID with the sessionIDManager; + */ + protected void addSession(Session session, boolean created) + { + synchronized (_sessionIdManager) + { + _sessionIdManager.addSession(session); + synchronized (this) + { + addSession(session); + if (getSessions()>this._maxSessions) + this._maxSessions=getSessions(); + } + } + + if (!created) + { + session.didActivate(); + } + else if (_sessionListeners!=null) + { + HttpSessionEvent event=new HttpSessionEvent(session); + for (int i=0; i<LazyList.size(_sessionListeners); i++) + ((HttpSessionListener)LazyList.get(_sessionListeners,i)).sessionCreated(event); + } + } + + /* ------------------------------------------------------------ */ + /** + * Get a known existingsession + * @param idInCluster The session ID in the cluster, stripped of any worker name. + * @return A Session or null if none exists. + */ + public abstract Session getSession(String idInCluster); + + protected abstract void invalidateSessions(); + + + /* ------------------------------------------------------------ */ + /** + * Create a new session instance + * @param request + * @return + */ + protected abstract Session newSession(HttpServletRequest request); + + + + /* ------------------------------------------------------------ */ + /** + * @return true if the cluster node id (worker id) is returned as part of the session id by {@link HttpSession#getId()}. Default is false. + */ + public boolean isNodeIdInSessionId() + { + return _nodeIdInSessionId; + } + + /* ------------------------------------------------------------ */ + /** + * @param nodeIdInSessionId true if the cluster node id (worker id) will be returned as part of the session id by {@link HttpSession#getId()}. Default is false. + */ + public void setNodeIdInSessionId(boolean nodeIdInSessionId) + { + _nodeIdInSessionId=nodeIdInSessionId; + } + + /* ------------------------------------------------------------ */ + /** Remove session from manager + * @param session The session to remove + * @param invalidate True if {@link HttpSessionListener#sessionDestroyed(HttpSessionEvent)} and + * {@link SessionIdManager#invalidateAll(String)} should be called. + */ + public void removeSession(HttpSession session, boolean invalidate) + { + Session s = ((SessionIf)session).getSession(); + removeSession(s,invalidate); + } + + /* ------------------------------------------------------------ */ + /** Remove session from manager + * @param session The session to remove + * @param invalidate True if {@link HttpSessionListener#sessionDestroyed(HttpSessionEvent)} and + * {@link SessionIdManager#invalidateAll(String)} should be called. + */ + public void removeSession(Session session, boolean invalidate) + { + // Remove session from context and global maps + synchronized (_sessionIdManager) + { + boolean removed = false; + + synchronized (this) + { + //take this session out of the map of sessions for this context + if (getSession(session.getClusterId()) != null) + { + removed = true; + removeSession(session.getClusterId()); + } + } + + if (removed) + { + // Remove session from all context and global id maps + _sessionIdManager.removeSession(session); + if (invalidate) + _sessionIdManager.invalidateAll(session.getClusterId()); + } + } + + if (invalidate && _sessionListeners!=null) + { + HttpSessionEvent event=new HttpSessionEvent(session); + for (int i=LazyList.size(_sessionListeners); i-->0;) + ((HttpSessionListener)LazyList.get(_sessionListeners,i)).sessionDestroyed(event); + } + if (!invalidate) + { + session.willPassivate(); + } + } + + /* ------------------------------------------------------------ */ + protected abstract void removeSession(String idInCluster); + + /* ------------------------------------------------------------ */ + /** + * Null returning implementation of HttpSessionContext + * + * + */ + public static class NullSessionContext implements HttpSessionContext + { + /* ------------------------------------------------------------ */ + private NullSessionContext() + { + } + + /* ------------------------------------------------------------ */ + /** + * @deprecated From HttpSessionContext + */ + public Enumeration getIds() + { + return Collections.enumeration(Collections.EMPTY_LIST); + } + + /* ------------------------------------------------------------ */ + /** + * @deprecated From HttpSessionContext + */ + public HttpSession getSession(String id) + { + return null; + } + } + + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + /** + * Interface that any session wrapper should implement so that + * SessionManager may access the Jetty session implementation. + * + */ + public interface SessionIf extends HttpSession + { + public Session getSession(); + } + + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + /** + * + * <p> + * Implements {@link javax.servlet.HttpSession} from the {@link javax.servlet} package. + * </p> + * + * + */ + public abstract class Session implements SessionIf, Serializable + { + protected final String _clusterId; // ID unique within cluster + protected final String _nodeId; // ID unique within node + protected boolean _idChanged; + protected final long _created; + protected long _cookieSet; + protected long _accessed; + protected long _lastAccessed; + protected boolean _invalid; + protected boolean _doInvalidate; + protected long _maxIdleMs=_dftMaxIdleSecs*1000; + protected boolean _newSession; + protected Map _values; + protected int _requests; + + /* ------------------------------------------------------------- */ + protected Session(HttpServletRequest request) + { + _newSession=true; + _created=System.currentTimeMillis(); + _clusterId=_sessionIdManager.newSessionId(request,_created); + _nodeId=_sessionIdManager.getNodeId(_clusterId,request); + _accessed=_created; + _requests=1; + } + + /* ------------------------------------------------------------- */ + protected Session(long created, String clusterId) + { + _created=created; + _clusterId=clusterId; + _nodeId=_sessionIdManager.getNodeId(_clusterId,null); + _accessed=_created; + } + + /* ------------------------------------------------------------- */ + public Session getSession() + { + return this; + } + + /* ------------------------------------------------------------- */ + protected void initValues() + { + _values=newAttributeMap(); + } + + /* ------------------------------------------------------------ */ + public Object getAttribute(String name) + { + synchronized (this) + { + if (_invalid) + throw new IllegalStateException(); + + if (null == _values) + return null; + + return _values.get(name); + } + } + + /* ------------------------------------------------------------ */ + public Enumeration getAttributeNames() + { + synchronized (this) + { + if (_invalid) + throw new IllegalStateException(); + List names=_values==null?Collections.EMPTY_LIST:new ArrayList(_values.keySet()); + return Collections.enumeration(names); + } + } + + /* ------------------------------------------------------------- */ + public long getCookieSetTime() + { + return _cookieSet; + } + + /* ------------------------------------------------------------- */ + public long getCreationTime() throws IllegalStateException + { + if (_invalid) + throw new IllegalStateException(); + return _created; + } + + /* ------------------------------------------------------------ */ + public String getId() throws IllegalStateException + { + return _nodeIdInSessionId?_nodeId:_clusterId; + } + + /* ------------------------------------------------------------- */ + protected String getNodeId() + { + return _nodeId; + } + + /* ------------------------------------------------------------- */ + protected String getClusterId() + { + return _clusterId; + } + + /* ------------------------------------------------------------- */ + public long getLastAccessedTime() throws IllegalStateException + { + if (_invalid) + throw new IllegalStateException(); + return _lastAccessed; + } + + /* ------------------------------------------------------------- */ + public int getMaxInactiveInterval() + { + if (_invalid) + throw new IllegalStateException(); + return (int)(_maxIdleMs/1000); + } + + /* ------------------------------------------------------------ */ + /* + * @see javax.servlet.http.HttpSession#getServletContext() + */ + public ServletContext getServletContext() + { + return _context; + } + + /* ------------------------------------------------------------- */ + /** + * @deprecated + */ + public HttpSessionContext getSessionContext() throws IllegalStateException + { + if (_invalid) + throw new IllegalStateException(); + return __nullSessionContext; + } + + /* ------------------------------------------------------------- */ + /** + * @deprecated As of Version 2.2, this method is replaced by + * {@link #getAttribute} + */ + public Object getValue(String name) throws IllegalStateException + { + return getAttribute(name); + } + + /* ------------------------------------------------------------- */ + /** + * @deprecated As of Version 2.2, this method is replaced by + * {@link #getAttributeNames} + */ + public String[] getValueNames() throws IllegalStateException + { + synchronized(this) + { + if (_invalid) + throw new IllegalStateException(); + if (_values==null) + return new String[0]; + String[] a=new String[_values.size()]; + return (String[])_values.keySet().toArray(a); + } + } + + /* ------------------------------------------------------------ */ + protected void access(long time) + { + synchronized(this) + { + _newSession=false; + _lastAccessed=_accessed; + _accessed=time; + _requests++; + } + } + + /* ------------------------------------------------------------ */ + protected void complete() + { + synchronized(this) + { + _requests--; + if (_doInvalidate && _requests<=0 ) + doInvalidate(); + } + } + + + /* ------------------------------------------------------------- */ + protected void timeout() throws IllegalStateException + { + // remove session from context and invalidate other sessions with same ID. + removeSession(this,true); + + // Notify listeners and unbind values + synchronized (this) + { + if (_requests<=0) + doInvalidate(); + else + _doInvalidate=true; + } + } + + /* ------------------------------------------------------------- */ + public void invalidate() throws IllegalStateException + { + // remove session from context and invalidate other sessions with same ID. + removeSession(this,true); + doInvalidate(); + } + + /* ------------------------------------------------------------- */ + protected void doInvalidate() throws IllegalStateException + { + try + { + // Notify listeners and unbind values + if (_invalid) + throw new IllegalStateException(); + + while (_values!=null && _values.size()>0) + { + ArrayList keys; + synchronized (this) + { + keys=new ArrayList(_values.keySet()); + } + + Iterator iter=keys.iterator(); + while (iter.hasNext()) + { + String key=(String)iter.next(); + + Object value; + synchronized (this) + { + value=_values.remove(key); + } + unbindValue(key,value); + + if (_sessionAttributeListeners!=null) + { + HttpSessionBindingEvent event=new HttpSessionBindingEvent(this,key,value); + + for (int i=0; i<LazyList.size(_sessionAttributeListeners); i++) + ((HttpSessionAttributeListener)LazyList.get(_sessionAttributeListeners,i)).attributeRemoved(event); + } + } + } + } + finally + { + // mark as invalid + _invalid=true; + } + } + + /* ------------------------------------------------------------- */ + public boolean isIdChanged() + { + return _idChanged; + } + + /* ------------------------------------------------------------- */ + public boolean isNew() throws IllegalStateException + { + if (_invalid) + throw new IllegalStateException(); + return _newSession; + } + + /* ------------------------------------------------------------- */ + /** + * @deprecated As of Version 2.2, this method is replaced by + * {@link #setAttribute} + */ + public void putValue(java.lang.String name, java.lang.Object value) throws IllegalStateException + { + setAttribute(name,value); + } + + /* ------------------------------------------------------------ */ + public void removeAttribute(String name) + { + Object old; + synchronized(this) + { + if (_invalid) + throw new IllegalStateException(); + if (_values==null) + return; + + old=_values.remove(name); + } + + if (old!=null) + { + unbindValue(name,old); + if (_sessionAttributeListeners!=null) + { + HttpSessionBindingEvent event=new HttpSessionBindingEvent(this,name,old); + + for (int i=0; i<LazyList.size(_sessionAttributeListeners); i++) + ((HttpSessionAttributeListener)LazyList.get(_sessionAttributeListeners,i)).attributeRemoved(event); + } + } + + } + + /* ------------------------------------------------------------- */ + /** + * @deprecated As of Version 2.2, this method is replaced by + * {@link #removeAttribute} + */ + public void removeValue(java.lang.String name) throws IllegalStateException + { + removeAttribute(name); + } + + /* ------------------------------------------------------------ */ + public void setAttribute(String name, Object value) + { + Object old_value; + if (value==null) + { + removeAttribute(name); + return; + } + + synchronized(this) + { + if (_invalid) + throw new IllegalStateException(); + if (_values==null) + _values=newAttributeMap(); + old_value=_values.put(name,value); + } + + if (old_value==null || !value.equals(old_value)) + { + unbindValue(name,old_value); + bindValue(name,value); + + if (_sessionAttributeListeners!=null) + { + HttpSessionBindingEvent event=new HttpSessionBindingEvent(this,name,old_value==null?value:old_value); + + for (int i=0; i<LazyList.size(_sessionAttributeListeners); i++) + { + HttpSessionAttributeListener l=(HttpSessionAttributeListener)LazyList.get(_sessionAttributeListeners,i); + + if (old_value==null) + l.attributeAdded(event); + else if (value==null) + l.attributeRemoved(event); + else + l.attributeReplaced(event); + } + } + } + } + + /* ------------------------------------------------------------- */ + public void setIdChanged(boolean changed) + { + _idChanged=changed; + } + + /* ------------------------------------------------------------- */ + public void setMaxInactiveInterval(int secs) + { + _maxIdleMs=(long)secs*1000; + } + + /* ------------------------------------------------------------- */ + public String toString() + { + return this.getClass().getName()+":"+getId()+"@"+hashCode(); + } + + /* ------------------------------------------------------------- */ + /** If value implements HttpSessionBindingListener, call valueBound() */ + protected void bindValue(java.lang.String name, Object value) + { + if (value!=null&&value instanceof HttpSessionBindingListener) + ((HttpSessionBindingListener)value).valueBound(new HttpSessionBindingEvent(this,name)); + } + + /* ------------------------------------------------------------ */ + protected boolean isValid() + { + return !_invalid; + } + + /* ------------------------------------------------------------ */ + protected abstract Map newAttributeMap(); + + /* ------------------------------------------------------------- */ + protected void cookieSet() + { + _cookieSet=_accessed; + } + + /* ------------------------------------------------------------- */ + /** If value implements HttpSessionBindingListener, call valueUnbound() */ + protected void unbindValue(java.lang.String name, Object value) + { + if (value!=null&&value instanceof HttpSessionBindingListener) + ((HttpSessionBindingListener)value).valueUnbound(new HttpSessionBindingEvent(this,name)); + } + + /* ------------------------------------------------------------- */ + protected void willPassivate() + { + synchronized(this) + { + HttpSessionEvent event = new HttpSessionEvent(this); + for (Iterator iter = _values.values().iterator(); iter.hasNext();) + { + Object value = iter.next(); + if (value instanceof HttpSessionActivationListener) + { + HttpSessionActivationListener listener = (HttpSessionActivationListener) value; + listener.sessionWillPassivate(event); + } + } + } + } + + /* ------------------------------------------------------------- */ + protected void didActivate() + { + synchronized(this) + { + HttpSessionEvent event = new HttpSessionEvent(this); + for (Iterator iter = _values.values().iterator(); iter.hasNext();) + { + Object value = iter.next(); + if (value instanceof HttpSessionActivationListener) + { + HttpSessionActivationListener listener = (HttpSessionActivationListener) value; + listener.sessionDidActivate(event); + } + } + } + } + } +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/HashSessionIdManager.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/HashSessionIdManager.java new file mode 100644 index 0000000000..ba72362d3d --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/HashSessionIdManager.java @@ -0,0 +1,256 @@ +// ======================================================================== +// Copyright (c) 2006-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server.session; + +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Random; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; + +import org.eclipse.jetty.server.SessionIdManager; +import org.eclipse.jetty.server.session.AbstractSessionManager.Session; +import org.eclipse.jetty.util.MultiMap; +import org.eclipse.jetty.util.component.AbstractLifeCycle; +import org.eclipse.jetty.util.log.Log; + +/* ------------------------------------------------------------ */ +/** + * HashSessionIdManager. An in-memory implementation of the session ID manager. + */ +public class HashSessionIdManager extends AbstractLifeCycle implements SessionIdManager +{ + private final static String __NEW_SESSION_ID="org.eclipse.jetty.server.newSessionId"; + protected final static String SESSION_ID_RANDOM_ALGORITHM = "SHA1PRNG"; + protected final static String SESSION_ID_RANDOM_ALGORITHM_ALT = "IBMSecureRandom"; + + MultiMap<String> _sessions; + protected Random _random; + private boolean _weakRandom; + private String _workerName; + + /* ------------------------------------------------------------ */ + public HashSessionIdManager() + { + } + + /* ------------------------------------------------------------ */ + public HashSessionIdManager(Random random) + { + _random=random; + + } + + /* ------------------------------------------------------------ */ + /** + * Get the workname. If set, the workername is dot appended to the session + * ID and can be used to assist session affinity in a load balancer. + * + * @return String or null + */ + public String getWorkerName() + { + return _workerName; + } + + /* ------------------------------------------------------------ */ + /** + * Set the workname. If set, the workername is dot appended to the session + * ID and can be used to assist session affinity in a load balancer. + * + * @param workerName + */ + public void setWorkerName(String workerName) + { + _workerName=workerName; + } + + /* ------------------------------------------------------------ */ + /** Get the session ID with any worker ID. + * + * @param request + * @return sessionId plus any worker ID. + */ + public String getNodeId(String clusterId,HttpServletRequest request) + { + String worker=request==null?null:(String)request.getAttribute("org.eclipse.http.ajp.JVMRoute"); + if (worker!=null) + return clusterId+'.'+worker; + + if (_workerName!=null) + return clusterId+'.'+_workerName; + + return clusterId; + } + + /* ------------------------------------------------------------ */ + /** Get the session ID without any worker ID. + * + * @param request + * @return sessionId without any worker ID. + */ + public String getClusterId(String nodeId) + { + int dot=nodeId.lastIndexOf('.'); + return (dot>0)?nodeId.substring(0,dot):nodeId; + } + + /* ------------------------------------------------------------ */ + protected void doStart() + { + if (_random==null) + { + try + { + //This operation may block on some systems with low entropy. See this page + //for workaround suggestions: + //http://docs.codehaus.org/display/JETTY/Connectors+slow+to+startup + Log.debug("Init SecureRandom."); + _random=SecureRandom.getInstance(SESSION_ID_RANDOM_ALGORITHM); + } + catch (NoSuchAlgorithmException e) + { + try + { + _random=SecureRandom.getInstance(SESSION_ID_RANDOM_ALGORITHM_ALT); + _weakRandom=false; + } + catch (NoSuchAlgorithmException e_alt) + { + Log.warn("Could not generate SecureRandom for session-id randomness",e); + _random=new Random(); + _weakRandom=true; + } + } + } + _random.setSeed(_random.nextLong()^System.currentTimeMillis()^hashCode()^Runtime.getRuntime().freeMemory()); + _sessions=new MultiMap<String>(true); + } + + /* ------------------------------------------------------------ */ + protected void doStop() + { + if (_sessions!=null) + _sessions.clear(); // Maybe invalidate? + _sessions=null; + } + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.jetty.server.server.SessionManager.MetaManager#idInUse(java.lang.String) + */ + public boolean idInUse(String id) + { + return _sessions.containsKey(id); + } + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.jetty.server.server.SessionManager.MetaManager#addSession(javax.servlet.http.HttpSession) + */ + public void addSession(HttpSession session) + { + _sessions.add(getClusterId(session.getId()),session); + } + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.jetty.server.server.SessionManager.MetaManager#addSession(javax.servlet.http.HttpSession) + */ + public void removeSession(HttpSession session) + { + _sessions.removeValue(getClusterId(session.getId()),session); + } + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.jetty.server.server.SessionManager.MetaManager#invalidateAll(java.lang.String) + */ + public void invalidateAll(String id) + { + // Do not use interators as this method tends to be called recursively + // by the invalidate calls. + while (_sessions.containsKey(id)) + { + Session session=(Session)_sessions.getValue(id,0); + if (session.isValid()) + session.invalidate(); + else + _sessions.removeValue(id,session); + } + } + + /* ------------------------------------------------------------ */ + /* + * new Session ID. If the request has a requestedSessionID which is unique, + * that is used. The session ID is created as a unique random long XORed with + * connection specific information, base 36. + * @param request + * @param created + * @return Session ID. + */ + public String newSessionId(HttpServletRequest request, long created) + { + synchronized (this) + { + // A requested session ID can only be used if it is in use already. + String requested_id=request.getRequestedSessionId(); + + if (requested_id!=null) + { + String cluster_id=getClusterId(requested_id); + if (idInUse(cluster_id)) + return cluster_id; + } + + // Else reuse any new session ID already defined for this request. + String new_id=(String)request.getAttribute(__NEW_SESSION_ID); + if (new_id!=null&&idInUse(new_id)) + return new_id; + + // pick a new unique ID! + String id=null; + while (id==null||id.length()==0||idInUse(id)) + { + long r=_weakRandom + ?(hashCode()^Runtime.getRuntime().freeMemory()^_random.nextInt()^(((long)request.hashCode())<<32)) + :_random.nextLong(); + r^=created; + if (request!=null && request.getRemoteAddr()!=null) + r^=request.getRemoteAddr().hashCode(); + if (r<0) + r=-r; + id=Long.toString(r,36); + } + + request.setAttribute(__NEW_SESSION_ID,id); + return id; + } + } + + /* ------------------------------------------------------------ */ + public Random getRandom() + { + return _random; + } + + /* ------------------------------------------------------------ */ + public void setRandom(Random random) + { + _random=random; + _weakRandom=false; + } + +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/HashSessionManager.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/HashSessionManager.java new file mode 100644 index 0000000000..a7b7c97727 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/HashSessionManager.java @@ -0,0 +1,652 @@ +// ======================================================================== +// Copyright (c) 1996-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server.session; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.ConcurrentHashMap; + +import javax.servlet.http.HttpServletRequest; + +import org.eclipse.jetty.util.LazyList; +import org.eclipse.jetty.util.log.Log; + + +/* ------------------------------------------------------------ */ +/** An in-memory implementation of SessionManager. + * + * + */ +public class HashSessionManager extends AbstractSessionManager +{ + private static int __id; + private Timer _timer; + private TimerTask _task; + private int _scavengePeriodMs=30000; + private int _savePeriodMs=0; //don't do period saves by default + private TimerTask _saveTask; + protected Map _sessions; + private File _storeDir; + private boolean _lazyLoad=false; + private boolean _sessionsLoaded=false; + + /* ------------------------------------------------------------ */ + public HashSessionManager() + { + super(); + } + + /* ------------------------------------------------------------ */ + /* (non-Javadoc) + * @see org.eclipse.jetty.servlet.AbstractSessionManager#doStart() + */ + public void doStart() throws Exception + { + _sessions=new ConcurrentHashMap(); // TODO: use syncronizedMap for JDK 1.4 + super.doStart(); + + _timer=new Timer("HashSessionScavenger-"+__id++, true); + + setScavengePeriod(getScavengePeriod()); + + if (_storeDir!=null) + { + if (!_storeDir.exists()) + _storeDir.mkdir(); + + if (!_lazyLoad) + restoreSessions(); + } + + setSavePeriod(getSavePeriod()); + } + + /* ------------------------------------------------------------ */ + /* (non-Javadoc) + * @see org.eclipse.jetty.servlet.AbstractSessionManager#doStop() + */ + public void doStop() throws Exception + { + + if (_storeDir != null) + saveSessions(); + + super.doStop(); + + _sessions.clear(); + _sessions=null; + + // stop the scavenger + synchronized(this) + { + if (_saveTask!=null) + _saveTask.cancel(); + if (_task!=null) + _task.cancel(); + if (_timer!=null) + _timer.cancel(); + _timer=null; + } + } + + /* ------------------------------------------------------------ */ + /** + * @return seconds + */ + public int getScavengePeriod() + { + return _scavengePeriodMs/1000; + } + + + /* ------------------------------------------------------------ */ + public Map getSessionMap() + { + return Collections.unmodifiableMap(_sessions); + } + + + /* ------------------------------------------------------------ */ + public int getSessions() + { + return _sessions.size(); + } + + + /* ------------------------------------------------------------ */ + public void setMaxInactiveInterval(int seconds) + { + super.setMaxInactiveInterval(seconds); + if (_dftMaxIdleSecs>0&&_scavengePeriodMs>_dftMaxIdleSecs*1000) + setScavengePeriod((_dftMaxIdleSecs+9)/10); + } + + /* ------------------------------------------------------------ */ + public void setSavePeriod (int seconds) + { + int oldSavePeriod = _savePeriodMs; + int period = (seconds * 1000); + if (period < 0) + period=0; + _savePeriodMs=period; + + if (_timer!=null) + { + synchronized (this) + { + if (_saveTask!=null) + _saveTask.cancel(); + if (_savePeriodMs > 0 && _storeDir!=null) //only save if we have a directory configured + { + _saveTask = new TimerTask() + { + public void run() + { + try + { + saveSessions(); + } + catch (Exception e) + { + Log.warn(e); + } + } + }; + _timer.schedule(_saveTask,_savePeriodMs,_savePeriodMs); + } + } + } + } + + /* ------------------------------------------------------------ */ + public int getSavePeriod () + { + if (_savePeriodMs<=0) + return 0; + + return _savePeriodMs/1000; + } + + /* ------------------------------------------------------------ */ + /** + * @param seconds + */ + public void setScavengePeriod(int seconds) + { + if (seconds==0) + seconds=60; + + int old_period=_scavengePeriodMs; + int period=seconds*1000; + if (period>60000) + period=60000; + if (period<1000) + period=1000; + + _scavengePeriodMs=period; + if (_timer!=null && (period!=old_period || _task==null)) + { + synchronized (this) + { + if (_task!=null) + _task.cancel(); + _task = new TimerTask() + { + public void run() + { + scavenge(); + } + }; + _timer.schedule(_task,_scavengePeriodMs,_scavengePeriodMs); + } + } + } + + /* -------------------------------------------------------------- */ + /** + * Find sessions that have timed out and invalidate them. This runs in the + * SessionScavenger thread. + */ + private void scavenge() + { + //don't attempt to scavenge if we are shutting down + if (isStopping() || isStopped()) + return; + + Thread thread=Thread.currentThread(); + ClassLoader old_loader=thread.getContextClassLoader(); + try + { + if (_loader!=null) + thread.setContextClassLoader(_loader); + + long now=System.currentTimeMillis(); + + try + { + if (!_sessionsLoaded && _lazyLoad) + restoreSessions(); + } + catch(Exception e) + { + Log.debug(e); + } + + // Since Hashtable enumeration is not safe over deletes, + // we build a list of stale sessions, then go back and invalidate + // them + Object stale=null; + + synchronized (HashSessionManager.this) + { + // For each session + for (Iterator i=_sessions.values().iterator(); i.hasNext();) + { + Session session=(Session)i.next(); + long idleTime=session._maxIdleMs; + if (idleTime>0&&session._accessed+idleTime<now) + { + // Found a stale session, add it to the list + stale=LazyList.add(stale,session); + } + } + } + + // Remove the stale sessions + for (int i=LazyList.size(stale); i-->0;) + { + // check it has not been accessed in the meantime + Session session=(Session)LazyList.get(stale,i); + long idleTime=session._maxIdleMs; + if (idleTime>0&&session._accessed+idleTime<System.currentTimeMillis()) + { + ((Session)session).timeout(); + int nbsess=this._sessions.size(); + if (nbsess<this._minSessions) + this._minSessions=nbsess; + } + } + } + catch (Throwable t) + { + if (t instanceof ThreadDeath) + throw ((ThreadDeath)t); + else + Log.warn("Problem scavenging sessions", t); + } + finally + { + thread.setContextClassLoader(old_loader); + } + } + + /* ------------------------------------------------------------ */ + protected void addSession(AbstractSessionManager.Session session) + { + _sessions.put(session.getClusterId(),session); + } + + /* ------------------------------------------------------------ */ + public AbstractSessionManager.Session getSession(String idInCluster) + { + try + { + if (!_sessionsLoaded && _lazyLoad) + restoreSessions(); + } + catch(Exception e) + { + Log.warn(e); + } + + if (_sessions==null) + return null; + + return (Session)_sessions.get(idInCluster); + } + + /* ------------------------------------------------------------ */ + protected void invalidateSessions() + { + // Invalidate all sessions to cause unbind events + ArrayList sessions=new ArrayList(_sessions.values()); + for (Iterator i=sessions.iterator(); i.hasNext();) + { + Session session=(Session)i.next(); + session.invalidate(); + } + _sessions.clear(); + + } + + /* ------------------------------------------------------------ */ + protected AbstractSessionManager.Session newSession(HttpServletRequest request) + { + return new Session(request); + } + + /* ------------------------------------------------------------ */ + protected AbstractSessionManager.Session newSession(long created, String clusterId) + { + return new Session(created,clusterId); + } + + /* ------------------------------------------------------------ */ + protected void removeSession(String clusterId) + { + _sessions.remove(clusterId); + } + + + /* ------------------------------------------------------------ */ + public void setStoreDirectory (File dir) + { + _storeDir=dir; + } + + /* ------------------------------------------------------------ */ + public File getStoreDirectory () + { + return _storeDir; + } + + /* ------------------------------------------------------------ */ + public void setLazyLoad(boolean lazyLoad) + { + _lazyLoad = lazyLoad; + } + + public boolean isLazyLoad() + { + return _lazyLoad; + } + + public void restoreSessions () throws Exception + { + if (_storeDir==null || !_storeDir.exists()) + { + return; + } + + if (!_storeDir.canRead()) + { + Log.warn ("Unable to restore Sessions: Cannot read from Session storage directory "+_storeDir.getAbsolutePath()); + return; + } + + File[] files = _storeDir.listFiles(); + for (int i=0;files!=null&&i<files.length;i++) + { + try + { + FileInputStream in = new FileInputStream(files[i]); + Session session = restoreSession(in); + in.close(); + addSession(session, false); + files[i].delete(); + } + catch (Exception e) + { + Log.warn("Problem restoring session "+files[i].getName(), e); + } + } + + _sessionsLoaded = true; + } + + /* ------------------------------------------------------------ */ + public void saveSessions () throws Exception + { + if (_storeDir==null || !_storeDir.exists()) + { + return; + } + + if (!_storeDir.canWrite()) + { + Log.warn ("Unable to save Sessions: Session persistence storage directory "+_storeDir.getAbsolutePath()+ " is not writeable"); + return; + } + + synchronized (this) + { + Iterator itor = _sessions.entrySet().iterator(); + while (itor.hasNext()) + { + Map.Entry entry = (Map.Entry)itor.next(); + String id = (String)entry.getKey(); + Session session = (Session)entry.getValue(); + try + { + File file = new File (_storeDir, id); + if (file.exists()) + file.delete(); + file.createNewFile(); + FileOutputStream fos = new FileOutputStream (file); + session.save(fos); + fos.close(); + } + catch (Exception e) + { + Log.warn("Problem persisting session "+id, e); + } + } + } + } + + /* ------------------------------------------------------------ */ + public Session restoreSession (InputStream is) throws Exception + { + /* + * Take care of this class's fields first by calling + * defaultReadObject + */ + DataInputStream in = new DataInputStream(is); + String clusterId = in.readUTF(); + String nodeId = in.readUTF(); + boolean idChanged = in.readBoolean(); + long created = in.readLong(); + long cookieSet = in.readLong(); + long accessed = in.readLong(); + long lastAccessed = in.readLong(); + //boolean invalid = in.readBoolean(); + //boolean invalidate = in.readBoolean(); + //long maxIdle = in.readLong(); + //boolean isNew = in.readBoolean(); + int requests = in.readInt(); + + Session session = (Session)newSession(created, clusterId); + session._cookieSet = cookieSet; + session._lastAccessed = lastAccessed; + + int size = in.readInt(); + if (size > 0) + { + ArrayList keys = new ArrayList(); + for (int i=0; i<size; i++) + { + String key = in.readUTF(); + keys.add(key); + } + ClassLoadingObjectInputStream ois = new ClassLoadingObjectInputStream(in); + for (int i=0;i<size;i++) + { + Object value = ois.readObject(); + session.setAttribute((String)keys.get(i),value); + } + ois.close(); + } + else + session.initValues(); + in.close(); + return session; + } + + + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + protected class Session extends AbstractSessionManager.Session + { + /* ------------------------------------------------------------ */ + private static final long serialVersionUID=-2134521374206116367L; + + /* ------------------------------------------------------------- */ + protected Session(HttpServletRequest request) + { + super(request); + } + + /* ------------------------------------------------------------- */ + protected Session(long created, String clusterId) + { + super(created, clusterId); + } + + /* ------------------------------------------------------------- */ + public void setMaxInactiveInterval(int secs) + { + super.setMaxInactiveInterval(secs); + if (_maxIdleMs>0&&(_maxIdleMs/10)<_scavengePeriodMs) + HashSessionManager.this.setScavengePeriod((secs+9)/10); + } + + /* ------------------------------------------------------------ */ + protected Map newAttributeMap() + { + return new HashMap(3); + } + + + /* ------------------------------------------------------------ */ + public void invalidate () + throws IllegalStateException + { + super.invalidate(); + + remove(); + } + + /* ------------------------------------------------------------ */ + public void remove() + { + String id=getId(); + if (id==null) + return; + + //all sessions are invalidated when jetty is stopped, make sure we don't + //remove all the sessions in this case + if (isStopping() || isStopped()) + return; + + if (_storeDir==null || !_storeDir.exists()) + { + return; + } + + File f = new File(_storeDir, id); + f.delete(); + } + + /* ------------------------------------------------------------ */ + public void save(OutputStream os) throws IOException + { + DataOutputStream out = new DataOutputStream(os); + out.writeUTF(_clusterId); + out.writeUTF(_nodeId); + out.writeBoolean(_idChanged); + out.writeLong( _created); + out.writeLong(_cookieSet); + out.writeLong(_accessed); + out.writeLong(_lastAccessed); + /* Don't write these out, as they don't make sense to store because they + * either they cannot be true or their value will be restored in the + * Session constructor. + */ + //out.writeBoolean(_invalid); + //out.writeBoolean(_doInvalidate); + //out.writeLong(_maxIdleMs); + //out.writeBoolean( _newSession); + out.writeInt(_requests); + if (_values != null) + { + out.writeInt(_values.size()); + Iterator itor = _values.keySet().iterator(); + while (itor.hasNext()) + { + String key = (String)itor.next(); + out.writeUTF(key); + } + itor = _values.values().iterator(); + ObjectOutputStream oos = new ObjectOutputStream(out); + while (itor.hasNext()) + { + oos.writeObject(itor.next()); + } + oos.close(); + } + else + out.writeInt(0); + out.close(); + } + + } + + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + protected class ClassLoadingObjectInputStream extends ObjectInputStream + { + /* ------------------------------------------------------------ */ + public ClassLoadingObjectInputStream(java.io.InputStream in) throws IOException + { + super(in); + } + + /* ------------------------------------------------------------ */ + public ClassLoadingObjectInputStream () throws IOException + { + super(); + } + + /* ------------------------------------------------------------ */ + public Class resolveClass (java.io.ObjectStreamClass cl) throws IOException, ClassNotFoundException + { + try + { + return Class.forName(cl.getName(), false, Thread.currentThread().getContextClassLoader()); + } + catch (ClassNotFoundException e) + { + return super.resolveClass(cl); + } + } + } + + +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionIdManager.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionIdManager.java new file mode 100644 index 0000000000..0e386e1f28 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionIdManager.java @@ -0,0 +1,695 @@ +// ======================================================================== +// Copyright (c) 2008-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server.session; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.sql.Blob; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Random; +import java.util.Timer; +import java.util.TimerTask; + +import javax.naming.InitialContext; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; +import javax.sql.DataSource; + +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.SessionManager; +import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.util.log.Log; + + + +/** + * JDBCSessionIdManager + * + * SessionIdManager implementation that uses a database to store in-use session ids, + * to support distributed sessions. + * + */ +public class JDBCSessionIdManager extends AbstractSessionIdManager +{ + protected HashSet<String> _sessionIds = new HashSet(); + protected String _driverClassName; + protected String _connectionUrl; + protected DataSource _datasource; + protected String _jndiName; + protected String _sessionIdTable = "JettySessionIds"; + protected String _sessionTable = "JettySessions"; + protected Timer _timer; //scavenge timer + protected TimerTask _task; //scavenge task + protected long _lastScavengeTime; + protected long _scavengeIntervalMs = 1000 * 60 * 10; //10mins + + + protected String _createSessionIdTable; + protected String _createSessionTable; + + protected String _selectExpiredSessions; + protected String _deleteOldExpiredSessions; + + protected String _insertId; + protected String _deleteId; + protected String _queryId; + + protected DatabaseAdaptor _dbAdaptor; + + + /** + * DatabaseAdaptor + * + * Handles differences between databases. + * + * Postgres uses the getBytes and setBinaryStream methods to access + * a "bytea" datatype, which can be up to 1Gb of binary data. MySQL + * is happy to use the "blob" type and getBlob() methods instead. + * + * TODO if the differences become more major it would be worthwhile + * refactoring this class. + */ + public class DatabaseAdaptor + { + String _dbName; + boolean _isLower; + boolean _isUpper; + + + public DatabaseAdaptor (DatabaseMetaData dbMeta) + throws SQLException + { + _dbName = dbMeta.getDatabaseProductName().toLowerCase(); + Log.debug ("Using database "+_dbName); + _isLower = dbMeta.storesLowerCaseIdentifiers(); + _isUpper = dbMeta.storesUpperCaseIdentifiers(); + } + + /** + * Convert a camel case identifier into either upper or lower + * depending on the way the db stores identifiers. + * + * @param identifier + * @return + */ + public String convertIdentifier (String identifier) + { + if (_isLower) + return identifier.toLowerCase(); + if (_isUpper) + return identifier.toUpperCase(); + + return identifier; + } + + public String getBlobType () + { + if (_dbName.startsWith("postgres")) + return "bytea"; + + return "blob"; + } + + public InputStream getBlobInputStream (ResultSet result, String columnName) + throws SQLException + { + if (_dbName.startsWith("postgres")) + { + byte[] bytes = result.getBytes(columnName); + return new ByteArrayInputStream(bytes); + } + + Blob blob = result.getBlob(columnName); + return blob.getBinaryStream(); + } + } + + + + public JDBCSessionIdManager(Server server) + { + super(server); + } + + public JDBCSessionIdManager(Server server, Random random) + { + super(server, random); + } + + /** + * Configure jdbc connection information via a jdbc Driver + * + * @param driverClassName + * @param connectionUrl + */ + public void setDriverInfo (String driverClassName, String connectionUrl) + { + _driverClassName=driverClassName; + _connectionUrl=connectionUrl; + } + + public String getDriverClassName() + { + return _driverClassName; + } + + public String getConnectionUrl () + { + return _connectionUrl; + } + + public void setDatasourceName (String jndi) + { + _jndiName=jndi; + } + + public String getDatasourceName () + { + return _jndiName; + } + + + public void setScavengeInterval (long sec) + { + if (sec<=0) + sec=60; + + long old_period=_scavengeIntervalMs; + long period=sec*1000; + + _scavengeIntervalMs=period; + + //add a bit of variability into the scavenge time so that not all + //nodes with the same scavenge time sync up + long tenPercent = _scavengeIntervalMs/10; + if ((System.currentTimeMillis()%2) == 0) + _scavengeIntervalMs += tenPercent; + + if (Log.isDebugEnabled()) Log.debug("Scavenging every "+_scavengeIntervalMs+" ms"); + if (_timer!=null && (period!=old_period || _task==null)) + { + synchronized (this) + { + if (_task!=null) + _task.cancel(); + _task = new TimerTask() + { + public void run() + { + scavenge(); + } + }; + _timer.schedule(_task,_scavengeIntervalMs,_scavengeIntervalMs); + } + } + } + + public long getScavengeInterval () + { + return _scavengeIntervalMs/1000; + } + + + public void addSession(HttpSession session) + { + if (session == null) + return; + + synchronized (_sessionIds) + { + String id = ((JDBCSessionManager.Session)session).getClusterId(); + try + { + insert(id); + _sessionIds.add(id); + } + catch (Exception e) + { + Log.warn("Problem storing session id="+id, e); + } + } + } + + public void removeSession(HttpSession session) + { + if (session == null) + return; + + removeSession(((JDBCSessionManager.Session)session).getClusterId()); + } + + + + public void removeSession (String id) + { + + if (id == null) + return; + + synchronized (_sessionIds) + { + if (Log.isDebugEnabled()) + Log.debug("Removing session id="+id); + try + { + _sessionIds.remove(id); + delete(id); + } + catch (Exception e) + { + Log.warn("Problem removing session id="+id, e); + } + } + + } + + + /** + * Get the session id without any node identifier suffix. + * + * @see org.eclipse.jetty.server.SessionIdManager#getClusterId(java.lang.String) + */ + public String getClusterId(String nodeId) + { + int dot=nodeId.lastIndexOf('.'); + return (dot>0)?nodeId.substring(0,dot):nodeId; + } + + + /** + * Get the session id, including this node's id as a suffix. + * + * @see org.eclipse.jetty.server.SessionIdManager#getNodeId(java.lang.String, javax.servlet.http.HttpServletRequest) + */ + public String getNodeId(String clusterId, HttpServletRequest request) + { + if (_workerName!=null) + return clusterId+'.'+_workerName; + + return clusterId; + } + + + public boolean idInUse(String id) + { + if (id == null) + return false; + + String clusterId = getClusterId(id); + + synchronized (_sessionIds) + { + if (_sessionIds.contains(clusterId)) + return true; //optimisation - if this session is one we've been managing, we can check locally + + //otherwise, we need to go to the database to check + try + { + return exists(clusterId); + } + catch (Exception e) + { + Log.warn("Problem checking inUse for id="+clusterId, e); + return false; + } + } + } + + /** + * Invalidate the session matching the id on all contexts. + * + * @see org.eclipse.jetty.server.SessionIdManager#invalidateAll(java.lang.String) + */ + public void invalidateAll(String id) + { + //take the id out of the list of known sessionids for this node + removeSession(id); + + synchronized (_sessionIds) + { + //tell all contexts that may have a session object with this id to + //get rid of them + Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class); + for (int i=0; contexts!=null && i<contexts.length; i++) + { + SessionManager manager = (SessionManager) + ((SessionHandler)((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class)).getSessionManager(); + + if (manager instanceof JDBCSessionManager) + { + ((JDBCSessionManager)manager).invalidateSession(id); + } + } + } + } + + + /** + * Start up the id manager. + * + * Makes necessary database tables and starts a Session + * scavenger thread. + * + * @see org.eclipse.jetty.server.session.AbstractSessionIdManager#doStart() + */ + public void doStart() + { + try + { + initializeDatabase(); + prepareTables(); + super.doStart(); + if (Log.isDebugEnabled()) Log.debug("Scavenging interval = "+getScavengeInterval()+" sec"); + _timer=new Timer("JDBCSessionScavenger", true); + setScavengeInterval(getScavengeInterval()); + } + catch (Exception e) + { + Log.warn("Problem initialising JettySessionIds table", e); + } + } + + /** + * Stop the scavenger. + * + * @see org.eclipse.jetty.util.component.AbstractLifeCycle#doStop() + */ + public void doStop () + throws Exception + { + synchronized(this) + { + if (_task!=null) + _task.cancel(); + if (_timer!=null) + _timer.cancel(); + _timer=null; + } + super.doStop(); + } + + /** + * Get a connection from the driver or datasource. + * + * @return + * @throws SQLException + */ + protected Connection getConnection () + throws SQLException + { + if (_datasource != null) + return _datasource.getConnection(); + else + return DriverManager.getConnection(_connectionUrl); + } + + + private void initializeDatabase () + throws Exception + { + if (_jndiName!=null) + { + InitialContext ic = new InitialContext(); + _datasource = (DataSource)ic.lookup(_jndiName); + } + else if (_driverClassName!=null && _connectionUrl!=null) + { + Class.forName(_driverClassName); + } + else + throw new IllegalStateException("No database configured for sessions"); + } + + + + /** + * Set up the tables in the database + * @throws SQLException + */ + private void prepareTables() + throws SQLException + { + _createSessionIdTable = "create table "+_sessionIdTable+" (id varchar(60), primary key(id))"; + _selectExpiredSessions = "select * from "+_sessionTable+" where expiryTime >= ? and expiryTime <= ?"; + _deleteOldExpiredSessions = "delete from "+_sessionTable+" where expiryTime >0 and expiryTime <= ?"; + + _insertId = "insert into "+_sessionIdTable+" (id) values (?)"; + _deleteId = "delete from "+_sessionIdTable+" where id = ?"; + _queryId = "select * from "+_sessionIdTable+" where id = ?"; + + Connection connection = null; + try + { + //make the id table + connection = getConnection(); + connection.setAutoCommit(true); + DatabaseMetaData metaData = connection.getMetaData(); + _dbAdaptor = new DatabaseAdaptor(metaData); + + //checking for table existence is case-sensitive, but table creation is not + String tableName = _dbAdaptor.convertIdentifier(_sessionIdTable); + ResultSet result = metaData.getTables(null, null, tableName, null); + if (!result.next()) + { + //table does not exist, so create it + connection.createStatement().executeUpdate(_createSessionIdTable); + } + + //make the session table if necessary + tableName = _dbAdaptor.convertIdentifier(_sessionTable); + result = metaData.getTables(null, null, tableName, null); + if (!result.next()) + { + //table does not exist, so create it + String blobType = _dbAdaptor.getBlobType(); + _createSessionTable = "create table "+_sessionTable+" (rowId varchar(60), sessionId varchar(60), "+ + " contextPath varchar(60), virtualHost varchar(60), lastNode varchar(60), accessTime bigint, "+ + " lastAccessTime bigint, createTime bigint, cookieTime bigint, "+ + " lastSavedTime bigint, expiryTime bigint, map "+blobType+", primary key(rowId))"; + connection.createStatement().executeUpdate(_createSessionTable); + } + + //make some indexes on the JettySessions table + String index1 = "idx_"+_sessionTable+"_expiry"; + String index2 = "idx_"+_sessionTable+"_session"; + + result = metaData.getIndexInfo(null, null, tableName, false, false); + boolean index1Exists = false; + boolean index2Exists = false; + while (result.next()) + { + String idxName = result.getString("INDEX_NAME"); + if (index1.equalsIgnoreCase(idxName)) + index1Exists = true; + else if (index2.equalsIgnoreCase(idxName)) + index2Exists = true; + } + if (!(index1Exists && index2Exists)) + { + Statement statement = connection.createStatement(); + if (!index1Exists) + statement.executeUpdate("create index "+index1+" on "+_sessionTable+" (expiryTime)"); + if (!index2Exists) + statement.executeUpdate("create index "+index2+" on "+_sessionTable+" (sessionId, contextPath)"); + } + } + finally + { + if (connection != null) + connection.close(); + } + } + + /** + * Insert a new used session id into the table. + * + * @param id + * @throws SQLException + */ + private void insert (String id) + throws SQLException + { + Connection connection = null; + try + { + connection = getConnection(); + connection.setAutoCommit(true); + PreparedStatement query = connection.prepareStatement(_queryId); + query.setString(1, id); + ResultSet result = query.executeQuery(); + //only insert the id if it isn't in the db already + if (!result.next()) + { + PreparedStatement statement = connection.prepareStatement(_insertId); + statement.setString(1, id); + statement.executeUpdate(); + } + } + finally + { + if (connection != null) + connection.close(); + } + } + + /** + * Remove a session id from the table. + * + * @param id + * @throws SQLException + */ + private void delete (String id) + throws SQLException + { + Connection connection = null; + try + { + connection = getConnection(); + connection.setAutoCommit(true); + PreparedStatement statement = connection.prepareStatement(_deleteId); + statement.setString(1, id); + statement.executeUpdate(); + } + finally + { + if (connection != null) + connection.close(); + } + } + + + /** + * Check if a session id exists. + * + * @param id + * @return + * @throws SQLException + */ + private boolean exists (String id) + throws SQLException + { + Connection connection = null; + try + { + connection = getConnection(); + connection.setAutoCommit(true); + PreparedStatement statement = connection.prepareStatement(_queryId); + statement.setString(1, id); + ResultSet result = statement.executeQuery(); + if (result.next()) + return true; + else + return false; + } + finally + { + if (connection != null) + connection.close(); + } + } + + /** + * Look for sessions in the database that have expired. + * + * We do this in the SessionIdManager and not the SessionManager so + * that we only have 1 scavenger, otherwise if there are n SessionManagers + * there would be n scavengers, all contending for the database. + * + * We look first for sessions that expired in the previous interval, then + * for sessions that expired previously - these are old sessions that no + * node is managing any more and have become stuck in the database. + */ + private void scavenge () + { + Connection connection = null; + List expiredSessionIds = new ArrayList(); + try + { + if (Log.isDebugEnabled()) Log.debug("Scavenge sweep started at "+System.currentTimeMillis()); + if (_lastScavengeTime > 0) + { + connection = getConnection(); + connection.setAutoCommit(true); + //"select sessionId from JettySessions where expiryTime > (lastScavengeTime - scanInterval) and expiryTime < lastScavengeTime"; + PreparedStatement statement = connection.prepareStatement(_selectExpiredSessions); + long lowerBound = (_lastScavengeTime - _scavengeIntervalMs); + long upperBound = _lastScavengeTime; + if (Log.isDebugEnabled()) Log.debug("Searching for sessions expired between "+lowerBound + " and "+upperBound); + statement.setLong(1, lowerBound); + statement.setLong(2, upperBound); + ResultSet result = statement.executeQuery(); + while (result.next()) + { + String sessionId = result.getString("sessionId"); + expiredSessionIds.add(sessionId); + if (Log.isDebugEnabled()) Log.debug("Found expired sessionId="+sessionId); + } + + + //tell the SessionManagers to expire any sessions with a matching sessionId in memory + Handler[] contexts = _server.getChildHandlersByClass(ContextHandler.class); + for (int i=0; contexts!=null && i<contexts.length; i++) + { + SessionManager manager = (SessionManager) + ((SessionHandler)((ContextHandler)contexts[i]).getChildHandlerByClass(SessionHandler.class)).getSessionManager(); + + if (manager instanceof JDBCSessionManager) + { + ((JDBCSessionManager)manager).expire(expiredSessionIds); + } + } + + //find all sessions that have expired at least a couple of scanIntervals ago and just delete them + upperBound = _lastScavengeTime - (2 * _scavengeIntervalMs); + if (upperBound > 0) + { + if (Log.isDebugEnabled()) Log.debug("Deleting old expired sessions expired before "+upperBound); + statement = connection.prepareStatement(_deleteOldExpiredSessions); + statement.setLong(1, upperBound); + statement.executeUpdate(); + } + } + } + catch (Exception e) + { + Log.warn("Problem selecting expired sessions", e); + } + finally + { + _lastScavengeTime=System.currentTimeMillis(); + if (Log.isDebugEnabled()) Log.debug("Scavenge sweep ended at "+_lastScavengeTime); + if (connection != null) + { + try + { + connection.close(); + } + catch (SQLException e) + { + Log.warn(e); + } + } + } + } +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionManager.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionManager.java new file mode 100644 index 0000000000..963a0a57dc --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/JDBCSessionManager.java @@ -0,0 +1,1080 @@ +// ======================================================================== +// Copyright (c) 2008-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + + +package org.eclipse.jetty.server.session; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Collections; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSessionEvent; +import javax.servlet.http.HttpSessionListener; + +import org.eclipse.jetty.server.SessionIdManager; +import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.util.LazyList; +import org.eclipse.jetty.util.log.Log; + +/** + * JDBCSessionManager + * + * SessionManager that persists sessions to a database to enable clustering. + * + * Session data is persisted to the JettySessions table: + * + * rowId (unique in cluster: webapp name/path + virtualhost + sessionId) + * contextPath (of the context owning the session) + * sessionId (unique in a context) + * lastNode (name of node last handled session) + * accessTime (time in ms session was accessed) + * lastAccessTime (previous time in ms session was accessed) + * createTime (time in ms session created) + * cookieTime (time in ms session cookie created) + * lastSavedTime (last time in ms session access times were saved) + * expiryTime (time in ms that the session is due to expire) + * map (attribute map) + * + * As an optimisation, to prevent thrashing the database, we do not persist + * the accessTime and lastAccessTime every time the session is accessed. Rather, + * we write it out every so often. The frequency is controlled by the saveIntervalSec + * field. + */ +public class JDBCSessionManager extends AbstractSessionManager +{ + protected String __insertSession; + protected String __deleteSession; + protected String __selectSession; + protected String __updateSession; + protected String __updateSessionNode; + protected String __updateSessionAccessTime; + + private ConcurrentHashMap _sessions; + protected long _saveIntervalSec = 60; //only persist changes to session access times every 60 secs + + /** + * SessionData + * + * Persistable data about a session. + */ + public class SessionData + { + private String _id; + private String _rowId; + private long _accessed; + private long _lastAccessed; + private long _maxIdleMs; + private long _cookieSet; + private long _created; + private Map _attributes; + private String _lastNode; + private String _canonicalContext; + private long _lastSaved; + private long _expiryTime; + private String _virtualHost; + + public SessionData (String sessionId) + { + _id=sessionId; + _created=System.currentTimeMillis(); + _accessed = _created; + _attributes = new ConcurrentHashMap(); + _lastNode = getIdManager().getWorkerName(); + } + + public synchronized String getId () + { + return _id; + } + + public synchronized long getCreated () + { + return _created; + } + + protected synchronized void setCreated (long ms) + { + _created = ms; + } + + public synchronized long getAccessed () + { + return _accessed; + } + + protected synchronized void setAccessed (long ms) + { + _accessed = ms; + } + + + public synchronized void setMaxIdleMs (long ms) + { + _maxIdleMs = ms; + } + + public synchronized long getMaxIdleMs() + { + return _maxIdleMs; + } + + public synchronized void setLastAccessed (long ms) + { + _lastAccessed = ms; + } + + public synchronized long getLastAccessed() + { + return _lastAccessed; + } + + public void setCookieSet (long ms) + { + _cookieSet = ms; + } + + public synchronized long getCookieSet () + { + return _cookieSet; + } + + public synchronized void setRowId (String rowId) + { + _rowId=rowId; + } + + protected synchronized String getRowId() + { + return _rowId; + } + + protected synchronized Map getAttributeMap () + { + return _attributes; + } + + protected synchronized void setAttributeMap (ConcurrentHashMap map) + { + _attributes = map; + } + + public synchronized void setLastNode (String node) + { + _lastNode=node; + } + + public synchronized String getLastNode () + { + return _lastNode; + } + + public synchronized void setCanonicalContext(String str) + { + _canonicalContext=str; + } + + public synchronized String getCanonicalContext () + { + return _canonicalContext; + } + + public synchronized long getLastSaved () + { + return _lastSaved; + } + + public synchronized void setLastSaved (long time) + { + _lastSaved=time; + } + + public synchronized void setExpiryTime (long time) + { + _expiryTime=time; + } + + public synchronized long getExpiryTime () + { + return _expiryTime; + } + + public synchronized void setVirtualHost (String vhost) + { + _virtualHost=vhost; + } + + public synchronized String getVirtualHost () + { + return _virtualHost; + } + + public String toString () + { + return "Session rowId="+_rowId+",id="+_id+",lastNode="+_lastNode+ + ",created="+_created+",accessed="+_accessed+ + ",lastAccessed="+_lastAccessed+",cookieSet="+_cookieSet+ + "lastSaved="+_lastSaved; + } + } + + + + /** + * Session + * + * Session instance in memory of this node. + */ + public class Session extends AbstractSessionManager.Session + { + private SessionData _data; + private boolean _dirty=false; + + /** + * Session from a request. + * + * @param request + */ + protected Session (HttpServletRequest request) + { + + super(request); + _data = new SessionData(_clusterId); + _data.setMaxIdleMs(_dftMaxIdleSecs*1000); + _data.setCanonicalContext(canonicalize(_context.getContextPath())); + _data.setVirtualHost(getVirtualHost(_context)); + _data.setExpiryTime(_maxIdleMs < 0 ? 0 : (System.currentTimeMillis() + _maxIdleMs)); + _values=_data.getAttributeMap(); + } + + /** + * Session restored in database. + * @param row + */ + protected Session (SessionData data) + { + super(data.getCreated(), data.getId()); + _data=data; + _values=data.getAttributeMap(); + } + + protected Map newAttributeMap() + { + return _data.getAttributeMap(); + } + + public void setAttribute (String name, Object value) + { + super.setAttribute(name, value); + _dirty=true; + } + + public void removeAttribute (String name) + { + super.removeAttribute(name); + _dirty=true; + } + + protected void cookieSet() + { + _data.setCookieSet(_data.getAccessed()); + } + + /** + * Entry to session. + * Called by SessionHandler on inbound request and the session already exists in this node's memory. + * + * @see org.eclipse.jetty.server.session.AbstractSessionManager.Session#access(long) + */ + protected void access(long time) + { + super.access(time); + _data.setLastAccessed(_data.getAccessed()); + _data.setAccessed(time); + _data.setExpiryTime(_maxIdleMs < 0 ? 0 : (time + _maxIdleMs)); + } + + /** + * Exit from session + * @see org.eclipse.jetty.server.session.AbstractSessionManager.Session#complete() + */ + protected void complete() + { + super.complete(); + try + { + if (_dirty) + { + //The session attributes have changed, write to the db, ensuring + //http passivation/activation listeners called + willPassivate(); + updateSession(_data); + didActivate(); + } + else if ((_data._accessed - _data._lastSaved) >= (getSaveInterval() * 1000)) + updateSessionAccessTime(_data); + + } + catch (Exception e) + { + Log.warn("Problem persisting changed session data id="+getId(), e); + } + finally + { + _dirty=false; + } + } + + protected void timeout() throws IllegalStateException + { + if (Log.isDebugEnabled()) Log.debug("Timing out session id="+getClusterId()); + super.timeout(); + } + } + + + + + /** + * ClassLoadingObjectInputStream + * + * + */ + protected class ClassLoadingObjectInputStream extends ObjectInputStream + { + public ClassLoadingObjectInputStream(java.io.InputStream in) throws IOException + { + super(in); + } + + public ClassLoadingObjectInputStream () throws IOException + { + super(); + } + + public Class resolveClass (java.io.ObjectStreamClass cl) throws IOException, ClassNotFoundException + { + try + { + return Class.forName(cl.getName(), false, Thread.currentThread().getContextClassLoader()); + } + catch (ClassNotFoundException e) + { + return super.resolveClass(cl); + } + } + } + + + + + /** + * Set the time in seconds which is the interval between + * saving the session access time to the database. + * + * This is an optimization that prevents the database from + * being overloaded when a session is accessed very frequently. + * + * On session exit, if the session attributes have NOT changed, + * the time at which we last saved the accessed + * time is compared to the current accessed time. If the interval + * is at least saveIntervalSecs, then the access time will be + * persisted to the database. + * + * If any session attribute does change, then the attributes and + * the accessed time are persisted. + * + * @param sec + */ + public void setSaveInterval (long sec) + { + _saveIntervalSec=sec; + } + + public long getSaveInterval () + { + return _saveIntervalSec; + } + + + /** + * A session has been requested by it's id on this node. + * + * Load the session by id AND context path from the database. + * Multiple contexts may share the same session id (due to dispatching) + * but they CANNOT share the same contents. + * + * Check if last node id is my node id, if so, then the session we have + * in memory cannot be stale. If another node used the session last, then + * we need to refresh from the db. + * + * NOTE: this method will go to the database, so if you only want to check + * for the existence of a Session in memory, use _sessions.get(id) instead. + * + * @see org.eclipse.jetty.server.session.AbstractSessionManager#getSession(java.lang.String) + */ + public Session getSession(String idInCluster) + { + Session session = (Session)_sessions.get(idInCluster); + + synchronized (this) + { + try + { + //check if we need to reload the session - don't do it on every call + //to reduce the load on the database. This introduces a window of + //possibility that the node may decide that the session is local to it, + //when the session has actually been live on another node, and then + //re-migrated to this node. This should be an extremely rare occurrence, + //as load-balancers are generally well-behaved and consistently send + //sessions to the same node, changing only iff that node fails. + SessionData data = null; + long now = System.currentTimeMillis(); + if (Log.isDebugEnabled()) Log.debug("now="+now+ + " lastSaved="+(session==null?0:session._data._lastSaved)+ + " interval="+(_saveIntervalSec * 1000)+ + " difference="+(now - (session==null?0:session._data._lastSaved))); + if (session==null || ((now - session._data._lastSaved) >= (_saveIntervalSec * 1000))) + { + data = loadSession(idInCluster, canonicalize(_context.getContextPath()), getVirtualHost(_context)); + } + else + data = session._data; + + if (data != null) + { + if (!data.getLastNode().equals(getIdManager().getWorkerName()) || session==null) + { + //session last used on a different node, or we don't have it in memory + session = new Session(data); + _sessions.put(idInCluster, session); + session.didActivate(); + //TODO is this the best way to do this? Or do this on the way out using + //the _dirty flag? + updateSessionNode(data); + } + else + if (Log.isDebugEnabled()) Log.debug("Session not stale "+session._data); + //session in db shares same id, but is not for this context + } + else + { + //No session in db with matching id and context path. + session=null; + if (Log.isDebugEnabled()) Log.debug("No session in database matching id="+idInCluster); + } + + return session; + } + catch (Exception e) + { + Log.warn("Unable to load session from database", e); + return null; + } + } + } + + + /** + * Get all the sessions as a map of id to Session. + * + * @see org.eclipse.jetty.server.session.AbstractSessionManager#getSessionMap() + */ + public Map getSessionMap() + { + return Collections.unmodifiableMap(_sessions); + } + + + /** + * Get the number of sessions. + * + * @see org.eclipse.jetty.server.session.AbstractSessionManager#getSessions() + */ + public int getSessions() + { + int size = 0; + synchronized (this) + { + size = _sessions.size(); + } + return size; + } + + + /** + * Start the session manager. + * + * @see org.eclipse.jetty.server.session.AbstractSessionManager#doStart() + */ + public void doStart() throws Exception + { + if (_sessionIdManager==null) + throw new IllegalStateException("No session id manager defined"); + + prepareTables(); + + _sessions = new ConcurrentHashMap(); + super.doStart(); + } + + + /** + * Stop the session manager. + * + * @see org.eclipse.jetty.server.session.AbstractSessionManager#doStop() + */ + public void doStop() throws Exception + { + _sessions.clear(); + _sessions = null; + + super.doStop(); + } + + protected void invalidateSessions() + { + //Do nothing - we don't want to remove and + //invalidate all the sessions because this + //method is called from doStop(), and just + //because this context is stopping does not + //mean that we should remove the session from + //any other nodes + } + + + /** + * Invalidate a session. + * + * @param idInCluster + */ + protected void invalidateSession (String idInCluster) + { + synchronized (this) + { + Session session = (Session)_sessions.get(idInCluster); + if (session != null) + { + session.invalidate(); + } + } + } + + /** + * Delete an existing session, both from the in-memory map and + * the database. + * + * @see org.eclipse.jetty.server.session.AbstractSessionManager#removeSession(java.lang.String) + */ + protected void removeSession(String idInCluster) + { + synchronized (this) + { + try + { + Session session = (Session)_sessions.remove(idInCluster); + deleteSession(session._data); + } + catch (Exception e) + { + Log.warn("Problem deleting session id="+idInCluster, e); + } + } + } + + + /** + * Add a newly created session to our in-memory list for this node and persist it. + * + * @see org.eclipse.jetty.server.session.AbstractSessionManager#addSession(org.eclipse.jetty.server.session.AbstractSessionManager.Session) + */ + protected void addSession(AbstractSessionManager.Session session) + { + if (session==null) + return; + + synchronized (this) + { + _sessions.put(session.getClusterId(), session); + //TODO or delay the store until exit out of session? If we crash before we store it + //then session data will be lost. + try + { + ((JDBCSessionManager.Session)session).willPassivate(); + storeSession(((JDBCSessionManager.Session)session)._data); + ((JDBCSessionManager.Session)session).didActivate(); + } + catch (Exception e) + { + Log.warn("Unable to store new session id="+session.getId() , e); + } + } + } + + + /** + * Make a new Session. + * + * @see org.eclipse.jetty.server.session.AbstractSessionManager#newSession(javax.servlet.http.HttpServletRequest) + */ + protected AbstractSessionManager.Session newSession(HttpServletRequest request) + { + return new Session(request); + } + + /* ------------------------------------------------------------ */ + /** Remove session from manager + * @param session The session to remove + * @param invalidate True if {@link HttpSessionListener#sessionDestroyed(HttpSessionEvent)} and + * {@link SessionIdManager#invalidateAll(String)} should be called. + */ + public void removeSession(AbstractSessionManager.Session session, boolean invalidate) + { + // Remove session from context and global maps + synchronized (_sessionIdManager) + { + boolean removed = false; + + synchronized (this) + { + //take this session out of the map of sessions for this context + if (_sessions.get(session.getClusterId()) != null) + { + removed = true; + removeSession(session.getClusterId()); + } + } + + if (removed) + { + // Remove session from all context and global id maps + _sessionIdManager.removeSession(session); + if (invalidate) + _sessionIdManager.invalidateAll(session.getClusterId()); + } + } + + if (invalidate && _sessionListeners!=null) + { + HttpSessionEvent event=new HttpSessionEvent(session); + for (int i=LazyList.size(_sessionListeners); i-->0;) + ((HttpSessionListener)LazyList.get(_sessionListeners,i)).sessionDestroyed(event); + } + if (!invalidate) + { + session.willPassivate(); + } + } + + + /** + * Expire any Sessions we have in memory matching the list of + * expired Session ids. + * + * @param sessionIds + */ + protected void expire (List sessionIds) + { + //don't attempt to scavenge if we are shutting down + if (isStopping() || isStopped()) + return; + + //Remove any sessions we already have in memory that match the ids + Thread thread=Thread.currentThread(); + ClassLoader old_loader=thread.getContextClassLoader(); + ListIterator itor = sessionIds.listIterator(); + + try + { + while (itor.hasNext()) + { + String sessionId = (String)itor.next(); + if (Log.isDebugEnabled()) Log.debug("Expiring session id "+sessionId); + Session session = (Session)_sessions.get(sessionId); + if (session != null) + { + session.timeout(); + itor.remove(); + int count = this._sessions.size(); + if (count < this._minSessions) + this._minSessions=count; + } + else + { + if (Log.isDebugEnabled()) Log.debug("Unrecognized session id="+sessionId); + } + } + } + catch (Throwable t) + { + if (t instanceof ThreadDeath) + throw ((ThreadDeath)t); + else + Log.warn("Problem expiring sessions", t); + } + finally + { + thread.setContextClassLoader(old_loader); + } + } + + + protected void prepareTables () + { + __insertSession = "insert into "+((JDBCSessionIdManager)_sessionIdManager)._sessionTable+ + " (rowId, sessionId, contextPath, virtualHost, lastNode, accessTime, lastAccessTime, createTime, cookieTime, lastSavedTime, expiryTime, map) "+ + " values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + + __deleteSession = "delete from "+((JDBCSessionIdManager)_sessionIdManager)._sessionTable+ + " where rowId = ?"; + + __selectSession = "select * from "+((JDBCSessionIdManager)_sessionIdManager)._sessionTable+ + " where sessionId = ? and contextPath = ? and virtualHost = ?"; + + __updateSession = "update "+((JDBCSessionIdManager)_sessionIdManager)._sessionTable+ + " set lastNode = ?, accessTime = ?, lastAccessTime = ?, lastSavedTime = ?, expiryTime = ?, map = ? where rowId = ?"; + + __updateSessionNode = "update "+((JDBCSessionIdManager)_sessionIdManager)._sessionTable+ + " set lastNode = ? where rowId = ?"; + + __updateSessionAccessTime = "update "+((JDBCSessionIdManager)_sessionIdManager)._sessionTable+ + " set lastNode = ?, accessTime = ?, lastAccessTime = ?, lastSavedTime = ?, expiryTime = ? where rowId = ?"; + } + + /** + * Load a session from the database + * @param id + * @return + * @throws Exception + */ + protected SessionData loadSession (String id, String canonicalContextPath, String vhost) + throws Exception + { + SessionData data = null; + Connection connection = getConnection(); + PreparedStatement statement = null; + try + { + statement = connection.prepareStatement(__selectSession); + statement.setString(1, id); + statement.setString(2, canonicalContextPath); + statement.setString(3, vhost); + ResultSet result = statement.executeQuery(); + if (result.next()) + { + data = new SessionData(id); + data.setRowId(result.getString("rowId")); + data.setCookieSet(result.getLong("cookieTime")); + data.setLastAccessed(result.getLong("lastAccessTime")); + data.setAccessed (result.getLong("accessTime")); + data.setCreated(result.getLong("createTime")); + data.setLastNode(result.getString("lastNode")); + data.setLastSaved(result.getLong("lastSavedTime")); + data.setExpiryTime(result.getLong("expiryTime")); + data.setCanonicalContext(result.getString("contextPath")); + data.setVirtualHost(result.getString("virtualHost")); + + InputStream is = ((JDBCSessionIdManager)getIdManager())._dbAdaptor.getBlobInputStream(result, "map"); + ClassLoadingObjectInputStream ois = new ClassLoadingObjectInputStream (is); + Object o = ois.readObject(); + data.setAttributeMap((ConcurrentHashMap)o); + ois.close(); + + if (Log.isDebugEnabled()) + Log.debug("LOADED session "+data); + } + return data; + } + finally + { + if (connection!=null) + connection.close(); + } + } + + /** + * Insert a session into the database. + * + * @param data + * @throws Exception + */ + protected void storeSession (SessionData data) + throws Exception + { + if (data==null) + return; + + //put into the database + Connection connection = getConnection(); + PreparedStatement statement = null; + try + { + String rowId = calculateRowId(data); + + long now = System.currentTimeMillis(); + connection.setAutoCommit(true); + statement = connection.prepareStatement(__insertSession); + statement.setString(1, rowId); //rowId + statement.setString(2, data.getId()); //session id + statement.setString(3, data.getCanonicalContext()); //context path + statement.setString(4, data.getVirtualHost()); //first vhost + statement.setString(5, getIdManager().getWorkerName());//my node id + statement.setLong(6, data.getAccessed());//accessTime + statement.setLong(7, data.getLastAccessed()); //lastAccessTime + statement.setLong(8, data.getCreated()); //time created + statement.setLong(9, data.getCookieSet());//time cookie was set + statement.setLong(10, now); //last saved time + statement.setLong(11, data.getExpiryTime()); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(data.getAttributeMap()); + byte[] bytes = baos.toByteArray(); + + ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + statement.setBinaryStream(12, bais, bytes.length);//attribute map as blob + + statement.executeUpdate(); + data.setRowId(rowId); //set it on the in-memory data as well as in db + data.setLastSaved(now); + + + if (Log.isDebugEnabled()) + Log.debug("Stored session "+data); + } + finally + { + if (connection!=null) + connection.close(); + } + } + + + /** + * Update data on an existing persisted session. + * + * @param data + * @throws Exception + */ + protected void updateSession (SessionData data) + throws Exception + { + if (data==null) + return; + + Connection connection = getConnection(); + PreparedStatement statement = null; + try + { + long now = System.currentTimeMillis(); + connection.setAutoCommit(true); + statement = connection.prepareStatement(__updateSession); + statement.setString(1, getIdManager().getWorkerName());//my node id + statement.setLong(2, data.getAccessed());//accessTime + statement.setLong(3, data.getLastAccessed()); //lastAccessTime + statement.setLong(4, now); //last saved time + statement.setLong(5, data.getExpiryTime()); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(data.getAttributeMap()); + byte[] bytes = baos.toByteArray(); + ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + + statement.setBinaryStream(6, bais, bytes.length);//attribute map as blob + statement.setString(7, data.getRowId()); //rowId + statement.executeUpdate(); + + data.setLastSaved(now); + if (Log.isDebugEnabled()) + Log.debug("Updated session "+data); + } + finally + { + if (connection!=null) + connection.close(); + } + } + + + /** + * Update the node on which the session was last seen to be my node. + * + * @param data + * @throws Exception + */ + protected void updateSessionNode (SessionData data) + throws Exception + { + String nodeId = getIdManager().getWorkerName(); + Connection connection = getConnection(); + PreparedStatement statement = null; + try + { + connection.setAutoCommit(true); + statement = connection.prepareStatement(__updateSessionNode); + statement.setString(1, nodeId); + statement.setString(2, data.getRowId()); + statement.executeUpdate(); + statement.close(); + if (Log.isDebugEnabled()) + Log.debug("Updated last node for session id="+data.getId()+", lastNode = "+nodeId); + } + finally + { + if (connection!=null) + connection.close(); + } + } + + /** + * Persist the time the session was last accessed. + * + * @param data + * @throws Exception + */ + private void updateSessionAccessTime (SessionData data) + throws Exception + { + Connection connection = getConnection(); + PreparedStatement statement = null; + try + { + long now = System.currentTimeMillis(); + connection.setAutoCommit(true); + statement = connection.prepareStatement(__updateSessionAccessTime); + statement.setString(1, getIdManager().getWorkerName()); + statement.setLong(2, data.getAccessed()); + statement.setLong(3, data.getLastAccessed()); + statement.setLong(4, now); + statement.setLong(5, data.getExpiryTime()); + statement.setString(6, data.getRowId()); + statement.executeUpdate(); + data.setLastSaved(now); + statement.close(); + if (Log.isDebugEnabled()) + Log.debug("Updated access time session id="+data.getId()); + } + finally + { + if (connection!=null) + connection.close(); + } + } + + + + + /** + * Delete a session from the database. Should only be called + * when the session has been invalidated. + * + * @param data + * @throws Exception + */ + protected void deleteSession (SessionData data) + throws Exception + { + Connection connection = getConnection(); + PreparedStatement statement = null; + try + { + connection.setAutoCommit(true); + statement = connection.prepareStatement(__deleteSession); + statement.setString(1, data.getRowId()); + statement.executeUpdate(); + if (Log.isDebugEnabled()) + Log.debug("Deleted Session "+data); + } + finally + { + if (connection!=null) + connection.close(); + } + } + + + + /** + * Get a connection from the driver. + * @return + * @throws SQLException + */ + private Connection getConnection () + throws SQLException + { + return ((JDBCSessionIdManager)getIdManager()).getConnection(); + } + + /** + * Calculate a unique id for this session across the cluster. + * + * Unique id is composed of: contextpath_virtualhost0_sessionid + * @param data + * @return + */ + private String calculateRowId (SessionData data) + { + String rowId = canonicalize(_context.getContextPath()); + rowId = rowId + "_" + getVirtualHost(_context); + rowId = rowId+"_"+data.getId(); + return rowId; + } + + /** + * Get the first virtual host for the context. + * + * Used to help identify the exact session/contextPath. + * + * @return 0.0.0.0 if no virtual host is defined + */ + private String getVirtualHost (ContextHandler.Context context) + { + String vhost = "0.0.0.0"; + + if (context==null) + return vhost; + + String [] vhosts = context.getContextHandler().getVirtualHosts(); + if (vhosts==null || vhosts.length==0 || vhosts[0]==null) + return vhost; + + return vhosts[0]; + } + + /** + * Make an acceptable file name from a context path. + * + * @param path + * @return + */ + private String canonicalize (String path) + { + if (path==null) + return ""; + + return path.replace('/', '_').replace('.','_').replace('\\','_'); + } +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionHandler.java new file mode 100644 index 0000000000..cac62c2e41 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/session/SessionHandler.java @@ -0,0 +1,291 @@ +// ======================================================================== +// Copyright (c) 2004-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server.session; + +import java.io.IOException; +import java.util.EnumSet; +import java.util.EventListener; + +import javax.servlet.DispatcherType; +import javax.servlet.ServletException; +import javax.servlet.SessionTrackingMode; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.eclipse.jetty.server.HttpConnection; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.RetryRequest; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.SessionManager; +import org.eclipse.jetty.server.handler.HandlerWrapper; +import org.eclipse.jetty.util.log.Log; + +/* ------------------------------------------------------------ */ +/** SessionHandler. + * + * + * + */ +public class SessionHandler extends HandlerWrapper +{ + public final static EnumSet<SessionTrackingMode> DEFAULT_TRACKING = EnumSet.of(SessionTrackingMode.COOKIE,SessionTrackingMode.URL); + + /* -------------------------------------------------------------- */ + private SessionManager _sessionManager; + + /* ------------------------------------------------------------ */ + /** Constructor. + * Construct a SessionHandler witha a HashSessionManager with a standard + * java.util.Random generator is created. + */ + public SessionHandler() + { + this(new HashSessionManager()); + } + + /* ------------------------------------------------------------ */ + /** + * @param manager The session manager + */ + public SessionHandler(SessionManager manager) + { + setSessionManager(manager); + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the sessionManager. + */ + public SessionManager getSessionManager() + { + return _sessionManager; + } + + /* ------------------------------------------------------------ */ + /** + * @param sessionManager The sessionManager to set. + */ + public void setSessionManager(SessionManager sessionManager) + { + if (isStarted()) + throw new IllegalStateException(); + SessionManager old_session_manager = _sessionManager; + + if (getServer()!=null) + getServer().getContainer().update(this, old_session_manager, sessionManager, "sessionManager",true); + + if (sessionManager!=null) + sessionManager.setSessionHandler(this); + + _sessionManager = sessionManager; + + if (old_session_manager!=null) + old_session_manager.setSessionHandler(null); + } + + + /* ------------------------------------------------------------ */ + public void setServer(Server server) + { + Server old_server=getServer(); + if (old_server!=null && old_server!=server) + old_server.getContainer().update(this, _sessionManager, null, "sessionManager",true); + super.setServer(server); + if (server!=null && server!=old_server) + server.getContainer().update(this, null,_sessionManager, "sessionManager",true); + } + + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.thread.AbstractLifeCycle#doStart() + */ + protected void doStart() throws Exception + { + _sessionManager.start(); + super.doStart(); + } + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.thread.AbstractLifeCycle#doStop() + */ + protected void doStop() throws Exception + { + super.doStop(); + _sessionManager.stop(); + } + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.jetty.server.Handler#handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, int) + */ + public void handle(String target, HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException + { + setRequestedId(request); + + Request base_request = (request instanceof Request) ? (Request)request:HttpConnection.getCurrentConnection().getRequest(); + SessionManager old_session_manager=null; + HttpSession old_session=null; + + try + { + old_session_manager = base_request.getSessionManager(); + old_session = base_request.getSession(false); + + if (old_session_manager != _sessionManager) + { + // new session context + base_request.setSessionManager(_sessionManager); + base_request.setSession(null); + } + + // access any existing session + HttpSession session=null; + if (_sessionManager!=null) + { + session=base_request.getSession(false); + if (session!=null) + { + if(session!=old_session) + { + Cookie cookie = _sessionManager.access(session,request.isSecure()); + if (cookie!=null ) // Handle changed ID or max-age refresh + response.addCookie(cookie); + } + } + else + { + session=base_request.recoverNewSession(_sessionManager); + if (session!=null) + base_request.setSession(session); + } + } + + if(Log.isDebugEnabled()) + { + Log.debug("sessionManager="+_sessionManager); + Log.debug("session="+session); + } + + getHandler().handle(target, request, response); + } + catch (RetryRequest r) + { + HttpSession session=base_request.getSession(false); + if (session!=null && session.isNew()) + base_request.saveNewSession(_sessionManager,session); + throw r; + } + finally + { + HttpSession session=request.getSession(false); + + if (old_session_manager != _sessionManager) + { + //leaving context, free up the session + if (session!=null) + _sessionManager.complete(session); + base_request.setSessionManager(old_session_manager); + base_request.setSession(old_session); + } + } + } + + /* ------------------------------------------------------------ */ + /** Look for a requested session ID in cookies and URI parameters + * @param request + * @param dispatch + */ + protected void setRequestedId(HttpServletRequest request) + { + Request base_request = (request instanceof Request) ? (Request)request:HttpConnection.getCurrentConnection().getRequest(); + String requested_session_id=request.getRequestedSessionId(); + if (!DispatcherType.REQUEST.equals(request.getDispatcherType()) || requested_session_id!=null) + { + return; + } + + SessionManager sessionManager = getSessionManager(); + boolean requested_session_id_from_cookie=false; + + // Look for session id cookie + if (_sessionManager.isUsingCookies()) + { + Cookie[] cookies=request.getCookies(); + if (cookies!=null && cookies.length>0) + { + for (int i=0;i<cookies.length;i++) + { + if (sessionManager.getSessionCookie().equalsIgnoreCase(cookies[i].getName())) + { + if (requested_session_id!=null) + { + // Multiple jsessionid cookies. Probably due to + // multiple paths and/or domains. Pick the first + // known session or the last defined cookie. + if (sessionManager.getHttpSession(requested_session_id)!=null) + break; + } + + requested_session_id=cookies[i].getValue(); + requested_session_id_from_cookie = true; + if(Log.isDebugEnabled())Log.debug("Got Session ID "+requested_session_id+" from cookie"); + } + } + } + } + + if (requested_session_id==null) + { + String uri = request.getRequestURI(); + + int semi = uri.lastIndexOf(';'); + if (semi>=0) + { + String path_params=uri.substring(semi+1); + + // check if there is a url encoded session param. + String param=sessionManager.getSessionIdPathParameterName(); + if (param!=null && path_params!=null && path_params.startsWith(param)) + { + requested_session_id = path_params.substring(sessionManager.getSessionIdPathParameterName().length()+1); + if(Log.isDebugEnabled())Log.debug("Got Session ID "+requested_session_id+" from URL"); + } + } + } + + base_request.setRequestedSessionId(requested_session_id); + base_request.setRequestedSessionIdFromCookie(requested_session_id!=null && requested_session_id_from_cookie); + } + + /* ------------------------------------------------------------ */ + /** + * @param listener + */ + public void addEventListener(EventListener listener) + { + if(_sessionManager!=null) + _sessionManager.addEventListener(listener); + } + + /* ------------------------------------------------------------ */ + public void clearEventListeners() + { + if(_sessionManager!=null) + _sessionManager.clearEventListeners(); + } +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ssl/ServletSSL.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ssl/ServletSSL.java new file mode 100644 index 0000000000..3c1cabf1d0 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ssl/ServletSSL.java @@ -0,0 +1,83 @@ +// ======================================================================== +// Copyright (c) 2001-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server.ssl; + +/* --------------------------------------------------------------------- */ +/** + * Jetty Servlet SSL support utilities. + * <p> + * A collection of utilities required to support the SSL requirements of the Servlet 2.2 and 2.3 + * specs. + * + * <p> + * Used by the SSL listener classes. + * + * + */ +public class ServletSSL +{ + /* ------------------------------------------------------------ */ + /** + * Given the name of a TLS/SSL cipher suite, return an int representing it effective stream + * cipher key strength. i.e. How much entropy material is in the key material being fed into the + * encryption routines. + * + * <p> + * This is based on the information on effective key lengths in RFC 2246 - The TLS Protocol + * Version 1.0, Appendix C. CipherSuite definitions: + * + * <pre> + * Effective + * Cipher Type Key Bits + * + * NULL * Stream 0 + * IDEA_CBC Block 128 + * RC2_CBC_40 * Block 40 + * RC4_40 * Stream 40 + * RC4_128 Stream 128 + * DES40_CBC * Block 40 + * DES_CBC Block 56 + * 3DES_EDE_CBC Block 168 + * </pre> + * + * @param cipherSuite String name of the TLS cipher suite. + * @return int indicating the effective key entropy bit-length. + */ + public static final int deduceKeyLength(String cipherSuite) + { + // Roughly ordered from most common to least common. + if (cipherSuite == null) + return 0; + else if (cipherSuite.indexOf("WITH_AES_256_") >= 0) + return 256; + else if (cipherSuite.indexOf("WITH_RC4_128_") >= 0) + return 128; + else if (cipherSuite.indexOf("WITH_AES_128_") >= 0) + return 128; + else if (cipherSuite.indexOf("WITH_RC4_40_") >= 0) + return 40; + else if (cipherSuite.indexOf("WITH_3DES_EDE_CBC_") >= 0) + return 168; + else if (cipherSuite.indexOf("WITH_IDEA_CBC_") >= 0) + return 128; + else if (cipherSuite.indexOf("WITH_RC2_CBC_40_") >= 0) + return 40; + else if (cipherSuite.indexOf("WITH_DES40_CBC_") >= 0) + return 40; + else if (cipherSuite.indexOf("WITH_DES_CBC_") >= 0) + return 56; + else + return 0; + } +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ssl/SslSelectChannelConnector.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ssl/SslSelectChannelConnector.java new file mode 100644 index 0000000000..4169cc3b65 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ssl/SslSelectChannelConnector.java @@ -0,0 +1,703 @@ +// ======================================================================== +// Copyright (c) 2004-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server.ssl; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.SocketChannel; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.security.Security; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; + +import org.eclipse.jetty.http.HttpParser; +import org.eclipse.jetty.http.HttpSchemes; +import org.eclipse.jetty.http.security.Password; +import org.eclipse.jetty.http.ssl.SslSelectChannelEndPoint; +import org.eclipse.jetty.io.Buffer; +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.bio.SocketEndPoint; +import org.eclipse.jetty.io.nio.DirectNIOBuffer; +import org.eclipse.jetty.io.nio.IndirectNIOBuffer; +import org.eclipse.jetty.io.nio.NIOBuffer; +import org.eclipse.jetty.io.nio.SelectChannelEndPoint; +import org.eclipse.jetty.io.nio.SelectorManager.SelectSet; +import org.eclipse.jetty.server.HttpConnection; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.nio.SelectChannelConnector; +import org.eclipse.jetty.util.TypeUtil; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.resource.Resource; + +/* ------------------------------------------------------------ */ +/** + * SslSelectChannelConnector. + * + * @org.apache.xbean.XBean element="sslConnector" description="Creates an NIO ssl connector" + * + * + * + */ +public class SslSelectChannelConnector extends SelectChannelConnector +{ + /** + * The name of the SSLSession attribute that will contain any cached + * information. + */ + static final String CACHED_INFO_ATTR=CachedInfo.class.getName(); + + /** Default value for the keystore location path. */ + public static final String DEFAULT_KEYSTORE=System.getProperty("user.home")+File.separator+".keystore"; + + /** String name of key password property. */ + public static final String KEYPASSWORD_PROPERTY="jetty.ssl.keypassword"; + + /** String name of keystore password property. */ + public static final String PASSWORD_PROPERTY="jetty.ssl.password"; + + /** Default value for the cipher Suites. */ + private String _excludeCipherSuites[]=null; + + /** Default value for the keystore location path. */ + private String _keystore=DEFAULT_KEYSTORE; + private String _keystoreType="JKS"; // type of the key store + + /** Set to true if we require client certificate authentication. */ + private boolean _needClientAuth=false; + private boolean _wantClientAuth=false; + + private transient Password _password; + private transient Password _keyPassword; + private transient Password _trustPassword; + private String _protocol="TLS"; + private String _algorithm="SunX509"; // cert algorithm + private String _provider; + private String _secureRandomAlgorithm; // cert algorithm + private String _sslKeyManagerFactoryAlgorithm=(Security.getProperty("ssl.KeyManagerFactory.algorithm")==null?"SunX509":Security + .getProperty("ssl.KeyManagerFactory.algorithm")); // cert + // algorithm + + private String _sslTrustManagerFactoryAlgorithm=(Security.getProperty("ssl.TrustManagerFactory.algorithm")==null?"SunX509":Security + .getProperty("ssl.TrustManagerFactory.algorithm")); // cert + // algorithm + + private String _truststore; + private String _truststoreType="JKS"; // type of the key store + private SSLContext _context; + + private int _packetBufferSize; + private int _applicationBufferSize; + private ConcurrentLinkedQueue<Buffer> _packetBuffers = new ConcurrentLinkedQueue<Buffer>(); + private ConcurrentLinkedQueue<Buffer> _applicationBuffers = new ConcurrentLinkedQueue<Buffer>(); + + /* ------------------------------------------------------------ */ + /* (non-Javadoc) + * @see org.eclipse.jetty.io.AbstractBuffers#getBuffer(int) + */ + public Buffer getBuffer(int size) + { + // TODO why is this reimplemented? + Buffer buffer; + if (size==_applicationBufferSize) + { + buffer = _applicationBuffers.poll(); + if (buffer==null) + buffer=new IndirectNIOBuffer(size); + } + else if (size==_packetBufferSize) + { + buffer = _packetBuffers.poll(); + if (buffer==null) + buffer=getUseDirectBuffers() + ?(NIOBuffer)new DirectNIOBuffer(size) + :(NIOBuffer)new IndirectNIOBuffer(size); + } + else + buffer=super.getBuffer(size); + + return buffer; + } + + /* ------------------------------------------------------------ */ + /* (non-Javadoc) + * @see org.eclipse.jetty.io.AbstractBuffers#returnBuffer(org.eclipse.io.Buffer) + */ + public void returnBuffer(Buffer buffer) + { + buffer.clear(); + int size=buffer.capacity(); + ByteBuffer bbuf = ((NIOBuffer)buffer).getByteBuffer(); + bbuf.position(0); + bbuf.limit(size); + + if (size==_applicationBufferSize) + _applicationBuffers.add(buffer); + else if (size==_packetBufferSize) + _packetBuffers.add(buffer); + else + super.returnBuffer(buffer); + } + + + + /** + * Return the chain of X509 certificates used to negotiate the SSL Session. + * <p> + * Note: in order to do this we must convert a + * javax.security.cert.X509Certificate[], as used by JSSE to a + * java.security.cert.X509Certificate[],as required by the Servlet specs. + * + * @param sslSession + * the javax.net.ssl.SSLSession to use as the source of the + * cert chain. + * @return the chain of java.security.cert.X509Certificates used to + * negotiate the SSL connection. <br> + * Will be null if the chain is missing or empty. + */ + private static X509Certificate[] getCertChain(SSLSession sslSession) + { + try + { + javax.security.cert.X509Certificate javaxCerts[]=sslSession.getPeerCertificateChain(); + if (javaxCerts==null||javaxCerts.length==0) + return null; + + int length=javaxCerts.length; + X509Certificate[] javaCerts=new X509Certificate[length]; + + java.security.cert.CertificateFactory cf=java.security.cert.CertificateFactory.getInstance("X.509"); + for (int i=0; i<length; i++) + { + byte bytes[]=javaxCerts[i].getEncoded(); + ByteArrayInputStream stream=new ByteArrayInputStream(bytes); + javaCerts[i]=(X509Certificate)cf.generateCertificate(stream); + } + + return javaCerts; + } + catch (SSLPeerUnverifiedException pue) + { + return null; + } + catch (Exception e) + { + Log.warn(Log.EXCEPTION,e); + return null; + } + } + + + /* ------------------------------------------------------------ */ + /** + * Allow the Listener a chance to customise the request. before the server + * does its stuff. <br> + * This allows the required attributes to be set for SSL requests. <br> + * The requirements of the Servlet specs are: + * <ul> + * <li> an attribute named "javax.servlet.request.ssl_session_id" of type + * String (since Servlet Spec 3.0).</li> + * <li> an attribute named "javax.servlet.request.cipher_suite" of type + * String.</li> + * <li> an attribute named "javax.servlet.request.key_size" of type Integer.</li> + * <li> an attribute named "javax.servlet.request.X509Certificate" of type + * java.security.cert.X509Certificate[]. This is an array of objects of type + * X509Certificate, the order of this array is defined as being in ascending + * order of trust. The first certificate in the chain is the one set by the + * client, the next is the one used to authenticate the first, and so on. + * </li> + * </ul> + * + * @param endpoint + * The Socket the request arrived on. This should be a + * {@link SocketEndPoint} wrapping a {@link SSLSocket}. + * @param request + * HttpRequest to be customised. + */ + @Override + public void customize(EndPoint endpoint, Request request) throws IOException + { + super.customize(endpoint,request); + request.setScheme(HttpSchemes.HTTPS); + + SslSelectChannelEndPoint sslHttpChannelEndpoint=(SslSelectChannelEndPoint)endpoint; + + SSLEngine sslEngine=sslHttpChannelEndpoint.getSSLEngine(); + + try + { + SSLSession sslSession=sslEngine.getSession(); + String cipherSuite=sslSession.getCipherSuite(); + Integer keySize; + X509Certificate[] certs; + String idStr; + + CachedInfo cachedInfo=(CachedInfo)sslSession.getValue(CACHED_INFO_ATTR); + if (cachedInfo!=null) + { + keySize=cachedInfo.getKeySize(); + certs=cachedInfo.getCerts(); + idStr=cachedInfo.getIdStr(); + } + else + { + keySize=new Integer(ServletSSL.deduceKeyLength(cipherSuite)); + certs=getCertChain(sslSession); + byte[] bytes = sslSession.getId(); + idStr = TypeUtil.toHexString(bytes); + cachedInfo=new CachedInfo(keySize,certs,idStr); + sslSession.putValue(CACHED_INFO_ATTR,cachedInfo); + } + + if (certs!=null) + request.setAttribute("javax.servlet.request.X509Certificate",certs); + + request.setAttribute("javax.servlet.request.cipher_suite",cipherSuite); + request.setAttribute("javax.servlet.request.key_size",keySize); + request.setAttribute("javax.servlet.request.ssl_session_id", idStr); + } + catch (Exception e) + { + Log.warn(Log.EXCEPTION,e); + } + } + + /* ------------------------------------------------------------ */ + public SslSelectChannelConnector() + { + } + + /** + * + * @deprecated As of Java Servlet API 2.0, with no replacement. + * + */ + public String[] getCipherSuites() + { + return getExcludeCipherSuites(); + } + + public String[] getExcludeCipherSuites() + { + return _excludeCipherSuites; + } + + /** + * + * @deprecated As of Java Servlet API 2.0, with no replacement. + * + * + */ + public void setCipherSuites(String[] cipherSuites) + { + setExcludeCipherSuites(cipherSuites); + } + + public void setExcludeCipherSuites(String[] cipherSuites) + { + this._excludeCipherSuites=cipherSuites; + } + + /* ------------------------------------------------------------ */ + public void setPassword(String password) + { + _password=Password.getPassword(PASSWORD_PROPERTY,password,null); + } + + /* ------------------------------------------------------------ */ + public void setTrustPassword(String password) + { + _trustPassword=Password.getPassword(PASSWORD_PROPERTY,password,null); + } + + /* ------------------------------------------------------------ */ + public void setKeyPassword(String password) + { + _keyPassword=Password.getPassword(KEYPASSWORD_PROPERTY,password,null); + } + + /* ------------------------------------------------------------ */ + public String getAlgorithm() + { + return (this._algorithm); + } + + /* ------------------------------------------------------------ */ + public void setAlgorithm(String algorithm) + { + this._algorithm=algorithm; + } + + /* ------------------------------------------------------------ */ + public String getProtocol() + { + return _protocol; + } + + /* ------------------------------------------------------------ */ + public void setProtocol(String protocol) + { + _protocol=protocol; + } + + /* ------------------------------------------------------------ */ + public void setKeystore(String keystore) + { + _keystore=keystore; + } + + /* ------------------------------------------------------------ */ + public String getKeystore() + { + return _keystore; + } + + /* ------------------------------------------------------------ */ + public String getKeystoreType() + { + return (_keystoreType); + } + + /* ------------------------------------------------------------ */ + public boolean getNeedClientAuth() + { + return _needClientAuth; + } + + /* ------------------------------------------------------------ */ + public boolean getWantClientAuth() + { + return _wantClientAuth; + } + + /* ------------------------------------------------------------ */ + /** + * Set the value of the needClientAuth property + * + * @param needClientAuth + * true iff we require client certificate authentication. + */ + public void setNeedClientAuth(boolean needClientAuth) + { + _needClientAuth=needClientAuth; + } + + public void setWantClientAuth(boolean wantClientAuth) + { + _wantClientAuth=wantClientAuth; + } + + /* ------------------------------------------------------------ */ + public void setKeystoreType(String keystoreType) + { + _keystoreType=keystoreType; + } + + /* ------------------------------------------------------------ */ + public String getProvider() + { + return _provider; + } + + public String getSecureRandomAlgorithm() + { + return (this._secureRandomAlgorithm); + } + + /* ------------------------------------------------------------ */ + public String getSslKeyManagerFactoryAlgorithm() + { + return (this._sslKeyManagerFactoryAlgorithm); + } + + /* ------------------------------------------------------------ */ + public String getSslTrustManagerFactoryAlgorithm() + { + return (this._sslTrustManagerFactoryAlgorithm); + } + + /* ------------------------------------------------------------ */ + public String getTruststore() + { + return _truststore; + } + + /* ------------------------------------------------------------ */ + public String getTruststoreType() + { + return _truststoreType; + } + + /* ------------------------------------------------------------ */ + public void setProvider(String _provider) + { + this._provider=_provider; + } + + /* ------------------------------------------------------------ */ + public void setSecureRandomAlgorithm(String algorithm) + { + this._secureRandomAlgorithm=algorithm; + } + + /* ------------------------------------------------------------ */ + public void setSslKeyManagerFactoryAlgorithm(String algorithm) + { + this._sslKeyManagerFactoryAlgorithm=algorithm; + } + + /* ------------------------------------------------------------ */ + public void setSslTrustManagerFactoryAlgorithm(String algorithm) + { + this._sslTrustManagerFactoryAlgorithm=algorithm; + } + + public void setTruststore(String truststore) + { + _truststore=truststore; + } + + public void setTruststoreType(String truststoreType) + { + _truststoreType=truststoreType; + } + + public void setSslContext(SSLContext sslContext) { + this._context = sslContext; + } + + /* ------------------------------------------------------------ */ + /** + * By default, we're confidential, given we speak SSL. But, if we've been + * told about an confidential port, and said port is not our port, then + * we're not. This allows separation of listeners providing INTEGRAL versus + * CONFIDENTIAL constraints, such as one SSL listener configured to require + * client certs providing CONFIDENTIAL, whereas another SSL listener not + * requiring client certs providing mere INTEGRAL constraints. + */ + public boolean isConfidential(Request request) + { + final int confidentialPort=getConfidentialPort(); + return confidentialPort==0||confidentialPort==request.getServerPort(); + } + + /* ------------------------------------------------------------ */ + /** + * By default, we're integral, given we speak SSL. But, if we've been told + * about an integral port, and said port is not our port, then we're not. + * This allows separation of listeners providing INTEGRAL versus + * CONFIDENTIAL constraints, such as one SSL listener configured to require + * client certs providing CONFIDENTIAL, whereas another SSL listener not + * requiring client certs providing mere INTEGRAL constraints. + */ + public boolean isIntegral(Request request) + { + final int integralPort=getIntegralPort(); + return integralPort==0||integralPort==request.getServerPort(); + } + + /* ------------------------------------------------------------------------------- */ + protected SelectChannelEndPoint newEndPoint(SocketChannel channel, SelectSet selectSet, SelectionKey key) throws IOException + { + return new SslSelectChannelEndPoint(this,channel,selectSet,key,createSSLEngine()) + { + // TODO remove this hack + public boolean isReadyForDispatch() + { + Request request = ((HttpConnection)getConnection()).getRequest(); + return super.isReadyForDispatch() && !(request.getAsyncRequest().isSuspended()); + } + }; + } + + /* ------------------------------------------------------------------------------- */ + protected Connection newConnection(SocketChannel channel, SelectChannelEndPoint endpoint) + { + HttpConnection connection=(HttpConnection)super.newConnection(channel,endpoint); + ((HttpParser)connection.getParser()).setForceContentBuffer(true); + return connection; + } + + /* ------------------------------------------------------------ */ + protected SSLEngine createSSLEngine() throws IOException + { + SSLEngine engine=null; + try + { + engine=_context.createSSLEngine(); + engine.setUseClientMode(false); + + if (_wantClientAuth) + engine.setWantClientAuth(_wantClientAuth); + if (_needClientAuth) + engine.setNeedClientAuth(_needClientAuth); + + if (_excludeCipherSuites!=null&&_excludeCipherSuites.length>0) + { + List<String> excludedCSList=Arrays.asList(_excludeCipherSuites); + String[] enabledCipherSuites=engine.getEnabledCipherSuites(); + List<String> enabledCSList=new ArrayList<String>(Arrays.asList(enabledCipherSuites)); + + for (String cipherName : excludedCSList) + { + if (enabledCSList.contains(cipherName)) + { + enabledCSList.remove(cipherName); + } + } + enabledCipherSuites=enabledCSList.toArray(new String[enabledCSList.size()]); + + engine.setEnabledCipherSuites(enabledCipherSuites); + } + + } + catch (Exception e) + { + Log.warn("Error creating sslEngine -- closing this connector",e); + close(); + throw new IllegalStateException(e); + } + return engine; + } + + + protected void doStart() throws Exception + { + if (_context == null) { + _context=createSSLContext(); + } + + SSLEngine engine=createSSLEngine(); + SSLSession ssl_session=engine.getSession(); + + setHeaderBufferSize(ssl_session.getApplicationBufferSize()); + setRequestBufferSize(ssl_session.getApplicationBufferSize()); + setResponseBufferSize(ssl_session.getApplicationBufferSize()); + + super.doStart(); + } + + protected SSLContext createSSLContext() throws Exception + { + if (_truststore==null) + { + _truststore=_keystore; + _truststoreType=_keystoreType; + } + + InputStream keystoreInputStream = null; + + KeyManager[] keyManagers=null; + KeyStore keyStore = null; + try + { + if (_keystore!=null) + { + keystoreInputStream=Resource.newResource(_keystore).getInputStream(); + keyStore = KeyStore.getInstance(_keystoreType); + keyStore.load(keystoreInputStream,_password==null?null:_password.toString().toCharArray()); + } + } + finally + { + if (keystoreInputStream != null) + keystoreInputStream.close(); + } + + KeyManagerFactory keyManagerFactory=KeyManagerFactory.getInstance(_sslKeyManagerFactoryAlgorithm); + keyManagerFactory.init(keyStore,_keyPassword==null?(_password==null?null:_password.toString().toCharArray()):_keyPassword.toString().toCharArray()); + keyManagers=keyManagerFactory.getKeyManagers(); + + + TrustManager[] trustManagers=null; + InputStream truststoreInputStream = null; + KeyStore trustStore = null; + try + { + if (_truststore!=null) + { + truststoreInputStream = Resource.newResource(_truststore).getInputStream(); + trustStore=KeyStore.getInstance(_truststoreType); + trustStore.load(truststoreInputStream,_trustPassword==null?null:_trustPassword.toString().toCharArray()); + } + } + finally + { + if (truststoreInputStream != null) + truststoreInputStream.close(); + } + + + TrustManagerFactory trustManagerFactory=TrustManagerFactory.getInstance(_sslTrustManagerFactoryAlgorithm); + trustManagerFactory.init(trustStore); + trustManagers=trustManagerFactory.getTrustManagers(); + + SecureRandom secureRandom=_secureRandomAlgorithm==null?null:SecureRandom.getInstance(_secureRandomAlgorithm); + SSLContext context=_provider==null?SSLContext.getInstance(_protocol):SSLContext.getInstance(_protocol,_provider); + context.init(keyManagers,trustManagers,secureRandom); + return context; + } + + /** + * Simple bundle of information that is cached in the SSLSession. Stores the + * effective keySize and the client certificate chain. + */ + private class CachedInfo + { + private X509Certificate[] _certs; + private Integer _keySize; + private String _idStr; + + CachedInfo(Integer keySize, X509Certificate[] certs,String idStr) + { + this._keySize=keySize; + this._certs=certs; + this._idStr=idStr; + } + + X509Certificate[] getCerts() + { + return _certs; + } + + Integer getKeySize() + { + return _keySize; + } + + String getIdStr() + { + return _idStr; + } + } + +} diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ssl/SslSocketConnector.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ssl/SslSocketConnector.java new file mode 100644 index 0000000000..2919302792 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ssl/SslSocketConnector.java @@ -0,0 +1,665 @@ +// ======================================================================== +// Copyright (c) 2000-2009 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== + +package org.eclipse.jetty.server.ssl; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketAddress; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.security.Security; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLServerSocket; +import javax.net.ssl.SSLServerSocketFactory; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; + +import org.eclipse.jetty.http.HttpSchemes; +import org.eclipse.jetty.http.security.Password; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.bio.SocketEndPoint; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.bio.SocketConnector; +import org.eclipse.jetty.util.TypeUtil; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.resource.Resource; + +/* ------------------------------------------------------------ */ +/** + * JSSE Socket Listener. + * + * This specialization of HttpListener is an abstract listener that can be used as the basis for a + * specific JSSE listener. + * + * This is heavily based on the work from Court Demas, which in turn is based on the work from Forge + * Research. + * + * @org.apache.xbean.XBean element="sslSocketConnector" description="Creates an ssl socket connector" + * + * + * + * + * + */ +public class SslSocketConnector extends SocketConnector +{ + /** + * The name of the SSLSession attribute that will contain any cached information. + */ + static final String CACHED_INFO_ATTR = CachedInfo.class.getName(); + + /** Default value for the keystore location path. */ + public static final String DEFAULT_KEYSTORE = System.getProperty("user.home") + File.separator + + ".keystore"; + + /** String name of key password property. */ + public static final String KEYPASSWORD_PROPERTY = "jetty.ssl.keypassword"; + + /** String name of keystore password property. */ + public static final String PASSWORD_PROPERTY = "jetty.ssl.password"; + + /** + * Return the chain of X509 certificates used to negotiate the SSL Session. + * <p> + * Note: in order to do this we must convert a javax.security.cert.X509Certificate[], as used by + * JSSE to a java.security.cert.X509Certificate[],as required by the Servlet specs. + * + * @param sslSession the javax.net.ssl.SSLSession to use as the source of the cert chain. + * @return the chain of java.security.cert.X509Certificates used to negotiate the SSL + * connection. <br> + * Will be null if the chain is missing or empty. + */ + private static X509Certificate[] getCertChain(SSLSession sslSession) + { + try + { + javax.security.cert.X509Certificate javaxCerts[] = sslSession.getPeerCertificateChain(); + if (javaxCerts == null || javaxCerts.length == 0) + return null; + + int length = javaxCerts.length; + X509Certificate[] javaCerts = new X509Certificate[length]; + + java.security.cert.CertificateFactory cf = java.security.cert.CertificateFactory.getInstance("X.509"); + for (int i = 0; i < length; i++) + { + byte bytes[] = javaxCerts[i].getEncoded(); + ByteArrayInputStream stream = new ByteArrayInputStream(bytes); + javaCerts[i] = (X509Certificate) cf.generateCertificate(stream); + } + + return javaCerts; + } + catch (SSLPeerUnverifiedException pue) + { + return null; + } + catch (Exception e) + { + Log.warn(Log.EXCEPTION, e); + return null; + } + } + + + + /** Default value for the cipher Suites. */ + private String _excludeCipherSuites[] = null; + + /** Default value for the keystore location path. */ + private String _keystore=DEFAULT_KEYSTORE ; + private String _keystoreType = "JKS"; // type of the key store + + /** Set to true if we require client certificate authentication. */ + private boolean _needClientAuth = false; + private transient Password _password; + private transient Password _keyPassword; + private transient Password _trustPassword; + private String _protocol= "TLS"; + private String _provider; + private String _secureRandomAlgorithm; // cert algorithm + private String _sslKeyManagerFactoryAlgorithm = (Security.getProperty("ssl.KeyManagerFactory.algorithm")==null?"SunX509":Security.getProperty("ssl.KeyManagerFactory.algorithm")); // cert algorithm + private String _sslTrustManagerFactoryAlgorithm = (Security.getProperty("ssl.TrustManagerFactory.algorithm")==null?"SunX509":Security.getProperty("ssl.TrustManagerFactory.algorithm")); // cert algorithm + + private String _truststore; + private String _truststoreType = "JKS"; // type of the key store + + /** Set to true if we would like client certificate authentication. */ + private boolean _wantClientAuth = false; + private int _handshakeTimeout = 0; //0 means use maxIdleTime + + private SSLContext _context; + + + /* ------------------------------------------------------------ */ + /** + * Constructor. + */ + public SslSocketConnector() + { + super(); + } + + + /* ------------------------------------------------------------ */ + public void accept(int acceptorID) + throws IOException, InterruptedException + { + Socket socket = _serverSocket.accept(); + configure(socket); + + Connection connection=new SslConnection(socket); + connection.dispatch(); + } + + /* ------------------------------------------------------------ */ + protected void configure(Socket socket) + throws IOException + { + super.configure(socket); + } + + /* ------------------------------------------------------------ */ + protected SSLServerSocketFactory createFactory() + throws Exception + { + SSLContext context = _context; + if (context == null) { + if (_truststore==null) + { + _truststore=_keystore; + _truststoreType=_keystoreType; + } + + KeyManager[] keyManagers = null; + InputStream keystoreInputStream = null; + if (_keystore != null) + keystoreInputStream = Resource.newResource(_keystore).getInputStream(); + KeyStore keyStore = KeyStore.getInstance(_keystoreType); + keyStore.load(keystoreInputStream, _password==null?null:_password.toString().toCharArray()); + + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(_sslKeyManagerFactoryAlgorithm); + keyManagerFactory.init(keyStore,_keyPassword==null?null:_keyPassword.toString().toCharArray()); + keyManagers = keyManagerFactory.getKeyManagers(); + + TrustManager[] trustManagers = null; + InputStream truststoreInputStream = null; + if (_truststore != null) + truststoreInputStream = Resource.newResource(_truststore).getInputStream(); + KeyStore trustStore = KeyStore.getInstance(_truststoreType); + trustStore.load(truststoreInputStream,_trustPassword==null?null:_trustPassword.toString().toCharArray()); + + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(_sslTrustManagerFactoryAlgorithm); + trustManagerFactory.init(trustStore); + trustManagers = trustManagerFactory.getTrustManagers(); + + + SecureRandom secureRandom = _secureRandomAlgorithm==null?null:SecureRandom.getInstance(_secureRandomAlgorithm); + + context = _provider==null?SSLContext.getInstance(_protocol):SSLContext.getInstance(_protocol, _provider); + + context.init(keyManagers, trustManagers, secureRandom); + } + + return context.getServerSocketFactory(); + } + + /* ------------------------------------------------------------ */ + /** + * Allow the Listener a chance to customise the request. before the server does its stuff. <br> + * This allows the required attributes to be set for SSL requests. <br> + * The requirements of the Servlet specs are: + * <ul> + * <li> an attribute named "javax.servlet.request.ssl_id" of type String (since Spec 3.0).</li> + * <li> an attribute named "javax.servlet.request.cipher_suite" of type String.</li> + * <li> an attribute named "javax.servlet.request.key_size" of type Integer.</li> + * <li> an attribute named "javax.servlet.request.X509Certificate" of type + * java.security.cert.X509Certificate[]. This is an array of objects of type X509Certificate, + * the order of this array is defined as being in ascending order of trust. The first + * certificate in the chain is the one set by the client, the next is the one used to + * authenticate the first, and so on. </li> + * </ul> + * + * @param endpoint The Socket the request arrived on. + * This should be a {@link SocketEndPoint} wrapping a {@link SSLSocket}. + * @param request HttpRequest to be customised. + */ + public void customize(EndPoint endpoint, Request request) + throws IOException + { + super.customize(endpoint, request); + request.setScheme(HttpSchemes.HTTPS); + + SocketEndPoint socket_end_point = (SocketEndPoint)endpoint; + SSLSocket sslSocket = (SSLSocket)socket_end_point.getTransport(); + + try + { + SSLSession sslSession = sslSocket.getSession(); + String cipherSuite = sslSession.getCipherSuite(); + Integer keySize; + String idStr; + X509Certificate[] certs; + + CachedInfo cachedInfo = (CachedInfo) sslSession.getValue(CACHED_INFO_ATTR); + if (cachedInfo != null) + { + keySize = cachedInfo.getKeySize(); + certs = cachedInfo.getCerts(); + idStr = cachedInfo.getIdStr(); + } + else + { + keySize = new Integer(ServletSSL.deduceKeyLength(cipherSuite)); + certs = getCertChain(sslSession); + byte[] idBytes = sslSession.getId(); + idStr = TypeUtil.toHexString(idBytes); + cachedInfo = new CachedInfo(keySize, certs,idStr); + sslSession.putValue(CACHED_INFO_ATTR, cachedInfo); + } + + if (certs != null) + request.setAttribute("javax.servlet.request.X509Certificate", certs); + else if (_needClientAuth) // Sanity check + throw new IllegalStateException("no client auth"); + + request.setAttribute("javax.servlet.request.ssl_session_id", idStr); + request.setAttribute("javax.servlet.request.cipher_suite", cipherSuite); + request.setAttribute("javax.servlet.request.key_size", keySize); + } + catch (Exception e) + { + Log.warn(Log.EXCEPTION, e); + } + } + + /* ------------------------------------------------------------ */ + public String[] getExcludeCipherSuites() { + return _excludeCipherSuites; + } + + /* ------------------------------------------------------------ */ + public String getKeystore() + { + return _keystore; + } + + /* ------------------------------------------------------------ */ + public String getKeystoreType() + { + return (_keystoreType); + } + + /* ------------------------------------------------------------ */ + public boolean getNeedClientAuth() + { + return _needClientAuth; + } + + /* ------------------------------------------------------------ */ + public String getProtocol() + { + return _protocol; + } + + /* ------------------------------------------------------------ */ + public String getProvider() { + return _provider; + } + + /* ------------------------------------------------------------ */ + public String getSecureRandomAlgorithm() + { + return (this._secureRandomAlgorithm); + } + + /* ------------------------------------------------------------ */ + public String getSslKeyManagerFactoryAlgorithm() + { + return (this._sslKeyManagerFactoryAlgorithm); + } + + /* ------------------------------------------------------------ */ + public String getSslTrustManagerFactoryAlgorithm() + { + return (this._sslTrustManagerFactoryAlgorithm); + } + + /* ------------------------------------------------------------ */ + public String getTruststore() + { + return _truststore; + } + + /* ------------------------------------------------------------ */ + public String getTruststoreType() + { + return _truststoreType; + } + + /* ------------------------------------------------------------ */ + public boolean getWantClientAuth() + { + return _wantClientAuth; + } + + /* ------------------------------------------------------------ */ + /** + * By default, we're confidential, given we speak SSL. But, if we've been told about an + * confidential port, and said port is not our port, then we're not. This allows separation of + * listeners providing INTEGRAL versus CONFIDENTIAL constraints, such as one SSL listener + * configured to require client certs providing CONFIDENTIAL, whereas another SSL listener not + * requiring client certs providing mere INTEGRAL constraints. + */ + public boolean isConfidential(Request request) + { + final int confidentialPort = getConfidentialPort(); + return confidentialPort == 0 || confidentialPort == request.getServerPort(); + } + + /* ------------------------------------------------------------ */ + /** + * By default, we're integral, given we speak SSL. But, if we've been told about an integral + * port, and said port is not our port, then we're not. This allows separation of listeners + * providing INTEGRAL versus CONFIDENTIAL constraints, such as one SSL listener configured to + * require client certs providing CONFIDENTIAL, whereas another SSL listener not requiring + * client certs providing mere INTEGRAL constraints. + */ + public boolean isIntegral(Request request) + { + final int integralPort = getIntegralPort(); + return integralPort == 0 || integralPort == request.getServerPort(); + } + + /* ------------------------------------------------------------ */ + /** + * @param addr The {@link SocketAddress address} that this server should listen on + * @param backlog See {@link ServerSocket#bind(java.net.SocketAddress, int)} + * @return A new {@link ServerSocket socket object} bound to the supplied address with all other + * settings as per the current configuration of this connector. + * @see #setWantClientAuth + * @see #setNeedClientAuth + * @see #setCipherSuites + * @exception IOException + */ + + /* ------------------------------------------------------------ */ + protected ServerSocket newServerSocket(String host, int port,int backlog) throws IOException + { + SSLServerSocketFactory factory = null; + SSLServerSocket socket = null; + + try + { + factory = createFactory(); + + socket = (SSLServerSocket) (host==null? + factory.createServerSocket(port,backlog): + factory.createServerSocket(port,backlog,InetAddress.getByName(host))); + + if (_wantClientAuth) + socket.setWantClientAuth(_wantClientAuth); + if (_needClientAuth) + socket.setNeedClientAuth(_needClientAuth); + + if (_excludeCipherSuites != null && _excludeCipherSuites.length >0) + { + List excludedCSList = Arrays.asList(_excludeCipherSuites); + String[] enabledCipherSuites = socket.getEnabledCipherSuites(); + List enabledCSList = new ArrayList(Arrays.asList(enabledCipherSuites)); + Iterator exIter = excludedCSList.iterator(); + + while (exIter.hasNext()) + { + String cipherName = (String)exIter.next(); + if (enabledCSList.contains(cipherName)) + { + enabledCSList.remove(cipherName); + } + } + enabledCipherSuites = (String[])enabledCSList.toArray(new String[enabledCSList.size()]); + + socket.setEnabledCipherSuites(enabledCipherSuites); + } + + } + catch (IOException e) + { + throw e; + } + catch (Exception e) + { + Log.warn(Log.EXCEPTION, e); + throw new IOException("Could not create JsseListener: " + e.toString()); + } + return socket; + } + + /* ------------------------------------------------------------ */ + /** + * + */ + public void setExcludeCipherSuites(String[] cipherSuites) { + this._excludeCipherSuites = cipherSuites; + } + + /* ------------------------------------------------------------ */ + public void setKeyPassword(String password) + { + _keyPassword = Password.getPassword(KEYPASSWORD_PROPERTY,password,null); + } + + /* ------------------------------------------------------------ */ + /** + * @param keystore The resource path to the keystore, or null for built in keystores. + */ + public void setKeystore(String keystore) + { + _keystore = keystore; + } + + /* ------------------------------------------------------------ */ + public void setKeystoreType(String keystoreType) + { + _keystoreType = keystoreType; + } + + /* ------------------------------------------------------------ */ + /** + * Set the value of the needClientAuth property + * + * @param needClientAuth true iff we require client certificate authentication. + */ + public void setNeedClientAuth(boolean needClientAuth) + { + _needClientAuth = needClientAuth; + } + + /* ------------------------------------------------------------ */ + public void setPassword(String password) + { + _password = Password.getPassword(PASSWORD_PROPERTY,password,null); + } + + /* ------------------------------------------------------------ */ + public void setTrustPassword(String password) + { + _trustPassword = Password.getPassword(PASSWORD_PROPERTY,password,null); + } + + /* ------------------------------------------------------------ */ + public void setProtocol(String protocol) + { + _protocol = protocol; + } + + /* ------------------------------------------------------------ */ + public void setProvider(String _provider) { + this._provider = _provider; + } + + /* ------------------------------------------------------------ */ + public void setSecureRandomAlgorithm(String algorithm) + { + this._secureRandomAlgorithm = algorithm; + } + + /* ------------------------------------------------------------ */ + public void setSslKeyManagerFactoryAlgorithm(String algorithm) + { + this._sslKeyManagerFactoryAlgorithm = algorithm; + } + + /* ------------------------------------------------------------ */ + public void setSslTrustManagerFactoryAlgorithm(String algorithm) + { + this._sslTrustManagerFactoryAlgorithm = algorithm; + } + + + public void setTruststore(String truststore) + { + _truststore = truststore; + } + + + public void setTruststoreType(String truststoreType) + { + _truststoreType = truststoreType; + } + + public void setSslContext(SSLContext sslContext) + { + _context = sslContext; + } + + /* ------------------------------------------------------------ */ + /** + * Set the value of the _wantClientAuth property. This property is used when + * {@link #newServerSocket(SocketAddress, int) opening server sockets}. + * + * @param wantClientAuth true iff we want client certificate authentication. + * @see SSLServerSocket#setWantClientAuth + */ + public void setWantClientAuth(boolean wantClientAuth) + { + _wantClientAuth = wantClientAuth; + } + + /** + * Set the time in milliseconds for so_timeout during ssl handshaking + * @param msec a non-zero value will be used to set so_timeout during + * ssl handshakes. A zero value means the maxIdleTime is used instead. + */ + public void setHandshakeTimeout (int msec) + { + _handshakeTimeout = msec; + } + + + public int getHandshakeTimeout () + { + return _handshakeTimeout; + } + /** + * Simple bundle of information that is cached in the SSLSession. Stores the effective keySize + * and the client certificate chain. + */ + private class CachedInfo + { + private X509Certificate[] _certs; + private Integer _keySize; + private String _idStr; + + + CachedInfo(Integer keySize, X509Certificate[] certs,String id) + { + this._keySize = keySize; + this._certs = certs; + this._idStr = id; + } + + X509Certificate[] getCerts() + { + return _certs; + } + + Integer getKeySize() + { + return _keySize; + } + + String getIdStr () + { + return _idStr; + } + } + + + public class SslConnection extends Connection + { + public SslConnection(Socket socket) throws IOException + { + super(socket); + } + + public void run() + { + try + { + int handshakeTimeout = getHandshakeTimeout(); + int oldTimeout = _socket.getSoTimeout(); + if (handshakeTimeout > 0) + _socket.setSoTimeout(handshakeTimeout); + + ((SSLSocket)_socket).startHandshake(); + + if (handshakeTimeout>0) + _socket.setSoTimeout(oldTimeout); + + super.run(); + } + catch (SSLException e) + { + Log.debug(e); + try{close();} + catch(IOException e2){Log.ignore(e2);} + } + catch (IOException e) + { + Log.debug(e); + try{close();} + catch(IOException e2){Log.ignore(e2);} + } + } + } + +} |