31 Commits

Author SHA1 Message Date
Wynne Crisman
dd48e2fe24 Bug fix. I think this fixed a long standing bug in the brainstorm web server. 2016-08-17 17:56:41 -07:00
Wynne Crisman
040a3b4ddc Modified build script to use jdk 1.7 instead of 1.5 for building the release compile; Added debug output to try to identify the error where interestOps isn't set properly for a socket that isn't yet closed; Fixed index exception in identifying the host; Fixed odd build errors in the development environment by removing and re-adding referenced projects. 2016-02-22 10:40:27 -08:00
wcrisman
439eca5e9d Fixed bug in host detection for non-SSL HTTP sockets. 2014-12-29 13:22:07 -08:00
wcrisman
eb4a935562 Changed key.channel().close() into just calling close on the SocketContext throughout the code. Comment changes and debugging additions and changes. 2014-12-29 11:57:32 -08:00
wcrisman
f9be168cc8 Changed SocketContext.writeClientBoundPlainMessage() to be more like the master branch. Removed commented code. 2014-12-29 11:08:28 -08:00
wcrisman
c0a4621a4c Enabled the queuing of messages in a linked list again (from the writeOutgoingMessages() method. Removed commented code and comments. 2014-12-29 10:57:06 -08:00
wcrisman
b6d57986f2 Reorganized SocketContext code so that methods are in the correct order to compare with the master branch. Added code back in and updated code as necessary to be as identical as possible to the master branch for the Websocket related methods and classes. Updated PassThroughSocketContext to be as identical as possible to the master branch. Updated NetworkListener to be as identical as possible to the master branch. Commented a number of TODO's in SocketContext for turning on bits of code that might cause problems, but will bring this branch up to speed with the master branch. 2014-12-29 10:35:10 -08:00
wcrisman
5aece2300e Finished fixing the bug in the last two commits where by the content filling the buffer was causing major problems with the resulting client display (not sure why and don't care). 2014-12-29 09:24:51 -08:00
wcrisman
b3648439e6 Fixed bug where initialization wasn't affecting the isClosed status of a message (not initialized meant that the buffer was null which indicated the message was closed before it had ever started being sent). Moved isInitialized into the MessageBuffer base class. 2014-12-29 09:18:59 -08:00
wcrisman
28e0787f94 Fixed compilation bug. 2014-12-28 23:08:21 -08:00
wcrisman
ce99adb839 Added HttpMessageBuffer and modified MessageBuffer to be identical to the code in the master branch. Modified SocketContext.queueOutboundClientMessage() to be identical to the master branch. Changed SocketContext.sendHttpResponse() to call the queueOutboundClientMessage() passing a new HttpMessageBuffer. Fixed errors as necessary to make it all compile. 2014-12-28 22:03:40 -08:00
wcrisman
6cbd188867 Modified prepareResponse() to call queueOutboundClientMessage instead of directly setting the currentOutboundMessage. 2014-12-28 21:13:24 -08:00
wcrisman
c7188095d0 Copied SocketContext.readIncommingMessages() from the master tree and updated as required to fix errors. 2014-12-28 20:55:16 -08:00
wcrisman
840fa8dd89 Changed the SocketContext.writeOutgoingMessages() to copy the code in the master tree. Changed the SSL reading code to call the new writeClientBoundMessage() with the proper parameters. Removed internalProcessResponses(). 2014-12-28 20:33:03 -08:00
wcrisman
bd3e9ac5cc Changed code in SocketContext.writeClientBoundSslMessage() to sync it up with the master branch (they are now identical (or nearly). 2014-12-28 17:45:22 -08:00
wcrisman
eb3574bb3f Made AbstractSocketContext.id private and added an accessor to help merge code between branches. Modified WebServer.debug to be private with a public accessor to make code mergable between branches. Fixed code in NetworkListener and SocketContext to use accessors instead of attributes for id and debug. Modified SocketContext.writeClientBoundSslMessage() to take the channel and currentOutboundMessage as parameters to make code more mergable, and added debug output from main branch. Removed calls in the SSL code to chain buffers in a MessageBuffer, which should not be used currently anyway, and won't be used in the future. 2014-12-28 17:15:57 -08:00
wcrisman
d1d5671229 Fixed bug in the internalProcessResponses() in SocketContext where a message was flagged as needing to send more data because of already encrypted but unsent data, but was not properly sending the data because the currentOutboundMessage was closed and cleared. 2014-12-28 17:09:06 -08:00
wcrisman
0c3b7d026b Fixed bug in MessageBuffer to close the buffer instead of just setting buffer = null. Modified MessageBuffer to close the response if closed its self. Modified SocketContext to get rid of the response linked list, and close the outbound message instead of the response (no more references to Response). Modified SocketContext to immediately create a MessageBuffer from a Response that is ready to be sent, and set it as the currentOutboundMessage (no list or linked list used currently). Modified writeSessionCookies() and prepareResponse() in SocketContext to take the Response as a parameter. 2014-12-28 16:03:49 -08:00
wcrisman
a9a0e799e2 Short circuited the response linked list since it shouldn't be being used. 2014-12-28 14:52:24 -08:00
wcrisman
7a75ba30de Split sending message code into SSL and plain. 2014-12-28 14:48:03 -08:00
wcrisman
7f4d6702a0 Revert "Modified SocketContext to use a queue and an active reference to the outbound messages instead of multiple linked lists."
This reverts commit 06cbff7cf0.
2014-12-28 14:38:27 -08:00
wcrisman
06cbff7cf0 Modified SocketContext to use a queue and an active reference to the outbound messages instead of multiple linked lists. 2014-12-28 10:43:25 -08:00
wcrisman
3e1a531fc3 Separated inner classes from WebServer. 2014-12-28 09:32:25 -08:00
wcrisman
ba35deff06 Revert "Switched to using HttpMessageBuffer in the code and moved the code from prepareResponse into initialize."
This reverts commit c36467d172.
2014-12-26 10:17:33 -08:00
wcrisman
6c9931d4f9 Revert "Simplified writeOutgoingMessages() and removed the call to internalProcessResponses(). Likely bugs in writeClientBoundMessage()."
This reverts commit 29ade9b7b0.
2014-12-26 10:17:15 -08:00
wcrisman
72fa63028e Revert "Fixed bugs in writeClientBoundMessage."
This reverts commit a30fcb4002.
2014-12-26 10:16:49 -08:00
wcrisman
7152465730 Revert "Updated/Fixed queueOutboundClientMessage(..), sendHttpResponse(..), and hasPendingWrite()."
This reverts commit 48724e445c.
2014-12-26 09:28:37 -08:00
wcrisman
48724e445c Updated/Fixed queueOutboundClientMessage(..), sendHttpResponse(..), and hasPendingWrite(). 2014-12-10 17:51:41 -08:00
wcrisman
a30fcb4002 Fixed bugs in writeClientBoundMessage. 2014-12-10 17:07:47 -08:00
wcrisman
29ade9b7b0 Simplified writeOutgoingMessages() and removed the call to internalProcessResponses(). Likely bugs in writeClientBoundMessage(). 2014-12-10 16:31:22 -08:00
wcrisman
c36467d172 Switched to using HttpMessageBuffer in the code and moved the code from prepareResponse into initialize. 2014-12-10 14:37:55 -08:00
15 changed files with 3601 additions and 3841 deletions

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.eclipse.ant.AntLaunchConfigurationType"> <launchConfiguration type="org.eclipse.ant.AntLaunchConfigurationType">
<stringAttribute key="org.eclipse.ant.ui.ATTR_BUILD_SCOPE" value="${none}"/> <stringAttribute key="org.eclipse.ant.ui.ATTR_BUILD_SCOPE" value="${none}"/>
<booleanAttribute key="org.eclipse.ant.ui.DEFAULT_VM_INSTALL" value="false"/> <booleanAttribute key="org.eclipse.ant.ui.DEFAULT_VM_INSTALL" value="true"/>
<booleanAttribute key="org.eclipse.ant.uiSET_INPUTHANDLER" value="false"/> <booleanAttribute key="org.eclipse.ant.uiSET_INPUTHANDLER" value="false"/>
<stringAttribute key="org.eclipse.debug.core.ATTR_REFRESH_SCOPE" value="${workspace}"/> <stringAttribute key="org.eclipse.debug.core.ATTR_REFRESH_SCOPE" value="${workspace}"/>
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS"> <listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
@@ -15,6 +15,8 @@
<listEntry value="org.eclipse.ui.externaltools.launchGroup"/> <listEntry value="org.eclipse.ui.externaltools.launchGroup"/>
</listAttribute> </listAttribute>
<stringAttribute key="org.eclipse.jdt.launching.CLASSPATH_PROVIDER" value="org.eclipse.ant.ui.AntClasspathProvider"/> <stringAttribute key="org.eclipse.jdt.launching.CLASSPATH_PROVIDER" value="org.eclipse.ant.ui.AntClasspathProvider"/>
<stringAttribute key="org.eclipse.jdt.launching.JRE_CONTAINER" value="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/jdk1.7"/>
<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="org.eclipse.ant.internal.launching.remote.InternalAntRunner"/>
<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="Foundation Builder"/> <stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="Foundation Builder"/>
<stringAttribute key="org.eclipse.jdt.launching.SOURCE_PATH_PROVIDER" value="org.eclipse.ant.ui.AntClasspathProvider"/> <stringAttribute key="org.eclipse.jdt.launching.SOURCE_PATH_PROVIDER" value="org.eclipse.ant.ui.AntClasspathProvider"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_LOCATION" value="${workspace_loc:\Foundation Builder\builder\build.xml}"/> <stringAttribute key="org.eclipse.ui.externaltools.ATTR_LOCATION" value="${workspace_loc:\Foundation Builder\builder\build.xml}"/>

View File

@@ -113,7 +113,7 @@
<target name="release-compile" depends="make-dirs"> <target name="release-compile" depends="make-dirs">
<mkdir dir="${archives}/release-bin"/> <mkdir dir="${archives}/release-bin"/>
<javac destdir="${archives}/release-bin" executable="c:/java/jdk1.5/bin/javac" encoding="utf-8" nowarn="true" verbose="no" fork="true" classpath="${swt-releases}/win32-win32-x86/swt.jar;${orb-exception-support};${workspace}/Foundation TCV SWT Client Application/proxies;"> <javac destdir="${archives}/release-bin" executable="c:/java/jdk1.7/bin/javac" encoding="utf-8" nowarn="true" verbose="no" fork="true" classpath="${swt-releases}/win32-win32-x86/swt.jar;${orb-exception-support};${workspace}/Foundation TCV SWT Client Application/proxies;">
<src path="${workspace}/Common/src"/> <src path="${workspace}/Common/src"/>
<src path="${workspace}/Class File Services/src"/> <src path="${workspace}/Class File Services/src"/>
<src path="${workspace}/Orb/src"/> <src path="${workspace}/Orb/src"/>

View File

@@ -4,11 +4,11 @@
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
<classpathentry combineaccessrules="false" kind="src" path="/Common"/> <classpathentry combineaccessrules="false" kind="src" path="/Common"/>
<classpathentry combineaccessrules="false" kind="src" path="/Foundation"/> <classpathentry combineaccessrules="false" kind="src" path="/Foundation"/>
<classpathentry combineaccessrules="false" kind="src" path="/Foundation Web Interfaces"/>
<classpathentry combineaccessrules="false" kind="src" path="/Foundation Builder"/> <classpathentry combineaccessrules="false" kind="src" path="/Foundation Builder"/>
<classpathentry combineaccessrules="false" kind="src" path="/Foundation SWT"/> <classpathentry combineaccessrules="false" kind="src" path="/Foundation SWT"/>
<classpathentry combineaccessrules="false" kind="src" path="/SWT"/> <classpathentry combineaccessrules="false" kind="src" path="/SWT"/>
<classpathentry combineaccessrules="false" kind="src" path="/Orb"/> <classpathentry combineaccessrules="false" kind="src" path="/Orb"/>
<classpathentry combineaccessrules="false" kind="src" path="/Foundation Web Server Shared"/> <classpathentry combineaccessrules="false" kind="src" path="/Foundation Web Server Shared"/>
<classpathentry combineaccessrules="false" kind="src" path="/Foundation Web Interfaces"/>
<classpathentry kind="output" path="bin"/> <classpathentry kind="output" path="bin"/>
</classpath> </classpath>

View File

@@ -0,0 +1,149 @@
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. */
private final int id;
/** 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 <code>key</code> 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.
* <p>This uses a thread local because the date format class is not thread safe :(.</p>
* @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 selection key used for this socket context by the Listener to identify when bytes are incoming or available for outgoing on the socket. */
public SelectionKey getKey() {return key;}
/**
* 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 void passThrough(ByteBuffer buffer);
/**
* Closes the socket context and cleans up.
*/
protected abstract void close();
/**
* Determines whether the socket context has been closed.
* Should synchronize on getLock() prior to calling this.
* @return Whether the socket context is closed.
*/
protected boolean isClosed() {
return !(key != null && key.channel().isOpen());
}//isClosed()//
/**
* 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()//
/**
* 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 Object getLock() {
return key;
}//getLock()//
/**
* Gets the related socket's selection key. This will be the key not returned by a call to getLock(), or will be null if there isn't a related socket context.
* Allows for PassThroughSocketContext which links to a SocketContext to pass all data through to a remote process. This will always return the PassThroughSocketContext's selection key.
* @return The selection key for the PassThroughSocketContext if one exists for this SocketContext (or if this is a PassThroughSocketContext).
*/
public SelectionKey getRelatedSocketContextKey() {
return null;
}//getRelatedSocketContextKey()//
/** Gets the debug id for the socket. */
public int getId() {return id;}
}//AbstractSocketContext//

View File

@@ -0,0 +1,369 @@
package com.foundation.web.server;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.text.SimpleDateFormat;
import java.util.Date;
import com.common.comparison.Comparator;
import com.common.debug.Debug;
import com.common.util.LiteHashMap;
import com.common.util.LiteList;
import com.foundation.web.interfaces.IContent;
import com.foundation.web.interfaces.IMimeType;
import com.foundation.web.interfaces.ISession;
public class HttpMessageBuffer extends MessageBuffer {
/** The optional response the message is based upon. */
private Response response = null;
/** The content if there is any. */
private IContent content = null;
/**
* HttpMessageBuffer constructor.
* @param socketContext The socket context associated with this message buffer.
* @param response The HTTP response object.
*/
public HttpMessageBuffer(AbstractSocketContext socketContext, Response response) {
super(socketContext);
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() {
if(!getIsInitialized()) {
super.initialize();
prepareResponse(response);
}//if//
return true;
}//initialize()//
private void writeSessionCookies(Response response, 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()//
/**
* Processes the next response in the sequence.
* <p>Note: The caller must synchronize on this context to prevent multiple threads from accessing the context at the same time.</p>
* @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) {
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(response, 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(response, 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(response, 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: " + getSocketContext().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 = getSocketContext().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(response, 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 = getSocketContext().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//
//Save the buffer as the current pending outbound message for this socket context.//
setBuffer(buffer);
if(content != null && request.getRequestType() != Request.TYPE_HEAD) {
this.content = content;
}//if//
}//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 {getSocketContext().close();} catch(Throwable e2) {}
//Clean up after the request and response.//
try {response.close();} catch(Throwable e2) {}
}//catch//
}//prepareResponse()//
/**
* Closes the message buffer.
*/
public void close() {
super.close();
if(response != null) {
response.close();
response = null;
}//if//
}//close()//
/**
* 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;
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) {
close();
}//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.//
close();
result = false;
}//else if//
}//if//
else {
result = false;
}//else//
return result;
}//loadBuffer()//
}//HttpMessageBuffer//

View File

@@ -0,0 +1,7 @@
package com.foundation.web.server;
/**
* Provides a place for channel oriented data.
*/
public interface IChannelContext {
}//IChannelContext//

View File

@@ -0,0 +1,100 @@
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 socket context that this buffer exists within. */
private AbstractSocketContext socketContext;
/** 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;
/** Flag indicating if initialization is required. */
private boolean isInitialized = false;
/**
* MessageBuffer constructor.
* @param socketContext The socket context associated with this message buffer.
*/
protected MessageBuffer(AbstractSocketContext socketContext) {
this.socketContext = socketContext;
}//MessageBuffer()//
/**
* MessageBuffer constructor.
* @param socketContext The socket context associated with this message buffer.
* @param buffer The buffer to use for assembling the message bytes.
*/
public MessageBuffer(AbstractSocketContext socketContext, ByteBuffer buffer) {
this.socketContext = socketContext;
setBuffer(buffer);
}//MessageBuffer()//
/** Gets the web server that this message buffer exists within. */
protected WebServer getWebServer() {return socketContext.getWebServer();}
/** Gets the socket context that this message buffer exists within. */
protected AbstractSocketContext getSocketContext() {return socketContext;}
/** Gets the whether the buffer has been initialized yet. */
protected boolean getIsInitialized() {return isInitialized;}
/**
* Sets the actual underlying buffer for the message buffer.
* @param buffer
*/
protected void setBuffer(ByteBuffer buffer) {
this.buffer = buffer;
//Flip the buffer (if not already flipped) so we can write out the bytes.//
if(buffer != null && buffer.position() != 0) buffer.flip();
}//setBuffer()//
/**
* Initializes the message buffer for use. Subclasses may implement but must call this method to set the isInitialized flag.
* @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() {
isInitialized = true;
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 isInitialized && buffer == null;
}//isClosed()//
/**
* Closes the message buffer.
*/
public void close() {
buffer = null;
isInitialized = true;
}//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 content into the buffer, compacts the remaining bytes in the buffer (if any), and preps the buffer for reading. Will close this MessageBuffer if there are no remaining bytes and no content to fill with.
* @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 (if the MessageBuffer is not closed). Will always be false if there is no content to load from, though calling again is okay since it will close the MessageBuffer if the buffer is empty and there is no content.
*/
public boolean loadBuffer() {
if(buffer != null) {
if(!buffer.hasRemaining()) {
//Clear the buffer pointer indicating the message buffer is done.//
close();
}//if//
else {
buffer.compact();
}//else//
}//if//
return false;
}//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;}
}//MessageBuffer//

View File

@@ -0,0 +1,260 @@
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.
* <p>Note that the HTTP protocol requires that the reponses be sent in order of the received requests.</p>
*/
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(getWebServer().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);
if(getWebServer().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(getWebServer().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! {" + context.toString() + "}"));
}//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//

View File

@@ -0,0 +1,258 @@
package com.foundation.web.server;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import javax.net.ssl.SSLException;
import com.common.debug.Debug;
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 currentOutboundMessage = null;
private MessageBuffer lastOutboundMessage = 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();
}//getLock()//
/* (non-Javadoc)
* @see com.foundation.web.server.WebServer.AbstractSocketContext#getRelatedSocketContextKey()
*/
public SelectionKey getRelatedSocketContextKey() {
return key;
}//getRelatedSocketContextKey()//
/** Gets whether the socket context is in use (reading or writing) such that the network listener doesn't attempt to read and write at the same time (not supported by Java's NIO sockets). Should only ever be used by the NetworkListener. */
public boolean isUsed() {return getRelatedSocketContext().getIsUsed();}
/** Sets whether the socket context is in use (reading or writing) such that the network listener doesn't attempt to read and write at the same time (not supported by Java's NIO sockets). Should only ever be used by the NetworkListener. */
public void isUsed(boolean isUsed) {getRelatedSocketContext().setIsUsed(isUsed);}
/* (non-Javadoc)
* @see com.foundation.web.server.WebServer.AbstractSocketContext#writeOutgoingMessages()
*/
protected void writeOutgoingMessages() throws IOException {
boolean keepSending;
MessageBuffer outboundMessage;
SocketChannel channel;
//Synchronized to avoid multiple threads accessing the currentOutboundMessage chain at one time.//
synchronized(getLock()) {
outboundMessage = currentOutboundMessage;
channel = (SocketChannel) key.channel();
keepSending = hasPendingWrite() && channel.isOpen();
}//synchronized//
//Keep sending responses while the buffers are not full and there is another response to send.//
while(keepSending) {
//Send the pending response object's prepared buffer of data.//
writeClientBoundMessage(channel, outboundMessage);
//Synchronized to avoid multiple threads accessing the currentOutboundMessage chain at one time.//
synchronized(getLock()) {
//If we finished the message then load the next message, otherwise flag that we need to stop sending (buffers full - flag a write on the socket's key and wait).//
if(currentOutboundMessage != null && currentOutboundMessage.isClosed()) {
currentOutboundMessage = currentOutboundMessage.getNext();
keepSending = hasPendingWrite() && key.channel().isOpen();
outboundMessage = currentOutboundMessage;
}//if//
else {
keepSending = false;
}//else//
}//synchronized//
}//while//
}//writeOutgoingMessages()//
/* (non-Javadoc)
* @see com.foundation.web.server.AbstractSocketContext#readIncomingMessages()
*/
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;
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 && 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()) {
relatedSocketContext.passThrough(socketReadBuffer);
socketReadBuffer.compact();
}//else//
else {
socketReadBuffer.compact();
break;
}//else//
}//while//
}//readIncomingMessages()//
/**
* Sends a message/response to the client.
* @param channel The channel to use for the sending.
* @param currentOutboundMessage The message to be sending. This may be null if using SSL and sending handshake data (in which case the encryptedWriteBuffer should have data on it).
* @return Whether the response could be fully sent. This will be false if there is still more data to be written for the current message OR in the encrypted SSL write buffer (for SSL handshake messages) when the call returns.
*/
private boolean writeClientBoundMessage(SocketChannel channel, MessageBuffer currentOutboundMessage) {
boolean sendMore = true;
// if(debug) {
// debugBuffer.append("Starting a write cycle.\n");
// }//if//
try {
//If we can send more data and we have an outbound message then initialize and send it! The currentOutboundMessage might be null if performing an SSL handshake.//
if(sendMore && currentOutboundMessage != null && !currentOutboundMessage.isClosed()) {
//Initialize the outbound message.//
if(!currentOutboundMessage.initialize()) {
getRelatedSocketContext().close();
}//if//
else {
currentOutboundMessage.loadBuffer();
//If we have an application response pending then send it now.//
if(sendMore && !currentOutboundMessage.isClosed()) {
//Keep sending encrypted frames until the output buffer is full, or we run out of message to send.//
while(sendMore && !currentOutboundMessage.isClosed()) {
//Write the bytes to the stream.//
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 {
sendMore = currentOutboundMessage.loadBuffer();
}//else//
}//while//
}//if//
}//else//
}//if//
}//try//
catch(ClosedChannelException e) {
getRelatedSocketContext().close();
}//catch//
catch(SSLException e) {
if(getWebServer().debug()) {
Debug.log(e);
}//if//
close();
}//catch//
catch(IOException e) {
if(getWebServer().debug()) {
Debug.log(e);
}//if//
getRelatedSocketContext().close();
}//catch//
//Return true if the current outbound message was fully sent and any SSL handshake messages were fully sent.//
return (currentOutboundMessage == null || currentOutboundMessage.isClosed());
}//writeClientBoundMessages()//
/* (non-Javadoc)
* @see com.foundation.web.server.WebServer.AbstractSocketContext#passThrough(java.nio.ByteBuffer)
*/
protected void passThrough(ByteBuffer buffer) {
if(buffer != null) {
boolean notify = false;
synchronized(getLock()) {
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(this, messageBytes);
//Chain the message into the linked list.
if(lastOutboundMessage == null) {
currentOutboundMessage = lastOutboundMessage = message;
notify = true;
}//if//
else {
lastOutboundMessage.setNext(message);
lastOutboundMessage = message;
}//else//
}//synchronized//
if(notify) {
notifyListenerOfPendingWrite();
}//if//
}//if//
}//passThrough()//
protected synchronized void close() {
synchronized(getLock()) {
if(key != null) {
synchronized(key) {
try {if(key.channel() != null) key.channel().close();} catch(Throwable e) {}
try {key.cancel();} catch(Throwable e) {}
}//synchronized//
}//if//
}//synchronized//
}//close()//
/* (non-Javadoc)
* @see com.foundation.web.server.WebServer.AbstractSocketContext#hasPendingWrite()
*/
protected boolean hasPendingWrite() {
return currentOutboundMessage != null;
}//hasPendingWrite()//
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
public String toString() {
return "PassThroughSocketContext: " + (relatedSocketContext != null ? relatedSocketContext.toString() : "null");
}//toString()//
}//PassThroughSocketContext//

View File

@@ -475,7 +475,7 @@ public class Request implements IRequest {
else if(fieldName.equals("if-modified-since")) { else if(fieldName.equals("if-modified-since")) {
if(fieldValue.trim().length() > 0) { //Mozilla sometimes sends an empty value. Bad Mozilla.// if(fieldValue.trim().length() > 0) { //Mozilla sometimes sends an empty value. Bad Mozilla.//
try { try {
cacheDate = WebServer.getHttpDateFormat().parse(fieldValue.trim()); cacheDate = SocketContext.getHttpDateFormat().parse(fieldValue.trim());
}//try// }//try//
catch(Throwable e) { 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); 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")) { else if(fieldName.equals("if-unmodified-since")) {
if(fieldValue.trim().length() > 0) { //Shouldn't ever happen.// if(fieldValue.trim().length() > 0) { //Shouldn't ever happen.//
try { try {
unmodifiedSince = WebServer.getHttpDateFormat().parse(fieldValue.trim()); unmodifiedSince = SocketContext.getHttpDateFormat().parse(fieldValue.trim());
}//try// }//try//
catch(Throwable e) { 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); 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);

View File

@@ -0,0 +1,18 @@
package com.foundation.web.server;
import com.foundation.web.server.WebServer.ServiceListener;
/**
* Provides a place connection oriented data.
*/
public class ServerSocketContext implements IChannelContext {
private ServiceListener serviceListener = null;
public ServerSocketContext(ServiceListener serviceListener) {
super();
this.serviceListener = serviceListener;
}//ServerSocketContext()//
public ServiceListener getServiceListener() {
return serviceListener;
}//getServiceListener()//
}//ServerSocketContext//

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,165 @@
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;
/**
* WebsocketMessageBuffer constructor.
* @param socketContext The socket context associated with this message buffer.
* @param message The message to be sent.
*/
public WebsocketMessageBuffer(AbstractSocketContext socketContext, Object message) {
super(socketContext);
this.message = message;
}//WebsocketMessageBuffer()//
/* (non-Javadoc)
* @see com.foundation.web.server.MessageBuffer#initialize()
*/
public boolean initialize() {
if(getIsInitialized()) {
super.initialize();
messagePart = stream(message, true);
message = null;
}//if//
return true;
}//initialize()//
/* (non-Javadoc)
* @see com.foundation.web.server.MessageBuffer#isClosed()
*/
public boolean isClosed() {
return super.isClosed() && messagePart == null && streamingMessage == null;
}//isClosed()//
/* (non-Javadoc)
* @see com.foundation.web.server.MessageBuffer#close()
*/
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//

View File

@@ -9,7 +9,7 @@
<classpathentry combineaccessrules="false" kind="src" path="/SWT"/> <classpathentry combineaccessrules="false" kind="src" path="/SWT"/>
<classpathentry combineaccessrules="false" kind="src" path="/Foundation Web Server Monitor Shared"/> <classpathentry combineaccessrules="false" kind="src" path="/Foundation Web Server Monitor Shared"/>
<classpathentry combineaccessrules="false" kind="src" path="/Foundation Web Interfaces"/> <classpathentry combineaccessrules="false" kind="src" path="/Foundation Web Interfaces"/>
<classpathentry combineaccessrules="false" kind="src" path="/Foundation Web Server Shared"/>
<classpathentry combineaccessrules="false" kind="src" path="/Class File Services"/> <classpathentry combineaccessrules="false" kind="src" path="/Class File Services"/>
<classpathentry combineaccessrules="false" kind="src" path="/Foundation Web Server Shared"/>
<classpathentry kind="output" path="bin"/> <classpathentry kind="output" path="bin"/>
</classpath> </classpath>