From 3e1a531fc387009a6dae27237555af93ca7b4028 Mon Sep 17 00:00:00 2001 From: wcrisman Date: Sun, 28 Dec 2014 09:32:25 -0800 Subject: [PATCH] Separated inner classes from WebServer. --- .../web/server/AbstractSocketContext.java | 129 + .../web/server/IChannelContext.java | 7 + .../foundation/web/server/MessageBuffer.java | 128 + .../web/server/NetworkListener.java | 267 ++ .../web/server/PassThroughSocketContext.java | 168 + .../com/foundation/web/server/Request.java | 4 +- .../web/server/ServerSocketContext.java | 15 + .../foundation/web/server/SocketContext.java | 2634 +++++++++++ .../com/foundation/web/server/WebServer.java | 3846 +---------------- .../web/server/WebsocketMessageBuffer.java | 150 + 10 files changed, 3510 insertions(+), 3838 deletions(-) create mode 100644 Foundation Web Core/src/com/foundation/web/server/AbstractSocketContext.java create mode 100644 Foundation Web Core/src/com/foundation/web/server/IChannelContext.java create mode 100644 Foundation Web Core/src/com/foundation/web/server/MessageBuffer.java create mode 100644 Foundation Web Core/src/com/foundation/web/server/NetworkListener.java create mode 100644 Foundation Web Core/src/com/foundation/web/server/PassThroughSocketContext.java create mode 100644 Foundation Web Core/src/com/foundation/web/server/ServerSocketContext.java create mode 100644 Foundation Web Core/src/com/foundation/web/server/SocketContext.java create mode 100644 Foundation Web Core/src/com/foundation/web/server/WebsocketMessageBuffer.java diff --git a/Foundation Web Core/src/com/foundation/web/server/AbstractSocketContext.java b/Foundation Web Core/src/com/foundation/web/server/AbstractSocketContext.java new file mode 100644 index 0000000..2508283 --- /dev/null +++ b/Foundation Web Core/src/com/foundation/web/server/AbstractSocketContext.java @@ -0,0 +1,129 @@ +package com.foundation.web.server; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.text.SimpleDateFormat; +import java.util.TimeZone; + +public abstract class AbstractSocketContext implements IChannelContext { + /** The size of the buffers used per connection. */ + protected static final int BUFFER_SIZE = 100000; + protected static final int SEND_BUFFER_SIZE = 20480; //2048 + protected static final int RECEIVE_BUFFER_SIZE = 20480; //2048 + protected static final byte[] DOUBLE_ENTER = new byte[] {0x0D, 0x0A, 0x0D, 0x0A}; + /** The character set used by the HTTP messages. */ + protected static final Charset charset = Charset.forName("us-ascii"); + /** The decoder used to decode the HTTP messages. */ + protected static final CharsetDecoder decoder = charset.newDecoder(); + /** The format used for dates in the HTTP header. */ + protected static final ThreadLocal httpDateFormat = new ThreadLocal(); + protected static final String httpDateFormatString = "EEE, d MMM yyyy HH:mm:ss z"; + + /** The next available socket context id. */ + private static int nextSocketContextId = 1; + /** The network listener that created this socket context. */ + private final NetworkListener networkListener; + /** The debug ID for the socket. */ + public final int id; + /** The web server that created the socket context. */ + protected WebServer webServer = null; + /** The key that represents the connection between the channel (socket) and the selector used to multiplex the listener. The code must synchronize on this attribute when accessing the isUsed functionality, or when interacting with the key's interestOps. */ + protected SelectionKey key = null; + /** Whether the socket is currently being used by a thread designated by the network listener thread to read or write to the socket. Currently the socket type we use in Java only allows one thread to read and write at a time. Note: Always synchronize on key before using this attribute. */ + private boolean isUsed = false; + /** A socket context related to this one (when two are tied together such that data from one immediately is sent to the other). */ + protected AbstractSocketContext relatedSocketContext = null; +/** + * Gets the next available socket id. + * @return The ID useful for debugging. + */ +protected static int getNextSocketContextId() {return nextSocketContextId++;} +/** + * Gets the threads date format for processing HTTP header dates. + *

This uses a thread local because the date format class is not thread safe :(.

+ * @return The date format for handling http header dates. + */ +protected static SimpleDateFormat getHttpDateFormat() { + SimpleDateFormat result = (SimpleDateFormat) httpDateFormat.get(); + + if(result == null) { + result = new SimpleDateFormat(httpDateFormatString); + result.setTimeZone(TimeZone.getTimeZone("GMT")); + httpDateFormat.set(result); + }//if// + + return result; +}//getHttpDateFormat()// +/** + * AbstractSocketContext constructor. + */ +public AbstractSocketContext(NetworkListener networkListener) { + this.networkListener = networkListener; + + synchronized(AbstractSocketContext.class) { + this.id = getNextSocketContextId(); + }//synchronized// +}//AbstractSocketContext()// +/** Gets the network listener the socket exists within. */ +public NetworkListener getNetworkListener() {return networkListener;} +/** Gets the web server the socket exists within. */ +public WebServer getWebServer() {return networkListener.getWebServer();} +/** Gets whether the socket context is currently in use by a thread. */ +public boolean getIsUsed() {return isUsed;} +/** Sets whether the socket context is currently in use by a thread. */ +public void setIsUsed(boolean isUsed) {this.isUsed = isUsed;} +/** + * Gets the lockable (synchronizable) object for this context. For contexts with a related context, only one of the two will be returned, such that a single synchronize block covers both contexts. + * @return The object to synchronize on such that two threads don't attempt to interact with the context at the same time (AsynchronousSocketChannel required for that). + */ +protected abstract Object getLock(); +/** + * Writes the next responses/messages in the sequence. + * @throws IOException + */ +protected abstract void writeOutgoingMessages() throws IOException; +/** + * Reads the next requests/messages received via the socket. + * @throws IOException + */ +protected abstract void readIncomingMessages() throws IOException; +/** + * Passes the message through to a receiving process via a second socket. + * @param buffer The buffer containing the message. This buffer will not be retained by this method call, and can be reused by the caller. + * @return Whether the whole message was transfered. + */ +protected abstract boolean passThrough(ByteBuffer buffer); +/** + * Closes the socket context and cleans up. + */ +protected abstract void close(); +/** + * Gets the socket context related to this one (when two are tied together such that data from one immediately is sent to the other). + * @return The related socket context, or null if none exists (data not forwarded to a remote server). + */ +protected AbstractSocketContext getRelatedSocketContext() {return relatedSocketContext;} +/** + * Determines whether the socket has a pending write operation. + */ +protected abstract boolean hasPendingWrite(); +/** + * Called to notify the network listener that a pending write operation exists for this socket. + */ +protected void notifyListenerOfPendingWrite() { + synchronized(key) { + //Ignore if a thread is using this socket currently since all operation flags will be set at the end of the use of the socket.// + if(!isUsed) { + int ops = key.interestOps(); + boolean hasWrite = (ops & SelectionKey.OP_WRITE) != 0; + + if(!hasWrite) { + key.interestOps(ops | SelectionKey.OP_WRITE); + key.selector().wakeup(); + }//if// + }//if// + }//synchronized// +}//notifyListenerOfPendingWrite()// +}//AbstractSocketContext// \ No newline at end of file diff --git a/Foundation Web Core/src/com/foundation/web/server/IChannelContext.java b/Foundation Web Core/src/com/foundation/web/server/IChannelContext.java new file mode 100644 index 0000000..7df1d80 --- /dev/null +++ b/Foundation Web Core/src/com/foundation/web/server/IChannelContext.java @@ -0,0 +1,7 @@ +package com.foundation.web.server; + +/** + * Provides a place for channel oriented data. + */ +public interface IChannelContext { +}//IChannelContext// \ No newline at end of file diff --git a/Foundation Web Core/src/com/foundation/web/server/MessageBuffer.java b/Foundation Web Core/src/com/foundation/web/server/MessageBuffer.java new file mode 100644 index 0000000..941fc22 --- /dev/null +++ b/Foundation Web Core/src/com/foundation/web/server/MessageBuffer.java @@ -0,0 +1,128 @@ +package com.foundation.web.server; + +import java.nio.ByteBuffer; + +import com.foundation.web.interfaces.IContent; + +/** + * The response message buffer encapsulating the request generating the response, and the content, and chainable into a linked list. + */ +class MessageBuffer { + /** The actual underlying buffer containing the bytes to be sent. Will be null if the message buffer needs initializing or has finished. */ + private ByteBuffer buffer = null; + /** The ability to chain message buffers into a linked list. */ + private MessageBuffer next = null; + + /** The optional response the message is based upon. */ + private Response response = null; + /** The content if there is any. */ + private IContent content = null; + + /** + * MessageBuffer constructor. + */ + public MessageBuffer() { + }//MessageBuffer()// + /** + * MessageBuffer constructor. + * @param buffer The buffer to use for assembling the message bytes. + */ + public MessageBuffer(ByteBuffer buffer) { + setBuffer(buffer); + }//MessageBuffer()// + /** + * Sets the actual underlying buffer for the message buffer. + * @param buffer + */ + protected void setBuffer(ByteBuffer buffer) { + this.buffer = buffer; + + if(buffer != null && buffer.position() != 0) buffer.flip(); + }//setBuffer()// + /** + * MessageBuffer constructor. + * @param buffer The buffer to use for assembling the message bytes. + * @param response The optional response that generates this message. + * @param content The content if the response is not just a header. + */ + public MessageBuffer(ByteBuffer buffer, Response response, IContent content) { + this.buffer = buffer; + this.content = content; + + //Fill the remaining buffer space with the content.// + if(content != null) { + content.get(buffer); + }//if// + + //Flip the buffer (if not already flipped) so we can write out the bytes.// + if(buffer.position() != 0) buffer.flip(); + this.response = response; + }//MessageBuffer()// + /** + * Initializes the message buffer for use. + * @return Whether initialization succeded. Intialization should be considered a success even if none is required or has already been performed. If it fails the caller should close the socket. + */ + public boolean initialize() { + //Does nothing by default. Subclasses may implement.// + return true; + }//initialize()// + /** + * Whether the message buffer is closed and all bytes have been sent. + * @return If the bytes have all been sent. + */ + public boolean isClosed() { + return buffer == null; + }//isClosed()// + /** + * Closes the message buffer. + */ + public void close() { + this.buffer = null; + }//close()// + /** + * Gets the byte buffer containing the current portion of the message to be sent. + * @return The buffer containing the next part of the message to be sent, or null if the message end has been reached. + */ + public ByteBuffer getBuffer() {return buffer;} + /** + * Loads the next part of the message into the buffer (any remaining bytes in the buffer will be compacted). + * @return Whether the buffer could be loaded with the next part of the message. If false, then the caller should try again in the future when additional message content may be available. Will always be false if there is no content to load from. + */ + public boolean loadBuffer() { + boolean result = true; + + if(buffer != null) { + if(content != null) { + int getResult; + + buffer.compact(); + getResult = content.get(buffer); + + if(getResult == IContent.CONTENT_PENDING) { + result = false; //Should never occur currently: See StreamedContent's javadocs.// + }//if// + else if(getResult == IContent.CONTENT_END) { + buffer = null; + }//else if// + + if(buffer != null && buffer.position() != 0) buffer.flip(); + }//if// + else if(!buffer.hasRemaining()) { + //Clear the buffer pointer indicating the message buffer is done.// + buffer = null; + result = false; + }//else if// + }//if// + else { + result = false; + }//else// + + return result; + }//loadBuffer()// + /** Gets the next message buffer (only used for pass through sockets). */ + public MessageBuffer getNext() {return next;} + /** Sets the next message buffer (only used for pass through sockets). */ + public void setNext(MessageBuffer next) {this.next = next;} + /** Gets the response object that created the message. This will be null for pass through sockets. */ + public Response getResponse() {return response;} +}//MessageBuffer// \ No newline at end of file diff --git a/Foundation Web Core/src/com/foundation/web/server/NetworkListener.java b/Foundation Web Core/src/com/foundation/web/server/NetworkListener.java new file mode 100644 index 0000000..1e05e1b --- /dev/null +++ b/Foundation Web Core/src/com/foundation/web/server/NetworkListener.java @@ -0,0 +1,267 @@ +package com.foundation.web.server; + +import java.nio.ByteBuffer; +import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.Iterator; + +import com.common.debug.Debug; +import com.common.thread.ThreadService; +import com.common.util.LiteList; +import com.foundation.web.server.WebServer.TlsFailureException; + +/** + * Encapsulates the code that threads incomming socket requests and messages over sockets. + *

Note that the HTTP protocol requires that the reponses be sent in order of the received requests.

+ */ +class NetworkListener implements Runnable { + private final WebServer webServer; + Selector selector = null; + private Iterator selectedKeys = null; + private volatile boolean stop = true; + private volatile boolean hasRunnables = false; + private LiteList runnables = new LiteList(10, 20); +public NetworkListener(WebServer webServer, Selector selector) { + this.webServer = webServer; + this.selector = selector; +}//NetworkListener()// +/** Gets the web server that created this network listener. */ +public WebServer getWebServer() {return webServer;} +/** + * Stops the network listener. + * Note that this may take a short time to complete. + */ +public void stop() { + if(!stop) { + stop = true; + selector.wakeup(); + }//if// +}//stop()// +/** + * Starts the network listener. + */ +public void start() { + if(stop) { + stop = false; + ThreadService.run(this); +// Thread t = new Thread(this); +// t.setName("Network Listener"); +// t.start(); + }//if// +}//start()// +/** + * Cleans up after the client channel. + * @param context The context associated with the client connection. + * @param channel The client connection that is now closed. + */ +private void cleanupClientChannel(SocketContext context, SocketChannel channel) { + if(this.webServer.debug) { + Debug.log("Connection closed to " + channel.socket().getInetAddress() + ":" + channel.socket().getPort()); + }//if// +}//cleanupClientChannel()// +/** + * Adds a runnable to the list of runnables to be run next time the loop is woken. + * @param runnable The runnable to be run by the thread that is listening for socket events. + */ +public synchronized void queue(Runnable runnable) { + runnables.add(runnable); + hasRunnables = true; +}//queue()// +/** + * Checks for runnables and runs them if there are any. + */ +private void checkForRunnables() { + if(hasRunnables) { + synchronized(this) { + while(runnables.getSize() > 0) { + ((Runnable) runnables.remove(0)).run(); + }//while// + + hasRunnables = false; + }//synchronized// + }//if// +}//checkForRunnables()// +/* (non-Javadoc) + * @see java.lang.Runnable#run() + */ +public void run() { + //Looping only occurs when we are at the maximum allowed number of threads handling messages.// + while(!stop) { + SelectionKey key = null; + + try { + //If we don't have an iterator over the active channels then get one and block if necessary.// + if(selectedKeys == null) { + int keyCount = 0; + + //Block until we have keys or were awakened by another thread.// + keyCount = selector.select(); + //Check for any pending runnables that need executing on this thread.// + checkForRunnables(); + + //If we have active keys then retrieve them.// + if(keyCount > 0) { + selectedKeys = selector.selectedKeys().iterator(); + }//if// + }//if// + + //If we have an iterator over the active channels then get and remove the next one (clean up the iterator if empty).// + if(selectedKeys != null) { + key = (SelectionKey) selectedKeys.next(); + selectedKeys.remove(); + + //Weed out invalid (cancelled) keys.// + if(!key.isValid()) { + key = null; + }//if// + + if(!selectedKeys.hasNext()) { + selectedKeys = null; + }//if// + }//if// + }//try// + catch(Throwable e) { + //TODO: Can we recover? + Debug.log(e); + }//catch// + + try { + if(key != null) { + final boolean isWrite = key.isWritable(); + final IChannelContext context = (IChannelContext) key.attachment(); + final SelectableChannel channel = key.channel(); + final SelectionKey selectionKey = key; + + if(channel instanceof ServerSocketChannel) { + try { + ServerSocketChannel serverSocketChannel = (ServerSocketChannel) channel; + SocketChannel socketChannel = serverSocketChannel.accept(); + ServerSocketContext serverSocketContext = (ServerSocketContext) context; + SocketContext socketContext = new SocketContext(serverSocketContext, this); + + socketChannel.configureBlocking(false); + socketChannel.socket().setSendBufferSize(AbstractSocketContext.SEND_BUFFER_SIZE); + socketChannel.socket().setReceiveBufferSize(AbstractSocketContext.RECEIVE_BUFFER_SIZE); + socketContext.key = socketChannel.register(selector, SelectionKey.OP_READ, socketContext); + socketContext.serverSocketContext = serverSocketContext; + + //Debug.log("Connection opened to " + socketChannel.socket().getInetAddress() + ":" + socketChannel.socket().getPort()); + + if(serverSocketContext.serviceListener.type != IServiceListener.TYPE_SSL) { + socketContext.socketReadBuffer = ByteBuffer.allocate(AbstractSocketContext.BUFFER_SIZE); + }//if// + + if(this.webServer.debug) { + Debug.log("Connection opened to " + socketChannel.socket().getInetAddress() + ":" + socketChannel.socket().getPort()); + }//if// + }//try// + catch(Throwable e) { + //TODO: Can we recover? + Debug.log(e); + }//catch// + }//if// + else if(channel instanceof SocketChannel) { +// boolean socketClosed = false; + + //Toggle the write or read flag.// + synchronized(key) { +// //Save the ops that will be set when the processing is complete.// +// ((AbstractSocketContext) context).setFlags(key.interestOps()); + + //Notes: Java (pre-jdk7) does not have the ability to read and write to a socket at the same time (two threads, one socket). Post jdk7 there is AsynchronousSocketChannel and AsynchronousServerSocketChannel which could be used to send/receive at the same time. + //Truely enabling Speedy would require a thread to read which when finished would flag read again BEFORE processing the message and BEFORE sending a response. + //For now (so we don't have to require jdk7 yet) we will simply allow Speedy to queue up messages, but only read, process, and then write them one at a time. Most of the speed loss is in the waiting for the WRITE to finish before handling the next request (due to it being broken into packets and the mechanics of TCP), and that is generally minimal (speed lose) since usually the bottleneck in speed is the browser's connection to the internet (most of us haven't got Gigabit Ethernet at home). Anyone with enough home juice to have this be a problem would only notice the difference for really porky websites (which is a problem in and of its self). + + //Not allowing either reads or writes to continue until all processing of this message is done.// + ((AbstractSocketContext) context).setIsUsed(true); + key.interestOps(0); + + //The problem with this is that we'd have to use AsynchronousSocketChannel which would appear to require a complete rewrite of everything since it operates completely differently.// +// key.interestOps(key.interestOps() ^ (isWrite ? SelectionKey.OP_WRITE : SelectionKey.OP_READ)); + }//synchronized// + + if(((SocketChannel) channel).isOpen()) { + ThreadService.run(new Runnable() { + public void run() { + boolean socketClosed = false; + + try { + if(isWrite) { + //Prevent another thread from reading/writing on the same socket at the same time (safety). This would have to be removed if SPEEDY (or similar pipelining) were allowed, and AsynchronousSocketChannel/AsynchronousServerSocketChannel would have to be used (requiring jdk7).// + synchronized(((AbstractSocketContext) context).getLock()) { + //Process the pending write to the socket as much as is possible, then return.// + ((AbstractSocketContext) context).writeOutgoingMessages(); + }//synchronized// + }//if// + else { + //Prevent another thread from reading/writing on the same socket at the same time (safety). This would have to be removed if SPEEDY (or similar pipelining) were allowed, and AsynchronousSocketChannel/AsynchronousServerSocketChannel would have to be used (requiring jdk7).// + synchronized(((AbstractSocketContext) context).getLock()) { + //Process the incoming request and send the response (a partial response may be sent in which case the socket will be set to wait for a write opportunity and not a read opportunity).// + ((AbstractSocketContext) context).readIncomingMessages(); + }//synchronized// + }//else// + }//try// + catch(TlsFailureException e) { + //Allow the failure to be ignored. This occurs when the client fails to use TLS or fails to send the host name as part of the TLS handshake.// + try {((SocketChannel) channel).close();}catch(Throwable e2) {} //Release the socket so the message doesn't continue to be processed.// + }//catch// + catch(Throwable e) { + if(NetworkListener.this.webServer.debug) Debug.log(e); + + //Force the socket to be closed (for sure).// + try {((SocketChannel) channel).close();} catch(Throwable e2) {} + //Debug.log(e); + socketClosed = true; + }//catch// + finally { + if(channel != null && !socketClosed && channel.isOpen() && context != null) { + try { + //Set the new ops for the selection key and notify the selector that ops have changed.// + synchronized(selectionKey) { + if(selectionKey.isValid()) { + //Always flag the socket for reading, only flag the socket for writing if a pending write operation exists.// + selectionKey.interestOps(SelectionKey.OP_READ | (((AbstractSocketContext) context).hasPendingWrite() ? SelectionKey.OP_WRITE : 0)); + }//if// + else { + Debug.log(new RuntimeException("Woops! Somehow the selection key isn't valid, but the socket isn't closed either!")); + try {((SocketChannel) channel).close();} catch(Throwable e2) {} + cleanupClientChannel((SocketContext) context, (SocketChannel) channel); + }//else// + + ((AbstractSocketContext) context).setIsUsed(false); + }//synchronized// + + selector.wakeup(); + }//try// + catch(Throwable e) { + Debug.log(e); + }//catch// + }//if// + else if(channel != null && (!channel.isOpen() || socketClosed) && channel instanceof SocketChannel && context instanceof SocketContext) { + cleanupClientChannel((SocketContext) context, (SocketChannel) channel); + }//else if// + else { + //This shouldn't be called I don't think.// + Debug.log(new RuntimeException("Woops! Somehow we aren't closed and we didn't setup the interestOps for the HTTP socket!")); + }//else// + }//finally// + }//run()// + }); + }//if// + }//else if// + }//if// + }//try// + catch(java.nio.channels.CancelledKeyException e) { + //Occurs if the socket is closed while we are handling the key.// + Debug.log(e); //TODO: Does anything need doing here? Should it be ignored? + }//catch// + catch(Throwable e) { + Debug.log(e); + //TODO: There needs to be more specfic error handling if we got here. + }//catch// + }//while// +}//run()// +}//NetworkListener// \ No newline at end of file diff --git a/Foundation Web Core/src/com/foundation/web/server/PassThroughSocketContext.java b/Foundation Web Core/src/com/foundation/web/server/PassThroughSocketContext.java new file mode 100644 index 0000000..ed41e8f --- /dev/null +++ b/Foundation Web Core/src/com/foundation/web/server/PassThroughSocketContext.java @@ -0,0 +1,168 @@ +package com.foundation.web.server; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.SocketChannel; +import com.foundation.web.server.WebServer.RegisterKeyRunnable; + +/** + * Used by the SocketContext to create a connection to a remote process that will receive all client data once decrypted, and whose output will be encrypted and sent directly to the client. + * Allows the web server to act as an SSL front to another web server or service. + */ +public class PassThroughSocketContext extends AbstractSocketContext { + private MessageBuffer pendingMessageBuffer = null; + private MessageBuffer lastAddedMessageBuffer = null; + /** The byte buffer used to read data from the socket. */ + public ByteBuffer socketReadBuffer = ByteBuffer.allocate(BUFFER_SIZE); +/** + * PassThroughSocketContext constructor. + * @param linkedClientContext + * @param address + * @param port + * @throws IOException + */ +public PassThroughSocketContext(SocketContext linkedClientContext, String address, int port) throws IOException { + super(linkedClientContext.getNetworkListener()); + this.relatedSocketContext = linkedClientContext; + SocketChannel channel = SocketChannel.open(); + RegisterKeyRunnable runnable; + + //Connect while still blocking - will wait until the connection finishes or times out.// + channel.connect(new InetSocketAddress(address, port)); + //Setup the channel for non-blocking from here on out.// + channel.configureBlocking(false); + channel.socket().setSendBufferSize(SEND_BUFFER_SIZE); + channel.socket().setReceiveBufferSize(RECEIVE_BUFFER_SIZE); + channel.socket().setTcpNoDelay(false); + getNetworkListener().queue(runnable = new RegisterKeyRunnable(this, channel, linkedClientContext.key.selector())); + linkedClientContext.key.selector().wakeup(); + runnable.waitForRun(); + key = runnable.getKey(); + + //Set the initial interest ops to read.// + synchronized(key) { + key.interestOps(SelectionKey.OP_READ); + }//synchronized// + + key.selector().wakeup(); +}//PassThroughSocketContext()// +/* (non-Javadoc) + * @see com.foundation.web.server.WebServer.AbstractSocketContext#getLock() + */ +protected Object getLock() { + return getRelatedSocketContext(); +}//getLock()// +/* (non-Javadoc) + * @see com.foundation.web.server.WebServer.AbstractSocketContext#processResponses() + */ +protected synchronized void writeOutgoingMessages() throws IOException { + //Actually this is called when a request is being sent via the pass through socket (sending the request to the remote server).// + //Synchronized to avoid accessing the pendingMessageBuffer and lastAddedMessageBuffer at the same time as a thread that is calling passThrough(ByteBuffer) which also accesses these variables.// + boolean result = true; + + if(result && pendingMessageBuffer != null) { + //Check to see if the outbound message is prepared to send more content.// + if(!pendingMessageBuffer.getBuffer().hasRemaining()) { + //Load the next pending outbound message in the chain.// + if(pendingMessageBuffer.getNext() != null) { + pendingMessageBuffer = pendingMessageBuffer.getNext(); + }//if// + else { + //Wait until additional message bytes are available.// + result = false; + pendingMessageBuffer = null; + lastAddedMessageBuffer = null; + }//else// + }//if// + + //Keep sending encrypted frames until the output buffer is full, or we run out of message to send.// + while(result && (pendingMessageBuffer != null) && pendingMessageBuffer.getBuffer().hasRemaining()) { + //Write the bytes to the stream.// + ((SocketChannel) key.channel()).write(pendingMessageBuffer.getBuffer()); + + //If not all the bytes could be written then we will need to wait until we can write more.// + if(pendingMessageBuffer.getBuffer().hasRemaining()) { + result = false; + }//if// + else { + //Load the next pending outbound message in the chain.// + if(pendingMessageBuffer.getNext() != null) { + pendingMessageBuffer = pendingMessageBuffer.getNext(); + }//if// + else { + //Wait until additional message bytes are available.// + result = false; + pendingMessageBuffer = null; + lastAddedMessageBuffer = null; + }//else// + }//else// + }//while// + }//if// +}//processResponses()// +/* (non-Javadoc) + * @see com.foundation.web.server.WebServer.AbstractSocketContext#processRequest() + */ +protected void readIncomingMessages() throws IOException { + //Actually this is called when a response is being processed via the pass through socket (remote process that received the request).// + int count = 1; + int loopCount = 0; + boolean result = true; + SocketChannel channel = (SocketChannel) key.channel(); + + //While we have a count greater than zero, indicating that some data is comming through, keep reading and processing the data.// + //Note: We are throddling this for active connections to prevent a single connection from hogging all the resources.// + while(loopCount < 10 && result && count > 0) { + loopCount++; + count = channel.read(socketReadBuffer); + socketReadBuffer.flip(); + + if(count == -1) { + //The socket has been closed by the client.// + try {relatedSocketContext.close();} catch(Throwable e) {} + }//if// + else if(socketReadBuffer.hasRemaining()) { + result = relatedSocketContext.passThrough(socketReadBuffer); + socketReadBuffer.compact(); + }//else// + else { + socketReadBuffer.compact(); + break; + }//else// + }//while// +}//processRequest()// +/* (non-Javadoc) + * @see com.foundation.web.server.WebServer.AbstractSocketContext#passThrough(java.nio.ByteBuffer) + */ +protected synchronized boolean passThrough(ByteBuffer buffer) { + ByteBuffer messageBytes = ByteBuffer.allocate(buffer.remaining()); + MessageBuffer message; + + //Create a new buffer to hold the data so we don't modify the passed buffer (other than to update its position).// + messageBytes = ByteBuffer.allocate(buffer.remaining()); + messageBytes.put(buffer); + message = new MessageBuffer(messageBytes); + + //Chain the message into the linked list. + if(lastAddedMessageBuffer == null) { + pendingMessageBuffer = lastAddedMessageBuffer = message; + }//if// + else { + lastAddedMessageBuffer.setNext(message); + lastAddedMessageBuffer = message; + }//else// + + return true; +}//passThrough()// +protected synchronized void close() { + try {if(key != null && key.channel() != null) key.channel().close();} catch(Throwable e) {} + try {if(key != null) key.cancel();} catch(Throwable e) {} +}//close()// +/* (non-Javadoc) + * @see com.foundation.web.server.WebServer.AbstractSocketContext#hasPendingWrite() + */ +protected boolean hasPendingWrite() { + return pendingMessageBuffer != null; +}//hasPendingWrite()// +}//PassThroughSocketContext// \ No newline at end of file diff --git a/Foundation Web Core/src/com/foundation/web/server/Request.java b/Foundation Web Core/src/com/foundation/web/server/Request.java index a952278..c3f860a 100644 --- a/Foundation Web Core/src/com/foundation/web/server/Request.java +++ b/Foundation Web Core/src/com/foundation/web/server/Request.java @@ -475,7 +475,7 @@ public class Request implements IRequest { else if(fieldName.equals("if-modified-since")) { if(fieldValue.trim().length() > 0) { //Mozilla sometimes sends an empty value. Bad Mozilla.// try { - cacheDate = WebServer.getHttpDateFormat().parse(fieldValue.trim()); + cacheDate = SocketContext.getHttpDateFormat().parse(fieldValue.trim()); }//try// catch(Throwable e) { Debug.log("Unexpected date format sent by the web browser when using an If-Modified-Since request header: '" + fieldValue + "'. The server can recover from this error.", e); @@ -514,7 +514,7 @@ public class Request implements IRequest { else if(fieldName.equals("if-unmodified-since")) { if(fieldValue.trim().length() > 0) { //Shouldn't ever happen.// try { - unmodifiedSince = WebServer.getHttpDateFormat().parse(fieldValue.trim()); + unmodifiedSince = SocketContext.getHttpDateFormat().parse(fieldValue.trim()); }//try// catch(Throwable e) { Debug.log("Unexpected date format sent by the web browser when using an Unmodified-Since request header: '" + fieldValue + "'. The server can recover from this error.", e); diff --git a/Foundation Web Core/src/com/foundation/web/server/ServerSocketContext.java b/Foundation Web Core/src/com/foundation/web/server/ServerSocketContext.java new file mode 100644 index 0000000..f9ba543 --- /dev/null +++ b/Foundation Web Core/src/com/foundation/web/server/ServerSocketContext.java @@ -0,0 +1,15 @@ +package com.foundation.web.server; + +import com.foundation.web.server.WebServer.ServiceListener; + +/** + * Provides a place connection oriented data. + */ +public class ServerSocketContext implements IChannelContext { + public ServiceListener serviceListener = null; + + public ServerSocketContext(ServiceListener serviceListener) { + super(); + this.serviceListener = serviceListener; + }//ServerSocketContext()// +}//ServerSocketContext// \ No newline at end of file diff --git a/Foundation Web Core/src/com/foundation/web/server/SocketContext.java b/Foundation Web Core/src/com/foundation/web/server/SocketContext.java new file mode 100644 index 0000000..e043a0d --- /dev/null +++ b/Foundation Web Core/src/com/foundation/web/server/SocketContext.java @@ -0,0 +1,2634 @@ +package com.foundation.web.server; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.io.UnsupportedEncodingException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.SelectionKey; +import java.nio.channels.SocketChannel; +import java.text.SimpleDateFormat; +import java.util.Date; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLEngineResult.HandshakeStatus; +import javax.net.ssl.SSLEngineResult.Status; + +import com.common.comparison.Comparator; +import com.common.debug.Debug; +import com.common.io.StreamSupport; +import com.common.util.IIterator; +import com.common.util.LiteHashMap; +import com.common.util.LiteList; +import com.common.util.Queue; +import com.common.util.StreamBuffer; +import com.foundation.web.interfaces.IConnectionContext; +import com.foundation.web.interfaces.IContent; +import com.foundation.web.interfaces.IMimeType; +import com.foundation.web.interfaces.IPassThroughDomain; +import com.foundation.web.interfaces.IRequest; +import com.foundation.web.interfaces.IResponse; +import com.foundation.web.interfaces.ISession; +import com.foundation.web.interfaces.ISessionLifecycleAware; +import com.foundation.web.interfaces.IStreamedWebsocketMessage; +import com.foundation.web.interfaces.IWebApplication; +import com.foundation.web.interfaces.WebsocketHandler; +import com.foundation.web.server.Request.ContentPart; +import com.foundation.web.server.WebServer.IWebApplicationContainerProvider; +import com.foundation.web.server.WebServer.IgnoredIOException; +import com.foundation.web.server.WebServer.TlsFailureException; + +/** + * Provides a place for connection oriented data. + *

Note that a client server session can have multiple socket contexts (one for each socket) and that the socket context may be used to access multiple applications hosted on this server.

+ */ +public class SocketContext extends AbstractSocketContext implements IWebApplicationContainerProvider, IConnectionContext { + /** The server socket reference that created the socket. This will be null if the socket was not created with a server socket (shouldn't happen in a web server). */ + public ServerSocketContext serverSocketContext = null; + /** The web application's container for the application associated with this connection to a client. A connection is only allowed to access a single application. */ + public WebApplicationContainer webApplicationContainer = null; + /** A temporary storage location for part of an HTTP message header. Warning: This is used by the reading thread ONLY - never by the processing/writing threads. */ + public StringBuffer messageHeaderFragment = null; + /** A reference to the request object currently being processed. Warning: This is used by the reading thread ONLY - never by the processing/writing threads. */ + public Request request = null; + /** The count of stored content bytes for the request. This is valid for any type of request. */ + public int requestContentPosition = 0; + /** The multi-part message characters remaining to be processed from the last message fragment received. */ + public byte[] remainingPartBytes = null; + /** The multi-part message count of characters/bytes read thus far. This ensures the entire message is read properly. */ + public int partReadCount = 0; + /** The mutli-part message count of characters/bytes written to the buffer as the message from the client is parsed. Used to index into the buffer by the various ContentPart instances generated. */ + public int partWriteCount = 0; + /** The part that is currently being read and spans more than one message fragment. */ + public ContentPart currentPart = null; + /** Whether the final part boundary has already been found. If this is true then the message bytes should keep getting read until the partReadCount == contentLength. */ + public boolean endPartBoundaryFound = false; + /** The bytes containing the unencrypted outbound message that is waiting for the socket to allow a write. */ + public MessageBuffer/*ByteBuffer*/ currentOutboundMessage = null; + /** The last message buffer added to the pending outbound message chain (linked list). Used only for pass through contexts currently since locally handled messages link the reponses together into a list. */ + private MessageBuffer lastOutboundMessage = null; + /** The byte buffer used to read data from the socket. This must be null if a SSL engine is provided. */ + public ByteBuffer socketReadBuffer = null; + /** The buffer used to store the initial data in a SSL/TLS connection. The buffering is necessary to allow us to pre-read the client's hello message to gather the domain the client is connecting to - allowing for the correct SSL engine to be used. */ + public StreamBuffer initialBuffer = null; + /** Whether the TLS domain extension was used. If false then the default SSL domain should be used to attempt an anonymous connection with the client such that an error page can be displayed prompting the user to upgrade their browser to something modern. */ + public boolean tlsFailure = false; + /** The domain name the client is connecting to. This is provided by the initial TLS hello message, and is used to select the correct SSL context for the desired web application. */ + public String domain = null; + /** Non-null if the socket is using SSL to secure communications. */ + public SSLEngine sslEngine = null; + /** Whether the ssl engine needs to send handshake data to the client and has not yet generated it. */ + public boolean sslNeedsWrap = false; + /** The reusable buffer containing encrypted data from the client. */ + public ByteBuffer encryptedReadBuffer = null; + /** The reusable buffer containing unencrypted data from the client. */ + public ByteBuffer unencryptedReadBuffer = null; + /** The reusable buffer containing encrypted data being sent to the client. */ + public ByteBuffer encryptedWriteBuffer = null; + /** The last used request number which identifies the sequence for the requests. */ + private int lastRequestNumber = 0; + /** The response we are currently processing. */ + private Response currentResponse = null; + /** The response we are will process last. */ + private Response lastResponse = null; + /** Tracks the number of bytes sent from the current response. This is only used when debugging. */ + private int sentBytes = 0; + /** Tracks the getWebServer().debug output for the current request/response cycle. This is only used when debugging. */ + public StringBuffer debugBuffer = null; //getWebServer().debug ? new StringBuffer(1000) : null; + /** The optional application specific data indexed by an application spectific key. Elements (values, not keys) which implement ISessionLifecycleAware will be released when the socket context is released. */ + private LiteHashMap applicationDataMap; + /** Used to identify the first unencrypted message (ignored if ssl is being used) so that forwarding to a remote server can be accomplished. */ + private boolean isFirstUnencryptedMessage = true; + /** Whether this is a websocket connection. */ + private boolean isWebsocket = false; + /** The protocol passed when this connection was upgraded to a websocket. */ + private String websocketProtocol = null; + /** The maximum number of bytes in an allowed websocket message (may be composed of multiple frames, does not include the frame header sizes). While this is a long, we really can't read longer than Integer.MAX_VALUE sized frames. So the message might be within size limits, but it might still be rejected if the frame exceeds the server's capacity to read a frame. */ + private long websocketMaxMessageLength = 0; + /** The reusable frame header buffer. */ + private byte[] websocketFrameHeader = null; + /** The index into the frame header of the last read byte. */ + private int websocketFrameHeaderIndex = 0; + /** The next partial message (single messages may be broken into frames by the client). */ + private StreamBuffer websocketPartialReceivedMessage = null; + /** The remaining bytes to be read on the current message frame (the frame header and part of the frame was read previously). */ + private int websocketFrameRemainder = 0; + /** Whether the frame currently being read in the current message being received is the last frame in the message (when the frame is fully read it will trigger the message to be processed). */ + private boolean websocketIsLastFrameInMessage = false; + /** The op code for the currently reading message, or zero if the last message was completed. */ + private int websocketMessageOpCode = 0; + /** The currently reading frame's mask key used to decode the frame data. */ + private byte[] websocketMessageMaskKey = null; + /** The queue used to hold outgoing messages before they are sent. Will contain only String, byte[], or IConnectionContext.IStreamedWebsocketMessage instances. */ + private Queue websocketPendingMessages = null; + /** The streambuffer for the currently sending message. This will be for the current part of the streaming message if websocketStreamingMessage is non-null. */ + private ByteBuffer websocketSendingMessage = null; + /** The streaming message handler which will be set only if the currently sending message is streaming. */ + private IStreamedWebsocketMessage websocketStreamingMessage = null; + /** The application specified handler called when websocket events occur (messages received, socket closed, etc). */ + private WebsocketHandler websocketHandler = null; +/** + * SocketContext constructor. + * @param serverSocketContext The context for the server socket that accepted this socket. + * @param networkListener The network listener that this socket exists within. + */ +public SocketContext(ServerSocketContext serverSocketContext, NetworkListener networkListener) { + super(networkListener); +}//SocketContext()// +/** + * Gets the pass through socket context associated with this socket context, or null if none exists. + * @return The socket context for the pass through socket used to handle all incoming requests from the client on this socket. + */ +protected PassThroughSocketContext getPassThroughSocketContext() { + return (PassThroughSocketContext) getRelatedSocketContext(); +}//getPassThroughSocketContext()// +/* (non-Javadoc) + * @see com.foundation.web.server.WebServer.AbstractSocketContext#getLock() + */ +protected Object getLock() { + return this; +}//getLock()// +protected synchronized void close() { + try { + if(websocketHandler != null) { + websocketHandler.connectionClosed(); + websocketHandler = null; + }//if// + }//try// + catch(Throwable e) { + Debug.log(e); + }//catch// + + try { + if(applicationDataMap != null) { + for(IIterator iterator = applicationDataMap.valueIterator(); iterator.hasNext(); ) { + Object next = iterator.next(); + + //Ensure the code keeps going even if there is a problem cleaning up.// + try { + if(next instanceof ISessionLifecycleAware) ((ISessionLifecycleAware) next).release(); + }//try// + catch(Throwable e) { + Debug.log(e); + }//catch// + }//for// + }//if// + }//try// + catch(Throwable e) { + Debug.log(e); + }//catch// + + try {if(key != null && key.channel() != null) key.channel().close();} catch(Throwable e) {} + try {if(key != null) key.cancel();} catch(Throwable e) {} + //Clean up after the response and request.// + //try {while(currentOutboundMessage != null) {currentOutboundMessage.close(); currentOutboundMessage = currentOutboundMessage.getNext();}} catch(Throwable e2) {} + try {if(currentResponse != null) currentResponse.close();} catch(Throwable e2) {} + + if(getPassThroughSocketContext() != null) { + getPassThroughSocketContext().close(); + }//if// +}//close()// +/* (non-Javadoc) + * @see com.foundation.web.interfaces.IConnectionContext#getApplicationData(java.lang.String) + */ +public Object getApplicationData(String key) { + return applicationDataMap == null ? null : applicationDataMap.get(key); +}//getApplicationData()// +/* (non-Javadoc) + * @see com.foundation.web.interfaces.IConnectionContext#setApplicationData(java.lang.String, java.lang.Object) + */ +public void setApplicationData(String key, Object applicationData) { + if(applicationDataMap == null) { + applicationDataMap = new LiteHashMap(10); + }//if// + + applicationDataMap.put(key, applicationData); +}//setApplicationData()// +/* (non-Javadoc) + * @see com.foundation.web.server.WebServer.IWebApplicationContainerProvider#getWebApplicationContainer() + */ +public WebApplicationContainer getWebApplicationContainer() { + return webApplicationContainer; +}//getWebApplicationContainer()// +/** + * Whether the socket is an SSL/TLS socket. + * @return Whether data streamed over the socket is encrypted using SSL/TLS. + */ +public boolean isSsl() { + return socketReadBuffer == null; +}//isSsl()// +/** + * Queues an outbound client message by adding it to the linked list of message buffers. Handles synchronizing on this SocketContext to prevent multiple threads from adding messages at once. Also handles notifying the ServiceListener that it should update it's write flags for the socket. + * @param messageBuffer The buffer to be added to the end. + */ +private void queueOutboundClientMessage(MessageBuffer messageBuffer) { + boolean notify = false; + + synchronized(this) { + if(currentOutboundMessage == null) { + lastOutboundMessage = currentOutboundMessage = messageBuffer; + notify = true; + }//if// + else { + lastOutboundMessage.setNext(messageBuffer); + lastOutboundMessage = messageBuffer; + }//else// + }//synchronized()// + + if(notify) { + notifyListenerOfPendingWrite(); + }//if// +}//queueOutboundClientMessage()// +private void writeSessionCookies(PrintStream pout) { + Response response = currentResponse; + ISession session = response.getSession(); + + if(session != null) { + //Write the session id only if it has changed.// + if(!Comparator.equals(response.getRequest().getSessionId(), session.getSessionId())) { + pout.print("Set-Cookie: sessionId=" + session.getSessionId() + ";path=/;\r\n"); + }//if// + + //Write the secure session id only if it has changed.// + if(!Comparator.equals(response.getRequest().getSecureSessionId(), session.getSecureSessionId())) { + pout.print("Set-Cookie: secureSessionId=" + (session.getSecureSessionId() == null ? "" : session.getSecureSessionId()) + ";path=/;secure\r\n"); + }//if// + + if(response.getRequest().isLoggedIn() != session.getIsLoggedIn()) { + pout.print("Set-Cookie: isLoggedIn=" + session.getIsLoggedIn() + ";path=/;\r\n"); + }//if// + }//if// +}//writeSessionCookies()// +/** + * Processes the next response in the sequence. + *

Note: The caller must synchronize on this context to prevent multiple threads from accessing the context at the same time.

+ * @result Whether request is in a receive state. Will be false if the request generated a response that could not be completely transmitted. + */ +private void prepareResponse() { + Response response = currentResponse; + Request request = (Request) response.getRequest(); + byte[] headerBytes = null; + IContent content = null; + ByteBuffer buffer = null; + + try { + //Wrap the response in http cloths. The HeaderFieldNames will be set if the response was provided with a completely custom HTTP header to be used.// + if(response.getHeaderFieldNames() != null) { + ByteArrayOutputStream bout = new ByteArrayOutputStream(1000); + PrintStream pout = new PrintStream(bout, true, "ASCII"); + LiteList headerFieldNames = response.getHeaderFieldNames(); + LiteHashMap headerFieldMap = response.getHeaderFieldMap(); + + //Write the response line which is mapped to the null field name.// + pout.print(headerFieldMap.get(null)); + pout.print("\r\n"); + + //Write the rest of the response header lines in order.// + for(int index = 0; index < headerFieldNames.getSize(); index++) { + String headerFieldName = (String) headerFieldNames.get(index); + String headerFieldValue = (String) headerFieldMap.get(headerFieldName); + + if(headerFieldName.equals("Server")) { + pout.print("Server: DE/1.0"); + }//if// + else { + pout.print(headerFieldName); + pout.print(": "); + pout.print(headerFieldValue); + }//else// + + pout.print("\r\n"); + }//for// + + //Write out any cookies necessary to retain our session.// + writeSessionCookies(pout); + + //End the header.// + pout.print("\r\n"); + pout.close(); + headerBytes = bout.toByteArray(); + + //Prepare the content for delivery.// + content = response.getContent(); + }//if// + else if(response.getForwardUri() != null) { + ByteArrayOutputStream bout = new ByteArrayOutputStream(1000); + PrintStream pout = new PrintStream(bout, true, "ASCII"); + //String today = format.format(new Date()); + + //The 303 code may not be fully supported by browsers.// + //if(request.getHttpVersion().equalsIgnoreCase("HTTP/1.1") || request.getHttpVersion().equalsIgnoreCase("HTTP/1.2")) { + // pout.print("HTTP/1.1 303 Forwarded\r\n"); + //}//if// + //else { + pout.print("HTTP/1.1 302 Moved Temporarily\r\n"); + //}//else// + + writeSessionCookies(pout); + + pout.print("Location: " + response.getForwardUri() + "\r\n"); + + //Note: Encoded URL's will have their parameters in the content, not in the URL.// + if(response.getContent() == null) { + pout.print("Content-Length: 0\r\n"); + }//if// + else { + pout.print("Content-Length: " + response.getContent().getSize() + "\r\n"); + pout.print("Content-Type: application/x-www-form-urlencoded\r\n"); + }//else// + + pout.print("\r\n"); + pout.close(); + headerBytes = bout.toByteArray(); + }//else if// + else if((content = response.getContent()) != null) { //Convert the result into a stream of bytes.// + ByteArrayOutputStream bout = new ByteArrayOutputStream(1000); + PrintStream pout = new PrintStream(bout, true, "ASCII"); + Date lastModifiedDate = content.getLastModifiedDate(); + String cacheDirective = null; + IMimeType mimeType = content.getMimeType(response.getApplication() != null ? response.getApplication().getMimeTypeProvider() : null); + boolean isDownloaded = content != null && (content.getIsDownloaded() == null ? mimeType != null && mimeType.isDownloaded() : content.getIsDownloaded().booleanValue()); + boolean compress = response.getCompress().booleanValue() && (mimeType == null || mimeType.isCompressable()); + int compressionType = 0; //0: none, 1: gzip, ..// + + if(compress) { + //Check for an encoding allowed by the client that we know how to use.// + if(request.getAllowedEncodings() == null || request.getAllowedEncodings().length < 1) { + compress = false; + }//if// + else { + compress = false; + + //Ensure we have an allowed encoding we know how to use.// + for(int index = 0; !compress && index < request.getAllowedEncodings().length; index++) { + String encoding = request.getAllowedEncodings()[index]; + + if(encoding.equalsIgnoreCase("gzip")) { + compress = true; + compressionType = 1; + }//if// + }//for// + }//else// + }//if// + + if(response.isError()) { + if(response.getHeader() != null) { + pout.print(response.getHeader()); + }//if// + else { + pout.print("HTTP/1.1 404 Resource Not Found\r\n"); + }//else// + }//if// + else if(response.getCustomHeader() != null) { + pout.print(response.getCustomHeader()); + }//else if// + else if(isDownloaded && request.getRange() != null) { + pout.print("HTTP/1.1 206 Partial Content\r\n"); + }//else if// + else { + pout.print("HTTP/1.1 200 OK\r\n"); + }//else// + + pout.print("Content-Length: " + (content != null ? content.getSize() : 0) + "\r\n"); + + if(compress) { + //TODO: Add others? + if(compressionType == 1) { + content = new GzipContent(content); + pout.print("Content-Encoding: gzip\r\n"); + }//if// + }//if// + + if(content != null) { + //Note: The character set gives IE indigestion for some reason.// + pout.print("Content-Type: " + (mimeType != null ? mimeType.getMimeName() : "text/html") + "; charset=" + (response.getCharacterSet() == null ? "UTF-8" : response.getCharacterSet()) + "\r\n"); + cacheDirective = content.getCacheDirective(); + + if(isDownloaded) { + pout.print("Content-Disposition: attachment; filename=\"" + content.getDownloadName() + "\";\r\n"); + pout.print("Accept-Ranges: bytes\r\n"); + + if(request.getRange() != null) { +// Debug.log("Sending a ranged response: " + request.getRange() + " content range: (" + content.getStart() + " - " + content.getEnd() + "/" + content.getSize() + ")."); + pout.print("Range: " + request.getRange() + "\r\n"); + pout.print("Content-Range: bytes " + content.getStart() + "-" + content.getEnd() + "/" + content.getSize() + "\r\n"); + }//if// + }//if// + }//if// + + writeSessionCookies(pout); + + pout.print("Server: DE/1.0\r\n"); + //TODO: IE has a problem with caching and forwarding/redirecting. A page that redirects to another page that was previously cached does not result in IE sending a request for the forwarded content.// + //private / no-cache + + if(content.getExpiresDirective() != null) { + pout.print("Expires: " + getHttpDateFormat().format(content.getExpiresDirective())); + }//if// + + if(cacheDirective != null) { + pout.print("Cache-Control: " + cacheDirective + "\r\n"); + }//if// + else { + int cacheLength = content.getCacheLength() != null ? content.getCacheLength().intValue() : mimeType != null ? mimeType.getDefaultCacheLength() : IMimeType.CACHE_LENGTH_NEVER_CACHE; + + if(cacheLength > 0) { + pout.print("Cache-Control: public, max-age=" + cacheLength + "\r\n"); + }//if// + else if(cacheLength == IMimeType.CACHE_LENGTH_ALWAYS_TEST) { + pout.print("Cache-Control: public, pre-check=0, post-check=120\r\n"); + }//else if// + else if(cacheLength == IMimeType.CACHE_LENGTH_NEVER_CACHE) { + pout.print("Cache-Control: no-cache\r\n"); + }//else if// + else { + pout.print("Cache-Control: no-store\r\n"); + }//else// + }//else// + + //TODO: Determine if we need to use age. + //pout.print("Age: 0\r\n"); + //TODO: Determine if we need to use ETags + + if(lastModifiedDate != null) { + SimpleDateFormat format = getHttpDateFormat(); + + pout.print("Last-Modified: " + format.format(lastModifiedDate) + "\r\n"); + pout.print("Date: " + format.format(new Date()) + "\r\n"); + }//if// + + pout.print("\r\n"); + headerBytes = bout.toByteArray(); + }//else if// + else { + ByteArrayOutputStream bout = new ByteArrayOutputStream(1000); + PrintStream pout = new PrintStream(bout, true, "ASCII"); + + if(response.isError()) { + if(response.getHeader() != null) { + pout.print(response.getHeader()); + }//if// + else { + pout.print("HTTP/1.1 404 Resource Not Found\r\n"); + }//else// + }//if// + else if(response.getCustomHeader() != null) { + pout.print(response.getCustomHeader()); + }//else if// + else { + Debug.log(new RuntimeException("The response to: " + response.getRequest().getHeaderText() + " had no response content!")); + pout.print("HTTP/1.1 200 OK\r\n"); + }//else// + + writeSessionCookies(pout); + pout.print("Content-Length: 0\r\n"); + pout.print("Server: DE/1.0\r\n"); + pout.print("\r\n"); + pout.close(); + headerBytes = bout.toByteArray(); + }//else// + + buffer = ByteBuffer.allocate(headerBytes.length > 2000 ? headerBytes.length : 2000); + buffer.put(headerBytes); + + if(getWebServer().debug) { + //Test code... + ByteBuffer buffer2 = ByteBuffer.allocate(headerBytes.length); + buffer2.put(headerBytes); + buffer2.flip(); + CharBuffer ch = decoder.decode(buffer2); +// debugBuffer.append("Sending message:\n"); +// debugBuffer.append(ch.toString()); +// debugBuffer.append("\nResponse Size: " + (headerBytes.length + (content != null ? content.getSize() : 0)) + "\n"); + Debug.log(ch.toString()); + }//if// + + //Ignore the content if we are only accessing the header.// +// if(content != null && request.getRequestType() != Request.TYPE_HEAD) { +// content.get(buffer); +// }//if// + + //Save the buffer as the current pending outbound message for this socket context.// + currentOutboundMessage = new MessageBuffer(buffer, response, content != null && request.getRequestType() != Request.TYPE_HEAD ? content : null); + }//try// + catch(Throwable e) { + Debug.log("Fatal Error: Failed to build and send the response message due to an exception.", e); + //Force the channel to close.// + try {key.channel().close();} catch(Throwable e2) {} + //Clean up after the request and response.// + try {response.close();} catch(Throwable e2) {} + }//catch// +}//prepareResponse()// +/** + * Adds a HTTP response to the socket context. + *

Note: We must synchronize since a socket could be used to access multiple applications and thus mutliple sessions.

+ * @param response The response to be added. + * @result Whether request is in a receive state. Will be false if the request generated a response that could not be completely transmitted. + */ +public synchronized boolean sendHttpResponse(Response response) { + if(currentResponse != null) { + lastResponse.setNextResponse(response); + lastResponse = response; + }//if// + else { + lastResponse = currentResponse = response; + sentBytes = 0; + prepareResponse(); + + //Note: Not going to process the response on this thread. Allow the flag to be set for writing to the socket, and have the next thread in the network listener handle the write. This allows for cleaner code and pipelining without all the synchronizing. +// result = internalProcessResponses(); + }//else// + + request = null; + + return false; +}//sendHttpResponse()// +/* (non-Javadoc) + * @see com.foundation.web.server.WebServer.AbstractSocketContext#passThrough(java.nio.ByteBuffer) + */ +protected synchronized boolean passThrough(ByteBuffer buffer) { + ByteBuffer messageBytes = ByteBuffer.allocate(buffer.remaining()); + MessageBuffer message; + + //Create a new buffer to hold the data so we don't modify the passed buffer (other than to update its position).// + messageBytes = ByteBuffer.allocate(buffer.remaining()); + messageBytes.put(buffer); + message = new MessageBuffer(messageBytes); + + //Chain the message into the linked list. + if(lastOutboundMessage == null || currentOutboundMessage == null) { + currentOutboundMessage = lastOutboundMessage = message; + }//if// + else { + lastOutboundMessage.setNext(message); + lastOutboundMessage = message; + }//else// + + return true; +}//passThrough()// +/* (non-Javadoc) + * @see com.foundation.web.server.WebServer.AbstractSocketContext#processResponses() + */ +protected void writeOutgoingMessages() throws IOException { + if(getPassThroughSocketContext() != null) { + //Synchronized to avoid multiple threads accessing the pendingOutboundMessage chain at one time and updating the write flag out of order (could happen if we enabled request chaining over a single socket).// + synchronized(this) { + writeClientBoundMessage(); + }//synchronized// + }//if// + else if(isWebsocket) { + //Right after upgrading the socket we have one last HTTP response to process.// + if(currentResponse != null) { + internalProcessResponses(); + }//if// + + internalProcessWebsocketMessages(); + }//else if// + else { + //Go directly to writing the client response if we are just passing everything through to another process.// + internalProcessResponses(); + }//else// +}//processCurrentResponse()// +/** + * Loads the next outbound websocket message and attempts to write it to the socket until all outbound messages have been sent, or the socket's buffers are full and a wait is required. + * If a message could only be partially sent then the next call will attempt to finish sending it. + */ +private void internalProcessWebsocketMessages() { + if(websocketSendingMessage == null) { + loadNextWebsocketMessage(); + }//if// + + while(websocketSendingMessage != null) { + //If the socket is open then send the next buffer of data.// + if(key.channel().isOpen()) { + if(currentOutboundMessage != null) { + //Put the sending message in a MessageBuffer (pendingOutboundMessage).// + currentOutboundMessage = new MessageBuffer(websocketSendingMessage); + }//if// + + //Write the pendingOutboundMessage to the socket.// + if(writeClientBoundMessage()) { + websocketSendingMessage = null; + currentOutboundMessage = null; + }//if// + }//if// + + //If we finished sending the message then load the next one.// + if(websocketSendingMessage == null) { + loadNextWebsocketMessage(); + }//if// + }//while// +}//internalProcessWebsocketMessages()// +/** + * Loads and prepares the next websocket message from the queue of pending messages. + * Clears the pending message attributes if there isn't a pending message to be processed. + * The caller can check websocketSendingMessage == null to see if there is a ready message. + */ +private void loadNextWebsocketMessage() { + Object next = null; + boolean isLastPart = true; + + if(websocketStreamingMessage != null && websocketStreamingMessage.hasNextPart()) { + next = websocketStreamingMessage.getNextPart(); + isLastPart = !websocketStreamingMessage.hasNextPart(); + + //Ensure that we got a string or byte array.// + if(!(next instanceof String || next instanceof byte[])) { + throw new RuntimeException("Invalid streaming message part type."); + }//if// + }//if// + else { + synchronized(websocketPendingMessages) { + if(websocketPendingMessages.getSize() > 0) { + next = websocketPendingMessages.dequeue(); + }//if// + }//synchronized// + }//else// + + if(next != null) { + byte[] bytes = null; + int opCode = 0; + int length = 0; + + if(next instanceof String) { + try {bytes = ((String) next).getBytes("UTF-8");} catch(Throwable e) {Debug.log(e);} + opCode = websocketStreamingMessage == null ? 0x01 : 0; + length = bytes.length; + }//if// + else if(next instanceof byte[]) { + bytes = (byte[]) next; + opCode = websocketStreamingMessage == null ? 0x02 : 0; + length = bytes.length; + }//else if// + else if(next instanceof Byte) { //Control Message// + opCode = ((Byte) next).byteValue(); + }//else if// + else if(next instanceof IStreamedWebsocketMessage) { + websocketStreamingMessage = (IStreamedWebsocketMessage) next; + next = websocketStreamingMessage.getNextPart(); + isLastPart = !websocketStreamingMessage.hasNextPart(); + + if(next instanceof String) { + try {bytes = ((String) next).getBytes("UTF-8");} catch(Throwable e) {Debug.log(e);} + opCode = 0x01; //Text// + length = bytes.length; + }//if// + else if(next instanceof byte[]) { + bytes = (byte[]) next; + opCode = 0x02; //Binary// + length = bytes.length; + }//else if// + else { + throw new RuntimeException("Invalid streaming message part type."); + }//if// + }//else if// + + websocketSendingMessage = ByteBuffer.allocate(14 + length); + websocketSendingMessage.put((byte) (isLastPart ? 0x8 : 0)); + websocketSendingMessage.put((byte) opCode); + + //Write the length differently based on how long the content is.// + if(length < 126) { + websocketSendingMessage.put((byte) length); +// websocketSendingMessage.putLong(0, StreamBuffer.NUMBER_MSF); + }//if// + else if(length < 65535) { + websocketSendingMessage.put((byte) 126); + websocketSendingMessage.putShort((short) (length & 0xFFFF)); +// websocketSendingMessage.putShort((short) 0); + websocketSendingMessage.putInt(0); + }//else if// + else { + websocketSendingMessage.put((byte) 127); + websocketSendingMessage.putLong((long) length); + }//else// + + //The server doesn't use a mask key.// +// websocketSendingMessage.putInt(0); + //Put the content at the end of the message.// + websocketSendingMessage.put(bytes); + }//if// + else { + websocketSendingMessage = null; + websocketStreamingMessage = null; + }//else// +}//loadNextWebsocketMessage()// +/** + * @return + */ +private synchronized void internalProcessResponses() { + boolean finishedSending = true; + + //Keep sending responses while the buffers are not full and there is another response to send.// + while(finishedSending && currentResponse != null) { + //If the socket is open then send the next buffer of data.// + if(key.channel().isOpen()) { + //Send the pending response object's prepared buffer of data.// + finishedSending = writeClientBoundMessage(); + }//if// + + //Close the response if successfully sent, or if the socket is closed.// + if(finishedSending || !key.channel().isOpen()) { + try {currentResponse.close();} catch(Throwable e) {} + }//if// + + //If we finished sending the current response then load the next one.// + if(finishedSending) { + currentResponse = currentResponse.getNextResponse(); + + if(currentResponse == null) { + lastResponse = null; + }//if// + else if(key.channel().isOpen()) { + //Prep the next response object for sending.// + prepareResponse(); + }//else// + else { + //Clean up after all the left over responses.// + while(currentResponse != null) { + currentResponse.close(); + currentResponse = currentResponse.getNextResponse(); + }//while// + + currentResponse = null; + lastResponse = null; + }//else// + }//if// + }//while// +}//processCurrentResponse()// +/** + * Sends a response to the client. + * @return Whether the response could be fully sent. This will be false if there is still more data to be written when the call returns. + */ +private boolean writeClientBoundMessage() { + boolean sendMore = true; + +// if(getWebServer().debug) { +// debugBuffer.append("Starting a write cycle.\n"); +// }//if// + + try { + //Process SSL output first.// + if(sslEngine != null) { + //If we have part of an SSL frame then try to send it first.// + if(encryptedWriteBuffer.hasRemaining()) { + int remaining = encryptedWriteBuffer.remaining(); + + //Write the bytes to the stream.// + ((SocketChannel) key.channel()).write(encryptedWriteBuffer); + +// if(getWebServer().debug) { +// debugBuffer.append("Wrote " + (remaining - encryptedWriteBuffer.remaining()) + " encrypted bytes to the stream. " + encryptedWriteBuffer.remaining() + " remain.\n"); +// }//if// + + //Check to see if we failed to send the whole frame.// + if(encryptedWriteBuffer.hasRemaining()) { + sendMore = false; + }//if// + }//if// + + while(sendMore && sslNeedsWrap) { + SSLEngineResult handshakeResult; + + //Reset the encrypted write buffer - note that since we will never read while waiting to write data, this should always be empty.// + encryptedWriteBuffer.position(0); + encryptedWriteBuffer.limit(encryptedWriteBuffer.capacity()); + //Generate the handshake message.// + handshakeResult = sslEngine.wrap(ByteBuffer.allocate(0), encryptedWriteBuffer); + encryptedWriteBuffer.flip(); + + if(handshakeResult.getStatus() == Status.BUFFER_OVERFLOW) { + //Should never happen.// + Debug.log(new RuntimeException("Unexpected ssl engine buffer overflow.")); + }//if// + else if(handshakeResult.getStatus() == Status.BUFFER_UNDERFLOW) { + //Should never happen.// + Debug.log(new RuntimeException("Unexpected ssl engine buffer underflow.")); + }//else if// + else if(handshakeResult.getStatus() == Status.CLOSED) { + //Should never happen.// + Debug.log(new RuntimeException("Unexpected ssl engine closed.")); + //TODO: Handle this closure without an infinate loop... + //Close the socket.// + try {key.channel().close();}catch(Throwable e2) {} + }//else if// + else if(handshakeResult.getStatus() == Status.OK) { + if(handshakeResult.getHandshakeStatus() == HandshakeStatus.NEED_TASK) { + //Should never happen.// + Debug.log(new RuntimeException("Unexpected ssl engine task.")); + }//if// + else if(encryptedWriteBuffer.hasRemaining()) { + int remaining = encryptedWriteBuffer.remaining(); + + //Write the bytes to the stream.// + ((SocketChannel) key.channel()).write(encryptedWriteBuffer); + +// if(getWebServer().debug) { +// debugBuffer.append("Sent " + (remaining - encryptedWriteBuffer.remaining()) + " encrypted bytes.\n"); +// }//if// + + //If not all the bytes could be written then we will need to wait until we can write more.// + if(encryptedWriteBuffer.hasRemaining()) { +// if(getWebServer().debug) { +// debugBuffer.append("Pausing due to a partially sent packet (while ssl handshaking). Bytes actually sent: " + encryptedWriteBuffer.position() + ". Bytes remaining: " + encryptedWriteBuffer.remaining() + ".\n"); +// }//if// + + //Leave the data in the encrypted write buffer for the writing operation to send it.// + sendMore = false; + }//if// + + //Update the SSL needs wrap flag.// + if(handshakeResult.getHandshakeStatus() != HandshakeStatus.NEED_WRAP) { + sslNeedsWrap = false; + }//if// + }//else if// + else { + sslNeedsWrap = false; + }//else// + }//else if// + else { + //Should never happen.// + Debug.log(new RuntimeException("Unexpected ssl engine status code.")); + }//else// + }//while// + +// if(getWebServer().debug) { +// debugBuffer.append("End Handshaking SSL\n"); +// }//if// + }//if// + + if(sendMore && currentOutboundMessage != null) { + //Check to see if the outbound message is prepared to send more content. For chunked transfers the outbound message may be waiting for additional content from another stream and we should return later.// + if(!currentOutboundMessage.getBuffer().hasRemaining()) { + if(!currentOutboundMessage.loadBuffer()) { + if(currentOutboundMessage.getBuffer() == null && currentOutboundMessage.getNext() != null) { + currentOutboundMessage = currentOutboundMessage.getNext(); + }//if// + else { + sendMore = false; + }//else// + }//if// + + if(currentOutboundMessage.getBuffer() == null) { + currentOutboundMessage = null; + lastOutboundMessage = null; + }//if// + }//if// + + //If we have an application response pending then send it now.// + if(sendMore && currentOutboundMessage.getBuffer().hasRemaining()) { + if(sslEngine != null) { + //Keep sending encrypted frames until the output buffer is full, or we run out of message to send.// + while(key.channel().isOpen() && sendMore && (currentOutboundMessage != null) && currentOutboundMessage.getBuffer().hasRemaining()) { + SSLEngineResult encryptResult; +// int offset = pendingOutboundMessage.getBuffer().position(); +//TODO: Comment me. +//int rem = pendingOutboundMessage.getBuffer().remaining(); + //Reset the encrypted write buffer.// + encryptedWriteBuffer.compact(); + //Encrypt the next message frame.// + encryptResult = sslEngine.wrap(currentOutboundMessage.getBuffer(), encryptedWriteBuffer); + encryptedWriteBuffer.flip(); +//TODO: Comment me. +//Debug.log("Encrypting/Sending to client from Git " + (rem - pendingOutboundMessage.getBuffer().remaining()) + " bytes."); + +// if(getWebServer().debug) { +// sentBytes += (pendingOutboundMessage.position() - offset); +// debugBuffer.append("Encrypted: " + (pendingOutboundMessage.position() - offset) + ". Total Encrypted: " + sentBytes + ". Encrypted size: " + encryptedWriteBuffer.limit() + ".\n"); +// }//if// + + if(encryptResult.getStatus() == Status.BUFFER_OVERFLOW) { + //Should never happen.// + Debug.log(new RuntimeException("Unexpected ssl engine buffer overflow.")); + }//if// + else if(encryptResult.getStatus() == Status.BUFFER_UNDERFLOW) { + //Should never happen.// + Debug.log(new RuntimeException("Unexpected ssl engine buffer underflow.")); + }//else if// + else if(encryptResult.getStatus() == Status.CLOSED) { + //Should never happen.// +// Debug.log(new RuntimeException("Unexpected ssl engine closed.")); + //TODO: Handle this closure without an infinate loop... + //Close the socket.// + try {key.channel().close();} catch(Throwable e2) {} + }//else if// + else if(encryptResult.getStatus() == Status.OK) { + //Write the bytes to the stream.// + try { + int remaining = encryptedWriteBuffer.remaining(); + + ((SocketChannel) key.channel()).write(encryptedWriteBuffer); + +// if(getWebServer().debug) { +// debugBuffer.append("Sent " + (remaining - encryptedWriteBuffer.remaining()) + " encrypted bytes.\n"); +// }//if// + }//try// + catch(IOException e) { + //Caught if the channel is forcably closed by the client. We will ignore it.// + }//catch// + + //If not all the bytes could be written then we will need to wait until we can write more.// + if(encryptedWriteBuffer.hasRemaining()) { + //Leave the data in the encrypted write buffer for the writing operation to send it.// + sendMore = false; + +// if(getWebServer().debug) { +// debugBuffer.append("Pausing due to a partially sent packet. Bytes actually sent: " + encryptedWriteBuffer.position() + ". Bytes remaining: " + encryptedWriteBuffer.remaining() + ".\n"); +// }//if// + }//if// + }//else if// + else { + //Should never happen.// + Debug.log(new RuntimeException("Unexpected ssl engine status code.")); + }//else// + + //Add more content to the buffer.// + //Note: Do this even if the last encrypted write buffer could not be fully sent - so that when it is sent there will be outbound message content.// + if(key.channel().isOpen() && currentOutboundMessage != null) { + if(!currentOutboundMessage.loadBuffer()) { + //Load the next pending outbound message in the chain. This is currently only used for content being passed through to another process via a second socket.// + if(currentOutboundMessage.getBuffer() == null && currentOutboundMessage.getNext() != null) { + currentOutboundMessage = currentOutboundMessage.getNext(); + }//if// + else { + //Wait until additional message bytes are available.// + sendMore = false; + }//else// + }//if// + + //If the message end has been reached then the buffer will be null.// + if(currentOutboundMessage.getBuffer() == null) { + currentOutboundMessage = null; + lastOutboundMessage = null; + }//if// + }//if// + }//while// + }//if// + else { + //Keep sending encrypted frames until the output buffer is full, or we run out of message to send.// + while(sendMore && (currentOutboundMessage != null) && currentOutboundMessage.getBuffer().hasRemaining()) { + //Write the bytes to the stream.// + ((SocketChannel) key.channel()).write(currentOutboundMessage.getBuffer()); + +// if(getWebServer().debug) { +// sentBytes += pendingOutboundMessage.position(); +// debugBuffer.append("Wrote " + pendingOutboundMessage.position() + " bytes to the client. Total sent: " + sentBytes + "\n"); +// }//if// + + //If not all the bytes could be written then we will need to wait until we can write more.// + if(currentOutboundMessage.getBuffer().hasRemaining()) { + sendMore = false; + }//if// + else { + if(!currentOutboundMessage.loadBuffer()) { + //Load the next pending outbound message in the chain. This is currently only used for content being passed through to another process via a second socket.// + if(currentOutboundMessage.getBuffer() == null && currentOutboundMessage.getNext() != null) { + currentOutboundMessage = currentOutboundMessage.getNext(); + }//if// + else { + //Wait until additional message bytes are available.// + sendMore = false; + }//else// + }//if// + + //If the message end has been reached then the buffer will be null.// + if(currentOutboundMessage.getBuffer() == null) { + currentOutboundMessage = null; + lastOutboundMessage = null; + }//if// + }//else// + }//while// + }//else// + }//if// + }//if// + }//try// + catch(ClosedChannelException e) { + close(); + }//catch// + catch(SSLException e) { + if(getWebServer().debug) { + Debug.log(e); + }//if// + + close(); + }//catch// + catch(IOException e) { + if(getWebServer().debug) { + Debug.log(e); + }//if// + + close(); + }//catch// + + return sendMore; +}//writeClientBoundMessage()// +/* (non-Javadoc) + * @see com.foundation.web.server.WebServer.AbstractSocketContext#processRequest() + */ +protected void readIncomingMessages() throws IOException { + boolean requiresRead = true; + SocketChannel channel = (SocketChannel) key.channel(); + + if(isSsl()) { + int loopCount = 0; + boolean keepReading = true; + + if(sslEngine == null) { + SSLSession session = null; + + if(parseFirstTlsMessage(this, channel)) { + SSLContext sslContext = null; + + //Error checking.// + if(domain == null || domain.length() == 0) { + //We seem to have a choice here. We can either throw an error which ends up forcably closing the connection which for most browsers allows the client to retry with different settings (not older IE6, 7, 8 implementations), or return a pretty error which prevents users of well behaved browsers from getting the correct credentials.// + throw new TlsFailureException("Connection did not provide a domain name in the TLS hello message."); +// context.tlsFailure = true; + }//if// + + //Get the web application for the given domain.// + //Synchronize to prevent another thread from altering the service's web applications while we are accessing it.// + synchronized(getWebServer()) { + if(tlsFailure) { + domain = serverSocketContext.serviceListener.getDefaultSslDomain(); + }//if// + + webApplicationContainer = serverSocketContext.serviceListener.getWebApplicationContainer(domain); + }//synchronized// + + //Error checking.// + if(webApplicationContainer == null) { + throw new IOException("Cannot find a web application for the domain " + domain + "."); + }//if// + + //Lock on the container so it doesn't change the object it contains while we access it.// + synchronized(webApplicationContainer) { + //Get the SSLContext from the web application.// + sslContext = webApplicationContainer.getWebApplication().getSslContext(domain); + + if(sslContext == null) { + throw new IOException("Cannot get an SSLContext for the domain " + domain + " for the domain associated web application."); + }//if// + + sslEngine = sslContext.createSSLEngine(); + + //Attempt an anonymous connection to the client so we can report that they have an old browser version that is not using TLS + domain extension.// +// if(context.tlsFailure) { +// context.sslEngine.setEnabledCipherSuites(new String[] { +// "SSL_DH_anon_WITH_3DES_EDE_CBC_SHA", +// "SSL_DH_anon_WITH_DES_CBC_SHA", +// "SSL_DH_anon_WITH_RC4_128_MD5"}); +// //"SSL_DH_anon_EXPORT_WITH_RC4_40_MD5", +// //"SSL_DH_anon_EXPORT_WITH_DES40_CBC_SHA", +// //"TLS_DH_anon_EXPORT_WITH_RC4_40_MD5", +// //"TLS_DH_anon_WITH_RC4_128_MD5", +// //"TLS_DH_anon_EXPORT_WITH_DES40_CBC_SHA", +// //"TLS_DH_anon_WITH_DES_CBC_SHA", +// //"TLS_DH_anon_WITH_3DES_EDE_CBC_SHA"}); +// }//if// + + //context.sslEngine.getEnabledProtocols(); + //context.sslEngine.getEnabledCipherSuites(); + sslEngine.setUseClientMode(false); + sslEngine.beginHandshake(); + session = sslEngine.getSession(); + encryptedReadBuffer = ByteBuffer.allocate(session.getPacketBufferSize()); + encryptedReadBuffer.position(encryptedReadBuffer.limit()); + unencryptedReadBuffer = ByteBuffer.allocate(session.getApplicationBufferSize()); + unencryptedReadBuffer.position(unencryptedReadBuffer.limit()); + encryptedWriteBuffer = ByteBuffer.allocate(session.getPacketBufferSize()); + encryptedWriteBuffer.position(encryptedWriteBuffer.limit()); + }//synchronized// + + //Create a pass through socket and context and attach it to this context if the application is setup as a pass through to another process.// + IWebApplication application = webApplicationContainer.getWebApplication(); + + if(application instanceof IPassThroughDomain) { + IPassThroughDomain passThroughDomain = ((IPassThroughDomain) application); + + //Setup the pass through socket context (and socket channel). All data will be sent to this context to be sent to the remote process.// + relatedSocketContext = new PassThroughSocketContext(this, passThroughDomain.getAddress(), passThroughDomain.getPort()); + }//if// + }//if// + }//if// + + if(sslEngine != null) { + //While we have a count greater than zero, indicating that some data is comming through, keep reading and processing the data.// + //Note: We are throddling this for active connections to prevent a single connection from hogging all the resources.// + while(channel.isOpen() && loopCount < 10 && requiresRead && keepReading) { + int readCount = -1; + + //Track how many loops we run so that we don't hog all the server's resources with one connection.// + loopCount++; + //Compact the buffer, placing remaining bytes at the start of the buffer and preping for reading more bytes from the stream.// + encryptedReadBuffer.compact(); + + //If there is an initial buffer of bytes then use those first. The initial buffer is created when parsing the initial TLS message to gather the domain the client is connecting to.// + if(initialBuffer != null) { + readCount = initialBuffer.readBytes(encryptedReadBuffer); + + if(initialBuffer.getSize() == 0) { + initialBuffer = null; + }//if// + }//if// + + //If there isn't an initial buffer then fill (or finish filling) the read buffer.// + if(readCount == -1 && initialBuffer == null) { + //Read a frame of encrypted data (or handshake data).// + try { + readCount = channel.read(encryptedReadBuffer); + }//try// + catch(IOException e) { + //Ignore the forcable close error here - the channel will be closed a few lines lower in the code. TODO: Not sure if all IOExceptions should be ignored here?// + if(!e.getMessage().equalsIgnoreCase("An existing connection was forcibly closed by the remote host")) { + throw e; + }//if// + }//catch// + }//else// + + //Make the encrypted frame buffer readable.// + encryptedReadBuffer.flip(); + + //If the socket was closed then handle it, if we received enough data to process a frame then do so and continue, otherwise we must wait for the client to send more data.// + if(readCount == -1) { + //The socket has been closed by the client.// + close(); + keepReading = false; + }//if// + else if(encryptedReadBuffer.remaining() != 0) { + SSLEngineResult sslResult; + + //Sometimes the SSL Engine requires a partial read of the encrypted bytes, then a task, then reading the rest of the encrypted bytes.// + while(key.channel().isOpen() && encryptedReadBuffer.hasRemaining()) { + //Reset the unencrypted read buffer prior to decrypting the next frame.// + unencryptedReadBuffer.position(0); + unencryptedReadBuffer.limit(unencryptedReadBuffer.capacity()); + //Decrypt the message and process any SSL messages.// + sslResult = sslEngine.unwrap(encryptedReadBuffer, unencryptedReadBuffer); + unencryptedReadBuffer.flip(); + + //Check the requiresRead of the SSL processing.// + if(sslResult.getStatus() == Status.BUFFER_UNDERFLOW) { + //Buffer underflow indicates we haven't enough bytes to finish processing the next frame of data.// + break; + }//if// + else if(sslResult.getStatus() == Status.BUFFER_OVERFLOW) { + //Should never happen.// +// if(getWebServer().debug) Debug.log(new RuntimeException("Unexpected ssl engine buffer overflow.")); + close(); + }//else if// + else if(sslResult.getStatus() == Status.CLOSED) { + //A normal close I think...// + //Debug.log(new RuntimeException("Unexpected ssl engine closed.")); + }//else if// + else if(sslResult.getStatus() == Status.OK) { + //Run any long running tasks now.// + if(sslResult.getHandshakeStatus() == HandshakeStatus.NEED_TASK) { + Runnable task = null; + + while((task = sslEngine.getDelegatedTask()) != null) { + task.run(); + }//while// + }//if// + + //If the engine requires handshake data to be wrapped and sent then do so now.// + if(sslResult.getHandshakeStatus() == HandshakeStatus.NEED_WRAP || sslResult.getHandshakeStatus() == HandshakeStatus.NEED_TASK) { + sslNeedsWrap = true; + + //Need to synchronize if this is a pass through socket so that multiple threads don't access pendingOutboundMessage or lastAddedMessageBuffer (via a call to passThrough(ByteBuffer) on another thread).// + if(getPassThroughSocketContext() == null) { + requiresRead = writeClientBoundMessage(); + }//if// + else { + synchronized(this) { + requiresRead = writeClientBoundMessage(); + }//synchronized// + }//else// + }//if// + + //If bytes were produced then process them.// + if(sslResult.bytesProduced() > 0) { + //If we are not passing all content to another process then handle it by calling processClientRequest, otherwise pass it through.// + if(getPassThroughSocketContext() == null) { + if(isWebsocket) { + requiresRead = processWebsocketFrame(unencryptedReadBuffer, key); + }//if// + else { + requiresRead = processClientRequest(unencryptedReadBuffer, key); + }//else// + }//if// + else { +//TODO: Comment me. +//Debug.log("Receiving message (" + unencryptedReadBuffer.remaining() + " bytes) from client for git."); + //Queue the data for sending to the remote process via the pass through socket context.// + getPassThroughSocketContext().passThrough(unencryptedReadBuffer); + requiresRead = true; + }//else// + }//if// + }//else if// + else { + //Should never happen.// + Debug.log(new RuntimeException("Unexpected ssl engine status code.")); + close(); + }//else// + }//while// + }//else if// + else { + //Wait for the client to send more data.// + keepReading = false; + }//else// + }//while// + }//if// + }//if// + else { + int count = 1; + ByteBuffer socketReadBuffer = this.socketReadBuffer; + int loopCount = 0; + + //While we have a count greater than zero, indicating that some data is comming through, keep reading and processing the data.// + //Note: We are throddling this for active connections to prevent a single connection from hogging all the resources.// + while(loopCount < 10 && requiresRead && count > 0) { + loopCount++; + //Don't allow data to be left on the socket read buffer.// + socketReadBuffer.position(0); + socketReadBuffer.limit(socketReadBuffer.capacity()); + count = channel.read(socketReadBuffer); + socketReadBuffer.flip(); + + //Setup the pass through socket if the application is an instance of IPassThroughDomain.// + if(count != -1 && isFirstUnencryptedMessage) { + //Read enough of the header to identify the application.// + if(processRequestedHost(socketReadBuffer, key)) { + //Create a pass through socket and context and attach it to this context if the application is setup as a pass through to another process.// + IWebApplication application = webApplicationContainer.getWebApplication(); + + if(application instanceof IPassThroughDomain) { + IPassThroughDomain passThroughDomain = ((IPassThroughDomain) application); + + //Setup the pass through socket context (and socket channel). All data will be sent to this context to be sent to the remote process.// + relatedSocketContext = new PassThroughSocketContext(this, passThroughDomain.getAddress(), passThroughDomain.getPort()); + }//if// + + isFirstUnencryptedMessage = false; + }//if// + else { + //We couldn't even read the host from the first bytes sent by the client - very unusual (it should be in the first couple hundred bytes - less than a single packet).// + //TODO: We could cycle and wait for the next packet. For now just close the socket since this should never really happen in the first place. + close(); + }//else// + }//if// + + if(count == -1) { + //The socket has been closed by the client.// + close(); + }//if// + else if(relatedSocketContext != null) { + relatedSocketContext.passThrough(socketReadBuffer); + + if(socketReadBuffer.remaining() > 0) { + Debug.log(new RuntimeException("Remaining bytes found on the read buffer!")); + }//if// + }//else if// + else if(isWebsocket) { + requiresRead = processWebsocketFrame(socketReadBuffer, key); + + if(socketReadBuffer.remaining() > 0) { + Debug.log(new RuntimeException("Remaining bytes found on the read buffer!")); + }//if// + }//else if// + else if(socketReadBuffer.hasRemaining()) { + requiresRead = processClientRequest(socketReadBuffer, key); + + if(socketReadBuffer.remaining() > 0) { + Debug.log(new RuntimeException("Remaining bytes found on the read buffer!")); + }//if// + }//else// + else { + break; + }//else// + }//while// + }//else// +}//processRequest()// +/** + * Processes a single websocket frame if there is enough data in the fragment. + *
Will not return until all data is read from the frame or the socket is closed. + * @param context The context for the socket. + * @param fragment The fragment of data off the socket. + * @param key The key for the socket. + * @return Whether more data is required in order to process a single frame. + * @throws IOException + */ +private boolean processWebsocketFrame(ByteBuffer fragment, SelectionKey key) throws IOException { + boolean isFrameFullyRead = false; + + while(fragment.remaining() > 0) { + byte[] frameHeader = websocketFrameHeader; + byte[] maskKey = websocketMessageMaskKey; + final int OP_CODE_INDEX = 0; + final int LENGTH_INDEX = 1; + final int EXTENDED_LENGTH_INDEX = 2; + + isFrameFullyRead = false; + + //Read the frame header (unless we are waiting on the remaining content of a previously read frame header).// + if(websocketFrameRemainder == 0) { + long length = -1; + + try { + boolean mask = false; + int expectedHeaderLength = 6; + int readLength = Math.min(2 - websocketFrameHeaderIndex, fragment.remaining()); + + if(readLength > 0) { + fragment.get(frameHeader, websocketFrameHeaderIndex, readLength); + websocketFrameHeaderIndex += readLength; + }//if// + + //Handle a partial header by checking the first two bytes to find the header's overall length (header length varies based on the message length and options used).// + if(websocketFrameHeaderIndex > 1) { + websocketIsLastFrameInMessage = (frameHeader[OP_CODE_INDEX] & 0x80) > 0; + mask = (frameHeader[LENGTH_INDEX] & 0x80) > 0; + length = frameHeader[LENGTH_INDEX] & 0x7F; + + if(length == 127) { + expectedHeaderLength = 8; + }//if// + else if(length == 128) { + expectedHeaderLength = 14; + }//else if// + }//if// + + readLength = Math.min(expectedHeaderLength - websocketFrameHeaderIndex, fragment.remaining()); + fragment.get(frameHeader, websocketFrameHeaderIndex, readLength); + websocketFrameHeaderIndex += readLength; + + //Ensure we have the full header before proceeding to read it.// + if(websocketFrameHeaderIndex == expectedHeaderLength) { + int maskKeyIndex = 2; + + if(length == 127) { + length = (int) (StreamSupport.readShort(frameHeader, EXTENDED_LENGTH_INDEX) & 0xFFFF); + maskKeyIndex = 4; + }//if// + else if(length == 128) { + //Note: Cannot handle unsigned longs in java. Assume negative numbers are over the length maximum for the server (since they most certainly should be - we'd run out of ram).// + length = StreamSupport.readLong(frameHeader, EXTENDED_LENGTH_INDEX); + maskKeyIndex = 10; + }//if// + + //Ensure we don't exceed the maximum message length applied by the web application when the socket was upgraded. Also ensure that we don't exceed the server's limitations on frame size (the highest possible positive value an integer can hold).// + if(length < 0 || length > Integer.MAX_VALUE || length + (websocketPartialReceivedMessage == null ? 0 : websocketPartialReceivedMessage.getSize()) > websocketMaxMessageLength) { + //TODO: Reply with a closed message? + //Kill the socket!// + close(); + //Notify ourselves via the log.// + Debug.log(new RuntimeException("Killed the client websocket due to excess frame size (Integer.MAX_VALUE) or the message exceeding the maximum size allowed by the web application (as specified when the websocket was accepted).")); + }//if// + else if(!mask) { + //Kill the socket - should never not have the mask set for the client calling the server.// + close(); + Debug.log(new RuntimeException("Killed the client websocket due to the mask not being set.")); + }//else if// + else { + //If this is the first frame in a message then parse the header for info.// + if(websocketMessageOpCode == 0) { + //Read the 4 bytes of the mask key.// + for(int maskIndex = 0, headerIndex = maskKeyIndex; maskIndex < maskKey.length; maskIndex++, headerIndex++) { + maskKey[maskIndex] = frameHeader[headerIndex]; + }//for// + + //The op code shouldn't be zero because zero is for a continuation frame (another frame in a message, not the first frame).// + websocketMessageOpCode = frameHeader[OP_CODE_INDEX] & 0x0F; + }//if// + + //If it is a content message then begin reading it.// + websocketFrameRemainder = (int) length; + }//else// + }//if// + }//try// + catch(BufferUnderflowException e) { + //Ignore - shouldn't occure.// + }//catch// + + //If we could read the next frame header then reset the frame header index to zero for the next frame header read.// + if(length != -1) { + websocketFrameHeaderIndex = 0; + }//if// + else if(websocketFrameRemainder == 0) { + isFrameFullyRead = true; + }//else if// + }//if// + + //Read the frame content.// + if(websocketFrameRemainder > 0) { + //If the whole message is here then read it.// + if(fragment.remaining() > 0) { + int transferCount = 0; + int streamPosition = 0; + int previousReadPosition; + + if(websocketPartialReceivedMessage == null) { + websocketPartialReceivedMessage = new StreamBuffer(); + }//if// + else { + streamPosition = websocketPartialReceivedMessage.getSize(); + }//else// + + //Retain the index of the last previously read byte so we start decoding using the correct mask offset.// + previousReadPosition = websocketPartialReceivedMessage.getSize(); + //Transfer either all the bytes in the fragment read from the socket, or transfer all the remaining bytes belonging to the current frame.// + transferCount = Math.max((int) websocketFrameRemainder, fragment.remaining()); + + //Move the bytes into our partial message buffer.// + websocketPartialReceivedMessage.writeBytes(fragment, transferCount); + + //Decode the message bytes with the mask key. The encoding is to protect intermediate servers that cache the contents from cache attacks. The mask key must be set by the client browser and must be random for each frame.// + for(int index = streamPosition, frameIndex = previousReadPosition, maxIndex = streamPosition + transferCount; index < maxIndex; index++, frameIndex++) { + //XOR each byte read with the next byte in the mask key (loops over the mask key starting at zero for the whole frame).// + websocketPartialReceivedMessage.putByte(index, websocketPartialReceivedMessage.getByte(index) ^ websocketMessageMaskKey[frameIndex % 4]); + }//for// + + //Track the remaining bytes to be read for the current frame.// + websocketFrameRemainder -= transferCount; + + if(websocketFrameRemainder == 0) { + //Mark the frame as is being fully read.// + isFrameFullyRead = true; + }//if// + }//if// + }//if// + + //If we finished reading this frame, and this is the last frame in the message, then handle it.// + if(isFrameFullyRead && websocketIsLastFrameInMessage) { + //Handle the completed message.// + switch(websocketMessageOpCode) { + case 0x01: //Text Frame// + //Convert the data into text and send to the application.// + try { + websocketHandler.receiveTextMessage(new String(websocketPartialReceivedMessage.toByteArray(), "UTF-8")); + }//try// + catch(Throwable e) { + Debug.log(e); + }//catch// + break; + case 0x02: //Binary Frame// + //Send binary data to the application.// + try { + websocketHandler.receiveBinaryMessage(websocketPartialReceivedMessage.toByteArray()); + }//try// + catch(Throwable e) { + Debug.log(e); + }//catch// + break; + case 0x08: //Socket Close// + close(); + break; + case 0x09: //Ping// + /* + sendWebsocketPong(); + //Notify the listener of the ping.// + try { + websocketHandler.receivePing(); + }//try// + catch(Throwable e) { + Debug.log(e); + }//catch// + */ + Debug.log(new RuntimeException("Received PING from the client. The client shouldn't ever send PING messages (only PONG).")); + break; + case 0x0A: //Pong// + //Notify the listener of the pong.// + try { + websocketHandler.receivePong(); + }//try// + catch(Throwable e) { + Debug.log(e); + }//catch// + break; + default: //Unsupported// + close(); + }//switch// + + //Clean up after the message.// + websocketFrameRemainder = 0; + websocketMessageOpCode = 0; + websocketIsLastFrameInMessage = false; + + if(websocketPartialReceivedMessage != null) { + websocketPartialReceivedMessage.release(); + websocketPartialReceivedMessage = null; + }//if// + }//if// + }//while// + + return !isFrameFullyRead; +}//processWebsocketFrame()// +/** + * Determines whether we have a complete HTML header in the string buffer. + * @param buffer The buffer containing the HTML ASCII header characters. + * @return Whether the header is complete. + */ +private boolean isCompleteHeader(StringBuffer buffer) { + return (buffer.length() > 4) && (buffer.charAt(buffer.length() - 4) == '\r') && (buffer.charAt(buffer.length() - 3) == '\n') && (buffer.charAt(buffer.length() - 2) == '\r') && (buffer.charAt(buffer.length() - 1) == '\n'); +}//isCompleteHeader()// +/** + * Processes enough of the header of this first request to identify the application and set it for the socket. Used to forward unencrypted message to a remote server. + * @param fragment + * @param key + * @return Whether enough of the request could be read to identify the application. The caller should ignore the result if key.channel() is closed since the request was incomplete, incorrectly formatted, or the socket failed. + */ +private boolean processRequestedHost(ByteBuffer fragment, SelectionKey key) throws IOException { + boolean result = false; + + try { + fragment.mark(); + + //Check whether we already read this message's header and we are simply appending to the end of it.// + if(request == null) { + String host = null; + StringBuilder buffer = new StringBuilder(4096); + int totalHeaderSize = 0; + + while(fragment.hasRemaining() && host == null) { + //Keep adding ASCII characters to the message header fragment until the next line is read or we exceed the maximum length of the header.// + while(fragment.hasRemaining() && (!(buffer.length() > 1 && (buffer.charAt(buffer.length() - 2) == '\r') && (buffer.charAt(buffer.length() - 1) == '\n')))) { + if(totalHeaderSize == 4096) { + //Force the connection to the client to be closed.// + try {key.channel().close();}catch(Throwable e2) {} + //Throw an exception that should not be logged. This happens occationally when an attacker tries to exploit any buffer overrun weaknesses.// + throw new IgnoredIOException(null); + }//if// + + buffer.append((char) fragment.get()); + totalHeaderSize++; + }//while// + + //If we have the minimum number of bytes and the last bytes are a line end, then check the line for "Host: xxxxxxx\r\n" + String line = buffer.toString().substring(0, buffer.length() - 2).trim(); + if(line.startsWith("Host: ")) { + host = line.substring(6).trim(); + }//if// + + if(host == null) { + if(buffer.length() == 2) { + //End of the header reached. No host provided. Kill the connection?// + //Force the connection to the client to be closed.// + try {key.channel().close();}catch(Throwable e2) {} + //Throw an exception that should not be logged. This happens occationally when an attacker tries to exploit any header reading weaknesses (all major browsers send a host header).// + throw new IgnoredIOException(null); + }//if// + else { + //Clear the line.// + buffer.setLength(0); + }//else// + }//if// + }//while// + + //If we found the complete first line of the header before running out of bytes, then identify the application.// + if(host != null) { + domain = host.toLowerCase(); + + //Get the web application for the given domain.// + //Synchronize to prevent another thread from altering the service's web applications while we are accessing it.// + synchronized(getWebServer()) { + webApplicationContainer = serverSocketContext.serviceListener.getWebApplicationContainer(domain); + }//synchronized// + + result = true; + }//if// + }//if// + + fragment.reset(); + }//try// + catch(IgnoredIOException e) {} + catch(Throwable e) { + Debug.log(e); + throw new IOException("Failed to process the request headers."); + }//catch// + + return result; +}//processRequestedHost()// +/** + * Processes the client request given the latest fragment of a message. + * @param messageFragment The message fragment. + * @param key The socket's key. + * @result Whether request is in a receive state. Will be false if the request generated a response that could not be completely transmitted. + */ +private boolean processClientRequest(ByteBuffer fragment, SelectionKey key) throws IOException { + boolean result = true; + + try { +// File f = new File("c:\\temp\\message.txt"); +// FileOutputStream fout = new FileOutputStream(f, true); +// fout.write(fragment.array(), fragment.position() + fragment.arrayOffset(), fragment.remaining()); +// fout.close(); + //The message will always start with a header which ends with a double '\r\n'.// + //Parse the message header to detect the Content-Type and the Content-Length.// + //Note that the message may contain extra content between the message header and the first --boundary, also there may be extra content after the last --boundary--, both of these bits of content should be ignored.// + //Detect a multipart/form-data message and read each part of the form, each of which begins with --boundary followed by either a double '\r\n' in which case the header is assumed to be 'Content-type: text/plain; charset=us-ascii', otherwise it will be followed by a single '\r\n' and then the header for the part and then a double '\r\n'.// + //The final boundary will show with two '-'s before and AFTER the boundary. Anything after this is ignoreable.// + + //Check whether we already read this message's header and we are simply appending to the end of it.// + if(request == null) { + //Create a buffer with the maximum size of a message header to hold the bytes that might be included in the header.// + if(messageHeaderFragment == null) { + messageHeaderFragment = new StringBuffer(4096); + }//if// + + //Keep adding ASCII characters to the message header fragment until it is a complete header, or the message fragment is empty, or we exceed the maximum allowed size of a message header.// + while(fragment.hasRemaining() && !isCompleteHeader(messageHeaderFragment)) { + if(messageHeaderFragment.length() == 4096) { + //Force the connection to the client to be closed.// + try {key.channel().close();}catch(Throwable e2) {} + //Throw an exception that should not be logged. This happens occationally when an attacker tries to exploit any buffer overrun weaknesses.// + throw new IgnoredIOException(null); + }//if// + + messageHeaderFragment.append((char) fragment.get()); + }//while// + + //If we found the complete header before running out of bytes, then setup the request object.// + if(isCompleteHeader(messageHeaderFragment)) { + IWebApplication application = webApplicationContainer != null ? webApplicationContainer.getWebApplication() : null; + +// if(getWebServer().debug) { +// context.debugBuffer.append("Processing:\r\n" + context.messageHeaderFragment.toString()); +// }//if// + + //Prepare the request.// + try { + request = new Request(++lastRequestNumber, messageHeaderFragment.toString(), ((SocketChannel) key.channel()).socket().getInetAddress().getHostAddress(), this, this, isSsl()); + + //Log the request header if running in getWebServer().debug mode.// + if(getWebServer().debug) { + Debug.log(request.toString()); + }//if// + }//try// + catch(IOException e) { + Debug.log(" ===== Begin exception output. ====="); + Debug.log("Processing:\r\n" + messageHeaderFragment.toString()); + Debug.log(e); + Debug.log("The following IOException is a result of this exception."); + Debug.log(" ===== End exception output. ====="); + throw e; + }//catch// + catch(Throwable e) { + Debug.log(" ===== Begin exception output. ====="); + Debug.log("Processing:\r\n" + messageHeaderFragment.toString()); + Debug.log(e); + Debug.log("The following IOException is a result of this exception."); + Debug.log(" ===== End exception output. ====="); + throw new IOException("Failed to process the request headers."); + }//catch// + + if(webApplicationContainer == null) { + //Save the domain.// + domain = request.getHost() != null ? request.getHost().toLowerCase() : null; + + if(domain != null) { + //Get the web application for the given domain.// + //Synchronize to prevent another thread from altering the service's web applications while we are accessing it.// + synchronized(getWebServer()) { + webApplicationContainer = serverSocketContext.serviceListener.getWebApplicationContainer(domain); + }//synchronized// + + if(webApplicationContainer != null) { + application = webApplicationContainer.getWebApplication(); + }//if// + }//if// + }//if// + + //If the request is tied to a web application then get or re-create the session. A new session will be created later if it is needed.// + if(application != null) { + int contentLength = 0; + + //TODO: Remove + if(getWebServer().debug) { + boolean sessionFound = application.getSession(request.getSessionId()) != null; + boolean canRecreate = request.getSessionId() != null; + + Debug.log("SC: " + id + "; Req#: " + request.getRequestNumber() + "; ReqURI: " + request.getUri() + "\n\t(SessionId: " + request.getSessionId() + "; SecureSessionId: " + request.getSecureSessionId() + ")\n\tSession Found: " + sessionFound + (!sessionFound ? "; Can Recreate: " + canRecreate : "")); + }//if// + + request.setSession(application.getSession(request.getSessionId())); + + //Try to re-create the session.// + if(request.getSession() == null && request.getSessionId() != null) { + //TODO: Find something better to synchronize on? We could somehow use the session ID and a hashmap of monitors I suppose... The problem with this is that it blocks multiple clients from recreating the sessions at the same time, and recreating the session might take some time if a database is involved and servers have to be notified. + synchronized(WebServer.class) { + //Try again to get the session - in case another thread has already re-created it.// + request.setSession(application.getSession(request.getSessionId())); + + if(request.getSession() == null) { + request.setSession(application.recreateSession(request.getSessionId())); + }//if// + }//synchronized// + }//if// + + //If the client set a secure session ID and we found session data, then ensure that the client's secure session ID matches the secure session ID in the session data.// + //This is a security mechanism because the client is SUPPOSED to keep the secure session ID very private and not allow access to it by other sites. It ensures that a recreated session actually comes from a client that had it created in the first place.// + if(request.getSecureSessionId() != null && request.getSession() != null && request.getSession().getSecureSessionId() != null) { + if(!Comparator.equals(request.getSecureSessionId(), request.getSession().getSecureSessionId())) { + Debug.log("SC: " + id + "Forcing connection closure."); + //Force the connection to the client to be closed.// + try {key.channel().close();}catch(Throwable e2) {} + //Throw an exception that should not be logged. This might happen when an attacker tries to reuse stored session data on a client.// + throw new IgnoredIOException(null); + }//if// + }//if// + + //Validate the content length.// + if(request.getContentTypeCode() == IRequest.CONTENT_TYPE_MULTI_PART_FORM_DATA || request.getContentTypeCode() == IRequest.CONTENT_TYPE_CUSTOM || request.getContentTypeCode() == IRequest.CONTENT_TYPE_URL_ENCODED) { + int maxContentLength = application.getMaxMessageSize(request, request.getSession() != null ? request.getSession().getApplicationData() : null, request.getSession() != null && isSsl() ? request.getSession().getApplicationSecureData() : null); + + //A zero max content length allows for unlimited content sizes.// + if(maxContentLength > 0 && request.getContentLength() > maxContentLength) { + //TODO: Include info on the IP address. + throw new IOException("Message content exceeds the server's maximum size of " + maxContentLength + " bytes. Header: " + messageHeaderFragment.toString()); + }//if// + else { + contentLength = request.getContentLength(); + }//else// + }//if// + + //Setup to retain the request bytes.// + if(application.retainWholeRequestBuffer(request)) { + //Note: Use the content length from above since we wouldn't want a non-MPFD or custom message to also specify a length when there should be none - a possible hack.// + request.setRequestBytes(ByteBuffer.allocate(messageHeaderFragment.length() + contentLength)); + //Place the message header bytes in the buffer.// + request.getRequestBytes().put(messageHeaderFragment.toString().getBytes("ASCII")); + }//if// + }//if// + else { + if(request.getContentTypeCode() == IRequest.CONTENT_TYPE_MULTI_PART_FORM_DATA || request.getContentTypeCode() == IRequest.CONTENT_TYPE_CUSTOM) { + //A zero max content length allows for unlimited content sizes.// + if(getWebServer().getMaxContentLength() > 0 && request.getContentLength() > getWebServer().getMaxContentLength()) { + //TODO: Include info on the IP address. + throw new IOException("Message content exceeds the server's maximum size of " + getWebServer().getMaxContentLength() + " bytes. Header: " + messageHeaderFragment.toString()); + }//if// + }//if// + }//else// + + //Clear the message header fragment since it is no longer required.// + messageHeaderFragment = null; + //Clear the attributes used to assist in reading the message body.// + remainingPartBytes = null; + partReadCount = 0; + partWriteCount = 0; + endPartBoundaryFound = false; + currentPart = null; + + //If this is a multipart or custom message then we should do as much verification of size validity as possible.// + if(request.getContentTypeCode() == IRequest.CONTENT_TYPE_MULTI_PART_FORM_DATA) { + //Ensure that the multi-part form data message begins with the \r\n from the end of the header, allowing for the boundary recognition to proceed.// + //This could be a problem if the fragment doesn't have the last two characters (it might have only the last 1).// + if(fragment.position() == 1) { + ByteBuffer temp = ByteBuffer.allocate(fragment.remaining() + 2); + + temp.putChar('\r'); + temp.putChar('\n'); + temp.put(fragment); + fragment = temp; + }//if// + else { + //Roll back 2 characters to include the second \r\n that ended the header. This allows the first boundary to be recognized properly if there is no junk between the header and boundary.// + fragment.position(fragment.position() - 2); + }//else// + + //Remove the \r\n from the request's requestedBytes buffer so they aren't added twice.// + if(request.getRequestBytes() != null) { + request.getRequestBytes().position(request.getRequestBytes().position() - 2); + }//if// + }//if// + }//if// + }//if// + + //Add the incoming bytes to the request's byte record (if we are recording the exact message as received by the web server). This shouldn't alter the incoming data in any way.// + if(request != null && request.getRequestBytes() != null) { + int remaining = request.getRequestBytes().remaining(); + int position = fragment.position(); + +// Debug.log("Reading content bytes for the current client request. Remaining to be read: " + remaining + ". Bytes available to read: " + fragment.remaining()); + + if(remaining >= fragment.remaining()) { + //Write the remaining bytes in the fragment into the buffer for the request bytes.// + request.getRequestBytes().put(fragment); + }//if// + else if(remaining > 0) { + byte[] bytes = new byte[remaining]; + + fragment.get(bytes); + request.getRequestBytes().put(bytes); + }//else// + +// Debug.log("Read " + (remaining - context.request.getRequestBytes().remaining()) + " bytes of the current client request."); + + //Reset the fragment back to its previous position.// + fragment.position(position); + }//if// + + //If a complete header has been obtained then we must read any remaining bytes in the message.// + if(request != null) { + if(request.getContentTypeCode() == IRequest.CONTENT_TYPE_MULTI_PART_FORM_DATA) { + int remainingBytes = request.getContentLength() - partReadCount; + byte[] bytes; + int currentIndex = 0; + int startIndex = -1; + + //Convert the fragment bytes into an ASCII string for easier manipulation.// + if(fragment.hasArray()) { + int remainingPartByteCount = remainingPartBytes == null ? 0 : remainingPartBytes.length; + + bytes = new byte[Math.min(remainingBytes, fragment.remaining()) + remainingPartByteCount]; + + //Prepend the remaining bytes from the last fragment.// + if(remainingPartByteCount != 0) { + System.arraycopy(remainingPartBytes, 0, bytes, 0, remainingPartByteCount); + }//if// + + //Add in the bytes from this fragment.// + System.arraycopy(fragment.array(), fragment.arrayOffset() + fragment.position(), bytes, remainingPartByteCount, bytes.length - remainingPartByteCount); + //Update the fragment's position.// + fragment.position(fragment.position() + bytes.length - (remainingPartBytes == null ? 0 : remainingPartBytes.length)); + }//if// + else { + int remainingPartByteCount = remainingPartBytes == null ? 0 : remainingPartBytes.length; + byte[] buffer = bytes = new byte[Math.min(remainingBytes, fragment.remaining()) + remainingPartByteCount]; + + //Prepend the remaining bytes from the last fragment.// + if(remainingPartByteCount != 0) { + System.arraycopy(remainingPartBytes, 0, bytes, 0, remainingPartByteCount); + }//if// + + //Collect the bytes and increment the buffer's position.// + fragment.get(buffer, remainingPartByteCount, buffer.length - remainingPartByteCount); + }//else// + + //Detect whether we need to find the first part's boundary.// + if(currentPart == null) { + //Find the first boundary.// + startIndex = indexOf(bytes, request.getMultiPartBoundary(), currentIndex); + + //Test to make sure we didn't find the end boundary while NOT reading a part of the message - indicates there are NO parts in the multipart message. Shouldn't ever happen.// + if(startIndex == -1) { + if((startIndex = indexOf(bytes, request.getMultiPartEndBoundary(), currentIndex)) != -1) { + //TODO: What do we do if there are no parts in a multi-part message? Here we found the end part while NOT reading a message - shouldn't ever happen. + + //Force the connection to the client to be closed.// + try {key.channel().close();}catch(Throwable e2) {} + //Throw an exception that should not be logged. This is a malformed message.// + throw new IgnoredIOException(null); + }//if// + else { + //Store the data between the boundary and the end of the fragment, except for those parts of the fragment which might be the beginning of a boundary.// + int remainingIndex = Math.max(0, bytes.length - request.getMultiPartEndBoundary().length); + + //Retain enough characters to identify any boundary that might partially exist in this fragment of the message.// + remainingPartBytes = new byte[bytes.length - remainingIndex]; + System.arraycopy(bytes, remainingIndex, remainingPartBytes, 0, remainingPartBytes.length); + //Update the part read count to include the bytes/chars read in this iteration.// + partReadCount += remainingIndex; + //Clear the bytes indicating we have used them all and the loop should end.// + bytes = null; + }//else// + }//if// + }//if// + + //Read parts of the message until the end is found or we need to wait for furthor fragments of the message to be received.// + while(!endPartBoundaryFound && (startIndex != -1 || (currentPart != null && bytes != null))) { + //If we don't have a part setup to be read then identify the next part.// + if(currentPart == null) { + int partStartIndex = startIndex; //Retain the start of the message part in case we need to store a fragment for later use when more of the message arrives.// + + //Update the current index to the beginning of the part's header.// + currentIndex = startIndex + request.getMultiPartBoundary().length; + + if(bytes[currentIndex] == '\r' && bytes[currentIndex] == '\n') { + currentIndex += 2; + //Create the content part object with the default header.// + currentPart = request.getHeader().addContentPart("Content-type: text/plain; charset=us-ascii\r\n", partWriteCount); + }//if// + else { + int headerEndIndex = indexOf(bytes, DOUBLE_ENTER, currentIndex); + + //Determine whether the whole header is available. If it is then parse it, else save it until more of the message arrives.// + if(headerEndIndex != -1) { + //Create the content part object with the header.// + currentPart = request.getHeader().addContentPart(new String(bytes, currentIndex, (headerEndIndex + 4) - currentIndex, "ASCII"), partWriteCount); + //Set the current index to the end of the header.// + currentIndex = headerEndIndex + 4; + }//if// + else { + //Store the bytes starting with the multipart boundary for later use when another fragment arrives. + remainingPartBytes = new byte[bytes.length - partStartIndex]; + System.arraycopy(bytes, partStartIndex, remainingPartBytes, 0, remainingPartBytes.length); + //Clear the current part stored in the context.// + currentPart = null; + //Update the part read count to include the bytes/chars read in this iteration.// + partReadCount += partStartIndex; + //Clear the start index so the while loop ends.// + startIndex = -1; + }//else// + }//else// + }//if// + + //If we have the part header then look for the next boundary and store the data between the header and boundary as the content for the part.// + //Note that the currentIndex should stay pointed to the beginning of the message part so that we can figure out what to read for the part's content.// + if(currentPart != null) { + //Identify the next part.// + startIndex = indexOf(bytes, request.getMultiPartBoundary(), currentIndex); + + if(startIndex == -1) { + //Identify the final boundary if possible. This indicates that this is the last part in the message (any trailing bytes at the end of the message should be ignored).// + startIndex = indexOf(bytes, request.getMultiPartEndBoundary(), currentIndex); + + if(startIndex != -1) { + //ByteBuffer content = ByteBuffer.wrap(chars.substring(currentIndex, startIndex).getBytes("ASCII")); //Doesn't properly convert the bytes back from ASCII.// + ByteBuffer content = ByteBuffer.wrap(bytes, currentIndex, startIndex - currentIndex); + + //Mark the context as having found the end boundary.// + endPartBoundaryFound = true; + //Increment the write count so that indices passed to the ContentPart instances are correct.// + partWriteCount += content.remaining(); + //Increment the read count so we know how much of the message has been read and how much remains.// + partReadCount += startIndex + request.getMultiPartEndBoundary().length; + //Set the size of the part's data segment.// + currentPart.setDataLength(partWriteCount - currentPart.getDataOffset()); + + //Store the data between the header and the boundary.// + if(request.getContentChannel() != null) { + request.getContentChannel().write(content); + }//if// + else { + request.getContent().put(content); + }//else// + + //Update the current index to the beginning of the boundary.// + currentIndex = startIndex; + }//if// + else { //Store the data between the boundary and the end of the fragment, except for those parts of the fragment which might be the beginning of a boundary.// + ByteBuffer content; + int remainingIndex = Math.max(currentIndex, bytes.length - request.getMultiPartEndBoundary().length); + + //Retain enough characters to identify any boundary that might partially exist in this fragment of the message.// + remainingPartBytes = new byte[bytes.length - remainingIndex]; + System.arraycopy(bytes, remainingIndex, remainingPartBytes, 0, remainingPartBytes.length); + //Update the part read count to include the bytes/chars read in this iteration.// + partReadCount += remainingIndex; + //Extract the content to be stored.// + content = ByteBuffer.wrap(bytes, currentIndex, remainingIndex - currentIndex); + //Increment the write count so that indices passed to the ContentPart instances are correct.// + partWriteCount += content.remaining(); + //Clear the bytes indicating we have used them all and the loop should end.// + bytes = null; + + //Store all but the remaining bytes.// + if(request.getContentChannel() != null) { + request.getContentChannel().write(content); + }//if// + else { + request.getContent().put(content); + }//else// + }//else// + }//if// + else { + ByteBuffer content = ByteBuffer.wrap(bytes, currentIndex, startIndex); + + //Increment the write count so that indices passed to the ContentPart instances are correct.// + partWriteCount += content.remaining(); + //Set the size of the part's data segment.// + currentPart.setDataLength(partWriteCount - currentPart.getDataOffset()); + + //Store the data between the header and the boundary.// + if(request.getContentChannel() != null) { + request.getContentChannel().write(content); + }//if// + else { + request.getContent().put(content); + }//else// + + //Update the current index to the beginning of the boundary.// + currentIndex = startIndex; + }//else// + }//if// + }//while// + + //Keep reading past the last part if there are more bytes in the message. We can safely ignore these bytes.// + //Note that we increment the content length by 2 because we re-add the \r\n that follows just after the main header in order to allow for matching the boundary if it follows the header immediately.// + if(endPartBoundaryFound && partReadCount != (request.getContentLength() + 2) && bytes != null) { + //Discard bytes until the message is all read.// + //Update the part read count to be the whole chunk of message. There may still be more in which case this if block will execute again in the future.// + //Note that we will not have read more of the fragment into the chars string than is in the message, so we can just use the size of the chars string to increment the part count.// + partReadCount += bytes.length; + bytes = null; + }//if// + }//if// + else if(request.getContentLength() > 0) { //Read the body of the message (for non-multi-part mime encodings).// + int remainingBytes = request.getContentLength() - partReadCount; + byte[] bytes = null; + + if(fragment.hasArray()) { + int remainingPartByteCount = remainingPartBytes == null ? 0 : remainingPartBytes.length; + + bytes = new byte[Math.min(remainingBytes, fragment.remaining()) + remainingPartByteCount]; + + //Prepend the remaining bytes from the last fragment.// + if(remainingPartByteCount != 0) { + System.arraycopy(remainingPartBytes, 0, bytes, 0, remainingPartByteCount); + }//if// + + //Add in the bytes from this fragment.// + System.arraycopy(fragment.array(), fragment.arrayOffset() + fragment.position(), bytes, remainingPartByteCount, bytes.length - remainingPartByteCount); + //Update the fragment's position.// + fragment.position(fragment.position() + bytes.length - (remainingPartBytes == null ? 0 : remainingPartBytes.length)); + }//if// + else { + int remainingPartByteCount = remainingPartBytes == null ? 0 : remainingPartBytes.length; + byte[] buffer = bytes = new byte[Math.min(remainingBytes, fragment.remaining()) + remainingPartByteCount]; + + //Prepend the remaining bytes from the last fragment.// + if(remainingPartByteCount != 0) { + System.arraycopy(remainingPartBytes, 0, bytes, 0, remainingPartByteCount); + }//if// + + //Collect the bytes and increment the buffer's position.// + fragment.get(buffer, remainingPartByteCount, buffer.length - remainingPartByteCount); + }//else// + + //Place the bytes in either the content file or buffer for processing by the request in the postProcessContent() method.// + if(bytes != null && bytes.length > 0) { + partReadCount += bytes.length; + + //Store the data in either the content file or content buffer.// + if(request.getContentChannel() != null) { + request.getContentChannel().write(ByteBuffer.wrap(bytes)); + }//if// + else { + request.getContent().put(bytes); + }//else// + }//if// + }//else if// + }//if// + + //If we have a complete request then process it.// + //Note that for multi-part data we increment the content length by 2 because we re-add the \r\n that follows just after the main header in order to allow for matching the boundary if it follows the header immediately.// + if((request != null) && (partReadCount == (request.getContentLength() + (request.getContentTypeCode() == IRequest.CONTENT_TYPE_MULTI_PART_FORM_DATA ? 2 : 0)))) { + //Post process the content as necessary.// + if(request.getContentTypeCode() == IRequest.CONTENT_TYPE_URL_ENCODED) { + request.postProcessContent(); + }//if// + + //Process the request and send a response.// + result = processClientRequest(request, key); + + //If we have a partial message left then process it if we can.// + if(result && fragment.hasRemaining()) { + result = processClientRequest(fragment, key); + }//if// + }//if// + }//try// + catch(IgnoredIOException e) {} + catch(IOException e) { + Debug.log(e); + throw e; + }//catch// + catch(Throwable e) { + Debug.log(e); + throw new IOException("Failed to process the request headers."); + }//catch// + + return result; +}//processClientRequest()// +/** + * Processes a request by the client. + * @param request The request to be handled. + * @param key The socket's key. + * @result Whether request is in a receive state. Will be false if the request generated a response that could not be completely transmitted. + */ +private boolean processClientRequest(final Request request, SelectionKey key) throws IOException { + boolean result = true; + Response response = null; + + try { + boolean applicationNotFound = false; + + //Moved this bit of code into the calling method near the top. Access to the application was required that far up in the sequence of events.// +// if(context.webApplicationContainer == null) { +// //Save the domain.// +// context.domain = request.getHost() != null ? request.getHost().toLowerCase() : null; +// +// if(context.domain != null) { +// //Get the web application for the given domain.// +// //Synchronize to prevent another thread from altering the service's web applications while we are accessing it.// +// synchronized(WebServer.this) { +// context.webApplicationContainer = context.serverSocketContext.serviceListener.getWebApplicationContainer(context.domain); +// }//synchronized// +// }//if// +// }//if// + + //Verify an application was found for the context.// + if(webApplicationContainer != null) { + //Increment the activity counter until we finish processing this request.// + if(webApplicationContainer.incrementActivityCounter()) { + try { + IWebApplication application = webApplicationContainer.getWebApplication(); + + //Verify the application was not unloaded.// + if(application != null) { + boolean allowSecureAccess = false; + boolean clientHadBadSession = false; + ISession session = request.getSession(); + boolean hasNewSessionData = false; + + if(session == null) { + //TODO: Remove + if(getWebServer().debug) { + Debug.log("SC: " + id + " Creating Session"); + }//if// + + request.setSession(session = application.createSession()); + clientHadBadSession = request.getSessionId() != null; + hasNewSessionData = true; + + //Removed this code: Cannot disallow null sessions because then forwarding apps would need to fake a session. +// //Basic error checking.// +// if(session == null) { +// throw new RuntimeException("WebApplication failure: Cannot return a null value from IWebApplication.createSession()"); +// }//if// + }//if// + + //If we are handling a secure connection then setup or locate the secure session object.// + if(sslEngine != null) { + if((request.getSecureSessionId() != null) && (session != null) && (session.getSecureSessionId() != null)) { + if(session.getSecureSessionId().equals(request.getSecureSessionId())) { + allowSecureAccess = true; + }//if// + else { + Debug.log(new RuntimeException("Error: The client did not send the correct secure session id with the request!")); + }//else// + }//if// + else if(session != null && session.getSecureSessionId() == null) { + //TODO: Remove + if(getWebServer().debug) { + Debug.log("SC: " + id + " Creating Secure Session"); + }//if// + + application.createSecureSession(session); + allowSecureAccess = true; + hasNewSessionData = true; + }//else if// + else { + //TODO: For some reason Mozilla causes this error, but it shouldn't be. It seems to only occur when opening a new window and it appears to try to reuse the old session, but not secure session cookies. + //Debug.log(new RuntimeException("Error: The client did not send the secure session id with the request!")); + }//else// + }//if// + + //Save the session immediately since the requested resource might not indicate to the application that the session was updated.// + //Note: We shouldn't have any problems with multiple threads from the same client each creating their own session data since every browser should start with a single thread requesting a single resource before multiple threads are used to download all the child resources.// + if(hasNewSessionData && session != null) { + //Store the session store in the db.// + session.updateRepository(); + }//if// + + //Create the response.// + response = new Response(request, session, application); + + //Move to the next step in the request processing.// + result = internalProcessClientRequest(key, application, request, response, session, allowSecureAccess, clientHadBadSession); + }//if// + else { + applicationNotFound = true; + }//else// + }//try// + finally { + webApplicationContainer.decrementActivityCounter(); + }//finally// + }//if// + else { + applicationNotFound = false; + }//else// + }//if// + else { + applicationNotFound = true; + }//else// + + //There is no application for the request. No need to thread the processing of it since we won't be using reflections to send the response to the request (the response will always be an error).// + if(applicationNotFound) { + IContent content = null; + + if(getWebServer().getErrorHandler() != null) { + content = getWebServer().getErrorHandler().domainNotHosted(request); + }//if// + + if(content == null) { + //For now we will simply close the connection to the client - this should never happen anyhow and if it does, it could be an attack.// + Debug.log(new RuntimeException("Application not found for the host: " + request.getHost())); + key.channel().close(); + }//if// + else { + response = new Response(request, null, null); + response.setContent(content); + result = sendHttpResponse(response); + }//else// + }//if// + }//try// + catch(Throwable e) { + Debug.log(e); + //TODO: Use a 307 temporary redirect to display an error... + //response = new Response(this, null, key); + //response.setResourceNotFound(true); + //For now simply close the connection. This simplifies threading.// + //Close the socket.// + try {key.channel().close();}catch(Throwable e2) {} + //Close the response (which also closes the request).// + try {response.close();} catch(Throwable e2) {} + }//catch// + + return result; +}//processClientRequest()// +/** + * Processes a client request. + * @param request The request. + * @param response The response container. + * @param session The session for the request. This may be null in the case of non-standard web applications such as a forwarding domain. + * @param allowSecureAccess Whether the session's secure sessions should be accessable. + * @param clientHadBadSession Whether the client's request contained a session reference that could not be found on the server. + * @return Whether request is in a receive state. Will be false if the request generated a response that could not be completely transmitted. + */ +private boolean internalProcessClientRequest(SelectionKey key, final IWebApplication application, final Request request, final Response response, final ISession session, final boolean allowSecureAccess, final boolean clientHadBadSession) { + boolean result = false; + + try { + if(tlsFailure) { + response.setError(IResponse.ERROR_TYPE_TLS_FAILURE); + }//if// + //Send the request to the application to be processed if we are not dealing with an error.// + else if(application != null) { + String upgrade = request.getHeaderFieldValue("Upgrade"); + + if(upgrade != null && "websocket".equalsIgnoreCase(upgrade)) { + String connection = request.getHeaderFieldValue("Connection"); + String secWebSocketKey = request.getHeaderFieldValue("Sec-WebSocket-Key"); + String secWebSocketProtocol = request.getHeaderFieldValue("Sec-WebSocket-Protocol"); + String secWebSocketVersion = request.getHeaderFieldValue("Sec-WebSocket-Version"); + String origin = request.getHeaderFieldValue("Origin"); + + application.handleWebSocketUpgrade(request, response, session, allowSecureAccess, clientHadBadSession, this, connection, secWebSocketKey, secWebSocketProtocol, secWebSocketVersion, origin); + }//if// + else { + application.processRequest(request, response, session, allowSecureAccess, clientHadBadSession, this); + }//else// + }//else if// + + //Convert the response into a byte stream and send it via the socket.// + result = sendHttpResponse(response); + }//try// + catch(Throwable e) { + Debug.log(e); + //Close the socket.// + try {key.channel().close();} catch(Throwable e2) {} + //Close the response (which also closes the request).// + try {response.close();} catch(Throwable e2) {} + }//catch// + + return result; +}//internalProcessClientRequest()// +/** + * Parses the initial client hello message sent in the TLS protocol to identify which SSL context to use for the connection. + * @param context The connection context. + * @param channel The socket channel to read from. + * @return Whether the message could be parsed. If false, the method will be called again until true as new bytes arrive on the channel. + */ +private boolean parseFirstTlsMessage(SocketContext context, SocketChannel channel) throws IOException { + boolean result = false; + ByteBuffer temp = ByteBuffer.allocate(500); + StreamBuffer input; + int originalInputSize; + + //Create the stream buffer for this operation if not already created.// + if(context.initialBuffer == null) { + context.initialBuffer = new StreamBuffer(); + }//if// + + //Simplify the code a bit with a local variable.// + input = context.initialBuffer; + originalInputSize = input.getSize(); + + //Exit if the initial buffer size is rediculously large or if the channel is closed or has no additional bytes.// + while(channel.read(temp) > 0 && context.initialBuffer.getSize() < 10000) { + temp.flip(); + context.initialBuffer.writeBytes(temp); + }//while// + + //Ensure we have enough data to read the header.// + if(input.getSize() > 4) { + int contentType = input.getByte(0) & 0xFF; + int highVersion = input.getByte(1) & 0xFF; + int lowVersion = input.getByte(2) & 0xFF; + int length = input.getShort(3, StreamBuffer.NUMBER_MSF) & 0xFFFF; + +// context.initialBuffer.getAsText(); + + //Check the version.// + if(highVersion != 3 || lowVersion > 3 || lowVersion < 1) { + //handleBrokenStream(); + return true; + }//if// + + //Validate we have enough bytes for the whole message.// + if(input.getSize() > 4 + length) { + StreamBuffer tempBuffer = new StreamBuffer(input.getBufferPool()); + + //Indicate we have parsed the initial message (or at least attempted to).// + result = true; + //Clone the input buffer so we don't consume any bytes needed by the SSLEngine later.// + //Copy the bytes, don't consume them.// + tempBuffer.writeBytes(input, 0, length + 5); + input = tempBuffer; + + //A full record will be read.// + result = true; + //Skip the record header bytes.// + input.skipBytes(5); + + //Read each message in this record.// + switch(contentType) { +// case CONTENT_TYPE_CHANGE_CIPHER_SPEC: { +// processChangeCipherSpec(input); +// break; +// }//case// +// case CONTENT_TYPE_ALERT: { +// //Only one alert may exist in the message.// +// processAlert(input); +// break; +// }//case// + case 0x16: { //CONTENT_TYPE_HANDSHAKE + //Multiple handshake messages may be contained in the single record.// + while(input.getSize() > 0) { + int messageType = input.readByte() & 0xFF; + int messageSize = input.readMediumInt(StreamBuffer.NUMBER_MSF, false); + int postMessageStreamSize = input.getSize() - messageSize; + + switch(messageType) { + case 0x01: { //HANDSHAKE_TYPE_CLIENT_HELLO + //long time; + byte[] randomClientBytes = new byte[28]; + int sessionIdSize; + byte[] sessionId = null; + int cipherSuiteCount; + int[] cipherSuites; + int compressionMethodCount; + int[] compressionMethods; + /*int protocolVersionHigh = */input.readByte()/* & 0xFF*/; //Note: This should be the same as when it was read as part of the record earlier - this is redundant.// + /*int protocolVersionLow = */input.readByte()/* & 0xFF*/; + + /*time = */input.readInt(StreamBuffer.NUMBER_MSF)/* & 0x00000000FFFFFFFFL*/; + input.readBytes(randomClientBytes); + sessionIdSize = input.readByte() & 0xFF; + + if(sessionIdSize > 0) { + sessionId = new byte[sessionIdSize]; + input.readBytes(sessionId); + }//if// + + cipherSuiteCount = (input.readShort(StreamBuffer.NUMBER_MSF) & 0xFFFF) / 2; + cipherSuites = new int[cipherSuiteCount]; + + for(int index = 0; index < cipherSuites.length; index++) { + cipherSuites[index] = input.readShort(StreamBuffer.NUMBER_MSF) & 0xFFFF; + }//for// + + compressionMethodCount = input.readByte() & 0xFF; + compressionMethods = new int[compressionMethodCount]; + + for(int index = 0; index < compressionMethods.length; index++) { + compressionMethods[index] = input.readByte() & 0xFF; + }//for// + + //Read extensions until the end of the stream is reached.// + if(input.getSize() != postMessageStreamSize) { + /*int extensionArraySize =*/ input.readShort(StreamBuffer.NUMBER_MSF)/* & 0xFFFF*/; + + //Read each extension.// + while(input.getSize() != postMessageStreamSize) { + int extensionType = input.readShort(StreamBuffer.NUMBER_MSF) & 0xFFFF; + int extensionDataSize = input.readShort(StreamBuffer.NUMBER_MSF) & 0xFFFF; + int expectedStreamSize = input.getSize() - extensionDataSize; + + //Read the extension data if it is known, otherwise skip the data.// + switch(extensionType) { + case 0x0000: { //EXT_SERVER_NAME + //The ServerName extension always starts with a ServerNameList structure.// + int serverNameListByteCount = input.readShort(StreamBuffer.NUMBER_MSF) & 0xFFFF; + int postServerNamesExpectedStreamSize = input.getSize() - serverNameListByteCount; + + //Read each of the server names (no idea how many there will be - ).// + while(input.getSize() != postServerNamesExpectedStreamSize) { + int nameType = input.readByte() & 0xFF; + + switch(nameType) { + case 0x00: { //NAME_TYPE_HOST_NAME + int nameByteCount = input.readShort(StreamBuffer.NUMBER_MSF) & 0xFFFF; + String name; + + try { + name = input.readText(nameByteCount, "UTF8").toLowerCase(); +// Debug.log(name); + + if(context.domain == null) { + context.domain = name; + }//if// + }//try// + catch(UnsupportedEncodingException e) { + Debug.log(e); + }//catch// + break; + }//case// + default: { + break; + }//default// + }//switch// + }//while// + + break; + }//case// + default: { + input.skipBytes(extensionDataSize); + break; + }//default// + }//switch// + + //Ignore any unused bytes.// + if(input.getSize() != expectedStreamSize) { + input.skipBytes(input.getSize() - expectedStreamSize); + }//if// + }//for// + }//if// + break; + }//case// + default: { + handleBrokenStream(); + break; + }//default// + }//switch// + + //Verify we read exactly the number of bytes in the message.// + if(input.getSize() != postMessageStreamSize) { + handleBrokenStream(); + }//if// + }//while// + break; + }//case// +// case CONTENT_TYPE_APPLICATION: { +// //TODO: Decrypt / Validate the message. +// //TODO: Place the decrypted bytes in the result buffer. +// break; +// }//case// + default: { + handleBrokenStream(); + }//default// + }//switch// + + //Ignore any padding or mac bytes. There will not be any for handshake messages.// + if(input.getSize() > 0) { + input.skipBytes(input.getSize()); + }//if// + }//if// + }//if// + else if(originalInputSize == input.getSize()) { + //TODO: Why would the channel be flagged for reading when there is nothing to read? + throw new TlsFailureException("Connection is being flagged as having input, but has none."); + }//else if// + + return result; +}//parseFirstTlsMessage()// +/** + * Simplification of the error throwing code. + */ +private void handleBrokenStream() throws IOException { + throw new IOException("Invalid TLS stream."); +}//handleBrokenStream()// +/** + * Locates the first pattern match in the source from the given offset. + * @param source The source to be searched. + * @param pattern The pattern to search for. + * @param fromOffset The offset from the beginning of the source at which the search will commence (inclusive). + * @return The index of the first match, or -1 if no match was found. + */ +private int indexOf(byte[] source, byte[] pattern, int fromOffset) { + int result = -1; + + for(int index = (pattern.length - 1) + fromOffset; result == -1 && index < source.length; index++) { + boolean match = true; + + for(int patternIndex = pattern.length - 1, sourceOffset = 0; match && sourceOffset < pattern.length; sourceOffset++, patternIndex--) { + match = source[index - sourceOffset] == pattern[patternIndex]; + }//for// + + if(match) { + result = index - (pattern.length - 1); + }//if// + }//for// + + return result; +}//indexOf()// +/* (non-Javadoc) + * @see com.foundation.web.server.WebServer.AbstractSocketContext#hasPendingWrite() + */ +protected boolean hasPendingWrite() { + return currentOutboundMessage != null || (encryptedWriteBuffer != null && encryptedWriteBuffer.hasRemaining()); +}//hasPendingWrite()// +/* (non-Javadoc) + * @see com.foundation.web.interfaces.IConnectionContext#upgradeToWebsocket(java.lang.String, long, com.foundation.web.interfaces.WebsocketHandler) + */ +public void upgradeToWebsocket(String protocol, long maxMessageLength, WebsocketHandler websocketHandler) { + if(!isWebsocket) { + this.isWebsocket = true; + this.websocketProtocol = protocol; + this.websocketMaxMessageLength = maxMessageLength; + this.websocketFrameHeader = new byte[14]; + this.websocketMessageMaskKey = new byte[4]; + this.websocketPendingMessages = new Queue(20); + this.websocketHandler = websocketHandler; + }//if// +}//upgradeToWebsocket()// +/* (non-Javadoc) + * @see com.foundation.web.interfaces.IConnectionContext#getWebsocketProtocol() + */ +public String getWebsocketProtocol() { + return websocketProtocol; +}//getWebsocketProtocol()// +/* (non-Javadoc) + * @see com.foundation.web.interfaces.IConnectionContext#isWebsocket() + */ +public boolean isWebsocket() { + return isWebsocket; +}//isWebsocket()// +/* (non-Javadoc) + * @see com.foundation.web.interfaces.IConnectionContext#sendWebsocketMessage(byte[]) + */ +public void sendWebsocketMessage(byte[] message) { + if(isWebsocket) { + synchronized(websocketPendingMessages) { + websocketPendingMessages.enqueue(message); + }//synchronized// + + notifyListenerOfPendingWrite(); + }//if// +}//sendWebsocketMessage()// +/* (non-Javadoc) + * @see com.foundation.web.interfaces.IConnectionContext#sendWebsocketMessage(java.lang.String) + */ +public void sendWebsocketMessage(String message) { + if(isWebsocket) { + synchronized(websocketPendingMessages) { + websocketPendingMessages.enqueue(message); + }//synchronized// + + notifyListenerOfPendingWrite(); + }//if// +}//sendWebsocketMessage()// +/* (non-Javadoc) + * @see com.foundation.web.interfaces.IConnectionContext#sendWebsocketPing() + */ +public void sendWebsocketPing() { + if(isWebsocket) { + synchronized(websocketPendingMessages) { + websocketPendingMessages.enqueue(new Byte((byte) 0x9)); + }//synchronized// + + notifyListenerOfPendingWrite(); + }//if// +}//sendWebsocketPing()// +/** + * Sends a PONG response to the client's ping. + */ +public void sendWebsocketPong() { + if(isWebsocket) { + synchronized(websocketPendingMessages) { + websocketPendingMessages.enqueue(new Byte((byte) 0xA)); + }//synchronized// + + notifyListenerOfPendingWrite(); + }//if// +}//sendWebsocketPong()// +/* (non-Javadoc) + * @see com.foundation.web.interfaces.IConnectionContext#sendWebsocketMessage(com.foundation.web.interfaces.IConnectionContext.IStreamedWebsocketMessage) + */ +public void sendWebsocketMessage(IStreamedWebsocketMessage message) { + if(isWebsocket) { + synchronized(websocketPendingMessages) { + websocketPendingMessages.enqueue(message); + }//synchronized// + + notifyListenerOfPendingWrite(); + }//if// +}//sendWebsocketMessage()// +}//SocketContext// \ No newline at end of file diff --git a/Foundation Web Core/src/com/foundation/web/server/WebServer.java b/Foundation Web Core/src/com/foundation/web/server/WebServer.java index 07a5208..d62882e 100644 --- a/Foundation Web Core/src/com/foundation/web/server/WebServer.java +++ b/Foundation Web Core/src/com/foundation/web/server/WebServer.java @@ -21,7 +21,6 @@ import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.channels.ClosedChannelException; -import java.nio.channels.SelectableChannel; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; @@ -36,7 +35,6 @@ import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.text.SimpleDateFormat; import java.util.Date; -import java.util.Iterator; import java.util.Locale; import java.util.TimeZone; @@ -56,7 +54,6 @@ import com.common.io.ObjectInputStream; import com.common.io.StreamSupport; import com.common.orb.Address; import com.common.thread.IRunnable; -import com.common.thread.ThreadService; import com.common.util.IHashMap; import com.common.util.IIterator; import com.common.util.LiteHashMap; @@ -72,26 +69,11 @@ import com.foundation.web.interfaces.*; import com.foundation.web.server.Request.ContentPart; public class WebServer { - /** The character set used by the HTTP messages. */ - private static final Charset charset = Charset.forName("us-ascii"); - /** The decoder used to decode the HTTP messages. */ - private static final CharsetDecoder decoder = charset.newDecoder(); - /** The size of the buffers used per connection. */ - private static final int BUFFER_SIZE = 100000; - /** The format used for dates in the HTTP header. */ - private static final ThreadLocal httpDateFormat = new ThreadLocal(); - private static final String httpDateFormatString = "EEE, d MMM yyyy HH:mm:ss z"; - public static final int SEND_BUFFER_SIZE = 20480; //2048 - public static final int RECEIVE_BUFFER_SIZE = 20480; //2048 - private static final byte[] DOUBLE_ENTER = new byte[] {0x0D, 0x0A, 0x0D, 0x0A}; - /** The standard HTTP service. */ public static final String SERVICE_HTTP = "http"; /** The SSL/TLS service. */ public static final String SERVICE_SSL = "ssl"; - /** The next available socket context id. */ - private static int nextSocketContextId = 1; /** The set of listeners organized by the service they are providing. See SERVICE_xxx identifiers for common services. Custom services are simple strings. */ private IHashMap listenersByService = new LiteHashMap(20, new Comparator.StringComparator(false), Comparator.getIdentityComparator()); /** The network listener which is the engine that drives the incomming data. */ @@ -101,7 +83,7 @@ public class WebServer { /** Flag indicating whether the web server is active. The attributes cannot be changed in a thread unsafe manner when this is set. */ private volatile boolean isStarted = false; /** Whether to report all errors. If false, many networking errors (usually results of browsers terminating connections) will not be reported. */ - private boolean debug = false; + boolean debug = false; /** The handler called on to get a result for a bad request. */ private IWebServerErrorHandler errorHandler = null; /** The maximum length of any request's content block. */ @@ -127,7 +109,7 @@ public class WebServer { /** TODO: Support wild cards!!! The mapping of domain String instances to WebApplicationContainer instances for the listener. All domain names are lower case. */ private IHashMap webApplicationContainersByDomain = new LiteHashMap(20, new Comparator.StringComparator(false), Comparator.getIdentityComparator()); /** Whether the listener uses SSL/TLS. */ - private int type = TYPE_HTTP; + int type = TYPE_HTTP; /** The default domain to return for SSL/TLS usage. */ private String defaultSslDomain = null; @@ -151,7 +133,7 @@ public class WebServer { * @param domain The domain. * @return The web application's container. */ - private WebApplicationContainer getWebApplicationContainer(String domain) { + public WebApplicationContainer getWebApplicationContainer(String domain) { //TODO: Use something other than a hashmap so we can support wild cards! return (WebApplicationContainer) webApplicationContainersByDomain.get(domain); }//getWebApplicationContainer()// @@ -159,7 +141,7 @@ public class WebServer { * Gets the default domain to be used for SSL if the client is not using TLS with the domain extension. * @return */ - private String getDefaultSslDomain() { + public String getDefaultSslDomain() { return defaultSslDomain; }//getDefaultSslDomain()// /** @@ -178,713 +160,14 @@ public class WebServer { }//setWebApplicationContainer()// }//ServiceListener// - /** - * Provides a place for channel oriented data. - */ - private static class ChannelContext { - public ChannelContext() { - }//ChannelContext()// - }//ChannelContext// - - /** - * Provides a place connection oriented data. - */ - private static class ServerSocketContext extends ChannelContext { - public ServiceListener serviceListener = null; - - public ServerSocketContext(ServiceListener serviceListener) { - super(); - this.serviceListener = serviceListener; - }//ServerSocketContext()// - }//ServerSocketContext// - - /** - * The response message buffer encapsulating the request generating the response, and the content, and chainable into a linked list. - */ - private static class MessageBuffer { - /** The actual underlying buffer containing the bytes to be sent. Will be null if the message buffer needs initializing or has finished. */ - private ByteBuffer buffer = null; - /** The ability to chain message buffers into a linked list. */ - private MessageBuffer next = null; - - /** The optional response the message is based upon. */ - private Response response = null; - /** The content if there is any. */ - private IContent content = null; - - /** - * MessageBuffer constructor. - */ - public MessageBuffer() { - }//MessageBuffer()// - /** - * MessageBuffer constructor. - * @param buffer The buffer to use for assembling the message bytes. - */ - public MessageBuffer(ByteBuffer buffer) { - setBuffer(buffer); - }//MessageBuffer()// - /** - * Sets the actual underlying buffer for the message buffer. - * @param buffer - */ - protected void setBuffer(ByteBuffer buffer) { - this.buffer = buffer; - - if(buffer != null && buffer.position() != 0) buffer.flip(); - }//setBuffer()// - /** - * MessageBuffer constructor. - * @param buffer The buffer to use for assembling the message bytes. - * @param response The optional response that generates this message. - * @param content The content if the response is not just a header. - */ - public MessageBuffer(ByteBuffer buffer, Response response, IContent content) { - this.buffer = buffer; - this.content = content; - - //Fill the remaining buffer space with the content.// - if(content != null) { - content.get(buffer); - }//if// - - //Flip the buffer (if not already flipped) so we can write out the bytes.// - if(buffer.position() != 0) buffer.flip(); - this.response = response; - }//MessageBuffer()// - /** - * Initializes the message buffer for use. - * @return Whether initialization succeded. Intialization should be considered a success even if none is required or has already been performed. If it fails the caller should close the socket. - */ - public boolean initialize() { - //Does nothing by default. Subclasses may implement.// - return true; - }//initialize()// - /** - * Whether the message buffer is closed and all bytes have been sent. - * @return If the bytes have all been sent. - */ - public boolean isClosed() { - return buffer == null; - }//isClosed()// - /** - * Closes the message buffer. - */ - public void close() { - this.buffer = null; - }//close()// - /** - * Gets the byte buffer containing the current portion of the message to be sent. - * @return The buffer containing the next part of the message to be sent, or null if the message end has been reached. - */ - public ByteBuffer getBuffer() {return buffer;} - /** - * Loads the next part of the message into the buffer (any remaining bytes in the buffer will be compacted). - * @return Whether the buffer could be loaded with the next part of the message. If false, then the caller should try again in the future when additional message content may be available. Will always be false if there is no content to load from. - */ - public boolean loadBuffer() { - boolean result = true; - - if(buffer != null) { - if(content != null) { - int getResult; - - buffer.compact(); - getResult = content.get(buffer); - - if(getResult == IContent.CONTENT_PENDING) { - result = false; //Should never occur currently: See StreamedContent's javadocs.// - }//if// - else if(getResult == IContent.CONTENT_END) { - buffer = null; - }//else if// - - if(buffer != null && buffer.position() != 0) buffer.flip(); - }//if// - else if(!buffer.hasRemaining()) { - //Clear the buffer pointer indicating the message buffer is done.// - buffer = null; - result = false; - }//else if// - }//if// - else { - result = false; - }//else// - - return result; - }//loadBuffer()// - /** Gets the next message buffer (only used for pass through sockets). */ - public MessageBuffer getNext() {return next;} - /** Sets the next message buffer (only used for pass through sockets). */ - public void setNext(MessageBuffer next) {this.next = next;} - /** Gets the response object that created the message. This will be null for pass through sockets. */ - public Response getResponse() {return response;} - }//MessageBuffer// - private class WebsocketMessageBuffer extends MessageBuffer { - /** The streaming message handler which will be set only if the currently sending message is streaming. */ - private IStreamedWebsocketMessage streamingMessage = null; - /** The buffer containing the next part of the streamed message, or the bytes of the whole message (if streamingMessage == null), or null if the buffer is closed or not yet initialized. */ - private ByteBuffer messagePart = null; - /** The message to be sent. Will be null after the message buffer has initialized. */ - private Object message = null; - - public WebsocketMessageBuffer(Object message) { - this.message = message; - }//WebsocketMessageBuffer()// - public boolean initialize() { - if(message != null) { - messagePart = stream(message, true); - message = null; - }//if// - - return super.initialize(); - }//initialize()// - public boolean isClosed() { - return super.isClosed() && messagePart == null && streamingMessage == null; - }//isClosed()// - public void close() { - super.close(); - messagePart = null; - streamingMessage = null; - }//close()// - private ByteBuffer stream(Object next, boolean isLast) { - ByteBuffer result = null; - byte[] bytes = null; - int opCode = 0; - int length = 0; - - if(next instanceof String) { - try {bytes = ((String) next).getBytes("UTF-8");} catch(Throwable e) {Debug.log(e);} - opCode = streamingMessage == null ? 0x01 : 0; - length = bytes.length; - }//if// - else if(next instanceof byte[]) { - bytes = (byte[]) next; - opCode = streamingMessage == null ? 0x02 : 0; - length = bytes.length; - }//else if// - else if(next instanceof Byte) { //Control Message// - opCode = ((Byte) next).byteValue(); - }//else if// - else if(next instanceof IStreamedWebsocketMessage) { - //TODO: Ensure that this is not recursive! - streamingMessage = (IStreamedWebsocketMessage) next; - next = streamingMessage.getNextPart(); - isLast = !streamingMessage.hasNextPart(); - - if(next instanceof String) { - try {bytes = ((String) next).getBytes("UTF-8");} catch(Throwable e) {Debug.log(e);} - opCode = 0x01; //Text// - length = bytes.length; - }//if// - else if(next instanceof byte[]) { - bytes = (byte[]) next; - opCode = 0x02; //Binary// - length = bytes.length; - }//else if// - else { - throw new RuntimeException("Invalid streaming message part type."); - }//if// - }//else if// - - result = ByteBuffer.allocate(14 + length); - result.put((byte) (isLast ? 0x8 : 0)); - result.put((byte) opCode); - - //Write the length differently based on how long the content is.// - if(length < 126) { - result.put((byte) length); - }//if// - else if(length < 65535) { - result.put((byte) 126); - result.putShort((short) (length & 0xFFFF)); - result.putInt(0); - }//else if// - else { - result.put((byte) 127); - result.putLong((long) length); - }//else// - - //Put the content at the end of the message.// - result.put(bytes); - - return result; - }//stream()// - public boolean loadBuffer() { - boolean result = true; - - getBuffer().compact(); - - //Copy remaining bytes from MessagePart to the buffer.// - if(messagePart != null && messagePart.remaining() > 0) { - int length = Math.min(getBuffer().remaining(), messagePart.remaining()); - getBuffer().put(messagePart.array(), messagePart.position(), length); - messagePart.position(messagePart.position() + length); - - if(messagePart.remaining() == 0) { - messagePart = null; - }//if// - }//if// - - //Load a new message part if streaming and copy as many bytes from the MessagePart into the buffer as possible.// - if(messagePart == null && streamingMessage != null) { - Object next = null; - boolean isLastPart = true; - - next = streamingMessage.getNextPart(); - isLastPart = !streamingMessage.hasNextPart(); - - //Ensure that we got a string or byte array.// - if(!(next instanceof String || next instanceof byte[])) { - throw new RuntimeException("Invalid streaming message part type."); - }//if// - - messagePart = stream(next, isLastPart); - - if(isLastPart) { - streamingMessage = null; - }//if// - - int length = Math.min(getBuffer().remaining(), messagePart.remaining()); - getBuffer().put(messagePart.array(), messagePart.position(), length); - messagePart.position(messagePart.position() + length); - - if(messagePart.remaining() == 0) { - messagePart = null; - }//if// - }//if// - - //Close the message buffer if we were unable to add any bytes and there is nothing left to send.// - if(getBuffer().remaining() == 0) { - close(); - result = false; - }//if// - - return result; - }//loadBuffer()// - }//WebsocketMessageBuffer// - private class HttpMessageBuffer extends MessageBuffer { - /** The response the message is based upon. */ - private Response response = null; - /** The content if there is any. */ - private IContent content = null; - - public HttpMessageBuffer(Response response) { - this.response = response; - }//HttpMessageBuffer()// - /** Gets the response object that created the message. This will be null for pass through sockets. */ - public Response getResponse() {return response;} - /* (non-Javadoc) - * @see com.foundation.web.server.WebServer.MessageBuffer#initialize() - */ - public boolean initialize() { - boolean result = true; - - if(response != null && getBuffer() == null) { - Request request = (Request) response.getRequest(); - byte[] headerBytes = null; - IContent content = null; - ByteBuffer buffer = null; - - try { - //Wrap the response in http cloths. The HeaderFieldNames will be set if the response was provided with a completely custom HTTP header to be used.// - if(response.getHeaderFieldNames() != null) { - ByteArrayOutputStream bout = new ByteArrayOutputStream(1000); - PrintStream pout = new PrintStream(bout, true, "ASCII"); - LiteList headerFieldNames = response.getHeaderFieldNames(); - LiteHashMap headerFieldMap = response.getHeaderFieldMap(); - - //Write the response line which is mapped to the null field name.// - pout.print(headerFieldMap.get(null)); - pout.print("\r\n"); - - //Write the rest of the response header lines in order.// - for(int index = 0; index < headerFieldNames.getSize(); index++) { - String headerFieldName = (String) headerFieldNames.get(index); - String headerFieldValue = (String) headerFieldMap.get(headerFieldName); - - if(headerFieldName.equals("Server")) { - pout.print("Server: DE/1.0"); - }//if// - else { - pout.print(headerFieldName); - pout.print(": "); - pout.print(headerFieldValue); - }//else// - - pout.print("\r\n"); - }//for// - - //Write out any cookies necessary to retain our session.// - writeSessionCookies(pout); - - //End the header.// - pout.print("\r\n"); - pout.close(); - headerBytes = bout.toByteArray(); - - //Prepare the content for delivery.// - content = response.getContent(); - }//if// - else if(response.getForwardUri() != null) { - ByteArrayOutputStream bout = new ByteArrayOutputStream(1000); - PrintStream pout = new PrintStream(bout, true, "ASCII"); - //String today = format.format(new Date()); - - //The 303 code may not be fully supported by browsers.// - //if(request.getHttpVersion().equalsIgnoreCase("HTTP/1.1") || request.getHttpVersion().equalsIgnoreCase("HTTP/1.2")) { - // pout.print("HTTP/1.1 303 Forwarded\r\n"); - //}//if// - //else { - pout.print("HTTP/1.1 302 Moved Temporarily\r\n"); - //}//else// - - writeSessionCookies(pout); - - pout.print("Location: " + response.getForwardUri() + "\r\n"); - - //Note: Encoded URL's will have their parameters in the content, not in the URL.// - if(response.getContent() == null) { - pout.print("Content-Length: 0\r\n"); - }//if// - else { - pout.print("Content-Length: " + response.getContent().getSize() + "\r\n"); - pout.print("Content-Type: application/x-www-form-urlencoded\r\n"); - }//else// - - pout.print("\r\n"); - pout.close(); - headerBytes = bout.toByteArray(); - }//else if// - else if((content = response.getContent()) != null) { //Convert the result into a stream of bytes.// - ByteArrayOutputStream bout = new ByteArrayOutputStream(1000); - PrintStream pout = new PrintStream(bout, true, "ASCII"); - Date lastModifiedDate = content.getLastModifiedDate(); - String cacheDirective = null; - IMimeType mimeType = content.getMimeType(response.getApplication() != null ? response.getApplication().getMimeTypeProvider() : null); - boolean isDownloaded = content != null && (content.getIsDownloaded() == null ? mimeType != null && mimeType.isDownloaded() : content.getIsDownloaded().booleanValue()); - boolean compress = response.getCompress().booleanValue() && (mimeType == null || mimeType.isCompressable()); - int compressionType = 0; //0: none, 1: gzip, ..// - - if(compress) { - //Check for an encoding allowed by the client that we know how to use.// - if(request.getAllowedEncodings() == null || request.getAllowedEncodings().length < 1) { - compress = false; - }//if// - else { - compress = false; - - //Ensure we have an allowed encoding we know how to use.// - for(int index = 0; !compress && index < request.getAllowedEncodings().length; index++) { - String encoding = request.getAllowedEncodings()[index]; - - if(encoding.equalsIgnoreCase("gzip")) { - compress = true; - compressionType = 1; - }//if// - }//for// - }//else// - }//if// - - if(response.isError()) { - if(response.getHeader() != null) { - pout.print(response.getHeader()); - }//if// - else { - pout.print("HTTP/1.1 404 Resource Not Found\r\n"); - }//else// - }//if// - else if(response.getCustomHeader() != null) { - pout.print(response.getCustomHeader()); - }//else if// - else if(isDownloaded && request.getRange() != null) { - pout.print("HTTP/1.1 206 Partial Content\r\n"); - }//else if// - else { - pout.print("HTTP/1.1 200 OK\r\n"); - }//else// - - pout.print("Content-Length: " + (content != null ? content.getSize() : 0) + "\r\n"); - - if(compress) { - //TODO: Add others? - if(compressionType == 1) { - content = new GzipContent(content); - pout.print("Content-Encoding: gzip\r\n"); - }//if// - }//if// - - if(content != null) { - //Note: The character set gives IE indigestion for some reason.// - pout.print("Content-Type: " + (mimeType != null ? mimeType.getMimeName() : "text/html") + "; charset=" + (response.getCharacterSet() == null ? "UTF-8" : response.getCharacterSet()) + "\r\n"); - cacheDirective = content.getCacheDirective(); - - if(isDownloaded) { - pout.print("Content-Disposition: attachment; filename=\"" + content.getDownloadName() + "\";\r\n"); - pout.print("Accept-Ranges: bytes\r\n"); - - if(request.getRange() != null) { -// Debug.log("Sending a ranged response: " + request.getRange() + " content range: (" + content.getStart() + " - " + content.getEnd() + "/" + content.getSize() + ")."); - pout.print("Range: " + request.getRange() + "\r\n"); - pout.print("Content-Range: bytes " + content.getStart() + "-" + content.getEnd() + "/" + content.getSize() + "\r\n"); - }//if// - }//if// - }//if// - - writeSessionCookies(pout); - - pout.print("Server: DE/1.0\r\n"); - //TODO: IE has a problem with caching and forwarding/redirecting. A page that redirects to another page that was previously cached does not result in IE sending a request for the forwarded content.// - //private / no-cache - - if(content.getExpiresDirective() != null) { - pout.print("Expires: " + getHttpDateFormat().format(content.getExpiresDirective())); - }//if// - - if(cacheDirective != null) { - pout.print("Cache-Control: " + cacheDirective + "\r\n"); - }//if// - else { - int cacheLength = content.getCacheLength() != null ? content.getCacheLength().intValue() : mimeType != null ? mimeType.getDefaultCacheLength() : IMimeType.CACHE_LENGTH_NEVER_CACHE; - - if(cacheLength > 0) { - pout.print("Cache-Control: public, max-age=" + cacheLength + "\r\n"); - }//if// - else if(cacheLength == IMimeType.CACHE_LENGTH_ALWAYS_TEST) { - pout.print("Cache-Control: public, pre-check=0, post-check=120\r\n"); - }//else if// - else if(cacheLength == IMimeType.CACHE_LENGTH_NEVER_CACHE) { - pout.print("Cache-Control: no-cache\r\n"); - }//else if// - else { - pout.print("Cache-Control: no-store\r\n"); - }//else// - }//else// - - //TODO: Determine if we need to use age. - //pout.print("Age: 0\r\n"); - //TODO: Determine if we need to use ETags - - if(lastModifiedDate != null) { - SimpleDateFormat format = getHttpDateFormat(); - - pout.print("Last-Modified: " + format.format(lastModifiedDate) + "\r\n"); - pout.print("Date: " + format.format(new Date()) + "\r\n"); - }//if// - - pout.print("\r\n"); - headerBytes = bout.toByteArray(); - }//else if// - else { - ByteArrayOutputStream bout = new ByteArrayOutputStream(1000); - PrintStream pout = new PrintStream(bout, true, "ASCII"); - - if(response.isError()) { - if(response.getHeader() != null) { - pout.print(response.getHeader()); - }//if// - else { - pout.print("HTTP/1.1 404 Resource Not Found\r\n"); - }//else// - }//if// - else if(response.getCustomHeader() != null) { - pout.print(response.getCustomHeader()); - }//else if// - else { - Debug.log(new RuntimeException("The response to: " + response.getRequest().getHeaderText() + " had no response content!")); - pout.print("HTTP/1.1 200 OK\r\n"); - }//else// - - writeSessionCookies(pout); - pout.print("Content-Length: 0\r\n"); - pout.print("Server: DE/1.0\r\n"); - pout.print("\r\n"); - pout.close(); - headerBytes = bout.toByteArray(); - }//else// - - buffer = ByteBuffer.allocate(headerBytes.length > 2000 ? headerBytes.length : 2000); - buffer.put(headerBytes); - - if(debug) { - //Test code... - ByteBuffer buffer2 = ByteBuffer.allocate(headerBytes.length); - buffer2.put(headerBytes); - buffer2.flip(); - CharBuffer ch = decoder.decode(buffer2); -// debugBuffer.append("Sending message:\n"); -// debugBuffer.append(ch.toString()); -// debugBuffer.append("\nResponse Size: " + (headerBytes.length + (content != null ? content.getSize() : 0)) + "\n"); - Debug.log(ch.toString()); - }//if// - - //Ignore the content if we are only accessing the header.// -// if(content != null && request.getRequestType() != Request.TYPE_HEAD) { -// content.get(buffer); -// }//if// - - //Save the content as the current outbound message's content.// - this.content = content != null && request.getRequestType() != Request.TYPE_HEAD ? content : null; - - //Fill the remaining buffer space with the content.// - if(this.content != null) { - this.content.get(buffer); - }//if// - - //Save the buffer as the current outbound message's buffer.// - setBuffer(buffer); - }//try// - catch(Throwable e) { - Debug.log("Fatal Error: Failed to build and send the response message due to an exception.", e); - //Clean up after the request and response.// - try {response.close();} catch(Throwable e2) {} - //Fail the initialization.// - result = false; - }//catch// - }//if// - - return result; - }//initialize()// - private void writeSessionCookies(PrintStream pout) { - ISession session = response.getSession(); - - if(session != null) { - //Write the session id only if it has changed.// - if(!Comparator.equals(response.getRequest().getSessionId(), session.getSessionId())) { - pout.print("Set-Cookie: sessionId=" + session.getSessionId() + ";path=/;\r\n"); - }//if// - - //Write the secure session id only if it has changed.// - if(!Comparator.equals(response.getRequest().getSecureSessionId(), session.getSecureSessionId())) { - pout.print("Set-Cookie: secureSessionId=" + (session.getSecureSessionId() == null ? "" : session.getSecureSessionId()) + ";path=/;secure\r\n"); - }//if// - - if(response.getRequest().isLoggedIn() != session.getIsLoggedIn()) { - pout.print("Set-Cookie: isLoggedIn=" + session.getIsLoggedIn() + ";path=/;\r\n"); - }//if// - }//if// - }//writeSessionCookies()// - public boolean isClosed() { - return super.isClosed() && response == null; - }//isClosed()// - /* (non-Javadoc) - * @see com.foundation.web.server.WebServer.MessageBuffer#close() - */ - public void close() { - super.close(); - try {if(response != null) response.close();} catch(Throwable e) {} - response = null; - content = null; - }//close()// - /* (non-Javadoc) - * @see com.foundation.web.server.WebServer.MessageBuffer#loadBuffer() - */ - public boolean loadBuffer() { - boolean result = true; - ByteBuffer buffer = getBuffer(); - - if(buffer != null) { - if(content != null) { - int getResult; - - buffer.compact(); - getResult = content.get(buffer); - - if(getResult == IContent.CONTENT_PENDING) { - result = false; //Should never occur currently: See StreamedContent's javadocs.// - }//if// - else if(getResult == IContent.CONTENT_END) { - content = null; - }//else if// - - if(buffer.position() != 0) buffer.flip(); - }//if// - else { - if(!buffer.hasRemaining()) { - //Clear the buffer pointer indicating the message buffer is done.// - close(); - }//if// - - result = false; - }//else// - }//if// - else { - result = false; - }//else// - - return result; - }//loadBuffer()// - }//HttpMessageBuffer// - - private abstract class AbstractSocketContext extends ChannelContext { - /** The key that represents the connection between the channel (socket) and the selector used to multiplex the listener. The code must synchronize on this attribute when accessing the isUsed functionality, or when interacting with the key's interestOps. */ - public SelectionKey key = null; - /** Whether the socket is currently being used by a thread designated by the network listener thread to read or write to the socket. Currently the socket type we use in Java only allows one thread to read and write at a time. Note: Always synchronize on key before using this attribute. */ - private boolean isUsed = false; - /** A socket context related to this one (when two are tied together such that data from one immediately is sent to the other). */ - protected AbstractSocketContext relatedSocketContext = null; - - /** - * Gets the lockable (synchronizable) object for this context. For contexts with a related context, only one of the two will be returned, such that a single synchronize block covers both contexts. - * @return The object to synchronize on such that two threads don't attempt to interact with the context at the same time (AsynchronousSocketChannel required for that). - */ - protected abstract Object getLock(); - /** - * Writes the next responses/messages in the sequence. - * @throws IOException - */ - protected abstract void writeOutgoingMessages() throws IOException; - /** - * Reads the next requests/messages received via the socket. - * @throws IOException - */ - protected abstract void readIncomingMessages() throws IOException; - /** - * Passes the message through to a receiving process via a second socket. - * @param buffer The buffer containing the message. This buffer will not be retained by this method call, and can be reused by the caller. - * @return Whether the whole message was transfered. - */ - protected abstract boolean passThrough(ByteBuffer buffer); - /** - * Closes the socket context and cleans up. - */ - protected abstract void close(); - /** - * Gets the socket context related to this one (when two are tied together such that data from one immediately is sent to the other). - * @return The related socket context, or null if none exists (data not forwarded to a remote server). - */ - protected AbstractSocketContext getRelatedSocketContext() {return relatedSocketContext;} - /** - * Determines whether the socket has a pending write operation. - */ - protected abstract boolean hasPendingWrite(); - /** - * Called to notify the network listener that a pending write operation exists for this socket. - */ - protected void notifyListenerOfPendingWrite() { - synchronized(key) { - //Ignore if a thread is using this socket currently since all operation flags will be set at the end of the use of the socket.// - if(!isUsed) { - int ops = key.interestOps(); - boolean hasWrite = (ops & SelectionKey.OP_WRITE) != 0; - - if(!hasWrite) { - key.interestOps(ops | SelectionKey.OP_WRITE); - key.selector().wakeup(); - }//if// - }//if// - }//synchronized// - }//notifyListenerOfPendingWrite()// - }//AbstractSocketContext// - - private static class RegisterKeyRunnable implements Runnable { + public static class RegisterKeyRunnable implements Runnable { private boolean isRun = false; private SocketChannel channel; private Selector selector; private SelectionKey key = null; - private ChannelContext context; + private IChannelContext context; - public RegisterKeyRunnable(ChannelContext context, SocketChannel channel, Selector selector) { + public RegisterKeyRunnable(IChannelContext context, SocketChannel channel, Selector selector) { this.context = context; this.channel = channel; this.selector = selector; @@ -915,1866 +198,10 @@ public class WebServer { public SelectionKey getKey() {return key;} }//RegisterKeyRunnable// - /** - * Used by the SocketContext to create a connection to a remote process that will receive all client data once decrypted, and whose output will be encrypted and sent directly to the client. - * Allows the web server to act as an SSL front to another web server or service. - */ - private class PassThroughSocketContext extends AbstractSocketContext { - private MessageBuffer pendingMessageBuffer = null; - private MessageBuffer lastAddedMessageBuffer = null; - /** The byte buffer used to read data from the socket. */ - public ByteBuffer socketReadBuffer = ByteBuffer.allocate(BUFFER_SIZE); - - public PassThroughSocketContext(SocketContext linkedClientContext, String address, int port) throws IOException { - this.relatedSocketContext = linkedClientContext; - SocketChannel channel = SocketChannel.open(); - RegisterKeyRunnable runnable; - - //Connect while still blocking - will wait until the connection finishes or times out.// - channel.connect(new InetSocketAddress(address, port)); - //Setup the channel for non-blocking from here on out.// - channel.configureBlocking(false); - channel.socket().setSendBufferSize(SEND_BUFFER_SIZE); - channel.socket().setReceiveBufferSize(RECEIVE_BUFFER_SIZE); - channel.socket().setTcpNoDelay(false); - networkListener.queue(runnable = new RegisterKeyRunnable(this, channel, linkedClientContext.key.selector())); - linkedClientContext.key.selector().wakeup(); - runnable.waitForRun(); - key = runnable.getKey(); - - //Set the initial interest ops to read.// - synchronized(key) { - key.interestOps(SelectionKey.OP_READ); - }//synchronized// - - key.selector().wakeup(); - }//PassThroughSocketContext()// - /* (non-Javadoc) - * @see com.foundation.web.server.WebServer.AbstractSocketContext#getLock() - */ - protected Object getLock() { - return getRelatedSocketContext(); - }//getLock()// - /* (non-Javadoc) - * @see com.foundation.web.server.WebServer.AbstractSocketContext#processResponses() - */ - protected synchronized void writeOutgoingMessages() throws IOException { - //Actually this is called when a request is being sent via the pass through socket (sending the request to the remote server).// - //Synchronized to avoid accessing the pendingMessageBuffer and lastAddedMessageBuffer at the same time as a thread that is calling passThrough(ByteBuffer) which also accesses these variables.// - boolean result = true; - - if(result && pendingMessageBuffer != null) { - //Check to see if the outbound message is prepared to send more content.// - if(!pendingMessageBuffer.getBuffer().hasRemaining()) { - //Load the next pending outbound message in the chain.// - if(pendingMessageBuffer.getNext() != null) { - pendingMessageBuffer = pendingMessageBuffer.getNext(); - }//if// - else { - //Wait until additional message bytes are available.// - result = false; - pendingMessageBuffer = null; - lastAddedMessageBuffer = null; - }//else// - }//if// - - //Keep sending encrypted frames until the output buffer is full, or we run out of message to send.// - while(result && (pendingMessageBuffer != null) && pendingMessageBuffer.getBuffer().hasRemaining()) { - //Write the bytes to the stream.// - ((SocketChannel) key.channel()).write(pendingMessageBuffer.getBuffer()); - - //If not all the bytes could be written then we will need to wait until we can write more.// - if(pendingMessageBuffer.getBuffer().hasRemaining()) { - result = false; - }//if// - else { - //Load the next pending outbound message in the chain.// - if(pendingMessageBuffer.getNext() != null) { - pendingMessageBuffer = pendingMessageBuffer.getNext(); - }//if// - else { - //Wait until additional message bytes are available.// - result = false; - pendingMessageBuffer = null; - lastAddedMessageBuffer = null; - }//else// - }//else// - }//while// - }//if// - }//processResponses()// - /* (non-Javadoc) - * @see com.foundation.web.server.WebServer.AbstractSocketContext#processRequest() - */ - protected void readIncomingMessages() throws IOException { - //Actually this is called when a response is being processed via the pass through socket (remote process that received the request).// - int count = 1; - int loopCount = 0; - boolean result = true; - SocketChannel channel = (SocketChannel) key.channel(); - - //While we have a count greater than zero, indicating that some data is comming through, keep reading and processing the data.// - //Note: We are throddling this for active connections to prevent a single connection from hogging all the resources.// - while(loopCount < 10 && result && count > 0) { - loopCount++; - count = channel.read(socketReadBuffer); - socketReadBuffer.flip(); - - if(count == -1) { - //The socket has been closed by the client.// - try {relatedSocketContext.close();} catch(Throwable e) {} - }//if// - else if(socketReadBuffer.hasRemaining()) { - result = relatedSocketContext.passThrough(socketReadBuffer); - socketReadBuffer.compact(); - }//else// - else { - socketReadBuffer.compact(); - break; - }//else// - }//while// - }//processRequest()// - /* (non-Javadoc) - * @see com.foundation.web.server.WebServer.AbstractSocketContext#passThrough(java.nio.ByteBuffer) - */ - protected synchronized boolean passThrough(ByteBuffer buffer) { - ByteBuffer messageBytes = ByteBuffer.allocate(buffer.remaining()); - MessageBuffer message; - - //Create a new buffer to hold the data so we don't modify the passed buffer (other than to update its position).// - messageBytes = ByteBuffer.allocate(buffer.remaining()); - messageBytes.put(buffer); - message = new MessageBuffer(messageBytes); - - //Chain the message into the linked list. - if(lastAddedMessageBuffer == null) { - pendingMessageBuffer = lastAddedMessageBuffer = message; - }//if// - else { - lastAddedMessageBuffer.setNext(message); - lastAddedMessageBuffer = message; - }//else// - - return true; - }//passThrough()// - protected synchronized void close() { - try {if(key != null && key.channel() != null) key.channel().close();} catch(Throwable e) {} - try {if(key != null) key.cancel();} catch(Throwable e) {} - }//close()// - /* (non-Javadoc) - * @see com.foundation.web.server.WebServer.AbstractSocketContext#hasPendingWrite() - */ - protected boolean hasPendingWrite() { - return pendingMessageBuffer != null; - }//hasPendingWrite()// - }//PassThroughSocketContext// - - /** - * Provides a place for connection oriented data. - *

Note that a client server session can have multiple socket contexts (one for each socket) and that the socket context may be used to access multiple applications hosted on this server.

- */ - private class SocketContext extends AbstractSocketContext implements IWebApplicationContainerProvider, IConnectionContext { - public final int id; - /** The server socket reference that created the socket. This will be null if the socket was not created with a server socket (shouldn't happen in a web server). */ - public ServerSocketContext serverSocketContext = null; - /** The web application's container for the application associated with this connection to a client. A connection is only allowed to access a single application. */ - public WebApplicationContainer webApplicationContainer = null; - /** A temporary storage location for part of an HTTP message header. Warning: This is used by the reading thread ONLY - never by the processing/writing threads. */ - public StringBuffer messageHeaderFragment = null; - /** A reference to the request object currently being processed. Warning: This is used by the reading thread ONLY - never by the processing/writing threads. */ - public Request request = null; - /** The count of stored content bytes for the request. This is valid for any type of request. */ - public int requestContentPosition = 0; - /** The multi-part message characters remaining to be processed from the last message fragment received. */ - public byte[] remainingPartBytes = null; - /** The multi-part message count of characters/bytes read thus far. This ensures the entire message is read properly. */ - public int partReadCount = 0; - /** The mutli-part message count of characters/bytes written to the buffer as the message from the client is parsed. Used to index into the buffer by the various ContentPart instances generated. */ - public int partWriteCount = 0; - /** The part that is currently being read and spans more than one message fragment. */ - public ContentPart currentPart = null; - /** Whether the final part boundary has already been found. If this is true then the message bytes should keep getting read until the partReadCount == contentLength. */ - public boolean endPartBoundaryFound = false; - /** The bytes containing the unencrypted outbound message that is waiting for the socket to allow a write. */ - public MessageBuffer/*ByteBuffer*/ currentOutboundMessage = null; - /** The last message buffer added to the pending outbound message chain (linked list). Used only for pass through contexts currently since locally handled messages link the reponses together into a list. */ - private MessageBuffer lastOutboundMessage = null; - /** The byte buffer used to read data from the socket. This must be null if a SSL engine is provided. */ - public ByteBuffer socketReadBuffer = null; - /** The buffer used to store the initial data in a SSL/TLS connection. The buffering is necessary to allow us to pre-read the client's hello message to gather the domain the client is connecting to - allowing for the correct SSL engine to be used. */ - public StreamBuffer initialBuffer = null; - /** Whether the TLS domain extension was used. If false then the default SSL domain should be used to attempt an anonymous connection with the client such that an error page can be displayed prompting the user to upgrade their browser to something modern. */ - public boolean tlsFailure = false; - /** The domain name the client is connecting to. This is provided by the initial TLS hello message, and is used to select the correct SSL context for the desired web application. */ - public String domain = null; - /** Non-null if the socket is using SSL to secure communications. */ - public SSLEngine sslEngine = null; - /** Whether the ssl engine needs to send handshake data to the client and has not yet generated it. */ - public boolean sslNeedsWrap = false; - /** The reusable buffer containing encrypted data from the client. */ - public ByteBuffer encryptedReadBuffer = null; - /** The reusable buffer containing unencrypted data from the client. */ - public ByteBuffer unencryptedReadBuffer = null; - /** The reusable buffer containing encrypted data being sent to the client. */ - public ByteBuffer encryptedWriteBuffer = null; - /** The last used request number which identifies the sequence for the requests. */ - private int lastRequestNumber = 0; - /** The response we are currently processing. */ - private Response currentResponse = null; - /** The response we are will process last. */ - private Response lastResponse = null; - /** Tracks the number of bytes sent from the current response. This is only used when debugging. */ - private int sentBytes = 0; - /** Tracks the debug output for the current request/response cycle. This is only used when debugging. */ - public StringBuffer debugBuffer = null; //debug ? new StringBuffer(1000) : null; - /** The optional application specific data indexed by an application spectific key. Elements (values, not keys) which implement ISessionLifecycleAware will be released when the socket context is released. */ - private LiteHashMap applicationDataMap; - /** Used to identify the first unencrypted message (ignored if ssl is being used) so that forwarding to a remote server can be accomplished. */ - private boolean isFirstUnencryptedMessage = true; - /** Whether this is a websocket connection. */ - private boolean isWebsocket = false; - /** The protocol passed when this connection was upgraded to a websocket. */ - private String websocketProtocol = null; - /** The maximum number of bytes in an allowed websocket message (may be composed of multiple frames, does not include the frame header sizes). While this is a long, we really can't read longer than Integer.MAX_VALUE sized frames. So the message might be within size limits, but it might still be rejected if the frame exceeds the server's capacity to read a frame. */ - private long websocketMaxMessageLength = 0; - /** The reusable frame header buffer. */ - private byte[] websocketFrameHeader = null; - /** The index into the frame header of the last read byte. */ - private int websocketFrameHeaderIndex = 0; - /** The next partial message (single messages may be broken into frames by the client). */ - private StreamBuffer websocketPartialReceivedMessage = null; - /** The remaining bytes to be read on the current message frame (the frame header and part of the frame was read previously). */ - private int websocketFrameRemainder = 0; - /** Whether the frame currently being read in the current message being received is the last frame in the message (when the frame is fully read it will trigger the message to be processed). */ - private boolean websocketIsLastFrameInMessage = false; - /** The op code for the currently reading message, or zero if the last message was completed. */ - private int websocketMessageOpCode = 0; - /** The currently reading frame's mask key used to decode the frame data. */ - private byte[] websocketMessageMaskKey = null; - /** The queue used to hold outgoing messages before they are sent. Will contain only String, byte[], or IConnectionContext.IStreamedWebsocketMessage instances. */ - private Queue websocketPendingMessages = null; - /** The streambuffer for the currently sending message. This will be for the current part of the streaming message if websocketStreamingMessage is non-null. */ - private ByteBuffer websocketSendingMessage = null; - /** The streaming message handler which will be set only if the currently sending message is streaming. */ - private IStreamedWebsocketMessage websocketStreamingMessage = null; - /** The application specified handler called when websocket events occur (messages received, socket closed, etc). */ - private WebsocketHandler websocketHandler = null; - - public SocketContext(ServerSocketContext serverSocketContext) { - super(); - synchronized(SocketContext.class) { - this.id = nextSocketContextId++; - }//synchronized// - }//SocketContext()// - /** - * Gets the pass through socket context associated with this socket context, or null if none exists. - * @return The socket context for the pass through socket used to handle all incoming requests from the client on this socket. - */ - protected PassThroughSocketContext getPassThroughSocketContext() { - return (PassThroughSocketContext) getRelatedSocketContext(); - }//getPassThroughSocketContext()// - /* (non-Javadoc) - * @see com.foundation.web.server.WebServer.AbstractSocketContext#getLock() - */ - protected Object getLock() { - return this; - }//getLock()// - protected synchronized void close() { - try { - if(websocketHandler != null) { - websocketHandler.connectionClosed(); - websocketHandler = null; - }//if// - }//try// - catch(Throwable e) { - Debug.log(e); - }//catch// - - try { - if(applicationDataMap != null) { - for(IIterator iterator = applicationDataMap.valueIterator(); iterator.hasNext(); ) { - Object next = iterator.next(); - - //Ensure the code keeps going even if there is a problem cleaning up.// - try { - if(next instanceof ISessionLifecycleAware) ((ISessionLifecycleAware) next).release(); - }//try// - catch(Throwable e) { - Debug.log(e); - }//catch// - }//for// - }//if// - }//try// - catch(Throwable e) { - Debug.log(e); - }//catch// - - try {if(key != null && key.channel() != null) key.channel().close();} catch(Throwable e) {} - try {if(key != null) key.cancel();} catch(Throwable e) {} - //Clean up after the response and request.// - //try {while(currentOutboundMessage != null) {currentOutboundMessage.close(); currentOutboundMessage = currentOutboundMessage.getNext();}} catch(Throwable e2) {} - try {if(currentResponse != null) currentResponse.close();} catch(Throwable e2) {} - - if(getPassThroughSocketContext() != null) { - getPassThroughSocketContext().close(); - }//if// - }//close()// - /* (non-Javadoc) - * @see com.foundation.web.interfaces.IConnectionContext#getApplicationData(java.lang.String) - */ - public Object getApplicationData(String key) { - return applicationDataMap == null ? null : applicationDataMap.get(key); - }//getApplicationData()// - /* (non-Javadoc) - * @see com.foundation.web.interfaces.IConnectionContext#setApplicationData(java.lang.String, java.lang.Object) - */ - public void setApplicationData(String key, Object applicationData) { - if(applicationDataMap == null) { - applicationDataMap = new LiteHashMap(10); - }//if// - - applicationDataMap.put(key, applicationData); - }//setApplicationData()// - /* (non-Javadoc) - * @see com.foundation.web.server.WebServer.IWebApplicationContainerProvider#getWebApplicationContainer() - */ - public WebApplicationContainer getWebApplicationContainer() { - return webApplicationContainer; - }//getWebApplicationContainer()// - /** - * Whether the socket is an SSL/TLS socket. - * @return Whether data streamed over the socket is encrypted using SSL/TLS. - */ - public boolean isSsl() { - return socketReadBuffer == null; - }//isSsl()// - /** - * Queues an outbound client message by adding it to the linked list of message buffers. Handles synchronizing on this SocketContext to prevent multiple threads from adding messages at once. Also handles notifying the ServiceListener that it should update it's write flags for the socket. - * @param messageBuffer The buffer to be added to the end. - */ - private void queueOutboundClientMessage(MessageBuffer messageBuffer) { - boolean notify = false; - - synchronized(this) { - if(currentOutboundMessage == null) { - lastOutboundMessage = currentOutboundMessage = messageBuffer; - notify = true; - }//if// - else { - lastOutboundMessage.setNext(messageBuffer); - lastOutboundMessage = messageBuffer; - }//else// - }//synchronized()// - - if(notify) { - notifyListenerOfPendingWrite(); - }//if// - }//queueOutboundClientMessage()// - private void writeSessionCookies(PrintStream pout) { - Response response = currentResponse; - ISession session = response.getSession(); - - if(session != null) { - //Write the session id only if it has changed.// - if(!Comparator.equals(response.getRequest().getSessionId(), session.getSessionId())) { - pout.print("Set-Cookie: sessionId=" + session.getSessionId() + ";path=/;\r\n"); - }//if// - - //Write the secure session id only if it has changed.// - if(!Comparator.equals(response.getRequest().getSecureSessionId(), session.getSecureSessionId())) { - pout.print("Set-Cookie: secureSessionId=" + (session.getSecureSessionId() == null ? "" : session.getSecureSessionId()) + ";path=/;secure\r\n"); - }//if// - - if(response.getRequest().isLoggedIn() != session.getIsLoggedIn()) { - pout.print("Set-Cookie: isLoggedIn=" + session.getIsLoggedIn() + ";path=/;\r\n"); - }//if// - }//if// - }//writeSessionCookies()// - /** - * Processes the next response in the sequence. - *

Note: The caller must synchronize on this context to prevent multiple threads from accessing the context at the same time.

- * @result Whether request is in a receive state. Will be false if the request generated a response that could not be completely transmitted. - */ - private void prepareResponse() { - Response response = currentResponse; - Request request = (Request) response.getRequest(); - byte[] headerBytes = null; - IContent content = null; - ByteBuffer buffer = null; - - try { - //Wrap the response in http cloths. The HeaderFieldNames will be set if the response was provided with a completely custom HTTP header to be used.// - if(response.getHeaderFieldNames() != null) { - ByteArrayOutputStream bout = new ByteArrayOutputStream(1000); - PrintStream pout = new PrintStream(bout, true, "ASCII"); - LiteList headerFieldNames = response.getHeaderFieldNames(); - LiteHashMap headerFieldMap = response.getHeaderFieldMap(); - - //Write the response line which is mapped to the null field name.// - pout.print(headerFieldMap.get(null)); - pout.print("\r\n"); - - //Write the rest of the response header lines in order.// - for(int index = 0; index < headerFieldNames.getSize(); index++) { - String headerFieldName = (String) headerFieldNames.get(index); - String headerFieldValue = (String) headerFieldMap.get(headerFieldName); - - if(headerFieldName.equals("Server")) { - pout.print("Server: DE/1.0"); - }//if// - else { - pout.print(headerFieldName); - pout.print(": "); - pout.print(headerFieldValue); - }//else// - - pout.print("\r\n"); - }//for// - - //Write out any cookies necessary to retain our session.// - writeSessionCookies(pout); - - //End the header.// - pout.print("\r\n"); - pout.close(); - headerBytes = bout.toByteArray(); - - //Prepare the content for delivery.// - content = response.getContent(); - }//if// - else if(response.getForwardUri() != null) { - ByteArrayOutputStream bout = new ByteArrayOutputStream(1000); - PrintStream pout = new PrintStream(bout, true, "ASCII"); - //String today = format.format(new Date()); - - //The 303 code may not be fully supported by browsers.// - //if(request.getHttpVersion().equalsIgnoreCase("HTTP/1.1") || request.getHttpVersion().equalsIgnoreCase("HTTP/1.2")) { - // pout.print("HTTP/1.1 303 Forwarded\r\n"); - //}//if// - //else { - pout.print("HTTP/1.1 302 Moved Temporarily\r\n"); - //}//else// - - writeSessionCookies(pout); - - pout.print("Location: " + response.getForwardUri() + "\r\n"); - - //Note: Encoded URL's will have their parameters in the content, not in the URL.// - if(response.getContent() == null) { - pout.print("Content-Length: 0\r\n"); - }//if// - else { - pout.print("Content-Length: " + response.getContent().getSize() + "\r\n"); - pout.print("Content-Type: application/x-www-form-urlencoded\r\n"); - }//else// - - pout.print("\r\n"); - pout.close(); - headerBytes = bout.toByteArray(); - }//else if// - else if((content = response.getContent()) != null) { //Convert the result into a stream of bytes.// - ByteArrayOutputStream bout = new ByteArrayOutputStream(1000); - PrintStream pout = new PrintStream(bout, true, "ASCII"); - Date lastModifiedDate = content.getLastModifiedDate(); - String cacheDirective = null; - IMimeType mimeType = content.getMimeType(response.getApplication() != null ? response.getApplication().getMimeTypeProvider() : null); - boolean isDownloaded = content != null && (content.getIsDownloaded() == null ? mimeType != null && mimeType.isDownloaded() : content.getIsDownloaded().booleanValue()); - boolean compress = response.getCompress().booleanValue() && (mimeType == null || mimeType.isCompressable()); - int compressionType = 0; //0: none, 1: gzip, ..// - - if(compress) { - //Check for an encoding allowed by the client that we know how to use.// - if(request.getAllowedEncodings() == null || request.getAllowedEncodings().length < 1) { - compress = false; - }//if// - else { - compress = false; - - //Ensure we have an allowed encoding we know how to use.// - for(int index = 0; !compress && index < request.getAllowedEncodings().length; index++) { - String encoding = request.getAllowedEncodings()[index]; - - if(encoding.equalsIgnoreCase("gzip")) { - compress = true; - compressionType = 1; - }//if// - }//for// - }//else// - }//if// - - if(response.isError()) { - if(response.getHeader() != null) { - pout.print(response.getHeader()); - }//if// - else { - pout.print("HTTP/1.1 404 Resource Not Found\r\n"); - }//else// - }//if// - else if(response.getCustomHeader() != null) { - pout.print(response.getCustomHeader()); - }//else if// - else if(isDownloaded && request.getRange() != null) { - pout.print("HTTP/1.1 206 Partial Content\r\n"); - }//else if// - else { - pout.print("HTTP/1.1 200 OK\r\n"); - }//else// - - pout.print("Content-Length: " + (content != null ? content.getSize() : 0) + "\r\n"); - - if(compress) { - //TODO: Add others? - if(compressionType == 1) { - content = new GzipContent(content); - pout.print("Content-Encoding: gzip\r\n"); - }//if// - }//if// - - if(content != null) { - //Note: The character set gives IE indigestion for some reason.// - pout.print("Content-Type: " + (mimeType != null ? mimeType.getMimeName() : "text/html") + "; charset=" + (response.getCharacterSet() == null ? "UTF-8" : response.getCharacterSet()) + "\r\n"); - cacheDirective = content.getCacheDirective(); - - if(isDownloaded) { - pout.print("Content-Disposition: attachment; filename=\"" + content.getDownloadName() + "\";\r\n"); - pout.print("Accept-Ranges: bytes\r\n"); - - if(request.getRange() != null) { -// Debug.log("Sending a ranged response: " + request.getRange() + " content range: (" + content.getStart() + " - " + content.getEnd() + "/" + content.getSize() + ")."); - pout.print("Range: " + request.getRange() + "\r\n"); - pout.print("Content-Range: bytes " + content.getStart() + "-" + content.getEnd() + "/" + content.getSize() + "\r\n"); - }//if// - }//if// - }//if// - - writeSessionCookies(pout); - - pout.print("Server: DE/1.0\r\n"); - //TODO: IE has a problem with caching and forwarding/redirecting. A page that redirects to another page that was previously cached does not result in IE sending a request for the forwarded content.// - //private / no-cache - - if(content.getExpiresDirective() != null) { - pout.print("Expires: " + getHttpDateFormat().format(content.getExpiresDirective())); - }//if// - - if(cacheDirective != null) { - pout.print("Cache-Control: " + cacheDirective + "\r\n"); - }//if// - else { - int cacheLength = content.getCacheLength() != null ? content.getCacheLength().intValue() : mimeType != null ? mimeType.getDefaultCacheLength() : IMimeType.CACHE_LENGTH_NEVER_CACHE; - - if(cacheLength > 0) { - pout.print("Cache-Control: public, max-age=" + cacheLength + "\r\n"); - }//if// - else if(cacheLength == IMimeType.CACHE_LENGTH_ALWAYS_TEST) { - pout.print("Cache-Control: public, pre-check=0, post-check=120\r\n"); - }//else if// - else if(cacheLength == IMimeType.CACHE_LENGTH_NEVER_CACHE) { - pout.print("Cache-Control: no-cache\r\n"); - }//else if// - else { - pout.print("Cache-Control: no-store\r\n"); - }//else// - }//else// - - //TODO: Determine if we need to use age. - //pout.print("Age: 0\r\n"); - //TODO: Determine if we need to use ETags - - if(lastModifiedDate != null) { - SimpleDateFormat format = getHttpDateFormat(); - - pout.print("Last-Modified: " + format.format(lastModifiedDate) + "\r\n"); - pout.print("Date: " + format.format(new Date()) + "\r\n"); - }//if// - - pout.print("\r\n"); - headerBytes = bout.toByteArray(); - }//else if// - else { - ByteArrayOutputStream bout = new ByteArrayOutputStream(1000); - PrintStream pout = new PrintStream(bout, true, "ASCII"); - - if(response.isError()) { - if(response.getHeader() != null) { - pout.print(response.getHeader()); - }//if// - else { - pout.print("HTTP/1.1 404 Resource Not Found\r\n"); - }//else// - }//if// - else if(response.getCustomHeader() != null) { - pout.print(response.getCustomHeader()); - }//else if// - else { - Debug.log(new RuntimeException("The response to: " + response.getRequest().getHeaderText() + " had no response content!")); - pout.print("HTTP/1.1 200 OK\r\n"); - }//else// - - writeSessionCookies(pout); - pout.print("Content-Length: 0\r\n"); - pout.print("Server: DE/1.0\r\n"); - pout.print("\r\n"); - pout.close(); - headerBytes = bout.toByteArray(); - }//else// - - buffer = ByteBuffer.allocate(headerBytes.length > 2000 ? headerBytes.length : 2000); - buffer.put(headerBytes); - - if(debug) { - //Test code... - ByteBuffer buffer2 = ByteBuffer.allocate(headerBytes.length); - buffer2.put(headerBytes); - buffer2.flip(); - CharBuffer ch = decoder.decode(buffer2); -// debugBuffer.append("Sending message:\n"); -// debugBuffer.append(ch.toString()); -// debugBuffer.append("\nResponse Size: " + (headerBytes.length + (content != null ? content.getSize() : 0)) + "\n"); - Debug.log(ch.toString()); - }//if// - - //Ignore the content if we are only accessing the header.// -// if(content != null && request.getRequestType() != Request.TYPE_HEAD) { -// content.get(buffer); -// }//if// - - //Save the buffer as the current pending outbound message for this socket context.// - currentOutboundMessage = new MessageBuffer(buffer, response, content != null && request.getRequestType() != Request.TYPE_HEAD ? content : null); - }//try// - catch(Throwable e) { - Debug.log("Fatal Error: Failed to build and send the response message due to an exception.", e); - //Force the channel to close.// - try {key.channel().close();} catch(Throwable e2) {} - //Clean up after the request and response.// - try {response.close();} catch(Throwable e2) {} - }//catch// - }//prepareResponse()// - /** - * Adds a HTTP response to the socket context. - *

Note: We must synchronize since a socket could be used to access multiple applications and thus mutliple sessions.

- * @param response The response to be added. - * @result Whether request is in a receive state. Will be false if the request generated a response that could not be completely transmitted. - */ - public synchronized boolean sendHttpResponse(Response response) { - if(currentResponse != null) { - lastResponse.setNextResponse(response); - lastResponse = response; - }//if// - else { - lastResponse = currentResponse = response; - sentBytes = 0; - prepareResponse(); - - //Note: Not going to process the response on this thread. Allow the flag to be set for writing to the socket, and have the next thread in the network listener handle the write. This allows for cleaner code and pipelining without all the synchronizing. -// result = internalProcessResponses(); - }//else// - - request = null; - - return false; - }//sendHttpResponse()// - /* (non-Javadoc) - * @see com.foundation.web.server.WebServer.AbstractSocketContext#passThrough(java.nio.ByteBuffer) - */ - protected synchronized boolean passThrough(ByteBuffer buffer) { - ByteBuffer messageBytes = ByteBuffer.allocate(buffer.remaining()); - MessageBuffer message; - - //Create a new buffer to hold the data so we don't modify the passed buffer (other than to update its position).// - messageBytes = ByteBuffer.allocate(buffer.remaining()); - messageBytes.put(buffer); - message = new MessageBuffer(messageBytes); - - //Chain the message into the linked list. - if(lastOutboundMessage == null || currentOutboundMessage == null) { - currentOutboundMessage = lastOutboundMessage = message; - }//if// - else { - lastOutboundMessage.setNext(message); - lastOutboundMessage = message; - }//else// - - return true; - }//passThrough()// - /* (non-Javadoc) - * @see com.foundation.web.server.WebServer.AbstractSocketContext#processResponses() - */ - protected void writeOutgoingMessages() throws IOException { - if(getPassThroughSocketContext() != null) { - //Synchronized to avoid multiple threads accessing the pendingOutboundMessage chain at one time and updating the write flag out of order (could happen if we enabled request chaining over a single socket).// - synchronized(this) { - writeClientBoundMessage(); - }//synchronized// - }//if// - else if(isWebsocket) { - //Right after upgrading the socket we have one last HTTP response to process.// - if(currentResponse != null) { - internalProcessResponses(); - }//if// - - internalProcessWebsocketMessages(); - }//else if// - else { - //Go directly to writing the client response if we are just passing everything through to another process.// - internalProcessResponses(); - }//else// - }//processCurrentResponse()// - /** - * Loads the next outbound websocket message and attempts to write it to the socket until all outbound messages have been sent, or the socket's buffers are full and a wait is required. - * If a message could only be partially sent then the next call will attempt to finish sending it. - */ - private void internalProcessWebsocketMessages() { - if(websocketSendingMessage == null) { - loadNextWebsocketMessage(); - }//if// - - while(websocketSendingMessage != null) { - //If the socket is open then send the next buffer of data.// - if(key.channel().isOpen()) { - if(currentOutboundMessage != null) { - //Put the sending message in a MessageBuffer (pendingOutboundMessage).// - currentOutboundMessage = new MessageBuffer(websocketSendingMessage); - }//if// - - //Write the pendingOutboundMessage to the socket.// - if(writeClientBoundMessage()) { - websocketSendingMessage = null; - currentOutboundMessage = null; - }//if// - }//if// - - //If we finished sending the message then load the next one.// - if(websocketSendingMessage == null) { - loadNextWebsocketMessage(); - }//if// - }//while// - }//internalProcessWebsocketMessages()// - /** - * Loads and prepares the next websocket message from the queue of pending messages. - * Clears the pending message attributes if there isn't a pending message to be processed. - * The caller can check websocketSendingMessage == null to see if there is a ready message. - */ - private void loadNextWebsocketMessage() { - Object next = null; - boolean isLastPart = true; - - if(websocketStreamingMessage != null && websocketStreamingMessage.hasNextPart()) { - next = websocketStreamingMessage.getNextPart(); - isLastPart = !websocketStreamingMessage.hasNextPart(); - - //Ensure that we got a string or byte array.// - if(!(next instanceof String || next instanceof byte[])) { - throw new RuntimeException("Invalid streaming message part type."); - }//if// - }//if// - else { - synchronized(websocketPendingMessages) { - if(websocketPendingMessages.getSize() > 0) { - next = websocketPendingMessages.dequeue(); - }//if// - }//synchronized// - }//else// - - if(next != null) { - byte[] bytes = null; - int opCode = 0; - int length = 0; - - if(next instanceof String) { - try {bytes = ((String) next).getBytes("UTF-8");} catch(Throwable e) {Debug.log(e);} - opCode = websocketStreamingMessage == null ? 0x01 : 0; - length = bytes.length; - }//if// - else if(next instanceof byte[]) { - bytes = (byte[]) next; - opCode = websocketStreamingMessage == null ? 0x02 : 0; - length = bytes.length; - }//else if// - else if(next instanceof Byte) { //Control Message// - opCode = ((Byte) next).byteValue(); - }//else if// - else if(next instanceof IStreamedWebsocketMessage) { - websocketStreamingMessage = (IStreamedWebsocketMessage) next; - next = websocketStreamingMessage.getNextPart(); - isLastPart = !websocketStreamingMessage.hasNextPart(); - - if(next instanceof String) { - try {bytes = ((String) next).getBytes("UTF-8");} catch(Throwable e) {Debug.log(e);} - opCode = 0x01; //Text// - length = bytes.length; - }//if// - else if(next instanceof byte[]) { - bytes = (byte[]) next; - opCode = 0x02; //Binary// - length = bytes.length; - }//else if// - else { - throw new RuntimeException("Invalid streaming message part type."); - }//if// - }//else if// - - websocketSendingMessage = ByteBuffer.allocate(14 + length); - websocketSendingMessage.put((byte) (isLastPart ? 0x8 : 0)); - websocketSendingMessage.put((byte) opCode); - - //Write the length differently based on how long the content is.// - if(length < 126) { - websocketSendingMessage.put((byte) length); -// websocketSendingMessage.putLong(0, StreamBuffer.NUMBER_MSF); - }//if// - else if(length < 65535) { - websocketSendingMessage.put((byte) 126); - websocketSendingMessage.putShort((short) (length & 0xFFFF)); -// websocketSendingMessage.putShort((short) 0); - websocketSendingMessage.putInt(0); - }//else if// - else { - websocketSendingMessage.put((byte) 127); - websocketSendingMessage.putLong((long) length); - }//else// - - //The server doesn't use a mask key.// -// websocketSendingMessage.putInt(0); - //Put the content at the end of the message.// - websocketSendingMessage.put(bytes); - }//if// - else { - websocketSendingMessage = null; - websocketStreamingMessage = null; - }//else// - }//loadNextWebsocketMessage()// - /** - * @return - */ - private synchronized void internalProcessResponses() { - boolean finishedSending = true; - - //Keep sending responses while the buffers are not full and there is another response to send.// - while(finishedSending && currentResponse != null) { - //If the socket is open then send the next buffer of data.// - if(key.channel().isOpen()) { - //Send the pending response object's prepared buffer of data.// - finishedSending = writeClientBoundMessage(); - }//if// - - //Close the response if successfully sent, or if the socket is closed.// - if(finishedSending || !key.channel().isOpen()) { - try {currentResponse.close();} catch(Throwable e) {} - }//if// - - //If we finished sending the current response then load the next one.// - if(finishedSending) { - currentResponse = currentResponse.getNextResponse(); - - if(currentResponse == null) { - lastResponse = null; - }//if// - else if(key.channel().isOpen()) { - //Prep the next response object for sending.// - prepareResponse(); - }//else// - else { - //Clean up after all the left over responses.// - while(currentResponse != null) { - currentResponse.close(); - currentResponse = currentResponse.getNextResponse(); - }//while// - - currentResponse = null; - lastResponse = null; - }//else// - }//if// - }//while// - }//processCurrentResponse()// - /** - * Sends a response to the client. - * @return Whether the response could be fully sent. This will be false if there is still more data to be written when the call returns. - */ - private boolean writeClientBoundMessage() { - boolean sendMore = true; - -// if(debug) { -// debugBuffer.append("Starting a write cycle.\n"); -// }//if// - - try { - //Process SSL output first.// - if(sslEngine != null) { - //If we have part of an SSL frame then try to send it first.// - if(encryptedWriteBuffer.hasRemaining()) { - int remaining = encryptedWriteBuffer.remaining(); - - //Write the bytes to the stream.// - ((SocketChannel) key.channel()).write(encryptedWriteBuffer); - -// if(debug) { -// debugBuffer.append("Wrote " + (remaining - encryptedWriteBuffer.remaining()) + " encrypted bytes to the stream. " + encryptedWriteBuffer.remaining() + " remain.\n"); -// }//if// - - //Check to see if we failed to send the whole frame.// - if(encryptedWriteBuffer.hasRemaining()) { - sendMore = false; - }//if// - }//if// - - while(sendMore && sslNeedsWrap) { - SSLEngineResult handshakeResult; - - //Reset the encrypted write buffer - note that since we will never read while waiting to write data, this should always be empty.// - encryptedWriteBuffer.position(0); - encryptedWriteBuffer.limit(encryptedWriteBuffer.capacity()); - //Generate the handshake message.// - handshakeResult = sslEngine.wrap(ByteBuffer.allocate(0), encryptedWriteBuffer); - encryptedWriteBuffer.flip(); - - if(handshakeResult.getStatus() == Status.BUFFER_OVERFLOW) { - //Should never happen.// - Debug.log(new RuntimeException("Unexpected ssl engine buffer overflow.")); - }//if// - else if(handshakeResult.getStatus() == Status.BUFFER_UNDERFLOW) { - //Should never happen.// - Debug.log(new RuntimeException("Unexpected ssl engine buffer underflow.")); - }//else if// - else if(handshakeResult.getStatus() == Status.CLOSED) { - //Should never happen.// - Debug.log(new RuntimeException("Unexpected ssl engine closed.")); - //TODO: Handle this closure without an infinate loop... - //Close the socket.// - try {key.channel().close();}catch(Throwable e2) {} - }//else if// - else if(handshakeResult.getStatus() == Status.OK) { - if(handshakeResult.getHandshakeStatus() == HandshakeStatus.NEED_TASK) { - //Should never happen.// - Debug.log(new RuntimeException("Unexpected ssl engine task.")); - }//if// - else if(encryptedWriteBuffer.hasRemaining()) { - int remaining = encryptedWriteBuffer.remaining(); - - //Write the bytes to the stream.// - ((SocketChannel) key.channel()).write(encryptedWriteBuffer); - -// if(debug) { -// debugBuffer.append("Sent " + (remaining - encryptedWriteBuffer.remaining()) + " encrypted bytes.\n"); -// }//if// - - //If not all the bytes could be written then we will need to wait until we can write more.// - if(encryptedWriteBuffer.hasRemaining()) { -// if(debug) { -// debugBuffer.append("Pausing due to a partially sent packet (while ssl handshaking). Bytes actually sent: " + encryptedWriteBuffer.position() + ". Bytes remaining: " + encryptedWriteBuffer.remaining() + ".\n"); -// }//if// - - //Leave the data in the encrypted write buffer for the writing operation to send it.// - sendMore = false; - }//if// - - //Update the SSL needs wrap flag.// - if(handshakeResult.getHandshakeStatus() != HandshakeStatus.NEED_WRAP) { - sslNeedsWrap = false; - }//if// - }//else if// - else { - sslNeedsWrap = false; - }//else// - }//else if// - else { - //Should never happen.// - Debug.log(new RuntimeException("Unexpected ssl engine status code.")); - }//else// - }//while// - -// if(debug) { -// debugBuffer.append("End Handshaking SSL\n"); -// }//if// - }//if// - - if(sendMore && currentOutboundMessage != null) { - //Check to see if the outbound message is prepared to send more content. For chunked transfers the outbound message may be waiting for additional content from another stream and we should return later.// - if(!currentOutboundMessage.getBuffer().hasRemaining()) { - if(!currentOutboundMessage.loadBuffer()) { - if(currentOutboundMessage.getBuffer() == null && currentOutboundMessage.getNext() != null) { - currentOutboundMessage = currentOutboundMessage.getNext(); - }//if// - else { - sendMore = false; - }//else// - }//if// - - if(currentOutboundMessage.getBuffer() == null) { - currentOutboundMessage = null; - lastOutboundMessage = null; - }//if// - }//if// - - //If we have an application response pending then send it now.// - if(sendMore && currentOutboundMessage.getBuffer().hasRemaining()) { - if(sslEngine != null) { - //Keep sending encrypted frames until the output buffer is full, or we run out of message to send.// - while(key.channel().isOpen() && sendMore && (currentOutboundMessage != null) && currentOutboundMessage.getBuffer().hasRemaining()) { - SSLEngineResult encryptResult; -// int offset = pendingOutboundMessage.getBuffer().position(); -//TODO: Comment me. -//int rem = pendingOutboundMessage.getBuffer().remaining(); - //Reset the encrypted write buffer.// - encryptedWriteBuffer.compact(); - //Encrypt the next message frame.// - encryptResult = sslEngine.wrap(currentOutboundMessage.getBuffer(), encryptedWriteBuffer); - encryptedWriteBuffer.flip(); -//TODO: Comment me. -//Debug.log("Encrypting/Sending to client from Git " + (rem - pendingOutboundMessage.getBuffer().remaining()) + " bytes."); - -// if(debug) { -// sentBytes += (pendingOutboundMessage.position() - offset); -// debugBuffer.append("Encrypted: " + (pendingOutboundMessage.position() - offset) + ". Total Encrypted: " + sentBytes + ". Encrypted size: " + encryptedWriteBuffer.limit() + ".\n"); -// }//if// - - if(encryptResult.getStatus() == Status.BUFFER_OVERFLOW) { - //Should never happen.// - Debug.log(new RuntimeException("Unexpected ssl engine buffer overflow.")); - }//if// - else if(encryptResult.getStatus() == Status.BUFFER_UNDERFLOW) { - //Should never happen.// - Debug.log(new RuntimeException("Unexpected ssl engine buffer underflow.")); - }//else if// - else if(encryptResult.getStatus() == Status.CLOSED) { - //Should never happen.// -// Debug.log(new RuntimeException("Unexpected ssl engine closed.")); - //TODO: Handle this closure without an infinate loop... - //Close the socket.// - try {key.channel().close();} catch(Throwable e2) {} - }//else if// - else if(encryptResult.getStatus() == Status.OK) { - //Write the bytes to the stream.// - try { - int remaining = encryptedWriteBuffer.remaining(); - - ((SocketChannel) key.channel()).write(encryptedWriteBuffer); - -// if(debug) { -// debugBuffer.append("Sent " + (remaining - encryptedWriteBuffer.remaining()) + " encrypted bytes.\n"); -// }//if// - }//try// - catch(IOException e) { - //Caught if the channel is forcably closed by the client. We will ignore it.// - }//catch// - - //If not all the bytes could be written then we will need to wait until we can write more.// - if(encryptedWriteBuffer.hasRemaining()) { - //Leave the data in the encrypted write buffer for the writing operation to send it.// - sendMore = false; - -// if(debug) { -// debugBuffer.append("Pausing due to a partially sent packet. Bytes actually sent: " + encryptedWriteBuffer.position() + ". Bytes remaining: " + encryptedWriteBuffer.remaining() + ".\n"); -// }//if// - }//if// - }//else if// - else { - //Should never happen.// - Debug.log(new RuntimeException("Unexpected ssl engine status code.")); - }//else// - - //Add more content to the buffer.// - //Note: Do this even if the last encrypted write buffer could not be fully sent - so that when it is sent there will be outbound message content.// - if(key.channel().isOpen() && currentOutboundMessage != null) { - if(!currentOutboundMessage.loadBuffer()) { - //Load the next pending outbound message in the chain. This is currently only used for content being passed through to another process via a second socket.// - if(currentOutboundMessage.getBuffer() == null && currentOutboundMessage.getNext() != null) { - currentOutboundMessage = currentOutboundMessage.getNext(); - }//if// - else { - //Wait until additional message bytes are available.// - sendMore = false; - }//else// - }//if// - - //If the message end has been reached then the buffer will be null.// - if(currentOutboundMessage.getBuffer() == null) { - currentOutboundMessage = null; - lastOutboundMessage = null; - }//if// - }//if// - }//while// - }//if// - else { - //Keep sending encrypted frames until the output buffer is full, or we run out of message to send.// - while(sendMore && (currentOutboundMessage != null) && currentOutboundMessage.getBuffer().hasRemaining()) { - //Write the bytes to the stream.// - ((SocketChannel) key.channel()).write(currentOutboundMessage.getBuffer()); - -// if(debug) { -// sentBytes += pendingOutboundMessage.position(); -// debugBuffer.append("Wrote " + pendingOutboundMessage.position() + " bytes to the client. Total sent: " + sentBytes + "\n"); -// }//if// - - //If not all the bytes could be written then we will need to wait until we can write more.// - if(currentOutboundMessage.getBuffer().hasRemaining()) { - sendMore = false; - }//if// - else { - if(!currentOutboundMessage.loadBuffer()) { - //Load the next pending outbound message in the chain. This is currently only used for content being passed through to another process via a second socket.// - if(currentOutboundMessage.getBuffer() == null && currentOutboundMessage.getNext() != null) { - currentOutboundMessage = currentOutboundMessage.getNext(); - }//if// - else { - //Wait until additional message bytes are available.// - sendMore = false; - }//else// - }//if// - - //If the message end has been reached then the buffer will be null.// - if(currentOutboundMessage.getBuffer() == null) { - currentOutboundMessage = null; - lastOutboundMessage = null; - }//if// - }//else// - }//while// - }//else// - }//if// - }//if// - }//try// - catch(ClosedChannelException e) { - close(); - }//catch// - catch(SSLException e) { - if(debug) { - Debug.log(e); - }//if// - - close(); - }//catch// - catch(IOException e) { - if(debug) { - Debug.log(e); - }//if// - - close(); - }//catch// - - return sendMore; - }//writeClientBoundMessage()// - /* (non-Javadoc) - * @see com.foundation.web.server.WebServer.AbstractSocketContext#processRequest() - */ - protected void readIncomingMessages() throws IOException { - boolean requiresRead = true; - SocketChannel channel = (SocketChannel) key.channel(); - - if(isSsl()) { - int loopCount = 0; - boolean keepReading = true; - - if(sslEngine == null) { - SSLSession session = null; - - if(parseFirstTlsMessage(this, channel)) { - SSLContext sslContext = null; - - //Error checking.// - if(domain == null || domain.length() == 0) { - //We seem to have a choice here. We can either throw an error which ends up forcably closing the connection which for most browsers allows the client to retry with different settings (not older IE6, 7, 8 implementations), or return a pretty error which prevents users of well behaved browsers from getting the correct credentials.// - throw new TlsFailureException("Connection did not provide a domain name in the TLS hello message."); -// context.tlsFailure = true; - }//if// - - //Get the web application for the given domain.// - //Synchronize to prevent another thread from altering the service's web applications while we are accessing it.// - synchronized(WebServer.this) { - if(tlsFailure) { - domain = serverSocketContext.serviceListener.getDefaultSslDomain(); - }//if// - - webApplicationContainer = serverSocketContext.serviceListener.getWebApplicationContainer(domain); - }//synchronized// - - //Error checking.// - if(webApplicationContainer == null) { - throw new IOException("Cannot find a web application for the domain " + domain + "."); - }//if// - - //Lock on the container so it doesn't change the object it contains while we access it.// - synchronized(webApplicationContainer) { - //Get the SSLContext from the web application.// - sslContext = webApplicationContainer.getWebApplication().getSslContext(domain); - - if(sslContext == null) { - throw new IOException("Cannot get an SSLContext for the domain " + domain + " for the domain associated web application."); - }//if// - - sslEngine = sslContext.createSSLEngine(); - - //Attempt an anonymous connection to the client so we can report that they have an old browser version that is not using TLS + domain extension.// -// if(context.tlsFailure) { -// context.sslEngine.setEnabledCipherSuites(new String[] { -// "SSL_DH_anon_WITH_3DES_EDE_CBC_SHA", -// "SSL_DH_anon_WITH_DES_CBC_SHA", -// "SSL_DH_anon_WITH_RC4_128_MD5"}); -// //"SSL_DH_anon_EXPORT_WITH_RC4_40_MD5", -// //"SSL_DH_anon_EXPORT_WITH_DES40_CBC_SHA", -// //"TLS_DH_anon_EXPORT_WITH_RC4_40_MD5", -// //"TLS_DH_anon_WITH_RC4_128_MD5", -// //"TLS_DH_anon_EXPORT_WITH_DES40_CBC_SHA", -// //"TLS_DH_anon_WITH_DES_CBC_SHA", -// //"TLS_DH_anon_WITH_3DES_EDE_CBC_SHA"}); -// }//if// - - //context.sslEngine.getEnabledProtocols(); - //context.sslEngine.getEnabledCipherSuites(); - sslEngine.setUseClientMode(false); - sslEngine.beginHandshake(); - session = sslEngine.getSession(); - encryptedReadBuffer = ByteBuffer.allocate(session.getPacketBufferSize()); - encryptedReadBuffer.position(encryptedReadBuffer.limit()); - unencryptedReadBuffer = ByteBuffer.allocate(session.getApplicationBufferSize()); - unencryptedReadBuffer.position(unencryptedReadBuffer.limit()); - encryptedWriteBuffer = ByteBuffer.allocate(session.getPacketBufferSize()); - encryptedWriteBuffer.position(encryptedWriteBuffer.limit()); - }//synchronized// - - //Create a pass through socket and context and attach it to this context if the application is setup as a pass through to another process.// - IWebApplication application = webApplicationContainer.getWebApplication(); - - if(application instanceof IPassThroughDomain) { - IPassThroughDomain passThroughDomain = ((IPassThroughDomain) application); - - //Setup the pass through socket context (and socket channel). All data will be sent to this context to be sent to the remote process.// - relatedSocketContext = new PassThroughSocketContext(this, passThroughDomain.getAddress(), passThroughDomain.getPort()); - }//if// - }//if// - }//if// - - if(sslEngine != null) { - //While we have a count greater than zero, indicating that some data is comming through, keep reading and processing the data.// - //Note: We are throddling this for active connections to prevent a single connection from hogging all the resources.// - while(channel.isOpen() && loopCount < 10 && requiresRead && keepReading) { - int readCount = -1; - - //Track how many loops we run so that we don't hog all the server's resources with one connection.// - loopCount++; - //Compact the buffer, placing remaining bytes at the start of the buffer and preping for reading more bytes from the stream.// - encryptedReadBuffer.compact(); - - //If there is an initial buffer of bytes then use those first. The initial buffer is created when parsing the initial TLS message to gather the domain the client is connecting to.// - if(initialBuffer != null) { - readCount = initialBuffer.readBytes(encryptedReadBuffer); - - if(initialBuffer.getSize() == 0) { - initialBuffer = null; - }//if// - }//if// - - //If there isn't an initial buffer then fill (or finish filling) the read buffer.// - if(readCount == -1 && initialBuffer == null) { - //Read a frame of encrypted data (or handshake data).// - try { - readCount = channel.read(encryptedReadBuffer); - }//try// - catch(IOException e) { - //Ignore the forcable close error here - the channel will be closed a few lines lower in the code. TODO: Not sure if all IOExceptions should be ignored here?// - if(!e.getMessage().equalsIgnoreCase("An existing connection was forcibly closed by the remote host")) { - throw e; - }//if// - }//catch// - }//else// - - //Make the encrypted frame buffer readable.// - encryptedReadBuffer.flip(); - - //If the socket was closed then handle it, if we received enough data to process a frame then do so and continue, otherwise we must wait for the client to send more data.// - if(readCount == -1) { - //The socket has been closed by the client.// - close(); - keepReading = false; - }//if// - else if(encryptedReadBuffer.remaining() != 0) { - SSLEngineResult sslResult; - - //Sometimes the SSL Engine requires a partial read of the encrypted bytes, then a task, then reading the rest of the encrypted bytes.// - while(key.channel().isOpen() && encryptedReadBuffer.hasRemaining()) { - //Reset the unencrypted read buffer prior to decrypting the next frame.// - unencryptedReadBuffer.position(0); - unencryptedReadBuffer.limit(unencryptedReadBuffer.capacity()); - //Decrypt the message and process any SSL messages.// - sslResult = sslEngine.unwrap(encryptedReadBuffer, unencryptedReadBuffer); - unencryptedReadBuffer.flip(); - - //Check the requiresRead of the SSL processing.// - if(sslResult.getStatus() == Status.BUFFER_UNDERFLOW) { - //Buffer underflow indicates we haven't enough bytes to finish processing the next frame of data.// - break; - }//if// - else if(sslResult.getStatus() == Status.BUFFER_OVERFLOW) { - //Should never happen.// -// if(debug) Debug.log(new RuntimeException("Unexpected ssl engine buffer overflow.")); - close(); - }//else if// - else if(sslResult.getStatus() == Status.CLOSED) { - //A normal close I think...// - //Debug.log(new RuntimeException("Unexpected ssl engine closed.")); - }//else if// - else if(sslResult.getStatus() == Status.OK) { - //Run any long running tasks now.// - if(sslResult.getHandshakeStatus() == HandshakeStatus.NEED_TASK) { - Runnable task = null; - - while((task = sslEngine.getDelegatedTask()) != null) { - task.run(); - }//while// - }//if// - - //If the engine requires handshake data to be wrapped and sent then do so now.// - if(sslResult.getHandshakeStatus() == HandshakeStatus.NEED_WRAP || sslResult.getHandshakeStatus() == HandshakeStatus.NEED_TASK) { - sslNeedsWrap = true; - - //Need to synchronize if this is a pass through socket so that multiple threads don't access pendingOutboundMessage or lastAddedMessageBuffer (via a call to passThrough(ByteBuffer) on another thread).// - if(getPassThroughSocketContext() == null) { - requiresRead = writeClientBoundMessage(); - }//if// - else { - synchronized(this) { - requiresRead = writeClientBoundMessage(); - }//synchronized// - }//else// - }//if// - - //If bytes were produced then process them.// - if(sslResult.bytesProduced() > 0) { - //If we are not passing all content to another process then handle it by calling processClientRequest, otherwise pass it through.// - if(getPassThroughSocketContext() == null) { - if(isWebsocket) { - requiresRead = WebServer.this.processWebsocketFrame((SocketContext) this, unencryptedReadBuffer, key); - }//if// - else { - requiresRead = WebServer.this.processClientRequest((SocketContext) this, unencryptedReadBuffer, key); - }//else// - }//if// - else { -//TODO: Comment me. -//Debug.log("Receiving message (" + unencryptedReadBuffer.remaining() + " bytes) from client for git."); - //Queue the data for sending to the remote process via the pass through socket context.// - getPassThroughSocketContext().passThrough(unencryptedReadBuffer); - requiresRead = true; - }//else// - }//if// - }//else if// - else { - //Should never happen.// - Debug.log(new RuntimeException("Unexpected ssl engine status code.")); - close(); - }//else// - }//while// - }//else if// - else { - //Wait for the client to send more data.// - keepReading = false; - }//else// - }//while// - }//if// - }//if// - else { - int count = 1; - ByteBuffer socketReadBuffer = this.socketReadBuffer; - int loopCount = 0; - - //While we have a count greater than zero, indicating that some data is comming through, keep reading and processing the data.// - //Note: We are throddling this for active connections to prevent a single connection from hogging all the resources.// - while(loopCount < 10 && requiresRead && count > 0) { - loopCount++; - //Don't allow data to be left on the socket read buffer.// - socketReadBuffer.position(0); - socketReadBuffer.limit(socketReadBuffer.capacity()); - count = channel.read(socketReadBuffer); - socketReadBuffer.flip(); - - //Setup the pass through socket if the application is an instance of IPassThroughDomain.// - if(count != -1 && isFirstUnencryptedMessage) { - //Read enough of the header to identify the application.// - if(WebServer.this.processRequestedHost((SocketContext) this, socketReadBuffer, key)) { - //Create a pass through socket and context and attach it to this context if the application is setup as a pass through to another process.// - IWebApplication application = webApplicationContainer.getWebApplication(); - - if(application instanceof IPassThroughDomain) { - IPassThroughDomain passThroughDomain = ((IPassThroughDomain) application); - - //Setup the pass through socket context (and socket channel). All data will be sent to this context to be sent to the remote process.// - relatedSocketContext = new PassThroughSocketContext(this, passThroughDomain.getAddress(), passThroughDomain.getPort()); - }//if// - - isFirstUnencryptedMessage = false; - }//if// - else { - //We couldn't even read the host from the first bytes sent by the client - very unusual (it should be in the first couple hundred bytes - less than a single packet).// - //TODO: We could cycle and wait for the next packet. For now just close the socket since this should never really happen in the first place. - close(); - }//else// - }//if// - - if(count == -1) { - //The socket has been closed by the client.// - close(); - }//if// - else if(relatedSocketContext != null) { - relatedSocketContext.passThrough(socketReadBuffer); - - if(socketReadBuffer.remaining() > 0) { - Debug.log(new RuntimeException("Remaining bytes found on the read buffer!")); - }//if// - }//else if// - else if(isWebsocket) { - requiresRead = WebServer.this.processWebsocketFrame((SocketContext) this, socketReadBuffer, key); - - if(socketReadBuffer.remaining() > 0) { - Debug.log(new RuntimeException("Remaining bytes found on the read buffer!")); - }//if// - }//else if// - else if(socketReadBuffer.hasRemaining()) { - requiresRead = WebServer.this.processClientRequest((SocketContext) this, socketReadBuffer, key); - - if(socketReadBuffer.remaining() > 0) { - Debug.log(new RuntimeException("Remaining bytes found on the read buffer!")); - }//if// - }//else// - else { - break; - }//else// - }//while// - }//else// - }//processRequest()// - /* (non-Javadoc) - * @see com.foundation.web.server.WebServer.AbstractSocketContext#hasPendingWrite() - */ - protected boolean hasPendingWrite() { - return currentOutboundMessage != null || (encryptedWriteBuffer != null && encryptedWriteBuffer.hasRemaining()); - }//hasPendingWrite()// - /* (non-Javadoc) - * @see com.foundation.web.interfaces.IConnectionContext#upgradeToWebsocket(java.lang.String, long, com.foundation.web.interfaces.WebsocketHandler) - */ - public void upgradeToWebsocket(String protocol, long maxMessageLength, WebsocketHandler websocketHandler) { - if(!isWebsocket) { - this.isWebsocket = true; - this.websocketProtocol = protocol; - this.websocketMaxMessageLength = maxMessageLength; - this.websocketFrameHeader = new byte[14]; - this.websocketMessageMaskKey = new byte[4]; - this.websocketPendingMessages = new Queue(20); - this.websocketHandler = websocketHandler; - }//if// - }//upgradeToWebsocket()// - /* (non-Javadoc) - * @see com.foundation.web.interfaces.IConnectionContext#getWebsocketProtocol() - */ - public String getWebsocketProtocol() { - return websocketProtocol; - }//getWebsocketProtocol()// - /* (non-Javadoc) - * @see com.foundation.web.interfaces.IConnectionContext#isWebsocket() - */ - public boolean isWebsocket() { - return isWebsocket; - }//isWebsocket()// - /* (non-Javadoc) - * @see com.foundation.web.interfaces.IConnectionContext#sendWebsocketMessage(byte[]) - */ - public void sendWebsocketMessage(byte[] message) { - if(isWebsocket) { - synchronized(websocketPendingMessages) { - websocketPendingMessages.enqueue(message); - }//synchronized// - - notifyListenerOfPendingWrite(); - }//if// - }//sendWebsocketMessage()// - /* (non-Javadoc) - * @see com.foundation.web.interfaces.IConnectionContext#sendWebsocketMessage(java.lang.String) - */ - public void sendWebsocketMessage(String message) { - if(isWebsocket) { - synchronized(websocketPendingMessages) { - websocketPendingMessages.enqueue(message); - }//synchronized// - - notifyListenerOfPendingWrite(); - }//if// - }//sendWebsocketMessage()// - /* (non-Javadoc) - * @see com.foundation.web.interfaces.IConnectionContext#sendWebsocketPing() - */ - public void sendWebsocketPing() { - if(isWebsocket) { - synchronized(websocketPendingMessages) { - websocketPendingMessages.enqueue(new Byte((byte) 0x9)); - }//synchronized// - - notifyListenerOfPendingWrite(); - }//if// - }//sendWebsocketPing()// - /** - * Sends a PONG response to the client's ping. - */ - public void sendWebsocketPong() { - if(isWebsocket) { - synchronized(websocketPendingMessages) { - websocketPendingMessages.enqueue(new Byte((byte) 0xA)); - }//synchronized// - - notifyListenerOfPendingWrite(); - }//if// - }//sendWebsocketPong()// - /* (non-Javadoc) - * @see com.foundation.web.interfaces.IConnectionContext#sendWebsocketMessage(com.foundation.web.interfaces.IConnectionContext.IStreamedWebsocketMessage) - */ - public void sendWebsocketMessage(IStreamedWebsocketMessage message) { - if(isWebsocket) { - synchronized(websocketPendingMessages) { - websocketPendingMessages.enqueue(message); - }//synchronized// - - notifyListenerOfPendingWrite(); - }//if// - }//sendWebsocketMessage()// - }//SocketContext// - - /** - * Encapsulates the code that threads incomming socket requests and messages over sockets. - *

Note that the HTTP protocol requires that the reponses be sent in order of the received requests.

- */ - private class NetworkListener implements Runnable { - private Selector selector = null; - private Iterator selectedKeys = null; -// private boolean hasListener = false; - private volatile boolean stop = true; - private volatile boolean hasRunnables = false; - private LiteList runnables = new LiteList(10, 20); - private int maxThreadCount = 0; - private int activeThreadCount = 0; - private int threadId = 1; - - public NetworkListener(Selector selector, int maxThreadCount) { - this.selector = selector; - this.maxThreadCount = maxThreadCount; - }//NetworkListener()// - /** - * Stops the network listener. - * Note that this may take a short time to complete. - */ - public void stop() { - if(!stop) { - stop = true; - selector.wakeup(); - }//if// - }//stop()// - /** - * Starts the network listener. - */ - public void start() { - if(stop) { - stop = false; - ThreadService.run(this); -// Thread t = new Thread(this); -// t.setName("Network Listener 1"); -// t.start(); - }//if// - }//start()// - /** - * Cleans up after the client channel. - * @param context The context associated with the client connection. - * @param channel The client connection that is now closed. - */ - private void cleanupClientChannel(SocketContext context, SocketChannel channel) { - if(debug) { - Debug.log("Connection closed to " + channel.socket().getInetAddress() + ":" + channel.socket().getPort()); - }//if// - }//cleanupClientChannel()// - /** - * Adds a runnable to the list of runnables to be run next time the loop is woken. - * @param runnable The runnable to be run by the thread that is listening for socket events. - */ - public synchronized void queue(Runnable runnable) { - runnables.add(runnable); - hasRunnables = true; - }//queue()// - /** - * Checks for runnables and runs them if there are any. - */ - private void checkForRunnables() { - if(hasRunnables) { - synchronized(this) { - while(runnables.getSize() > 0) { - ((Runnable) runnables.remove(0)).run(); - }//while// - - hasRunnables = false; - }//synchronized// - }//if// - }//checkForRunnables()// - /* (non-Javadoc) - * @see java.lang.Runnable#run() - */ - public void run() { -// boolean loop = true; - - //Looping only occurs when we are at the maximum allowed number of threads handling messages.// - while(!stop /*&& loop*/) { - SelectionKey key = null; - - try { - //Synchronize so that we ensure thread safe access to the activeThreadCount variable.// -// synchronized(this) { //TODO: Remove me when we are sure that the threading works correctly. -// if(hasListener) { -// Debug.log(new RuntimeException("Failed to properly thread the NetworkListener.")); -// }//if// -// -// hasListener = true; -// }//synchronized// - - //If we don't have an iterator over the active channels then get one and block if necessary.// - if(selectedKeys == null) { - int keyCount = 0; - - //Block until we have keys or were awakened by another thread.// - keyCount = selector.select(); - //Check for any pending runnables that need executing on this thread.// - checkForRunnables(); - - //If we have active keys then retrieve them.// - if(keyCount > 0) { - selectedKeys = selector.selectedKeys().iterator(); - }//if// - }//if// - - //If we have an iterator over the active channels then get and remove the next one (clean up the iterator if empty).// - if(selectedKeys != null) { - key = (SelectionKey) selectedKeys.next(); - selectedKeys.remove(); - - //Weed out invalid (cancelled) keys.// - if(!key.isValid()) { - key = null; - }//if// - - if(!selectedKeys.hasNext()) { - selectedKeys = null; - }//if// - }//if// - -// synchronized(this) { //TODO: Remove me when we are sure that the threading works correctly. -// hasListener = false; -// }//synchronized// - }//try// - catch(Throwable e) { - //TODO: Can we recover? - Debug.log(e); - }//catch// - - try { - if(key != null) { - final boolean isWrite = key.isWritable(); - final ChannelContext context = (ChannelContext) key.attachment(); - final SelectableChannel channel = key.channel(); - final SelectionKey selectionKey = key; - - if(channel instanceof ServerSocketChannel) { - try { - ServerSocketChannel serverSocketChannel = (ServerSocketChannel) channel; - SocketChannel socketChannel = serverSocketChannel.accept(); - ServerSocketContext serverSocketContext = (ServerSocketContext) context; - SocketContext socketContext = new SocketContext(serverSocketContext); - - socketChannel.configureBlocking(false); - socketChannel.socket().setSendBufferSize(SEND_BUFFER_SIZE); - socketChannel.socket().setReceiveBufferSize(RECEIVE_BUFFER_SIZE); - socketContext.key = socketChannel.register(selector, SelectionKey.OP_READ, socketContext); - socketContext.serverSocketContext = serverSocketContext; - - //Debug.log("Connection opened to " + socketChannel.socket().getInetAddress() + ":" + socketChannel.socket().getPort()); - - if(serverSocketContext.serviceListener.type != IServiceListener.TYPE_SSL) { - socketContext.socketReadBuffer = ByteBuffer.allocate(BUFFER_SIZE); - }//if// - - if(debug) { - Debug.log("Connection opened to " + socketChannel.socket().getInetAddress() + ":" + socketChannel.socket().getPort()); - }//if// - }//try// - catch(Throwable e) { - //TODO: Can we recover? - Debug.log(e); - }//catch// - }//if// - else if(channel instanceof SocketChannel) { - // boolean socketClosed = false; - - //Toggle the write or read flag.// - synchronized(key) { - // //Save the ops that will be set when the processing is complete.// - // ((AbstractSocketContext) context).setFlags(key.interestOps()); - - //Notes: Java (pre-jdk7) does not have the ability to read and write to a socket at the same time (two threads, one socket). Post jdk7 there is AsynchronousSocketChannel and AsynchronousServerSocketChannel which could be used to send/receive at the same time. - //Truely enabling Speedy would require a thread to read which when finished would flag read again BEFORE processing the message and BEFORE sending a response. - //For now (so we don't have to require jdk7 yet) we will simply allow Speedy to queue up messages, but only read, process, and then write them one at a time. Most of the speed loss is in the waiting for the WRITE to finish before handling the next request (due to it being broken into packets and the mechanics of TCP), and that is generally minimal (speed lose) since usually the bottleneck in speed is the browser's connection to the internet (most of us haven't got Gigabit Ethernet at home). Anyone with enough home juice to have this be a problem would only notice the difference for really porky websites (which is a problem in and of its self). - - //Not allowing either reads or writes to continue until all processing of this message is done.// - ((AbstractSocketContext) context).isUsed = true; - key.interestOps(0); - - //The problem with this is that we'd have to use AsynchronousSocketChannel which would appear to require a complete rewrite of everything since it operates completely differently.// -// key.interestOps(key.interestOps() ^ (isWrite ? SelectionKey.OP_WRITE : SelectionKey.OP_READ)); - }//synchronized// - - if(((SocketChannel) channel).isOpen()) { - ThreadService.run(new Runnable() { - public void run() { - boolean socketClosed = false; - - try { - if(isWrite) { - //Prevent another thread from reading/writing on the same socket at the same time (safety). This would have to be removed if SPEEDY (or similar pipelining) were allowed, and AsynchronousSocketChannel/AsynchronousServerSocketChannel would have to be used (requiring jdk7).// - synchronized(((AbstractSocketContext) context).getLock()) { - //Process the pending write to the socket as much as is possible, then return.// - ((AbstractSocketContext) context).writeOutgoingMessages(); - }//synchronized// - }//if// - else { - //Prevent another thread from reading/writing on the same socket at the same time (safety). This would have to be removed if SPEEDY (or similar pipelining) were allowed, and AsynchronousSocketChannel/AsynchronousServerSocketChannel would have to be used (requiring jdk7).// - synchronized(((AbstractSocketContext) context).getLock()) { - //Process the incoming request and send the response (a partial response may be sent in which case the socket will be set to wait for a write opportunity and not a read opportunity).// - ((AbstractSocketContext) context).readIncomingMessages(); - }//synchronized// - }//else// - }//try// - catch(TlsFailureException e) { - //Allow the failure to be ignored. This occurs when the client fails to use TLS or fails to send the host name as part of the TLS handshake.// - try {((SocketChannel) channel).close();}catch(Throwable e2) {} //Release the socket so the message doesn't continue to be processed.// - }//catch// - catch(Throwable e) { - if(debug) Debug.log(e); - - //Force the socket to be closed (for sure).// - try {((SocketChannel) channel).close();} catch(Throwable e2) {} - //Debug.log(e); - socketClosed = true; - }//catch// - finally { - if(channel != null && !socketClosed && channel.isOpen() && context != null) { - try { - //Set the new ops for the selection key and notify the selector that ops have changed.// - synchronized(selectionKey) { - if(selectionKey.isValid()) { - //Always flag the socket for reading, only flag the socket for writing if a pending write operation exists.// - selectionKey.interestOps(SelectionKey.OP_READ | (((AbstractSocketContext) context).hasPendingWrite() ? SelectionKey.OP_WRITE : 0)); - }//if// - else { - Debug.log(new RuntimeException("Woops! Somehow the selection key isn't valid, but the socket isn't closed either!")); - try {((SocketChannel) channel).close();} catch(Throwable e2) {} - cleanupClientChannel((SocketContext) context, (SocketChannel) channel); - }//else// - - ((AbstractSocketContext) context).isUsed = false; - }//synchronized// - - selector.wakeup(); - }//try// - catch(Throwable e) { - Debug.log(e); - }//catch// - }//if// - else if(channel != null && (!channel.isOpen() || socketClosed) && channel instanceof SocketChannel && context instanceof SocketContext) { - cleanupClientChannel((SocketContext) context, (SocketChannel) channel); - }//else if// - else { - //This shouldn't be called I don't think.// - Debug.log(new RuntimeException("Woops! Somehow we aren't closed and we didn't setup the interestOps for the HTTP socket!")); - }//else// - }//finally// - }//run()// - }); - /* - try { - synchronized(this) { - // if(++activeThreadCount != maxThreadCount) { - //Start another thread to take this thread's place.// - ThreadService.run(this); - // }//if// - }//synchronized// - - if(isWrite) { - // if(debug) { - // ((SocketContext) context).debugBuffer.append("Socket is now write available.\n"); - // Debug.log("Socket is write available: " + ((SocketChannel) channel).socket().getInetAddress() + ":" + ((SocketChannel) channel).socket().getPort()); - // }//if// - - //Prevent another thread from reading/writing on the same socket at the same time (safety). This would have to be removed if SPEEDY (or similar pipelining) were allowed, and AsynchronousSocketChannel/AsynchronousServerSocketChannel would have to be used (requiring jdk7).// - synchronized(((AbstractSocketContext) context).getLock()) { - //Process the pending write to the socket as much as is possible, then return.// - ((AbstractSocketContext) context).processResponses(); - }//synchronized// - }//if// - else { - // if(debug) { - // ((SocketContext) context).debugBuffer.append("Socket is now read available.\n"); - // Debug.log("Socket is read available: " + ((SocketChannel) channel).socket().getInetAddress() + ":" + ((SocketChannel) channel).socket().getPort()); - // }//if// - - //Prevent another thread from reading/writing on the same socket at the same time (safety). This would have to be removed if SPEEDY (or similar pipelining) were allowed, and AsynchronousSocketChannel/AsynchronousServerSocketChannel would have to be used (requiring jdk7).// - synchronized(((AbstractSocketContext) context).getLock()) { - //Process the incoming request and send the response (a partial response may be sent in which case the socket will be set to wait for a write opportunity and not a read opportunity).// - ((AbstractSocketContext) context).processRequest(); - }//synchronized// - }//else// - }//try// - catch(TlsFailureException e) { - //Allow the failure to be ignored. This occurs when the client fails to use TLS or fails to send the host name as part of the TLS handshake.// - try {((SocketChannel) channel).close();}catch(Throwable e2) {} //Release the socket so the message doesn't continue to be processed.// - }//catch// - catch(Throwable e) { - if(debug) Debug.log(e); - - //Force the socket to be closed (for sure).// - try {((SocketChannel) channel).close();}catch(Throwable e2) {} - //Debug.log(e); - socketClosed = true; - }//catch// - finally { - boolean requiresWakeup = false; - - if(channel != null && !socketClosed && channel.isOpen() && key != null && context != null) { - requiresWakeup = true; - }//if// - else if(channel != null && (!channel.isOpen() || socketClosed) && channel instanceof SocketChannel && context instanceof SocketContext) { - cleanupClientChannel((SocketContext) context, (SocketChannel) channel); - }//else if// - - //Loop if the last thread to wait for a message couldn't start another thread due to the max number of threads allowed.// - synchronized(this) { - // if(activeThreadCount-- != maxThreadCount) { - loop = false; - - if(requiresWakeup) { - selector.wakeup(); - }//if// - // }//if// - }//synchronized// - }//finally// - */ - }//if// - }//else if// - }//if// - }//try// - catch(java.nio.channels.CancelledKeyException e) { - //Occurs if the socket is closed while we are handling the key.// - Debug.log(e); //TODO: Does anything need doing here? Should it be ignored? - }//catch// - catch(Throwable e) { - Debug.log(e); - //TODO: There needs to be more specfic error handling if we got here. - }//catch// - }//while// - }//run()// - }//NetworkListener// - /** * A simple extension of the IOException to allow us to ignore it. */ - private static class IgnoredIOException extends IOException { + public static class IgnoredIOException extends IOException { public IgnoredIOException(String s) { super(s); }//IgnoredIOException()// @@ -2783,27 +210,11 @@ public class WebServer { /** * A simple extension of the IOException to allow us to ignore it. */ - private static class TlsFailureException extends IOException { + public static class TlsFailureException extends IOException { public TlsFailureException(String s) { super(s); }//IgnoredIOException()// }//TlsFailureException// -/** - * Gets the threads date format for processing HTTP header dates. - *

This uses a thread local because the date format class is not thread safe :(.

- * @return The date format for handling http header dates. - */ -public static SimpleDateFormat getHttpDateFormat() { - SimpleDateFormat result = (SimpleDateFormat) httpDateFormat.get(); - - if(result == null) { - result = new SimpleDateFormat(httpDateFormatString); - result.setTimeZone(TimeZone.getTimeZone("GMT")); - httpDateFormat.set(result); - }//if// - - return result; -}//getHttpDateFormat()// /** * WebServer constructor. */ @@ -3054,18 +465,7 @@ public synchronized boolean start() throws IOException { }//else// if(success) { - String threadCount = System.getProperty("webserver.listener.threads"); - int count = 10; - - if(threadCount != null) { - try {count = Integer.parseInt(threadCount);} catch(Throwable e) {Debug.log(e);} - - if(count < 1) { - count = 10; - }//if// - }//if// - - networkListener = new NetworkListener(selector, count); + networkListener = new NetworkListener(this, selector); networkListener.start(); }//if// else { @@ -3099,1230 +499,4 @@ public synchronized void stop() { networkListener = null; }//if// }//stop()// -/** - * Simplification of the error throwing code. - */ -private void handleBrokenStream() throws IOException { - throw new IOException("Invalid TLS stream."); -}//handleBrokenStream()// -/** - * Parses the initial client hello message sent in the TLS protocol to identify which SSL context to use for the connection. - * @param context The connection context. - * @param channel The socket channel to read from. - * @return Whether the message could be parsed. If false, the method will be called again until true as new bytes arrive on the channel. - */ -private boolean parseFirstTlsMessage(SocketContext context, SocketChannel channel) throws IOException { - boolean result = false; - ByteBuffer temp = ByteBuffer.allocate(500); - StreamBuffer input; - int originalInputSize; - - //Create the stream buffer for this operation if not already created.// - if(context.initialBuffer == null) { - context.initialBuffer = new StreamBuffer(); - }//if// - - //Simplify the code a bit with a local variable.// - input = context.initialBuffer; - originalInputSize = input.getSize(); - - //Exit if the initial buffer size is rediculously large or if the channel is closed or has no additional bytes.// - while(channel.read(temp) > 0 && context.initialBuffer.getSize() < 10000) { - temp.flip(); - context.initialBuffer.writeBytes(temp); - }//while// - - //Ensure we have enough data to read the header.// - if(input.getSize() > 4) { - int contentType = input.getByte(0) & 0xFF; - int highVersion = input.getByte(1) & 0xFF; - int lowVersion = input.getByte(2) & 0xFF; - int length = input.getShort(3, StreamBuffer.NUMBER_MSF) & 0xFFFF; - -// context.initialBuffer.getAsText(); - - //Check the version.// - if(highVersion != 3 || lowVersion > 3 || lowVersion < 1) { - //handleBrokenStream(); - return true; - }//if// - - //Validate we have enough bytes for the whole message.// - if(input.getSize() > 4 + length) { - StreamBuffer tempBuffer = new StreamBuffer(input.getBufferPool()); - - //Indicate we have parsed the initial message (or at least attempted to).// - result = true; - //Clone the input buffer so we don't consume any bytes needed by the SSLEngine later.// - //Copy the bytes, don't consume them.// - tempBuffer.writeBytes(input, 0, length + 5); - input = tempBuffer; - - //A full record will be read.// - result = true; - //Skip the record header bytes.// - input.skipBytes(5); - - //Read each message in this record.// - switch(contentType) { -// case CONTENT_TYPE_CHANGE_CIPHER_SPEC: { -// processChangeCipherSpec(input); -// break; -// }//case// -// case CONTENT_TYPE_ALERT: { -// //Only one alert may exist in the message.// -// processAlert(input); -// break; -// }//case// - case 0x16: { //CONTENT_TYPE_HANDSHAKE - //Multiple handshake messages may be contained in the single record.// - while(input.getSize() > 0) { - int messageType = input.readByte() & 0xFF; - int messageSize = input.readMediumInt(StreamBuffer.NUMBER_MSF, false); - int postMessageStreamSize = input.getSize() - messageSize; - - switch(messageType) { - case 0x01: { //HANDSHAKE_TYPE_CLIENT_HELLO - //long time; - byte[] randomClientBytes = new byte[28]; - int sessionIdSize; - byte[] sessionId = null; - int cipherSuiteCount; - int[] cipherSuites; - int compressionMethodCount; - int[] compressionMethods; - /*int protocolVersionHigh = */input.readByte()/* & 0xFF*/; //Note: This should be the same as when it was read as part of the record earlier - this is redundant.// - /*int protocolVersionLow = */input.readByte()/* & 0xFF*/; - - /*time = */input.readInt(StreamBuffer.NUMBER_MSF)/* & 0x00000000FFFFFFFFL*/; - input.readBytes(randomClientBytes); - sessionIdSize = input.readByte() & 0xFF; - - if(sessionIdSize > 0) { - sessionId = new byte[sessionIdSize]; - input.readBytes(sessionId); - }//if// - - cipherSuiteCount = (input.readShort(StreamBuffer.NUMBER_MSF) & 0xFFFF) / 2; - cipherSuites = new int[cipherSuiteCount]; - - for(int index = 0; index < cipherSuites.length; index++) { - cipherSuites[index] = input.readShort(StreamBuffer.NUMBER_MSF) & 0xFFFF; - }//for// - - compressionMethodCount = input.readByte() & 0xFF; - compressionMethods = new int[compressionMethodCount]; - - for(int index = 0; index < compressionMethods.length; index++) { - compressionMethods[index] = input.readByte() & 0xFF; - }//for// - - //Read extensions until the end of the stream is reached.// - if(input.getSize() != postMessageStreamSize) { - /*int extensionArraySize =*/ input.readShort(StreamBuffer.NUMBER_MSF)/* & 0xFFFF*/; - - //Read each extension.// - while(input.getSize() != postMessageStreamSize) { - int extensionType = input.readShort(StreamBuffer.NUMBER_MSF) & 0xFFFF; - int extensionDataSize = input.readShort(StreamBuffer.NUMBER_MSF) & 0xFFFF; - int expectedStreamSize = input.getSize() - extensionDataSize; - - //Read the extension data if it is known, otherwise skip the data.// - switch(extensionType) { - case 0x0000: { //EXT_SERVER_NAME - //The ServerName extension always starts with a ServerNameList structure.// - int serverNameListByteCount = input.readShort(StreamBuffer.NUMBER_MSF) & 0xFFFF; - int postServerNamesExpectedStreamSize = input.getSize() - serverNameListByteCount; - - //Read each of the server names (no idea how many there will be - ).// - while(input.getSize() != postServerNamesExpectedStreamSize) { - int nameType = input.readByte() & 0xFF; - - switch(nameType) { - case 0x00: { //NAME_TYPE_HOST_NAME - int nameByteCount = input.readShort(StreamBuffer.NUMBER_MSF) & 0xFFFF; - String name; - - try { - name = input.readText(nameByteCount, "UTF8").toLowerCase(); -// Debug.log(name); - - if(context.domain == null) { - context.domain = name; - }//if// - }//try// - catch(UnsupportedEncodingException e) { - Debug.log(e); - }//catch// - break; - }//case// - default: { - break; - }//default// - }//switch// - }//while// - - break; - }//case// - default: { - input.skipBytes(extensionDataSize); - break; - }//default// - }//switch// - - //Ignore any unused bytes.// - if(input.getSize() != expectedStreamSize) { - input.skipBytes(input.getSize() - expectedStreamSize); - }//if// - }//for// - }//if// - break; - }//case// - default: { - handleBrokenStream(); - break; - }//default// - }//switch// - - //Verify we read exactly the number of bytes in the message.// - if(input.getSize() != postMessageStreamSize) { - handleBrokenStream(); - }//if// - }//while// - break; - }//case// -// case CONTENT_TYPE_APPLICATION: { -// //TODO: Decrypt / Validate the message. -// //TODO: Place the decrypted bytes in the result buffer. -// break; -// }//case// - default: { - handleBrokenStream(); - }//default// - }//switch// - - //Ignore any padding or mac bytes. There will not be any for handshake messages.// - if(input.getSize() > 0) { - input.skipBytes(input.getSize()); - }//if// - }//if// - }//if// - else if(originalInputSize == input.getSize()) { - //TODO: Why would the channel be flagged for reading when there is nothing to read? - throw new TlsFailureException("Connection is being flagged as having input, but has none."); - }//else if// - - return result; -}//parseFirstTlsMessage()// -/** - * Processes a single websocket frame if there is enough data in the fragment. - *
Will not return until all data is read from the frame or the socket is closed. - * @param context The context for the socket. - * @param fragment The fragment of data off the socket. - * @param key The key for the socket. - * @return Whether more data is required in order to process a single frame. - * @throws IOException - */ -private boolean processWebsocketFrame(SocketContext context, ByteBuffer fragment, SelectionKey key) throws IOException { - boolean isFrameFullyRead = false; - - while(fragment.remaining() > 0) { - byte[] frameHeader = context.websocketFrameHeader; - byte[] maskKey = context.websocketMessageMaskKey; - final int OP_CODE_INDEX = 0; - final int LENGTH_INDEX = 1; - final int EXTENDED_LENGTH_INDEX = 2; - - isFrameFullyRead = false; - - //Read the frame header (unless we are waiting on the remaining content of a previously read frame header).// - if(context.websocketFrameRemainder == 0) { - long length = -1; - - try { - boolean mask = false; - int expectedHeaderLength = 6; - int readLength = Math.min(2 - context.websocketFrameHeaderIndex, fragment.remaining()); - - if(readLength > 0) { - fragment.get(frameHeader, context.websocketFrameHeaderIndex, readLength); - context.websocketFrameHeaderIndex += readLength; - }//if// - - //Handle a partial header by checking the first two bytes to find the header's overall length (header length varies based on the message length and options used).// - if(context.websocketFrameHeaderIndex > 1) { - context.websocketIsLastFrameInMessage = (frameHeader[OP_CODE_INDEX] & 0x80) > 0; - mask = (frameHeader[LENGTH_INDEX] & 0x80) > 0; - length = frameHeader[LENGTH_INDEX] & 0x7F; - - if(length == 127) { - expectedHeaderLength = 8; - }//if// - else if(length == 128) { - expectedHeaderLength = 14; - }//else if// - }//if// - - readLength = Math.min(expectedHeaderLength - context.websocketFrameHeaderIndex, fragment.remaining()); - fragment.get(frameHeader, context.websocketFrameHeaderIndex, readLength); - context.websocketFrameHeaderIndex += readLength; - - //Ensure we have the full header before proceeding to read it.// - if(context.websocketFrameHeaderIndex == expectedHeaderLength) { - int maskKeyIndex = 2; - - if(length == 127) { - length = (int) (StreamSupport.readShort(frameHeader, EXTENDED_LENGTH_INDEX) & 0xFFFF); - maskKeyIndex = 4; - }//if// - else if(length == 128) { - //Note: Cannot handle unsigned longs in java. Assume negative numbers are over the length maximum for the server (since they most certainly should be - we'd run out of ram).// - length = StreamSupport.readLong(frameHeader, EXTENDED_LENGTH_INDEX); - maskKeyIndex = 10; - }//if// - - //Ensure we don't exceed the maximum message length applied by the web application when the socket was upgraded. Also ensure that we don't exceed the server's limitations on frame size (the highest possible positive value an integer can hold).// - if(length < 0 || length > Integer.MAX_VALUE || length + (context.websocketPartialReceivedMessage == null ? 0 : context.websocketPartialReceivedMessage.getSize()) > context.websocketMaxMessageLength) { - //TODO: Reply with a closed message? - //Kill the socket!// - context.close(); - //Notify ourselves via the log.// - Debug.log(new RuntimeException("Killed the client websocket due to excess frame size (Integer.MAX_VALUE) or the message exceeding the maximum size allowed by the web application (as specified when the websocket was accepted).")); - }//if// - else if(!mask) { - //Kill the socket - should never not have the mask set for the client calling the server.// - context.close(); - Debug.log(new RuntimeException("Killed the client websocket due to the mask not being set.")); - }//else if// - else { - //If this is the first frame in a message then parse the header for info.// - if(context.websocketMessageOpCode == 0) { - //Read the 4 bytes of the mask key.// - for(int maskIndex = 0, headerIndex = maskKeyIndex; maskIndex < maskKey.length; maskIndex++, headerIndex++) { - maskKey[maskIndex] = frameHeader[headerIndex]; - }//for// - - //The op code shouldn't be zero because zero is for a continuation frame (another frame in a message, not the first frame).// - context.websocketMessageOpCode = frameHeader[OP_CODE_INDEX] & 0x0F; - }//if// - - //If it is a content message then begin reading it.// - context.websocketFrameRemainder = (int) length; - }//else// - }//if// - }//try// - catch(BufferUnderflowException e) { - //Ignore - shouldn't occure.// - }//catch// - - //If we could read the next frame header then reset the frame header index to zero for the next frame header read.// - if(length != -1) { - context.websocketFrameHeaderIndex = 0; - }//if// - else if(context.websocketFrameRemainder == 0) { - isFrameFullyRead = true; - }//else if// - }//if// - - //Read the frame content.// - if(context.websocketFrameRemainder > 0) { - //If the whole message is here then read it.// - if(fragment.remaining() > 0) { - int transferCount = 0; - int streamPosition = 0; - int previousReadPosition; - - if(context.websocketPartialReceivedMessage == null) { - context.websocketPartialReceivedMessage = new StreamBuffer(); - }//if// - else { - streamPosition = context.websocketPartialReceivedMessage.getSize(); - }//else// - - //Retain the index of the last previously read byte so we start decoding using the correct mask offset.// - previousReadPosition = context.websocketPartialReceivedMessage.getSize(); - //Transfer either all the bytes in the fragment read from the socket, or transfer all the remaining bytes belonging to the current frame.// - transferCount = Math.max((int) context.websocketFrameRemainder, fragment.remaining()); - - //Move the bytes into our partial message buffer.// - context.websocketPartialReceivedMessage.writeBytes(fragment, transferCount); - - //Decode the message bytes with the mask key. The encoding is to protect intermediate servers that cache the contents from cache attacks. The mask key must be set by the client browser and must be random for each frame.// - for(int index = streamPosition, frameIndex = previousReadPosition, maxIndex = streamPosition + transferCount; index < maxIndex; index++, frameIndex++) { - //XOR each byte read with the next byte in the mask key (loops over the mask key starting at zero for the whole frame).// - context.websocketPartialReceivedMessage.putByte(index, context.websocketPartialReceivedMessage.getByte(index) ^ context.websocketMessageMaskKey[frameIndex % 4]); - }//for// - - //Track the remaining bytes to be read for the current frame.// - context.websocketFrameRemainder -= transferCount; - - if(context.websocketFrameRemainder == 0) { - //Mark the frame as is being fully read.// - isFrameFullyRead = true; - }//if// - }//if// - }//if// - - //If we finished reading this frame, and this is the last frame in the message, then handle it.// - if(isFrameFullyRead && context.websocketIsLastFrameInMessage) { - //Handle the completed message.// - switch(context.websocketMessageOpCode) { - case 0x01: //Text Frame// - //Convert the data into text and send to the application.// - try { - context.websocketHandler.receiveTextMessage(new String(context.websocketPartialReceivedMessage.toByteArray(), "UTF-8")); - }//try// - catch(Throwable e) { - Debug.log(e); - }//catch// - break; - case 0x02: //Binary Frame// - //Send binary data to the application.// - try { - context.websocketHandler.receiveBinaryMessage(context.websocketPartialReceivedMessage.toByteArray()); - }//try// - catch(Throwable e) { - Debug.log(e); - }//catch// - break; - case 0x08: //Socket Close// - context.close(); - break; - case 0x09: //Ping// - /* - context.sendWebsocketPong(); - //Notify the listener of the ping.// - try { - context.websocketHandler.receivePing(); - }//try// - catch(Throwable e) { - Debug.log(e); - }//catch// - */ - Debug.log(new RuntimeException("Received PING from the client. The client shouldn't ever send PING messages (only PONG).")); - break; - case 0x0A: //Pong// - //Notify the listener of the pong.// - try { - context.websocketHandler.receivePong(); - }//try// - catch(Throwable e) { - Debug.log(e); - }//catch// - break; - default: //Unsupported// - context.close(); - }//switch// - - //Clean up after the message.// - context.websocketFrameRemainder = 0; - context.websocketMessageOpCode = 0; - context.websocketIsLastFrameInMessage = false; - - if(context.websocketPartialReceivedMessage != null) { - context.websocketPartialReceivedMessage.release(); - context.websocketPartialReceivedMessage = null; - }//if// - }//if// - }//while// - - return !isFrameFullyRead; -}//processWebsocketFrame()// -/** - * Determines whether we have a complete HTML header in the string buffer. - * @param buffer The buffer containing the HTML ASCII header characters. - * @return Whether the header is complete. - */ -private boolean isCompleteHeader(StringBuffer buffer) { - return (buffer.length() > 4) && (buffer.charAt(buffer.length() - 4) == '\r') && (buffer.charAt(buffer.length() - 3) == '\n') && (buffer.charAt(buffer.length() - 2) == '\r') && (buffer.charAt(buffer.length() - 1) == '\n'); -}//isCompleteHeader()// -/** - * Processes enough of the header of this first request to identify the application and set it for the socket. Used to forward unencrypted message to a remote server. - * @param context - * @param fragment - * @param key - * @return Whether enough of the request could be read to identify the application. The caller should ignore the result if key.channel() is closed since the request was incomplete, incorrectly formatted, or the socket failed. - */ -private boolean processRequestedHost(SocketContext context, ByteBuffer fragment, SelectionKey key) throws IOException { - boolean result = false; - - try { - fragment.mark(); - - //Check whether we already read this message's header and we are simply appending to the end of it.// - if(context.request == null) { - String host = null; - StringBuilder buffer = new StringBuilder(4096); - int totalHeaderSize = 0; - - while(fragment.hasRemaining() && host == null) { - //Keep adding ASCII characters to the message header fragment until the next line is read or we exceed the maximum length of the header.// - while(fragment.hasRemaining() && (!(buffer.length() > 1 && (buffer.charAt(buffer.length() - 2) == '\r') && (buffer.charAt(buffer.length() - 1) == '\n')))) { - if(totalHeaderSize == 4096) { - //Force the connection to the client to be closed.// - try {key.channel().close();}catch(Throwable e2) {} - //Throw an exception that should not be logged. This happens occationally when an attacker tries to exploit any buffer overrun weaknesses.// - throw new IgnoredIOException(null); - }//if// - - buffer.append((char) fragment.get()); - totalHeaderSize++; - }//while// - - //If we have the minimum number of bytes and the last bytes are a line end, then check the line for "Host: xxxxxxx\r\n" - String line = buffer.toString().substring(0, buffer.length() - 2).trim(); - if(line.startsWith("Host: ")) { - host = line.substring(6).trim(); - }//if// - - if(host == null) { - if(buffer.length() == 2) { - //End of the header reached. No host provided. Kill the connection?// - //Force the connection to the client to be closed.// - try {key.channel().close();}catch(Throwable e2) {} - //Throw an exception that should not be logged. This happens occationally when an attacker tries to exploit any header reading weaknesses (all major browsers send a host header).// - throw new IgnoredIOException(null); - }//if// - else { - //Clear the line.// - buffer.setLength(0); - }//else// - }//if// - }//while// - - //If we found the complete first line of the header before running out of bytes, then identify the application.// - if(host != null) { - context.domain = host.toLowerCase(); - - //Get the web application for the given domain.// - //Synchronize to prevent another thread from altering the service's web applications while we are accessing it.// - synchronized(WebServer.this) { - context.webApplicationContainer = context.serverSocketContext.serviceListener.getWebApplicationContainer(context.domain); - }//synchronized// - - result = true; - }//if// - }//if// - - fragment.reset(); - }//try// - catch(IgnoredIOException e) {} - catch(Throwable e) { - Debug.log(e); - throw new IOException("Failed to process the request headers."); - }//catch// - - return result; -}//processRequestedHost()// -/** - * Processes the client request given the latest fragment of a message. - * @param context The client context. - * @param messageFragment The message fragment. - * @param key The socket's key. - * @result Whether request is in a receive state. Will be false if the request generated a response that could not be completely transmitted. - */ -private boolean processClientRequest(SocketContext context, ByteBuffer fragment, SelectionKey key) throws IOException { - boolean result = true; - - try { -// File f = new File("c:\\temp\\message.txt"); -// FileOutputStream fout = new FileOutputStream(f, true); -// fout.write(fragment.array(), fragment.position() + fragment.arrayOffset(), fragment.remaining()); -// fout.close(); - //The message will always start with a header which ends with a double '\r\n'.// - //Parse the message header to detect the Content-Type and the Content-Length.// - //Note that the message may contain extra content between the message header and the first --boundary, also there may be extra content after the last --boundary--, both of these bits of content should be ignored.// - //Detect a multipart/form-data message and read each part of the form, each of which begins with --boundary followed by either a double '\r\n' in which case the header is assumed to be 'Content-type: text/plain; charset=us-ascii', otherwise it will be followed by a single '\r\n' and then the header for the part and then a double '\r\n'.// - //The final boundary will show with two '-'s before and AFTER the boundary. Anything after this is ignoreable.// - - //Check whether we already read this message's header and we are simply appending to the end of it.// - if(context.request == null) { - //Create a buffer with the maximum size of a message header to hold the bytes that might be included in the header.// - if(context.messageHeaderFragment == null) { - context.messageHeaderFragment = new StringBuffer(4096); - }//if// - - //Keep adding ASCII characters to the message header fragment until it is a complete header, or the message fragment is empty, or we exceed the maximum allowed size of a message header.// - while(fragment.hasRemaining() && !isCompleteHeader(context.messageHeaderFragment)) { - if(context.messageHeaderFragment.length() == 4096) { - //Force the connection to the client to be closed.// - try {key.channel().close();}catch(Throwable e2) {} - //Throw an exception that should not be logged. This happens occationally when an attacker tries to exploit any buffer overrun weaknesses.// - throw new IgnoredIOException(null); - }//if// - - context.messageHeaderFragment.append((char) fragment.get()); - }//while// - - //If we found the complete header before running out of bytes, then setup the request object.// - if(isCompleteHeader(context.messageHeaderFragment)) { - IWebApplication application = context.webApplicationContainer != null ? context.webApplicationContainer.getWebApplication() : null; - -// if(debug) { -// context.debugBuffer.append("Processing:\r\n" + context.messageHeaderFragment.toString()); -// }//if// - - //Prepare the request.// - try { - context.request = new Request(++context.lastRequestNumber, context.messageHeaderFragment.toString(), ((SocketChannel) context.key.channel()).socket().getInetAddress().getHostAddress(), this, context, context.isSsl()); - - //Log the request header if running in debug mode.// - if(debug) { - Debug.log(context.request.toString()); - }//if// - }//try// - catch(IOException e) { - Debug.log(" ===== Begin exception output. ====="); - Debug.log("Processing:\r\n" + context.messageHeaderFragment.toString()); - Debug.log(e); - Debug.log("The following IOException is a result of this exception."); - Debug.log(" ===== End exception output. ====="); - throw e; - }//catch// - catch(Throwable e) { - Debug.log(" ===== Begin exception output. ====="); - Debug.log("Processing:\r\n" + context.messageHeaderFragment.toString()); - Debug.log(e); - Debug.log("The following IOException is a result of this exception."); - Debug.log(" ===== End exception output. ====="); - throw new IOException("Failed to process the request headers."); - }//catch// - - if(context.webApplicationContainer == null) { - //Save the domain.// - context.domain = context.request.getHost() != null ? context.request.getHost().toLowerCase() : null; - - if(context.domain != null) { - //Get the web application for the given domain.// - //Synchronize to prevent another thread from altering the service's web applications while we are accessing it.// - synchronized(WebServer.this) { - context.webApplicationContainer = context.serverSocketContext.serviceListener.getWebApplicationContainer(context.domain); - }//synchronized// - - if(context.webApplicationContainer != null) { - application = context.webApplicationContainer.getWebApplication(); - }//if// - }//if// - }//if// - - //If the request is tied to a web application then get or re-create the session. A new session will be created later if it is needed.// - if(application != null) { - int contentLength = 0; - - //TODO: Remove - if(debug) { - boolean sessionFound = application.getSession(context.request.getSessionId()) != null; - boolean canRecreate = context.request.getSessionId() != null; - - Debug.log("SC: " + context.id + "; Req#: " + context.request.getRequestNumber() + "; ReqURI: " + context.request.getUri() + "\n\t(SessionId: " + context.request.getSessionId() + "; SecureSessionId: " + context.request.getSecureSessionId() + ")\n\tSession Found: " + sessionFound + (!sessionFound ? "; Can Recreate: " + canRecreate : "")); - }//if// - - context.request.setSession(application.getSession(context.request.getSessionId())); - - //Try to re-create the session.// - if(context.request.getSession() == null && context.request.getSessionId() != null) { - //TODO: Find something better to synchronize on? We could somehow use the session ID and a hashmap of monitors I suppose... The problem with this is that it blocks multiple clients from recreating the sessions at the same time, and recreating the session might take some time if a database is involved and servers have to be notified. - synchronized(WebServer.class) { - //Try again to get the session - in case another thread has already re-created it.// - context.request.setSession(application.getSession(context.request.getSessionId())); - - if(context.request.getSession() == null) { - context.request.setSession(application.recreateSession(context.request.getSessionId())); - }//if// - }//synchronized// - }//if// - - //If the client set a secure session ID and we found session data, then ensure that the client's secure session ID matches the secure session ID in the session data.// - //This is a security mechanism because the client is SUPPOSED to keep the secure session ID very private and not allow access to it by other sites. It ensures that a recreated session actually comes from a client that had it created in the first place.// - if(context.request.getSecureSessionId() != null && context.request.getSession() != null && context.request.getSession().getSecureSessionId() != null) { - if(!Comparator.equals(context.request.getSecureSessionId(), context.request.getSession().getSecureSessionId())) { - Debug.log("SC: " + context.id + "Forcing connection closure."); - //Force the connection to the client to be closed.// - try {key.channel().close();}catch(Throwable e2) {} - //Throw an exception that should not be logged. This might happen when an attacker tries to reuse stored session data on a client.// - throw new IgnoredIOException(null); - }//if// - }//if// - - //Validate the content length.// - if(context.request.getContentTypeCode() == IRequest.CONTENT_TYPE_MULTI_PART_FORM_DATA || context.request.getContentTypeCode() == IRequest.CONTENT_TYPE_CUSTOM || context.request.getContentTypeCode() == IRequest.CONTENT_TYPE_URL_ENCODED) { - int maxContentLength = application.getMaxMessageSize(context.request, context.request.getSession() != null ? context.request.getSession().getApplicationData() : null, context.request.getSession() != null && context.isSsl() ? context.request.getSession().getApplicationSecureData() : null); - - //A zero max content length allows for unlimited content sizes.// - if(maxContentLength > 0 && context.request.getContentLength() > maxContentLength) { - //TODO: Include info on the IP address. - throw new IOException("Message content exceeds the server's maximum size of " + maxContentLength + " bytes. Header: " + context.messageHeaderFragment.toString()); - }//if// - else { - contentLength = context.request.getContentLength(); - }//else// - }//if// - - //Setup to retain the request bytes.// - if(application.retainWholeRequestBuffer(context.request)) { - //Note: Use the content length from above since we wouldn't want a non-MPFD or custom message to also specify a length when there should be none - a possible hack.// - context.request.setRequestBytes(ByteBuffer.allocate(context.messageHeaderFragment.length() + contentLength)); - //Place the message header bytes in the buffer.// - context.request.getRequestBytes().put(context.messageHeaderFragment.toString().getBytes("ASCII")); - }//if// - }//if// - else { - if(context.request.getContentTypeCode() == IRequest.CONTENT_TYPE_MULTI_PART_FORM_DATA || context.request.getContentTypeCode() == IRequest.CONTENT_TYPE_CUSTOM) { - //A zero max content length allows for unlimited content sizes.// - if(this.maxContentLength > 0 && context.request.getContentLength() > this.maxContentLength) { - //TODO: Include info on the IP address. - throw new IOException("Message content exceeds the server's maximum size of " + this.maxContentLength + " bytes. Header: " + context.messageHeaderFragment.toString()); - }//if// - }//if// - }//else// - - //Clear the message header fragment since it is no longer required.// - context.messageHeaderFragment = null; - //Clear the attributes used to assist in reading the message body.// - context.remainingPartBytes = null; - context.partReadCount = 0; - context.partWriteCount = 0; - context.endPartBoundaryFound = false; - context.currentPart = null; - - //If this is a multipart or custom message then we should do as much verification of size validity as possible.// - if(context.request.getContentTypeCode() == IRequest.CONTENT_TYPE_MULTI_PART_FORM_DATA) { - //Ensure that the multi-part form data message begins with the \r\n from the end of the header, allowing for the boundary recognition to proceed.// - //This could be a problem if the fragment doesn't have the last two characters (it might have only the last 1).// - if(fragment.position() == 1) { - ByteBuffer temp = ByteBuffer.allocate(fragment.remaining() + 2); - - temp.putChar('\r'); - temp.putChar('\n'); - temp.put(fragment); - fragment = temp; - }//if// - else { - //Roll back 2 characters to include the second \r\n that ended the header. This allows the first boundary to be recognized properly if there is no junk between the header and boundary.// - fragment.position(fragment.position() - 2); - }//else// - - //Remove the \r\n from the request's requestedBytes buffer so they aren't added twice.// - if(context.request.getRequestBytes() != null) { - context.request.getRequestBytes().position(context.request.getRequestBytes().position() - 2); - }//if// - }//if// - }//if// - }//if// - - //Add the incoming bytes to the request's byte record (if we are recording the exact message as received by the web server). This shouldn't alter the incoming data in any way.// - if(context.request != null && context.request.getRequestBytes() != null) { - int remaining = context.request.getRequestBytes().remaining(); - int position = fragment.position(); - -// Debug.log("Reading content bytes for the current client request. Remaining to be read: " + remaining + ". Bytes available to read: " + fragment.remaining()); - - if(remaining >= fragment.remaining()) { - //Write the remaining bytes in the fragment into the buffer for the request bytes.// - context.request.getRequestBytes().put(fragment); - }//if// - else if(remaining > 0) { - byte[] bytes = new byte[remaining]; - - fragment.get(bytes); - context.request.getRequestBytes().put(bytes); - }//else// - -// Debug.log("Read " + (remaining - context.request.getRequestBytes().remaining()) + " bytes of the current client request."); - - //Reset the fragment back to its previous position.// - fragment.position(position); - }//if// - - //If a complete header has been obtained then we must read any remaining bytes in the message.// - if(context.request != null) { - if(context.request.getContentTypeCode() == IRequest.CONTENT_TYPE_MULTI_PART_FORM_DATA) { - int remainingBytes = context.request.getContentLength() - context.partReadCount; - byte[] bytes; - int currentIndex = 0; - int startIndex = -1; - - //Convert the fragment bytes into an ASCII string for easier manipulation.// - if(fragment.hasArray()) { - int remainingPartByteCount = context.remainingPartBytes == null ? 0 : context.remainingPartBytes.length; - - bytes = new byte[Math.min(remainingBytes, fragment.remaining()) + remainingPartByteCount]; - - //Prepend the remaining bytes from the last fragment.// - if(remainingPartByteCount != 0) { - System.arraycopy(context.remainingPartBytes, 0, bytes, 0, remainingPartByteCount); - }//if// - - //Add in the bytes from this fragment.// - System.arraycopy(fragment.array(), fragment.arrayOffset() + fragment.position(), bytes, remainingPartByteCount, bytes.length - remainingPartByteCount); - //Update the fragment's position.// - fragment.position(fragment.position() + bytes.length - (context.remainingPartBytes == null ? 0 : context.remainingPartBytes.length)); - }//if// - else { - int remainingPartByteCount = context.remainingPartBytes == null ? 0 : context.remainingPartBytes.length; - byte[] buffer = bytes = new byte[Math.min(remainingBytes, fragment.remaining()) + remainingPartByteCount]; - - //Prepend the remaining bytes from the last fragment.// - if(remainingPartByteCount != 0) { - System.arraycopy(context.remainingPartBytes, 0, bytes, 0, remainingPartByteCount); - }//if// - - //Collect the bytes and increment the buffer's position.// - fragment.get(buffer, remainingPartByteCount, buffer.length - remainingPartByteCount); - }//else// - - //Detect whether we need to find the first part's boundary.// - if(context.currentPart == null) { - //Find the first boundary.// - startIndex = indexOf(bytes, context.request.getMultiPartBoundary(), currentIndex); - - //Test to make sure we didn't find the end boundary while NOT reading a part of the message - indicates there are NO parts in the multipart message. Shouldn't ever happen.// - if(startIndex == -1) { - if((startIndex = indexOf(bytes, context.request.getMultiPartEndBoundary(), currentIndex)) != -1) { - //TODO: What do we do if there are no parts in a multi-part message? Here we found the end part while NOT reading a message - shouldn't ever happen. - - //Force the connection to the client to be closed.// - try {key.channel().close();}catch(Throwable e2) {} - //Throw an exception that should not be logged. This is a malformed message.// - throw new IgnoredIOException(null); - }//if// - else { - //Store the data between the boundary and the end of the fragment, except for those parts of the fragment which might be the beginning of a boundary.// - int remainingIndex = Math.max(0, bytes.length - context.request.getMultiPartEndBoundary().length); - - //Retain enough characters to identify any boundary that might partially exist in this fragment of the message.// - context.remainingPartBytes = new byte[bytes.length - remainingIndex]; - System.arraycopy(bytes, remainingIndex, context.remainingPartBytes, 0, context.remainingPartBytes.length); - //Update the part read count to include the bytes/chars read in this iteration.// - context.partReadCount += remainingIndex; - //Clear the bytes indicating we have used them all and the loop should end.// - bytes = null; - }//else// - }//if// - }//if// - - //Read parts of the message until the end is found or we need to wait for furthor fragments of the message to be received.// - while(!context.endPartBoundaryFound && (startIndex != -1 || (context.currentPart != null && bytes != null))) { - //If we don't have a part setup to be read then identify the next part.// - if(context.currentPart == null) { - int partStartIndex = startIndex; //Retain the start of the message part in case we need to store a fragment for later use when more of the message arrives.// - - //Update the current index to the beginning of the part's header.// - currentIndex = startIndex + context.request.getMultiPartBoundary().length; - - if(bytes[currentIndex] == '\r' && bytes[currentIndex] == '\n') { - currentIndex += 2; - //Create the content part object with the default header.// - context.currentPart = context.request.getHeader().addContentPart("Content-type: text/plain; charset=us-ascii\r\n", context.partWriteCount); - }//if// - else { - int headerEndIndex = indexOf(bytes, DOUBLE_ENTER, currentIndex); - - //Determine whether the whole header is available. If it is then parse it, else save it until more of the message arrives.// - if(headerEndIndex != -1) { - //Create the content part object with the header.// - context.currentPart = context.request.getHeader().addContentPart(new String(bytes, currentIndex, (headerEndIndex + 4) - currentIndex, "ASCII"), context.partWriteCount); - //Set the current index to the end of the header.// - currentIndex = headerEndIndex + 4; - }//if// - else { - //Store the bytes starting with the multipart boundary for later use when another fragment arrives. - context.remainingPartBytes = new byte[bytes.length - partStartIndex]; - System.arraycopy(bytes, partStartIndex, context.remainingPartBytes, 0, context.remainingPartBytes.length); - //Clear the current part stored in the context.// - context.currentPart = null; - //Update the part read count to include the bytes/chars read in this iteration.// - context.partReadCount += partStartIndex; - //Clear the start index so the while loop ends.// - startIndex = -1; - }//else// - }//else// - }//if// - - //If we have the part header then look for the next boundary and store the data between the header and boundary as the content for the part.// - //Note that the currentIndex should stay pointed to the beginning of the message part so that we can figure out what to read for the part's content.// - if(context.currentPart != null) { - //Identify the next part.// - startIndex = indexOf(bytes, context.request.getMultiPartBoundary(), currentIndex); - - if(startIndex == -1) { - //Identify the final boundary if possible. This indicates that this is the last part in the message (any trailing bytes at the end of the message should be ignored).// - startIndex = indexOf(bytes, context.request.getMultiPartEndBoundary(), currentIndex); - - if(startIndex != -1) { - //ByteBuffer content = ByteBuffer.wrap(chars.substring(currentIndex, startIndex).getBytes("ASCII")); //Doesn't properly convert the bytes back from ASCII.// - ByteBuffer content = ByteBuffer.wrap(bytes, currentIndex, startIndex - currentIndex); - - //Mark the context as having found the end boundary.// - context.endPartBoundaryFound = true; - //Increment the write count so that indices passed to the ContentPart instances are correct.// - context.partWriteCount += content.remaining(); - //Increment the read count so we know how much of the message has been read and how much remains.// - context.partReadCount += startIndex + context.request.getMultiPartEndBoundary().length; - //Set the size of the part's data segment.// - context.currentPart.setDataLength(context.partWriteCount - context.currentPart.getDataOffset()); - - //Store the data between the header and the boundary.// - if(context.request.getContentChannel() != null) { - context.request.getContentChannel().write(content); - }//if// - else { - context.request.getContent().put(content); - }//else// - - //Update the current index to the beginning of the boundary.// - currentIndex = startIndex; - }//if// - else { //Store the data between the boundary and the end of the fragment, except for those parts of the fragment which might be the beginning of a boundary.// - ByteBuffer content; - int remainingIndex = Math.max(currentIndex, bytes.length - context.request.getMultiPartEndBoundary().length); - - //Retain enough characters to identify any boundary that might partially exist in this fragment of the message.// - context.remainingPartBytes = new byte[bytes.length - remainingIndex]; - System.arraycopy(bytes, remainingIndex, context.remainingPartBytes, 0, context.remainingPartBytes.length); - //Update the part read count to include the bytes/chars read in this iteration.// - context.partReadCount += remainingIndex; - //Extract the content to be stored.// - content = ByteBuffer.wrap(bytes, currentIndex, remainingIndex - currentIndex); - //Increment the write count so that indices passed to the ContentPart instances are correct.// - context.partWriteCount += content.remaining(); - //Clear the bytes indicating we have used them all and the loop should end.// - bytes = null; - - //Store all but the remaining bytes.// - if(context.request.getContentChannel() != null) { - context.request.getContentChannel().write(content); - }//if// - else { - context.request.getContent().put(content); - }//else// - }//else// - }//if// - else { - ByteBuffer content = ByteBuffer.wrap(bytes, currentIndex, startIndex); - - //Increment the write count so that indices passed to the ContentPart instances are correct.// - context.partWriteCount += content.remaining(); - //Set the size of the part's data segment.// - context.currentPart.setDataLength(context.partWriteCount - context.currentPart.getDataOffset()); - - //Store the data between the header and the boundary.// - if(context.request.getContentChannel() != null) { - context.request.getContentChannel().write(content); - }//if// - else { - context.request.getContent().put(content); - }//else// - - //Update the current index to the beginning of the boundary.// - currentIndex = startIndex; - }//else// - }//if// - }//while// - - //Keep reading past the last part if there are more bytes in the message. We can safely ignore these bytes.// - //Note that we increment the content length by 2 because we re-add the \r\n that follows just after the main header in order to allow for matching the boundary if it follows the header immediately.// - if(context.endPartBoundaryFound && context.partReadCount != (context.request.getContentLength() + 2) && bytes != null) { - //Discard bytes until the message is all read.// - //Update the part read count to be the whole chunk of message. There may still be more in which case this if block will execute again in the future.// - //Note that we will not have read more of the fragment into the chars string than is in the message, so we can just use the size of the chars string to increment the part count.// - context.partReadCount += bytes.length; - bytes = null; - }//if// - }//if// - else if(context.request.getContentLength() > 0) { //Read the body of the message (for non-multi-part mime encodings).// - int remainingBytes = context.request.getContentLength() - context.partReadCount; - byte[] bytes = null; - - if(fragment.hasArray()) { - int remainingPartByteCount = context.remainingPartBytes == null ? 0 : context.remainingPartBytes.length; - - bytes = new byte[Math.min(remainingBytes, fragment.remaining()) + remainingPartByteCount]; - - //Prepend the remaining bytes from the last fragment.// - if(remainingPartByteCount != 0) { - System.arraycopy(context.remainingPartBytes, 0, bytes, 0, remainingPartByteCount); - }//if// - - //Add in the bytes from this fragment.// - System.arraycopy(fragment.array(), fragment.arrayOffset() + fragment.position(), bytes, remainingPartByteCount, bytes.length - remainingPartByteCount); - //Update the fragment's position.// - fragment.position(fragment.position() + bytes.length - (context.remainingPartBytes == null ? 0 : context.remainingPartBytes.length)); - }//if// - else { - int remainingPartByteCount = context.remainingPartBytes == null ? 0 : context.remainingPartBytes.length; - byte[] buffer = bytes = new byte[Math.min(remainingBytes, fragment.remaining()) + remainingPartByteCount]; - - //Prepend the remaining bytes from the last fragment.// - if(remainingPartByteCount != 0) { - System.arraycopy(context.remainingPartBytes, 0, bytes, 0, remainingPartByteCount); - }//if// - - //Collect the bytes and increment the buffer's position.// - fragment.get(buffer, remainingPartByteCount, buffer.length - remainingPartByteCount); - }//else// - - //Place the bytes in either the content file or buffer for processing by the request in the postProcessContent() method.// - if(bytes != null && bytes.length > 0) { - context.partReadCount += bytes.length; - - //Store the data in either the content file or content buffer.// - if(context.request.getContentChannel() != null) { - context.request.getContentChannel().write(ByteBuffer.wrap(bytes)); - }//if// - else { - context.request.getContent().put(bytes); - }//else// - }//if// - }//else if// - }//if// - - //If we have a complete request then process it.// - //Note that for multi-part data we increment the content length by 2 because we re-add the \r\n that follows just after the main header in order to allow for matching the boundary if it follows the header immediately.// - if((context.request != null) && (context.partReadCount == (context.request.getContentLength() + (context.request.getContentTypeCode() == IRequest.CONTENT_TYPE_MULTI_PART_FORM_DATA ? 2 : 0)))) { - //Post process the content as necessary.// - if(context.request.getContentTypeCode() == IRequest.CONTENT_TYPE_URL_ENCODED) { - context.request.postProcessContent(); - }//if// - - //Process the request and send a response.// - result = processClientRequest(context, context.request, key); - - //If we have a partial message left then process it if we can.// - if(result && fragment.hasRemaining()) { - result = processClientRequest(context, fragment, key); - }//if// - }//if// - }//try// - catch(IgnoredIOException e) {} - catch(IOException e) { - Debug.log(e); - throw e; - }//catch// - catch(Throwable e) { - Debug.log(e); - throw new IOException("Failed to process the request headers."); - }//catch// - - return result; -}//processClientRequest()// -/** - * Locates the first pattern match in the source from the given offset. - * @param source The source to be searched. - * @param pattern The pattern to search for. - * @param fromOffset The offset from the beginning of the source at which the search will commence (inclusive). - * @return The index of the first match, or -1 if no match was found. - */ -private int indexOf(byte[] source, byte[] pattern, int fromOffset) { - int result = -1; - - for(int index = (pattern.length - 1) + fromOffset; result == -1 && index < source.length; index++) { - boolean match = true; - - for(int patternIndex = pattern.length - 1, sourceOffset = 0; match && sourceOffset < pattern.length; sourceOffset++, patternIndex--) { - match = source[index - sourceOffset] == pattern[patternIndex]; - }//for// - - if(match) { - result = index - (pattern.length - 1); - }//if// - }//for// - - return result; -}//indexOf()// -/** - * Processes a request by the client. - * @param context - * @param request The request to be handled. - * @param key The socket's key. - * @result Whether request is in a receive state. Will be false if the request generated a response that could not be completely transmitted. - */ -private boolean processClientRequest(SocketContext context, final Request request, SelectionKey key) throws IOException { - boolean result = true; - Response response = null; - - try { - boolean applicationNotFound = false; - - //Moved this bit of code into the calling method near the top. Access to the application was required that far up in the sequence of events.// -// if(context.webApplicationContainer == null) { -// //Save the domain.// -// context.domain = request.getHost() != null ? request.getHost().toLowerCase() : null; -// -// if(context.domain != null) { -// //Get the web application for the given domain.// -// //Synchronize to prevent another thread from altering the service's web applications while we are accessing it.// -// synchronized(WebServer.this) { -// context.webApplicationContainer = context.serverSocketContext.serviceListener.getWebApplicationContainer(context.domain); -// }//synchronized// -// }//if// -// }//if// - - //Verify an application was found for the context.// - if(context.webApplicationContainer != null) { - //Increment the activity counter until we finish processing this request.// - if(context.webApplicationContainer.incrementActivityCounter()) { - try { - IWebApplication application = context.webApplicationContainer.getWebApplication(); - - //Verify the application was not unloaded.// - if(application != null) { - boolean allowSecureAccess = false; - boolean clientHadBadSession = false; - ISession session = request.getSession(); - boolean hasNewSessionData = false; - - if(session == null) { - //TODO: Remove - if(debug) { - Debug.log("SC: " + context.id + " Creating Session"); - }//if// - - request.setSession(session = application.createSession()); - clientHadBadSession = request.getSessionId() != null; - hasNewSessionData = true; - - //Removed this code: Cannot disallow null sessions because then forwarding apps would need to fake a session. -// //Basic error checking.// -// if(session == null) { -// throw new RuntimeException("WebApplication failure: Cannot return a null value from IWebApplication.createSession()"); -// }//if// - }//if// - - //If we are handling a secure connection then setup or locate the secure session object.// - if(context.sslEngine != null) { - if((request.getSecureSessionId() != null) && (session != null) && (session.getSecureSessionId() != null)) { - if(session.getSecureSessionId().equals(request.getSecureSessionId())) { - allowSecureAccess = true; - }//if// - else { - Debug.log(new RuntimeException("Error: The client did not send the correct secure session id with the request!")); - }//else// - }//if// - else if(session != null && session.getSecureSessionId() == null) { - //TODO: Remove - if(debug) { - Debug.log("SC: " + context.id + " Creating Secure Session"); - }//if// - - application.createSecureSession(session); - allowSecureAccess = true; - hasNewSessionData = true; - }//else if// - else { - //TODO: For some reason Mozilla causes this error, but it shouldn't be. It seems to only occur when opening a new window and it appears to try to reuse the old session, but not secure session cookies. - //Debug.log(new RuntimeException("Error: The client did not send the secure session id with the request!")); - }//else// - }//if// - - //Save the session immediately since the requested resource might not indicate to the application that the session was updated.// - //Note: We shouldn't have any problems with multiple threads from the same client each creating their own session data since every browser should start with a single thread requesting a single resource before multiple threads are used to download all the child resources.// - if(hasNewSessionData && session != null) { - //Store the session store in the db.// - session.updateRepository(); - }//if// - - //Create the response.// - response = new Response(request, session, application); - - //Move to the next step in the request processing.// - result = internalProcessClientRequest(context, key, application, request, response, session, allowSecureAccess, clientHadBadSession); - }//if// - else { - applicationNotFound = true; - }//else// - }//try// - finally { - context.webApplicationContainer.decrementActivityCounter(); - }//finally// - }//if// - else { - applicationNotFound = false; - }//else// - }//if// - else { - applicationNotFound = true; - }//else// - - //There is no application for the request. No need to thread the processing of it since we won't be using reflections to send the response to the request (the response will always be an error).// - if(applicationNotFound) { - IContent content = null; - - if(getErrorHandler() != null) { - content = getErrorHandler().domainNotHosted(request); - }//if// - - if(content == null) { - //For now we will simply close the connection to the client - this should never happen anyhow and if it does, it could be an attack.// - Debug.log(new RuntimeException("Application not found for the host: " + request.getHost())); - key.channel().close(); - }//if// - else { - response = new Response(request, null, null); - response.setContent(content); - result = context.sendHttpResponse(response); - }//else// - }//if// - }//try// - catch(Throwable e) { - Debug.log(e); - //TODO: Use a 307 temporary redirect to display an error... - //response = new Response(this, null, key); - //response.setResourceNotFound(true); - //For now simply close the connection. This simplifies threading.// - //Close the socket.// - try {key.channel().close();}catch(Throwable e2) {} - //Close the response (which also closes the request).// - try {response.close();} catch(Throwable e2) {} - }//catch// - - return result; -}//processClientRequest()// -/** - * Processes a client request. - * @param request The request. - * @param response The response container. - * @param session The session for the request. This may be null in the case of non-standard web applications such as a forwarding domain. - * @param allowSecureAccess Whether the session's secure sessions should be accessable. - * @param clientHadBadSession Whether the client's request contained a session reference that could not be found on the server. - * @return Whether request is in a receive state. Will be false if the request generated a response that could not be completely transmitted. - */ -private boolean internalProcessClientRequest(final SocketContext context, SelectionKey key, final IWebApplication application, final Request request, final Response response, final ISession session, final boolean allowSecureAccess, final boolean clientHadBadSession) { - boolean result = false; - - try { - if(context.tlsFailure) { - response.setError(IResponse.ERROR_TYPE_TLS_FAILURE); - }//if// - //Send the request to the application to be processed if we are not dealing with an error.// - else if(application != null) { - String upgrade = request.getHeaderFieldValue("Upgrade"); - - if(upgrade != null && "websocket".equalsIgnoreCase(upgrade)) { - String connection = request.getHeaderFieldValue("Connection"); - String secWebSocketKey = request.getHeaderFieldValue("Sec-WebSocket-Key"); - String secWebSocketProtocol = request.getHeaderFieldValue("Sec-WebSocket-Protocol"); - String secWebSocketVersion = request.getHeaderFieldValue("Sec-WebSocket-Version"); - String origin = request.getHeaderFieldValue("Origin"); - - application.handleWebSocketUpgrade(request, response, session, allowSecureAccess, clientHadBadSession, context, connection, secWebSocketKey, secWebSocketProtocol, secWebSocketVersion, origin); - }//if// - else { - application.processRequest(request, response, session, allowSecureAccess, clientHadBadSession, context); - }//else// - }//else if// - - //Convert the response into a byte stream and send it via the socket.// - result = context.sendHttpResponse(response); - }//try// - catch(Throwable e) { - Debug.log(e); - //Close the socket.// - try {key.channel().close();} catch(Throwable e2) {} - //Close the response (which also closes the request).// - try {response.close();} catch(Throwable e2) {} - }//catch// - - return result; -}//internalProcessClientRequest()// }//WebServer// \ No newline at end of file diff --git a/Foundation Web Core/src/com/foundation/web/server/WebsocketMessageBuffer.java b/Foundation Web Core/src/com/foundation/web/server/WebsocketMessageBuffer.java new file mode 100644 index 0000000..de8b2b2 --- /dev/null +++ b/Foundation Web Core/src/com/foundation/web/server/WebsocketMessageBuffer.java @@ -0,0 +1,150 @@ +package com.foundation.web.server; + +import java.nio.ByteBuffer; + +import com.common.debug.Debug; +import com.foundation.web.interfaces.IStreamedWebsocketMessage; + +class WebsocketMessageBuffer extends MessageBuffer { + /** The streaming message handler which will be set only if the currently sending message is streaming. */ + private IStreamedWebsocketMessage streamingMessage = null; + /** The buffer containing the next part of the streamed message, or the bytes of the whole message (if streamingMessage == null), or null if the buffer is closed or not yet initialized. */ + private ByteBuffer messagePart = null; + /** The message to be sent. Will be null after the message buffer has initialized. */ + private Object message = null; + + public WebsocketMessageBuffer(Object message) { + this.message = message; + }//WebsocketMessageBuffer()// + public boolean initialize() { + if(message != null) { + messagePart = stream(message, true); + message = null; + }//if// + + return super.initialize(); + }//initialize()// + public boolean isClosed() { + return super.isClosed() && messagePart == null && streamingMessage == null; + }//isClosed()// + public void close() { + super.close(); + messagePart = null; + streamingMessage = null; + }//close()// + private ByteBuffer stream(Object next, boolean isLast) { + ByteBuffer result = null; + byte[] bytes = null; + int opCode = 0; + int length = 0; + + if(next instanceof String) { + try {bytes = ((String) next).getBytes("UTF-8");} catch(Throwable e) {Debug.log(e);} + opCode = streamingMessage == null ? 0x01 : 0; + length = bytes.length; + }//if// + else if(next instanceof byte[]) { + bytes = (byte[]) next; + opCode = streamingMessage == null ? 0x02 : 0; + length = bytes.length; + }//else if// + else if(next instanceof Byte) { //Control Message// + opCode = ((Byte) next).byteValue(); + }//else if// + else if(next instanceof IStreamedWebsocketMessage) { + //TODO: Ensure that this is not recursive! + streamingMessage = (IStreamedWebsocketMessage) next; + next = streamingMessage.getNextPart(); + isLast = !streamingMessage.hasNextPart(); + + if(next instanceof String) { + try {bytes = ((String) next).getBytes("UTF-8");} catch(Throwable e) {Debug.log(e);} + opCode = 0x01; //Text// + length = bytes.length; + }//if// + else if(next instanceof byte[]) { + bytes = (byte[]) next; + opCode = 0x02; //Binary// + length = bytes.length; + }//else if// + else { + throw new RuntimeException("Invalid streaming message part type."); + }//if// + }//else if// + + result = ByteBuffer.allocate(14 + length); + result.put((byte) (isLast ? 0x8 : 0)); + result.put((byte) opCode); + + //Write the length differently based on how long the content is.// + if(length < 126) { + result.put((byte) length); + }//if// + else if(length < 65535) { + result.put((byte) 126); + result.putShort((short) (length & 0xFFFF)); + result.putInt(0); + }//else if// + else { + result.put((byte) 127); + result.putLong((long) length); + }//else// + + //Put the content at the end of the message.// + result.put(bytes); + + return result; + }//stream()// + public boolean loadBuffer() { + boolean result = true; + + getBuffer().compact(); + + //Copy remaining bytes from MessagePart to the buffer.// + if(messagePart != null && messagePart.remaining() > 0) { + int length = Math.min(getBuffer().remaining(), messagePart.remaining()); + getBuffer().put(messagePart.array(), messagePart.position(), length); + messagePart.position(messagePart.position() + length); + + if(messagePart.remaining() == 0) { + messagePart = null; + }//if// + }//if// + + //Load a new message part if streaming and copy as many bytes from the MessagePart into the buffer as possible.// + if(messagePart == null && streamingMessage != null) { + Object next = null; + boolean isLastPart = true; + + next = streamingMessage.getNextPart(); + isLastPart = !streamingMessage.hasNextPart(); + + //Ensure that we got a string or byte array.// + if(!(next instanceof String || next instanceof byte[])) { + throw new RuntimeException("Invalid streaming message part type."); + }//if// + + messagePart = stream(next, isLastPart); + + if(isLastPart) { + streamingMessage = null; + }//if// + + int length = Math.min(getBuffer().remaining(), messagePart.remaining()); + getBuffer().put(messagePart.array(), messagePart.position(), length); + messagePart.position(messagePart.position() + length); + + if(messagePart.remaining() == 0) { + messagePart = null; + }//if// + }//if// + + //Close the message buffer if we were unable to add any bytes and there is nothing left to send.// + if(getBuffer().remaining() == 0) { + close(); + result = false; + }//if// + + return result; + }//loadBuffer()// +}//WebsocketMessageBuffer// \ No newline at end of file