/*
* Copyright (c) xlightweb.org, 2008 - 2009. All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* Please refer to the LGPL license at: http://www.gnu.org/copyleft/lesser.txt
* The latest copy of this software may be found on http://www.xlightweb.org/
*/
package org.xlightweb.server;
import java.io.Closeable;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.net.ConnectException;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.xlightweb.AbstractHttpConnection;
import org.xlightweb.BadMessageException;
import org.xlightweb.BodyDataSink;
import org.xlightweb.RequestHandlerInfo;
import org.xlightweb.HttpResponse;
import org.xlightweb.ResponseHandlerInfo;
import org.xlightweb.HttpResponseHeader;
import org.xlightweb.IHttpSession;
import org.xlightweb.HttpUtils;
import org.xlightweb.IBodyCompleteListener;
import org.xlightweb.IBodyDataHandler;
import org.xlightweb.IHttpConnection;
import org.xlightweb.IHttpConnectionHandler;
import org.xlightweb.IHttpExchange;
import org.xlightweb.IHttpMessage;
import org.xlightweb.IHttpRequest;
import org.xlightweb.IHttpRequestHandler;
import org.xlightweb.IHttpRequestHeader;
import org.xlightweb.IHttpRequestTimeoutHandler;
import org.xlightweb.IHttpResponse;
import org.xlightweb.IHttpResponseHandler;
import org.xlightweb.IHttpResponseHeader;
import org.xlightweb.NonBlockingBodyDataSource;
import org.xsocket.DataConverter;
import org.xsocket.Execution;
import org.xsocket.connection.INonBlockingConnection;
/**
* Represents the server side endpoint implementation of a http connection. Typically, a
* HttpServerConnection will be created by the {@link HttpProtocolAdapter} which is used
* internally by the {@link HttpServer}.
*
* @author grro@xlightweb.org
*/
public final class HttpServerConnection extends AbstractHttpConnection {
private static final Logger LOG = Logger.getLogger(HttpServerConnection.class.getName());
public static final String CALL_DEREGISTER_HANDLER_KEY = "org.xsocket.connection.http.server.call.deregisterHandler";
public static final String CALL_DEREGISTER_HANDLER_DEFAULT = "false";
private static boolean deregisterHandle;
static {
deregisterHandle = Boolean.parseBoolean(System.getProperty(CALL_DEREGISTER_HANDLER_KEY, CALL_DEREGISTER_HANDLER_DEFAULT));
}
public static final boolean DEFAULT_REMOVE_REQUEST_CONNECTION_HEADER = false;
public static final Integer DEFAULT_RECEIVE_TIMEOUT_MILLIS = Integer.MAX_VALUE;
public static final boolean DEFAULT_AUTOCONFIRM_EXPECT_100CONTINUE_HEADER = true;
public static final boolean DEFAULT_AUTOHANDLE_CONNECTION_UPGRADE_HEADER = true;
public static final boolean DEFAULT_SESSION_MANAGEMENT = true;
static final int MIN_REQUEST_TIMEOUT_MILLIS = 1000;
// close management
private final ConnectionCloser connectionCloser = new ConnectionCloser();
private boolean isCloseOnSendingError = true;
// timeout support
private static final long MIN_WATCHDOG_PERIOD_MILLIS = 10 * 1000;
private TimeoutWatchDogTask watchDogTask;
private Long requestTimeoutMillis;
// flags
boolean isAutconfirmExpect100ContinueHeader = DEFAULT_AUTOCONFIRM_EXPECT_100CONTINUE_HEADER;
boolean isAutohandleUpgadeHeader = DEFAULT_AUTOHANDLE_CONNECTION_UPGRADE_HEADER;
// session support
private final ISessionManager sessionManager;
private final int sessionMaxInactiveIntervalSec;
private boolean useCookies = true;
// max transaction support
private Integer maxTransactions;
// message handling
private final ConcurrentLinkedQueue<IHttpRequest> requestQueue = new ConcurrentLinkedQueue<IHttpRequest>();
private final IMessageHandler messageHandler = new MessageHandler();
// handler support
private final IHttpRequestHandler requestHandler;
private final RequestHandlerInfo requestHandlerInfo;
/**
* constructor
*
* @param sessionManager the session manager
* @param tcpConnection the tcp connection
* @param requestHandler the request handler
* @param requestHandlerInfo the request handle info
* @param isCloseOnSendingError true, if connection should be closed by sending an error
* @param connectionHandlers the connection handler
* @param useCookies true, if cookies is used for session state management
* @throws IOException if an exception occurs
*/
HttpServerConnection(ISessionManager sessionManager, int defaultMaxInactiveIntervalSec, INonBlockingConnection tcpConnection, IHttpRequestHandler requestHandler, RequestHandlerInfo requestHandlerInfo, boolean isCloseOnSendingError, List<IHttpConnectionHandler> connectionHandlers, boolean useCookies) throws IOException {
super(tcpConnection, false);
this.sessionManager = sessionManager;
this.sessionMaxInactiveIntervalSec = defaultMaxInactiveIntervalSec;
this.requestHandler = requestHandler;
this.requestHandlerInfo = requestHandlerInfo;
this.isCloseOnSendingError = isCloseOnSendingError;
this.useCookies = useCookies;
if (connectionHandlers != null) {
for (IHttpConnectionHandler connectionHandler : connectionHandlers) {
addConnectionHandler(connectionHandler);
}
}
init();
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("[" + getId() + "] http server connection established");
}
}
/**
* {@inheritDoc}
*/
public boolean isServerSide() {
return true;
}
/**
* set the max transactions per connection. Setting this filed causes that
* a keep-alive response header will be added
*
* @param maxTransactions the max transactions
*/
public void setMaxTransactions(int maxTransactions) {
this.maxTransactions = maxTransactions;
}
/**
* schedules the task
*
* @param task the task to schedule
* @param delay the delay
* @param period the period
*/
protected static void schedule(TimerTask task, long delay, long period) {
AbstractHttpConnection.schedule(task, delay, period);
}
/**
* returns the request handler info
* @param requestHandler the request handler
* @return the request handler info
*/
protected static RequestHandlerInfo getRequestHandlerInfo(IHttpRequestHandler requestHandler) {
return AbstractHttpConnection.getRequestHandlerInfo(requestHandler);
}
/**
* returns the response handler info
* @param responseHandler the response handler
* @return the response handler info
*/
protected static ResponseHandlerInfo getResponseHandlerInfo(IHttpResponseHandler responseHandler) {
return AbstractHttpConnection.getResponseHandlerInfo(responseHandler);
}
/**
* set the max receive timeout which is accepted for the connection. Setting this
* field cases, that a keep-alive response header will be added
*
* @param requestTimout the timeout
*/
public void setRequestTimeoutMillis(long requestTimeoutMillis) {
if (requestTimeoutMillis < MIN_REQUEST_TIMEOUT_MILLIS) {
LOG.warning("try to set request timeout with " + requestTimeoutMillis + " millis. This value will be ignored because it is smaller that " + MIN_REQUEST_TIMEOUT_MILLIS + " millis");
}
if (requestTimeoutMillis <= 0) {
performRequestTimeoutHandler();
return;
}
setLastTimeDataReceivedMillis(System.currentTimeMillis());
if ((this.requestTimeoutMillis == null) || (this.requestTimeoutMillis != requestTimeoutMillis)) {
this.requestTimeoutMillis = requestTimeoutMillis;
if (requestTimeoutMillis == Long.MAX_VALUE) {
terminateWatchDogTask();
} else {
long watchdogPeriod = 100;
if (requestTimeoutMillis > 1000) {
watchdogPeriod = requestTimeoutMillis / 10;
}
if (watchdogPeriod > MIN_WATCHDOG_PERIOD_MILLIS) {
watchdogPeriod = MIN_WATCHDOG_PERIOD_MILLIS;
}
updateWatchDog(watchdogPeriod);
}
}
}
private void checkRequestTimeout(long currentTimeMillis) {
if (requestTimeoutMillis != null) {
long timeoutReceived = getLastTimeDataReceivedMillis() + requestTimeoutMillis;
long timeoutSend = getLastTimeWritten() + requestTimeoutMillis;
if ((currentTimeMillis > timeoutReceived) && (currentTimeMillis > timeoutSend)) {
onRequestTimeout();
}
}
}
private void onRequestTimeout() {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("request timeout " + DataConverter.toFormatedDuration(getBodyDataReceiveTimeoutMillis()) + " reached");
}
terminateWatchDogTask();
performRequestTimeoutHandler();
}
private void performRequestTimeoutHandler() {
if (requestHandlerInfo.isRequestTimeoutHandler()) {
Runnable requestTimeoutHandlerCaller = new Runnable() {
public void run() {
try {
boolean isHandled = ((IHttpRequestTimeoutHandler) requestHandler).onRequestTimeout(HttpServerConnection.this);
if (!isHandled) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("request timeout " + DataConverter.toFormatedDuration(requestTimeoutMillis) + " reached for http server connection. terminate connection (timeout handler returned false)");
}
closeSilence();
}
} catch (Exception e) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("[" + getId() + "] error occured by calling on request " + requestHandler + " " + e.toString());
}
destroy();
throw new RuntimeException(e);
}
}
};
// ... and perform the handler
if (requestHandlerInfo.isRequestTimeoutHandlerMultithreaded()) {
getExecutor().processMultithreaded(requestTimeoutHandlerCaller);
} else {
getExecutor().processNonthreaded(requestTimeoutHandlerCaller);
}
} else {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("request timeout " + DataConverter.toFormatedDuration(requestTimeoutMillis) + " for http server connection reached. terminate connection");
}
closeSilence();
}
}
private synchronized void updateWatchDog(long watchDogPeriod) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("update timeout watchdog task period to " + DataConverter.toFormatedDuration(watchDogPeriod));
}
terminateWatchDogTask();
watchDogTask = new TimeoutWatchDogTask(this);
AbstractHttpConnection.schedule(watchDogTask, watchDogPeriod, watchDogPeriod);
}
private synchronized void terminateWatchDogTask() {
if (watchDogTask != null) {
watchDogTask.close();
watchDogTask = null;
}
}
private static final class TimeoutWatchDogTask extends TimerTask implements Closeable {
private WeakReference<HttpServerConnection> connectionRef = null;
public TimeoutWatchDogTask(HttpServerConnection connection) {
connectionRef = new WeakReference<HttpServerConnection>(connection);
}
@Override
public void run() {
WeakReference<HttpServerConnection> ref = connectionRef;
if (ref != null) {
HttpServerConnection connection = ref.get();
if (connection == null) {
this.close();
} else {
connection.checkRequestTimeout(System.currentTimeMillis());
}
}
}
public void close() {
this.cancel();
connectionRef = null;
}
}
/**
* {@inheritDoc}
*/
@Override
protected void onProtocolException(Exception ex) {
if (ex instanceof BadMessageException) {
setPersistent(false);
try {
send(new HttpResponse(400, "text/html", generateErrorMessageHtml(400, ex.getMessage(), getId())));
} catch (IOException ioe) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("could not send error message " + 400 + " reason " + ioe.toString());
}
destroy();
}
} else {
super.onProtocolException(ex);
}
}
/**
* {@inheritDoc}
*/
@Override
protected IMessageHandler getMessageHandler() {
return messageHandler;
}
/**
* {@inheritDoc}
*/
@Override
protected void onDisconnect() {
super.onDisconnect();
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("[" + getId() + "] http server connection destroyed");
}
}
private void suspendMessageReceiving() {
try {
super.suspendReceiving();
} catch (IOException ioe) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("[" + getId() + "] error occured by suspending read " + ioe.toString());
}
}
}
private void resumeMessageReceiving() {
try {
super.resumeReceiving();
} catch (IOException ioe) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("[" + getId() + "] error occured by resuming read " + ioe.toString());
}
}
}
private void handleLifeCycleHeaders(IHttpRequest request) {
IHttpRequestHeader header = request.getRequestHeader();
String keepAliveHeader = header.getKeepAlive();
if (keepAliveHeader != null) {
String[] tokens = keepAliveHeader.split(",");
for (String token : tokens) {
handleKeepAlive(token);
}
}
String connectionHeader = header.getConnection();
if (connectionHeader != null) {
String[] values = connectionHeader.split(",");
for (String value : values) {
value = value.trim();
if (value.equalsIgnoreCase("close")) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("received connection: closed header. destroying connection");
}
setPersistent(false);
}
if (value.equalsIgnoreCase("Keep-Alive")) {
setPersistent(true);
}
}
}
}
private void handleKeepAlive(String option) {
Integer timeoutSec = null;
if (option.toUpperCase().startsWith("TIMEOUT=")) {
timeoutSec = Integer.parseInt(option.substring("TIMEOUT=".length(), option.length()));
} else {
timeoutSec = Integer.parseInt(option);
}
if ((timeoutSec != null) && (getBodyDataReceiveTimeoutMillis() == Long.MAX_VALUE)) {
setBodyDataReceiveTimeoutMillis(timeoutSec * 1000L);
}
}
/**
* processes a task multi threaded
*
* @param task the task
*/
void processMultithreaded(Runnable task) {
getExecutor().processMultithreaded(task);
}
/**
* processes a task non threaded
*
* @param task the task
*/
void processNonthreaded(Runnable task) {
getExecutor().processNonthreaded(task);
}
private BodyDataSink send(IHttpResponseHeader header) throws IOException {
try{
if (deregisterHandle) {
resumeMessageReceiving();
}
enhanceResponseHeader(header);
BodyDataSink bodyDataSink = writeMessage(header, !isPersistent());
if (!isPersistent()) {
setBodyCloseListener(bodyDataSink, connectionCloser);
}
return bodyDataSink;
} catch (IOException ioe) {
destroy();
throw ioe;
}
}
private BodyDataSink send(IHttpResponseHeader header, int contentLength) throws IOException {
try{
if (deregisterHandle) {
resumeMessageReceiving();
}
enhanceResponseHeader(header);
BodyDataSink bodyDataSink = writeMessage(header, !isPersistent(), contentLength);
if(!isPersistent()) {
setBodyCloseListener(bodyDataSink, connectionCloser);
}
return bodyDataSink;
} catch (IOException ioe) {
destroy();
throw ioe;
}
}
/**
* send the response
*
* @param response the response
* @throws IOException if an exception occurs
*/
public void send(IHttpResponse response) throws IOException {
try{
if (deregisterHandle) {
resumeMessageReceiving();
}
IHttpResponseHeader responseHeader = response.getResponseHeader();
enhanceResponseHeader(responseHeader);
// body less request?
if (response.getNonBlockingBody() == null) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("[" + getId() + "] sending (bodyless): " + response.getResponseHeader());
}
if (response.getContentLength() == -1) {
response.setContentLength(0);
}
BodyDataSink bodyDataSink = writeMessage(responseHeader, !isPersistent(), 0);
bodyDataSink.setFlushmode(FlushMode.ASYNC);
bodyDataSink.close();
if(!isPersistent()) {
closeSilence();
}
// no, request has a body
} else {
if (response.getNonBlockingBody().getDataHandler() != null) {
LOG.warning("a body handler is already assigned to the message body. current body handler will be removed");
}
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("[" + getId() + "] sending: " + response.getResponseHeader());
}
writeMessage(response, !isPersistent());
}
} catch (IOException ioe) {
destroy();
throw ioe;
}
}
private void enhanceResponseHeader(IHttpResponseHeader header) {
String server = header.getServer();
if (server == null) {
header.setServer(ServerUtils.getComponentInfo());
}
int remainingTransactions = 0;
if (maxTransactions != null) {
remainingTransactions = maxTransactions - getCountMessagesReceived();
if (remainingTransactions <= 0) {
setPersistent(false);
}
}
if ((isPersistent() && (header.getConnection() == null)) &&
((maxTransactions != null) || (requestTimeoutMillis != null))) {
header.setConnection("Keep-Alive");
String keepAliveValue = null;
if (maxTransactions != null) {
keepAliveValue = "max=" + remainingTransactions;
}
if (requestTimeoutMillis != null) {
if (keepAliveValue == null) {
keepAliveValue = "timeout=" + requestTimeoutMillis / 1000;
} else {
keepAliveValue += ", timeout=" + requestTimeoutMillis / 1000;
}
}
header.setHeader("Keep-Alive", keepAliveValue);
}
String connectionHeader = header.getConnection();
if ((connectionHeader != null) && connectionHeader.equalsIgnoreCase("close")) {
setPersistent(false);
} else {
if (!isPersistent()) {
header.setConnection("close");
}
}
}
private HttpSession getSession(IHttpRequest request) throws IOException {
HttpSession session = null;
if (useCookies) {
List<String> cookieHeaders = request.getHeaderList("Cookie");
for (String cookieHeader : cookieHeaders) {
String[] cookies = cookieHeader.split(";");
for (String cookie : cookies) {
cookie = cookie.trim();
int idx = cookie.indexOf("=");
if (idx != -1) {
String name = cookie.substring(0, idx);
if (name.equals("JSESSIONID")) {
String sessionId = cookie.substring(idx + 1, cookie.length()).trim();
session = sessionManager.getSession(sessionId);
if (session != null) {
return session;
}
}
}
}
}
} else {
String uri = request.getRequestURI();
int pos = uri.lastIndexOf(";");
if (pos != -1) {
String sessionToken = uri.substring(pos + 1, uri.length());
if ((sessionToken != null) && (sessionToken.startsWith("jsessionid="))) {
session = sessionManager.getSession(sessionToken.substring("jsessionid=".length(), sessionToken.length()));
if (session != null) {
return session;
}
}
}
}
return null;
}
private boolean isLargerOrEquals(String protocolVersion, String otherProtocolVersion) {
int idx = protocolVersion.indexOf(".");
int major = Integer.parseInt(protocolVersion.substring(0, idx));
int minor = Integer.parseInt(protocolVersion.substring(idx + 1, protocolVersion.length()));
int idxOther = otherProtocolVersion.indexOf(".");
int majorOther = Integer.parseInt(otherProtocolVersion.substring(0, idxOther));
int minorOther = Integer.parseInt(otherProtocolVersion.substring(idxOther + 1, otherProtocolVersion.length()));
if (major > majorOther) {
return true;
} else if (major < majorOther) {
return false;
}
if (minor > minorOther) {
return true;
} else if (minor < minorOther) {
return false;
}
return true;
}
private boolean isContentTypeFormUrlencoded(IHttpMessage message) {
if (!message.hasBody()) {
return false;
}
String contentType = message.getContentType();
if ((contentType != null) && (contentType.startsWith("application/x-www-form-urlencoded"))) {
return true;
}
return false;
}
private final class ConnectionCloser implements Runnable {
public void run() {
closeSilence();
}
}
private final class MessageHandler implements IMessageHandler, Runnable {
public boolean isBodylessMessageExpected() {
return false;
}
public void onException(IOException ioe) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("error occured by receiving request " + ioe.toString());
}
destroy();
}
public void onMessage(IHttpMessage message) throws IOException {
IHttpRequest request = (IHttpRequest) message;
// handle Connection header
handleLifeCycleHeaders(request);
if (LOG.isLoggable(Level.FINE)) {
if (request.getNonBlockingBody() == null) {
LOG.fine("[" + getId() + "] bodyless request received from " + getRemoteAddress() +
":" + getRemotePort() +
" (" + getCountMessagesReceived() + ". request) " + request.getRequestHeader().toString());
} else {
String body = "";
String contentType = request.getContentType();
if ((contentType != null) && (contentType.startsWith("application/x-www-form-urlencode"))) {
body = request.getNonBlockingBody().toString() + "\n";
}
LOG.fine("[" + getId() + "] request received from " + getRemoteAddress() +
":" + getRemotePort() +
" (" + getCountMessagesReceived() + ". request) " + request.getRequestHeader().toString() + body);
}
}
// handle upgrade header
if (isAutohandleUpgadeHeader) {
String upgrade = request.getRequestHeader().getUpgrade();
if ((upgrade != null) && upgrade.equalsIgnoreCase("TLS/1.0")) {
if (getUnderlyingTcpConnection().isSecuredModeActivateable()) {
suspendReceiving();
HttpResponse response = new HttpResponse(101);
response.setHeader("Connection", "Upgrade");
response.setHeader("Upgrade", "TLS/1.0, HTTP/1.1");
writeMessage(response, false);
getUnderlyingTcpConnection().activateSecuredMode();
resumeReceiving();
} else {
send(new HttpResponse(400, "text/html", generateErrorMessageHtml(400, "upgrade TLS is not supported", getId())));
return;
}
return;
}
}
// handle 100 continue header
if (isAutconfirmExpect100ContinueHeader) {
String expectHeader = request.getHeader("Expect");
if ((expectHeader != null) && expectHeader.equalsIgnoreCase("100-Continue")) {
writeMessage(new HttpResponse(100), false);
}
}
if (message.hasBody()) {
if (getBodyDataReceiveTimeoutMillis() != Long.MAX_VALUE) {
message.getNonBlockingBody().setBodyDataReceiveTimeoutMillis(getBodyDataReceiveTimeoutMillis());
}
// is FORM encoded request?
if (isContentTypeFormUrlencoded(request)) {
final IHttpRequest req = request;
IBodyCompleteListener cl = new IBodyCompleteListener() {
@Execution(Execution.NONTHREADED)
public void onComplete() throws IOException {
handleMessage(newFormEncodedRequestWrapper(req));
}
};
request.getNonBlockingBody().addCompleteListener(cl);
return;
}
}
handleMessage(request);
}
private void handleMessage(final IHttpRequest request) throws IOException {
// handler deregistering?
if (deregisterHandle) {
IBodyCompleteListener deregisterHandlerListener = new IBodyCompleteListener() {
@Execution(Execution.NONTHREADED)
public void onComplete() throws IOException {
HttpServerConnection.this.suspendMessageReceiving();
}
};
request.getNonBlockingBody().addCompleteListener(deregisterHandlerListener);
}
// InvokeOn message received?
if (requestHandlerInfo.isRequestHandlerInvokeOnMessageReceived() && request.hasBody()) {
IBodyCompleteListener messageReceivedListener = new IBodyCompleteListener() {
@Execution(Execution.NONTHREADED)
public void onComplete() throws IOException {
handle(request);
}
};
request.getNonBlockingBody().addCompleteListener(messageReceivedListener);
// ... no
} else {
handle(request);
}
}
private void handle(IHttpRequest request) {
if (requestHandler != null) {
// add message to the queue
requestQueue.add(request);
// ... and perform the handler
if (requestHandlerInfo.isRequestHandlerMultithreaded()) {
getExecutor().processMultithreaded(this);
} else {
getExecutor().processNonthreaded(this);
}
}
}
public void run() {
IHttpRequest request = null;
do {
request = requestQueue.poll();
if (request != null) {
ServerExchange exchange = new ServerExchange(request);
// perform call back method
try {
if (requestHandlerInfo.isRequestHandlerSynchronizedOnSession()) {
SessionSynchronizedRequestHandlerWrapper wrapper = new SessionSynchronizedRequestHandlerWrapper(requestHandler);
wrapper.onRequest(exchange);
} else {
requestHandler.onRequest(exchange);
}
} catch (BadMessageException bme) {
if (HttpUtils.isShowDetailedError()) {
exchange.sendError(400, DataConverter.toString(bme));
} else {
exchange.sendError(400);
}
} catch (Exception e) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("[" +getId() + "] error occured by calling on request " + requestHandler + " " + e.toString());
}
if (!exchange.isResponseCommitted()) {
if (HttpUtils.isShowDetailedError()) {
exchange.sendError(500, DataConverter.toString(e));
} else {
exchange.sendError(500);
}
}
closeSilence();
}
}
} while (request != null);
}
}
private class ServerExchange implements IHttpExchange {
private final AtomicBoolean isResponseCommitted = new AtomicBoolean(false);
private IHttpRequest request = null;
private HttpSession session = null;
private boolean isSessionCreated = false;
protected ServerExchange(IHttpRequest request) {
this.request = request;
if (!sessionManager.isEmtpy()) {
resolveSession();
}
}
public IHttpSession getSession(boolean create) {
resolveSession();
if (session != null) {
return session;
}
if (create) {
isSessionCreated = true;
try {
int prefix = request.getContextPath().hashCode();
session = sessionManager.getSession(sessionManager.newSession(Integer.toHexString(prefix)));
session.setMaxInactiveInterval(sessionMaxInactiveIntervalSec);
return session;
} catch (IOException ioe) {
throw new RuntimeException(ioe.toString());
}
}
return null;
}
public String encodeURL(String url) {
if (useCookies) {
return url;
} else {
// add path parameter (for URL path paramter see http://doriantaylor.com/policy/http-url-path-parameter-syntax)
int pos = url.indexOf('?');
if (pos == -1) {
pos = url.indexOf('#');
}
if (pos == -1) {
return url + ";jsessionid=" + session.getId();
} else {
return url.substring(0, pos) + ";jsessionid=" + session.getId() + url.substring(pos);
}
}
}
private void resolveSession() {
if (session != null) {
return;
}
try {
session = HttpServerConnection.this.getSession(request);
if (session != null) {
String idPrefix = Integer.toHexString(request.getContextPath().hashCode());
if (session.getId().startsWith(idPrefix)) {
session.setLastAccessTime(System.currentTimeMillis());
} else {
session = null;
}
}
} catch (IOException ioe) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("error occured by resolving session " + ioe.toString());
}
}
}
public final IHttpRequest getRequest() {
return request;
}
public IHttpConnection getConnection() {
return HttpServerConnection.this;
}
private final void ensureResponseHasNotBeenCommitted() throws IOException {
if (isResponseCommitted.get() == true) {
throw new IOException("response has already been committed");
}
}
public final boolean isResponseCommitted() {
return isResponseCommitted.get();
}
protected final void setResponseCommited(boolean isCommitted) {
isResponseCommitted.set(isCommitted);
}
public final void destroy() {
HttpServerConnection.this.destroy();
}
/**
* {@inheritDoc}
*/
public BodyDataSink send(final IHttpResponseHeader header) throws IOException {
ensureResponseHasNotBeenCommitted();
handleCookieOnSend(header);
if (!AbstractHttpConnection.isAcceptingChunkedResponseBody(getRequest())) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("the requestor does not support chunked response messages (request protocol: " + getRequest().getProtocol() + "). Converting chunked response into length-defined response.");
}
HttpResponseHeader newHeader = new HttpResponseHeader(header.getStatus(), header.getReason());
newHeader.copyHeaderFrom(header);
newHeader.setProtocol(getRequest().getProtocol());
newHeader.setHeader("Connection", "close");
IBodyDataHandler bdh = new BodyHandlerDecorador(newHeader);
return newBufferedBodyDataSink(bdh, header.getCharacterEncoding());
}
setResponseCommited(true);
assert (header.getContentLength() == -1);
header.setTransferEncoding("chunked");
BodyDataSink bodyDataSink = HttpServerConnection.this.send(header);
return bodyDataSink;
}
/**
* send the response in a plain body non-blocking mode
*
* @param header the header
* @param contentLength the body content length
* @return the body handle to write
* @throws IOException if an exception occurs
*/
public BodyDataSink send(IHttpResponseHeader header, int contentLength) throws IOException {
ensureResponseHasNotBeenCommitted();
handleCookieOnSend(header);
// http protocol version downgrade necessary?
if ((!getRequest().getProtocolVersion().equals(header.getProtocolVersion())) &&
(isLargerOrEquals(header.getProtocolVersion(), getRequest().getRequestHeader().getProtocolVersion()))) {
header.setHeader("Connection", "close");
header.setProtocol(getRequest().getProtocol());
}
setResponseCommited(true);
BodyDataSink bodyDataSink = HttpServerConnection.this.send(header, contentLength);
return bodyDataSink;
}
/**
* send the response
*
* @param response the response
* @throws IOException if an exception occurs
*/
public void send(IHttpResponse response) throws IOException {
ensureResponseHasNotBeenCommitted();
handleCookieOnSend(response.getResponseHeader());
// request protocol version not equals response protocol version?
if (!response.getProtocolVersion().equals(getRequest().getProtocolVersion())) {
// simple (HTTP/0.9) response?
if (response.getProtocolVersion().equals("0.9") && (response.getContentLength() == -1)) {
HttpResponseHeader header = new HttpResponseHeader(200);
header.copyHeaderFrom(response.getResponseHeader());
header.setProtocol(getRequest().getProtocol());
header.setHeader("Connection", "close");
IBodyDataHandler bdh = new BodyHandlerDecorador(header);
response.getNonBlockingBody().setDataHandler(bdh);
return;
}
// http protocol version downgrade necessary?
if (isLargerOrEquals(response.getResponseHeader().getProtocolVersion(), getRequest().getRequestHeader().getProtocolVersion())) {
response.getResponseHeader().setProtocol(getRequest().getProtocol());
response.getResponseHeader().setHeader("Connection", "close");
}
}
setResponseCommited(true);
HttpServerConnection.this.send(response);
}
private final class BodyHandlerDecorador implements IBodyDataHandler {
private final IHttpResponseHeader header;
private boolean isHeaderWritten = false;
private BodyDataSink bodyDataSink = null;
public BodyHandlerDecorador(IHttpResponseHeader header) {
this.header = header;
}
@Execution(Execution.NONTHREADED)
public boolean onData(NonBlockingBodyDataSource bodyDataSource) {
try {
if (!isHeaderWritten) {
isHeaderWritten = true;
bodyDataSink = HttpServerConnection.this.writeMessage(header, !isPersistent());
bodyDataSink.setFlushmode(FlushMode.ASYNC);
}
int available = bodyDataSource.available();
if (available > 0) {
ByteBuffer[] bufs = bodyDataSource.readByteBufferByLength(available);
bodyDataSink.write(bufs);
}
if (available == -1) {
bodyDataSink.close();
bodyDataSource.setDataHandler(null);
}
} catch (IOException exception) {
sendError(500);
}
return true;
}
}
private void handleCookieOnSend(IHttpResponseHeader header) {
if (session != null) {
try {
sessionManager.saveSession(session.getId());
} catch (IOException ioe) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("error occured by saving session " + session.getId());
}
}
}
if (isSessionCreated && useCookies) {
StringBuilder sb = new StringBuilder("JSESSIONID=" + session.getId() + ";path=/");
if (isSecure()) {
sb.append(";secure");
}
header.addHeader("Set-Cookie", sb.toString());
}
}
public BodyDataSink forward(IHttpRequestHeader requestHeader) throws IOException, ConnectException, IllegalStateException {
return forward(requestHeader, new ForwardingResponseHandler(this));
}
public BodyDataSink forward(IHttpRequestHeader requestHeader, IHttpResponseHandler responseHandler) throws IOException, ConnectException {
if (responseHandler == null) {
responseHandler = new DoNothingResponseHandler();
}
BodyDataSink bodyDataSink = newEmtpyBodyDataSink();
// send not handled error after the data sink is closed
setBodyCloseListener(bodyDataSink, newCloseListener(responseHandler));
return bodyDataSink;
}
public BodyDataSink forward(IHttpRequestHeader requestHeader, int contentLength) throws IOException, ConnectException, IllegalStateException {
return forward(requestHeader, contentLength, new ForwardingResponseHandler(this));
}
public BodyDataSink forward(IHttpRequestHeader requestHeader, int contentLength, IHttpResponseHandler responseHandler) throws IOException, ConnectException {
if (responseHandler == null) {
responseHandler = new DoNothingResponseHandler();
}
BodyDataSink bodyDataSink = newEmtpyBodyDataSink();
// send not handled error after the data sink is closed
setBodyCloseListener(bodyDataSink, newCloseListener(responseHandler));
return bodyDataSink;
}
private Runnable newCloseListener(final IHttpResponseHandler responseHandler) {
return new Runnable() {
public void run() {
sendNotHandledError(responseHandler);
}
};
}
public void forward(IHttpRequest request, IHttpResponseHandler responseHandler) throws IOException, ConnectException {
if (responseHandler == null) {
responseHandler = new DoNothingResponseHandler();
}
sendNotHandledError(responseHandler);
}
public void forward(IHttpRequest request) throws IOException, ConnectException, IllegalStateException {
forward(request, new ForwardingResponseHandler(this));
}
private void sendNotHandledError(final IHttpResponseHandler responseHandler) {
try {
final IHttpResponse response = new HttpResponse(404, "text/html", generateErrorMessageHtml(404, null, getId()));
Runnable task = new Runnable() {
public void run() {
try {
responseHandler.onResponse(response);
} catch (IOException ioe) {
closeSilence();
}
}
};
if (HttpServerConnection.getResponseHandlerInfo(responseHandler).isResponseHandlerMultithreaded()) {
getExecutor().processMultithreaded(task);
} else {
getExecutor().processNonthreaded(task);
}
} catch (IOException ioe) {
throw new RuntimeException(ioe.toString());
}
}
/**
* {@inheritDoc}
*/
public void sendError(Exception e) {
int code = 500;
if (e instanceof BadMessageException) {
code = 400;
}
if (HttpUtils.isShowDetailedError()) {
sendError(code, DataConverter.toString(e));
} else {
sendError(code);
}
}
/**
* send an error response
*
* @param errorCode the error code
*/
public void sendError(int errorCode) {
sendError(errorCode, HttpUtils.getReason(errorCode));
}
/**
* send an error response
*
* @param errorCode the error code
* @param msg the error message
*/
public void sendError(int errorCode, String msg) {
if (isCloseOnSendingError) {
setPersistent(false);
}
if (isResponseCommitted()) {
throw new IllegalStateException("response is already committed");
}
try {
send(new HttpResponse(errorCode, "text/html", generateErrorMessageHtml(errorCode, msg, getId())));
} catch (IOException ioe) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("could not send error message " + errorCode + " reason " + ioe.toString());
}
destroy();
}
}
}
@Execution(Execution.NONTHREADED)
private static final class ForwardingResponseHandler implements IHttpResponseHandler {
private IHttpExchange exchange = null;
public ForwardingResponseHandler(IHttpExchange exchange) {
this.exchange = exchange;
}
public void onResponse(IHttpResponse response) throws IOException {
exchange.send(response);
}
public void onException(IOException ioe) throws IOException {
exchange.sendError(ioe);
}
}
@Execution(Execution.NONTHREADED)
private static final class DoNothingResponseHandler implements IHttpResponseHandler {
public void onResponse(IHttpResponse response) throws IOException { }
public void onException(IOException ioe) throws IOException { }
}
private static final class SessionSynchronizedRequestHandlerWrapper implements IHttpRequestHandler {
private IHttpRequestHandler delegee = null;
public SessionSynchronizedRequestHandlerWrapper(IHttpRequestHandler delegee) {
this.delegee = delegee;
}
public void onRequest(IHttpExchange exchange) throws IOException {
IHttpSession session = exchange.getSession(false);
if (session == null) {
delegee.onRequest(exchange);
} else {
synchronized (session) {
delegee.onRequest(exchange);
}
}
}
}
}