diff options
Diffstat (limited to 'simple/simple-http/src/main/java/org/simpleframework/http/socket')
39 files changed, 4916 insertions, 0 deletions
diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/BinaryData.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/BinaryData.java new file mode 100644 index 0000000..bea3c63 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/BinaryData.java @@ -0,0 +1,75 @@ +/* + * BinaryData.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket; + +/** + * The <code>BinaryData</code> object represents a binary payload for + * a WebScoket frame. This can be used to send any type of data. If + * however it is used to send text data then it is decoded as UTF-8. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.DataFrame + */ +public class BinaryData implements Data { + + /** + * This is used to convert the binary payload to text. + */ + private final DataConverter converter; + + /** + * This is the byte array that represents the binary payload. + */ + private final byte[] data; + + /** + * Constructor for the <code>BinaryData</code> object. It requires + * an array of binary data that will be send within a frame. + * + * @param data the byte array representing the frame payload + */ + public BinaryData(byte[] data) { + this.converter = new DataConverter(); + this.data = data; + } + + /** + * This returns the binary payload that is to be sent with a frame. + * It contains no headers or other meta data. If the original data + * was text this converts it to UTF-8. + * + * @return the binary payload to be sent with the frame + */ + public byte[] getBinary() { + return data; + } + + /** + * This returns the text payload that is to be sent with a frame. + * It contains no header information or meta data. Caution should + * be used with this method as binary payloads will encode to + * garbage when decoded as UTF-8. + * + * @return the text payload to be sent with the frame + */ + public String getText() { + return converter.convert(data); + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/CloseCode.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/CloseCode.java new file mode 100644 index 0000000..c64c605 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/CloseCode.java @@ -0,0 +1,150 @@ +/* + * CloseCode.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket; + +/** + * The <code>CloseCode</code> enumerates the closure codes specified in + * RFC 6455. When closing an established connection an endpoint may + * indicate a reason for closure. The interpretation of this reason by + * an endpoint, and the action an endpoint should take given this reason, + * are left undefined by RFC 6455. The specification defines a set of + * status codes and specifies which ranges may be used by extensions, + * frameworks, and end applications. The status code and any associated + * textual message are optional components of a Close frame. + * + * @author niall.gallagher + */ +public enum CloseCode { + + /** + * Indicates the purpose for the connection has been fulfilled. + */ + NORMAL_CLOSURE(1000), + + /** + * Indicates that the server is going down or the client browsed away. + */ + GOING_AWAY(1001), + + /** + * Indicates the connection is terminating due to a protocol error. + */ + PROTOCOL_ERROR(1002), + + /** + * Indicates the connection received a data type it cannot accept. + */ + UNSUPPORTED_DATA(1003), + + /** + * According to RFC 6455 this has been reserved for future use. + */ + RESERVED(1004), + + /** + * Indicates that no status code was present and should not be used. + */ + NO_STATUS_CODE(1005), + + /** + * Indicates an abnormal closure and should not be used. + */ + ABNORMAL_CLOSURE(1006), + + /** + * Indicates that a payload was not consistent with the message type. + */ + INVALID_FRAME_DATA(1007), + + /** + * Indicates an endpoint received a message that violates its policy. + */ + POLICY_VIOLATION(1008), + + /** + * Indicates that a payload is too big to be processed. + */ + TOO_BIG(1009), + + /** + * Indicates that the server did not negotiate an extension properly. + */ + NO_EXTENSION(1010), + + /** + * Indicates an unexpected error within the server. + */ + INTERNAL_SERVER_ERROR(1011), + + /** + * Indicates a validation failure for TLS and should not be used. + */ + TLS_HANDSHAKE_FAILURE(1015); + + /** + * This is the actual integer value representing the code. + */ + public final int code; + + /** + * This is the high order byte for the closure code. + */ + public final int high; + + /** + * This is the low order byte for the closure code. + */ + public final int low; + + /** + * Constructor for the <code>CloseCode</code> object. This is used + * to create a closure code using one of the pre-defined values + * within RFC 6455. + * + * @param code this is the code that is to be used + */ + private CloseCode(int code) { + this.high = code & 0x0f; + this.low = code & 0xf0; + this.code = code; + } + + /** + * This is the data that represents the closure code. The array + * contains the high order byte and the low order byte as taken + * from the pre-defined closure code. + * + * @return a byte array representing the closure code + */ + public byte[] getData() { + return new byte[] { (byte)high, (byte)low }; + } + + + public static CloseCode resolveCode(int high, int low) { + for(CloseCode code : values()) { + if(code.high == high) { + if(code.low == low) { + return code; + } + } + } + return NO_STATUS_CODE; + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/Data.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/Data.java new file mode 100644 index 0000000..bb79830 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/Data.java @@ -0,0 +1,51 @@ +/* + * Data.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket; + +/** + * The <code>Data</code> interface represents a payload for a WebScoket + * frame. It can hold either binary data or text data. For performance + * binary frames are a better choice as all text frames need to be + * encoded as UTF-8 from the native UCS2 format. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.DataFrame + */ +public interface Data { + + /** + * This returns the binary payload that is to be sent with a frame. + * It contains no headers or other meta data. If the original data + * was text this converts it to UTF-8. + * + * @return the binary payload to be sent with the frame + */ + byte[] getBinary(); + + /** + * This returns the text payload that is to be sent with a frame. + * It contains no header information or meta data. Caution should + * be used with this method as binary payloads will encode to + * garbage when decoded as UTF-8. + * + * @return the text payload to be sent with the frame + */ + String getText(); +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/DataConverter.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/DataConverter.java new file mode 100644 index 0000000..5713fd6 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/DataConverter.java @@ -0,0 +1,111 @@ +/* + * DataConverter.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket; + +/** + * The <code>DataConverter</code> object is used to convert binary data + * to text data and vice versa. According to RFC 6455 a particular text + * frame might include a partial UTF-8 sequence; however, the whole + * message MUST contain valid UTF-8. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.DataFrame + */ +public class DataConverter { + + /** + * This is the character encoding used to convert the text data. + */ + private final String charset; + + /** + * Constructor for the <code>DataConverter</code> object. By default + * this uses UTF-8 character encoding to convert text data as this + * is what is required for RFC 6455 section 5.6. + */ + public DataConverter() { + this("UTF-8"); + } + + /** + * Constructor for the <code>DataConverter</code> object. This can be + * used to specific a character encoding other than UTF-8. However it + * is not recommended as RFC 6455 section 5.6 suggests the frame must + * contain valid UTF-8 data. + * + * @param charset the character encoding to be used + */ + public DataConverter(String charset) { + this.charset = charset; + } + + /** + * This method is used to convert text using the character encoding + * specified when constructing the converter. Typically this will use + * UTF-8 as required by RFC 6455. + * + * @param text this is the string to convert to a byte array + * + * @return a byte array decoded using the specified encoding + */ + public byte[] convert(String text) { + try { + return text.getBytes(charset); + } catch(Exception e) { + throw new IllegalStateException("Could not encode text as " + charset, e); + } + } + + /** + * This method is used to convert data using the character encoding + * specified when constructing the converter. Typically this will use + * UTF-8 as required by RFC 6455. + * + * @param text this is the byte array to convert to a string + * + * @return a string encoded using the specified encoding + */ + public String convert(byte[] binary) { + try { + return new String(binary, charset); + } catch(Exception e) { + throw new IllegalStateException("Could not decode data as " + charset, e); + } + } + + /** + * This method is used to convert data using the character encoding + * specified when constructing the converter. Typically this will use + * UTF-8 as required by RFC 6455. + * + * @param text this is the byte array to convert to a string + * @param offset the is the offset to read the bytes from + * @param size this is the number of bytes to be used + * + * @return a string encoded using the specified encoding + */ + public String convert(byte[] binary, int offset, int size) { + try { + return new String(binary, offset, size, charset); + } catch(Exception e) { + throw new IllegalStateException("Could not decode data as " + charset, e); + } + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/DataFrame.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/DataFrame.java new file mode 100644 index 0000000..b51cd2b --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/DataFrame.java @@ -0,0 +1,212 @@ +/* + * DataFrame.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket; + +/** + * The <code>DataFrame</code> object represents a frame as defined in + * RFC 6455. A frame is a very lightweight envelope used to send + * control information and either text or binary user data. Typically + * a frame will represent a single message however, it is possible + * to fragment a single frame up in to several frames. A fragmented + * frame has a specific <code>FrameType</code> indicating that it + * is a continuation frame. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.Data + */ +public class DataFrame implements Frame { + + /** + * This is the type used to determine the intent of the frame. + */ + private final FrameType type; + + /** + * This contains the payload to be sent with the frame. + */ + private final Data data; + + /** + * This determines if the frame is the last of a sequence. + */ + private final boolean last; + + /** + * Constructor for the <code>DataFrame</code> object. This is used + * to create a frame using the specified data and frame type. A + * zero payload is created using this constructor and is suitable + * only for specific control frames such as connection termination. + * + * @param type this is the frame type used for this instance + */ + public DataFrame(FrameType type) { + this(type, new byte[0]); + } + + /** + * Constructor for the <code>DataFrame</code> object. This is used + * to create a frame using the specified data and frame type. In + * some cases a control frame may require a zero length payload. + * + * @param type this is the frame type used for this instance + * @param data this is the payload for this frame + */ + public DataFrame(FrameType type, byte[] data) { + this(type, data, true); + } + + /** + * Constructor for the <code>DataFrame</code> object. This is used + * to create a frame using the specified data and frame type. In + * some cases a control frame may require a zero length payload. + * + * @param type this is the frame type used for this instance + * @param data this is the payload for this frame + * @param last true if this is not a fragment in a sequence + */ + public DataFrame(FrameType type, byte[] data, boolean last) { + this(type, new BinaryData(data), last); + } + + /** + * Constructor for the <code>DataFrame</code> object. This is used + * to create a frame using the specified data and frame type. In + * some cases a control frame may require a zero length payload. + * + * @param type this is the frame type used for this instance + * @param data this is the payload for this frame + */ + public DataFrame(FrameType type, String text) { + this(type, text, true); + } + + /** + * Constructor for the <code>DataFrame</code> object. This is used + * to create a frame using the specified data and frame type. In + * some cases a control frame may require a zero length payload. + * + * @param type this is the frame type used for this instance + * @param data this is the payload for this frame + * @param last true if this is not a fragment in a sequence + */ + public DataFrame(FrameType type, String text, boolean last) { + this(type, new TextData(text), last); + } + + /** + * Constructor for the <code>DataFrame</code> object. This is used + * to create a frame using the specified data and frame type. In + * some cases a control frame may require a zero length payload. + * + * @param type this is the frame type used for this instance + * @param data this is the payload for this frame + */ + public DataFrame(FrameType type, Data data) { + this(type, data, true); + } + + /** + * Constructor for the <code>DataFrame</code> object. This is used + * to create a frame using the specified data and frame type. In + * some cases a control frame may require a zero length payload. + * + * @param type this is the frame type used for this instance + * @param data this is the payload for this frame + * @param last true if this is not a fragment in a sequence + */ + public DataFrame(FrameType type, Data data, boolean last) { + this.data = data; + this.type = type; + this.last = last; + } + + /** + * This is used to determine if the frame is the final frame in + * a sequence of fragments or a whole frame. If this returns false + * then the frame is a continuation from from a sequence of + * fragments, otherwise it is a whole frame or the last fragment. + * + * @return this returns false if the frame is a fragment + */ + public boolean isFinal() { + return last; + } + + /** + * This returns the binary payload that is to be sent with the frame. + * It contains no headers or other meta data. If the original data + * was text this converts it to UTF-8. + * + * @return the binary payload to be sent with the frame + */ + public byte[] getBinary() { + return data.getBinary(); + } + + /** + * This returns the text payload that is to be sent with the frame. + * It contains no header information or meta data. Caution should + * be used with this method as binary payloads will encode to + * garbage when decoded as UTF-8. + * + * @return the text payload to be sent with the frame + */ + public String getText(){ + return data.getText(); + } + + /** + * This method is used to convert from one frame type to another. + * Converting a frame type is useful in scenarios such as when a + * ping needs to respond to a pong or when it is more convenient + * to send a text frame as binary. + * + * @param type this is the frame type to convert to + * + * @return a new frame using the specified frame type + */ + public Frame getFrame(FrameType type) { + return new DataFrame(type, data, last); + } + + /** + * This is used to determine the type of frame. Interpretation of + * this type is outlined in RFC 6455 and can be loosely categorised + * as control frames and either data or binary frames. + * + * @return this returns the type of frame that this represents + */ + public FrameType getType(){ + return type; + } + + /** + * This returns the text payload that is to be sent with the frame. + * It contains no header information or meta data. Caution should + * be used with this method as binary payloads will encode to + * garbage when decoded as UTF-8. + * + * @return the text payload to be sent with the frame + */ + @Override + public String toString() { + return getText(); + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/Frame.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/Frame.java new file mode 100644 index 0000000..7f5ad0f --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/Frame.java @@ -0,0 +1,85 @@ +/* + * Frame.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket; + +/** + * The <code>Frame</code> interface represents a frame as defined in + * RFC 6455. A frame is a very lightweight envelope used to send + * control information and either text or binary user data. Typically + * a frame will represent a single message however, it is possible + * to fragment a single frame up in to several frames. A fragmented + * frame has a specific <code>FrameType</code> indicating that it + * is a continuation frame. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.DataFrame + */ +public interface Frame { + + /** + * This is used to determine if the frame is the final frame in + * a sequence of fragments or a whole frame. If this returns false + * then the frame is a continuation from from a sequence of + * fragments, otherwise it is a whole frame or the last fragment. + * + * @return this returns false if the frame is a fragment + */ + boolean isFinal(); + + /** + * This returns the binary payload that is to be sent with the frame. + * It contains no headers or other meta data. If the original data + * was text this converts it to UTF-8. + * + * @return the binary payload to be sent with the frame + */ + byte[] getBinary(); + + /** + * This returns the text payload that is to be sent with the frame. + * It contains no header information or meta data. Caution should + * be used with this method as binary payloads will encode to + * garbage when decoded as UTF-8. + * + * @return the text payload to be sent with the frame + */ + String getText(); + + /** + * This method is used to convert from one frame type to another. + * Converting a frame type is useful in scenarios such as when a + * ping needs to respond to a pong or when it is more convenient + * to send a text frame as binary. + * + * @param type this is the frame type to convert to + * + * @return a new frame using the specified frame type + */ + Frame getFrame(FrameType type); + + /** + * This is used to determine the type of frame. Interpretation of + * this type is outlined in RFC 6455 and can be loosely categorised + * as control frames and either data or binary frames. + * + * @return this returns the type of frame that this represents + */ + FrameType getType(); +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/FrameChannel.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/FrameChannel.java new file mode 100644 index 0000000..bcacc43 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/FrameChannel.java @@ -0,0 +1,117 @@ +/* + * FrameChannel.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket; + +import java.io.IOException; + +/** + * The <code>FrameChannel</code> represents a full duplex communication + * channel as defined by RFC 6455. Any instance of this will provide + * a means to perform asynchronous writes and reads to a remote client + * using a lightweight framing protocol. A frame is a finite length + * sequence of bytes that can hold either text or binary data. Also, + * control frames are used to perform heartbeat monitoring and closure. + * <p> + * For convenience frames can be consumed from the socket via a + * callback to a registered listener. This avoids having to poll each + * socket for data and provides a asynchronous event driven model of + * communication, which greatly reduces overhead and complication. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.FrameListener + * @see org.simpleframework.http.socket.Frame + */ +public interface FrameChannel { + + /** + * This is used to send data to the connected client. To prevent + * an application code from causing resource issues this will block + * as soon as a configured linked list of mapped memory buffers has + * been exhausted. Caution should be taken when writing a broadcast + * implementation that can write to multiple sockets as a badly + * behaving socket that has filled its output buffering capacity + * can cause congestion. + * + * @param data this is the data that is to be sent + */ + void send(byte[] data) throws IOException; + + /** + * This is used to send text to the connected client. To prevent + * an application code from causing resource issues this will block + * as soon as a configured linked list of mapped memory buffers has + * been exhausted. Caution should be taken when writing a broadcast + * implementation that can write to multiple sockets as a badly + * behaving socket that has filled its output buffering capacity + * can cause congestion. + * + * @param text this is the text that is to be sent + */ + void send(String text) throws IOException; + + /** + * This is used to send data to the connected client. To prevent + * an application code from causing resource issues this will block + * as soon as a configured linked list of mapped memory buffers has + * been exhausted. Caution should be taken when writing a broadcast + * implementation that can write to multiple sockets as a badly + * behaving socket that has filled its output buffering capacity + * can cause congestion. + * + * @param frame this is the frame that is to be sent + */ + void send(Frame frame) throws IOException; + + /** + * This is used to register a <code>FrameListener</code> to this + * instance. The registered listener will receive all user frames + * and control frames sent from the client. Also, when the frame + * is closed or when an unexpected error occurs the listener is + * notified. Any number of listeners can be registered at any time. + * + * @param listener this is the listener that is to be registered + */ + void register(FrameListener listener) throws IOException; + + /** + * This is used to remove a <code>FrameListener</code> from this + * instance. After removal the listener will no longer receive + * any user frames or control messages from this specific instance. + * + * @param listener this is the listener to be removed + */ + void remove(FrameListener listener) throws IOException; + + /** + * This is used to close the connection with a specific reason. + * The close reason will be sent as a control frame before the + * TCP connection is terminated. + * + * @param reason the reason for closing the connection + */ + void close(Reason reason) throws IOException; + + /** + * This is used to close the connection without a specific reason. + * The close reason will be sent as a control frame before the + * TCP connection is terminated. + */ + void close() throws IOException; +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/FrameListener.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/FrameListener.java new file mode 100644 index 0000000..6892e9c --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/FrameListener.java @@ -0,0 +1,64 @@ +/* + * FrameListener.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket; + +/** + * The <code>FrameListener</code> is used to listen for incoming frames + * on a <code>WebSocket</code>. Any number of listeners can listen on + * a single web socket and it will receive all incoming events. For + * consistency this interface is modelled on the WebSocket API as + * defined by W3C Candidate Recommendation as of 20 September 2012. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.FrameChannel + */ +public interface FrameListener { + + /** + * This is called when a new frame arrives on the WebSocket. It + * will receive control frames as well as binary and text user + * frames. Control frames should not be acted on or responded + * to as they are provided for informational purposes only. + * + * @param session this is the associated session + * @param frame this is the frame that has been received + */ + void onFrame(Session session, Frame frame); + + /** + * This is called when an error occurs on the WebSocket. After + * an error the connection it is closed with an opcode indicating + * an internal server error. + * + * @param session this is the associated session + * @param frame this is the exception that has been thrown + */ + void onError(Session session, Exception cause); + + /** + * This is called when the connection is closed from the other + * side. Typically a frame with an opcode of close is sent + * before the close callback is issued. + * + * @param session this is the associated session + * @param reason this is the reason the connection was closed + */ + void onClose(Session session, Reason reason); +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/FrameType.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/FrameType.java new file mode 100644 index 0000000..8237701 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/FrameType.java @@ -0,0 +1,142 @@ +/* + * FrameType.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket; + +/** + * The <code>FrameType</code> represents the set of opcodes defined + * in RFC 6455. The base framing protocol uses a opcode to define the + * interpretation of the payload data for the frame. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.Frame + */ +public enum FrameType { + + /** + * A continuation frame identifies a fragment from a larger message. + */ + CONTINUATION(0x00), + + /** + * A text frame identifies a message that contains UTF-8 text data. + */ + TEXT(0x01), + + /** + * A binary frame identifies a message that contains binary data. + */ + BINARY(0x02), + + /** + * A close frame identifies a frame used to terminate a connection. + */ + CLOSE(0x08), + + /** + * A ping frame is a heartbeat used to determine connection health. + */ + PING(0x09), + + /** + * A pong frame is sent is sent in response to a ping frame. + */ + PONG(0x0a); + + /** + * This is the integer value for the opcode. + */ + public final int code; + + /** + * Constructor for the <code>Frame</code> type enumeration. This is + * given the opcode that is used to identify a specific frame type. + * + * @param code this is the opcode representing the frame type + */ + private FrameType(int code) { + this.code = code; + } + + /** + * This is used to determine if a frame is a text frame. It can be + * useful to know if a frame is a user based frame as it reduces + * the need to convert from or to certain character sets. + * + * @return this returns true if the frame represents a text frame + */ + public boolean isText() { + return this == TEXT; + } + + /** + * This is used to determine if a frame is a close frame. A close + * frame contains an optional payload, which if present contains + * an error code in network byte order in the first two bytes, + * followed by an optional UTF-8 text reason of the closure. + * + * @return this returns true if the frame represents a close frame + */ + public boolean isClose() { + return this == CLOSE; + } + + /** + * This is used to determine if a frame is a pong frame. A pong + * frame is sent in response to a ping and is used to determine if + * a WebSocket connection is still active and healthy. + * + * @return this returns true if the frame represents a pong frame + */ + public boolean isPong() { + return this == PONG; + } + + /** + * This is used to determine if a frame is a ping frame. A ping + * frame is sent to check if a WebSocket connection is still healthy. + * A connection is determined healthy if it responds with a pong + * frame is a reasonable length of time. + * + * @return this returns true if the frame represents a ping frame + */ + public boolean isPing() { + return this == PING; + } + + /** + * This is used to acquire the frame type given an opcode. If no + * frame type can be determined from the opcode provided then this + * will return a null value. + * + * @param octet this is the octet representing the opcode + * + * @return this returns the frame type from the opcode + */ + public static FrameType resolveType(int octet) { + int value = octet & 0xff; + + for(FrameType code : values()) { + if(code.code == value) { + return code; + } + } + return null; + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/Reason.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/Reason.java new file mode 100644 index 0000000..c7438e5 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/Reason.java @@ -0,0 +1,97 @@ +/* + * Reason.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket; + +/** + * The <code>Reason</code> object is used to hold a textual reason + * for connection closure and an RFC 6455 defined code. When a + * connection is to be closed a control frame with an opcode of + * close is sent with the text reason, if one is provided. + * + * @author Niall Gallagher + */ +public class Reason { + + /** + * This is the close code to be sent with a control frame. + */ + private final CloseCode code; + + /** + * This is the textual description of the close reason. + */ + private final String text; + + /** + * Constructor for the <code>Reason</code> object. This is used + * to create a reason and a textual description of that reason + * to be delivered as a control frame. + * + * @param code this is the code to be sent with the frame + */ + public Reason(CloseCode code) { + this(code, null); + } + + /** + * Constructor for the <code>Reason</code> object. This is used + * to create a reason and a textual description of that reason + * to be delivered as a control frame. + * + * @param code this is the code to be sent with the frame + * @param text this is textual description of the close reason + */ + public Reason(CloseCode code, String text) { + this.code = code; + this.text = text; + } + + /** + * This is used to get the RFC 6455 code describing the type + * of close event. It is the code that should be used by + * applications to determine why the connection was terminated. + * + * @return returns the close code for the connection + */ + public CloseCode getCode() { + return code; + } + + /** + * This is used to get the textual description for the closure. + * In many scenarios there will be no textual reason as it is + * an optional attribute. + * + * @return this returns the description for the closure + */ + public String getText() { + return text; + } + + /** + * This is used to provide a textual representation of the reason. + * For consistency this will only return the enumerated value for + * the close code, or if none exists a "null" text string. + * + * @return this returns a string representation of the reason + */ + public String toString() { + return String.valueOf(code); + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/Session.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/Session.java new file mode 100644 index 0000000..7c9a7db --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/Session.java @@ -0,0 +1,91 @@ +/* + * Session.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket; + +import java.util.Map; + +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; + +/** + * The <code>Session</code> object represents a simple WebSocket session + * that contains the connection handshake details and the actual socket. + * In order to determine how the session should be interacted with the + * protocol is conveniently exposed, however all attributes of the + * original HTTP request are available. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.FrameChannel + */ +public interface Session { + + /** + * This can be used to retrieve the response attributes. These can + * be used to keep state with the response when it is passed to + * other systems for processing. Attributes act as a convenient + * model for storing objects associated with the response. This + * also inherits attributes associated with the client connection. + * + * @return the attributes of that have been set on the request + */ + Map getAttributes(); + + /** + * This is used as a shortcut for acquiring attributes for the + * response. This avoids acquiring the attribute <code>Map</code> + * in order to retrieve the attribute directly from that object. + * The attributes contain data specific to the response. + * + * @param key this is the key of the attribute to acquire + * + * @return this returns the attribute for the specified name + */ + Object getAttribute(Object key); + + /** + * Provides a <code>FrameChannel</code> that can be used to communicate + * with the connected client. Communication is full duplex and also + * asynchronous through the use of a <code>FrameListener</code> that + * can be registered with the channel. + * + * @return a web socket for full duplex communication + */ + FrameChannel getChannel(); + + /** + * Provides the <code>Request</code> used to initiate the session. + * This is useful in establishing the identity of the user, acquiring + * an security information and also for determining the request path + * that was used, which be used to establish context. + * + * @return the request used to initiate the session + */ + Request getRequest(); + + /** + * Provides the <code>Response</code> used to establish the session + * with the remote client. This is useful in establishing the protocol + * used to create the session and also for determining various other + * useful contextual information. + * + * @return the response used to establish the session + */ + Response getResponse(); +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/TextData.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/TextData.java new file mode 100644 index 0000000..24ee97d --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/TextData.java @@ -0,0 +1,75 @@ +/* + * TextData.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket; + +/** + * The <code>TextData</code> object represents a text payload for + * a WebScoket frame. This can be used to send any type of data. If + * however it is used to send binary data then it is encoded as UTF-8. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.DataFrame + */ +public class TextData implements Data { + + /** + * This is used to convert the text payload to a byte array. + */ + private final DataConverter converter; + + /** + * This is the text string representing a frame payload. + */ + private final String data; + + /** + * Constructor for the <code>TextData</code> object. It requires + * an text string that will be sent as UTF-8 within a frame. + * + * @param data the text string representing the frame payload + */ + public TextData(String data) { + this.converter = new DataConverter(); + this.data = data; + } + + /** + * This returns the binary payload that is to be sent with a frame. + * It contains no headers or other meta data. If the original data + * was text this converts it to UTF-8. + * + * @return the binary payload to be sent with the frame + */ + public byte[] getBinary() { + return converter.convert(data); + } + + /** + * This returns the text payload that is to be sent with a frame. + * It contains no header information or meta data. Caution should + * be used with this method as binary payloads will encode to + * garbage when decoded as UTF-8. + * + * @return the text payload to be sent with the frame + */ + public String getText() { + return data; + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/AcceptToken.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/AcceptToken.java new file mode 100644 index 0000000..2fe2521 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/AcceptToken.java @@ -0,0 +1,127 @@ +/* + * AcceptToken.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import static org.simpleframework.http.Protocol.SEC_WEBSOCKET_KEY; + +import java.io.IOException; +import java.security.MessageDigest; + +import org.simpleframework.common.encode.Base64Encoder; +import org.simpleframework.http.Request; + +/** + * The <code>AcceptToken</code> is used to create a unique token based + * on a random key sent by the client. This is used to prove that the + * handshake was received, the server has to take two pieces of + * information and combine them to form a response. The first piece + * of information comes from the <code>Sec-WebSocket-Key</code> header + * field in the client handshake, the second is the globally unique + * identifier <code>258EAFA5-E914-47DA-95CA-C5AB0DC85B11</code>. Both + * are concatenated and an SHA-1 has is generated and used in the + * session initiating response. + * + * @author Niall Gallagher + */ +class AcceptToken { + + /** + * This is the globally unique identifier used in the handshake. + */ + private static final byte[] MAGIC = { + '2', '5', '8', 'E', 'A', 'F', 'A', '5', '-', + 'E', '9', '1', '4', '-', '4', '7', 'D', 'A', + '-', '9', '5', 'C', 'A', '-', 'C', '5', 'A', + 'B', '0', 'D', 'C', '8', '5', 'B', '1', '1' }; + + /** + * This is used to generate the SHA-1 has from the user key. + */ + private final MessageDigest digest; + + /** + * This is the original request used to initiate the session. + */ + private final Request request; + + /** + * This is the character encoding to decode the key with. + */ + private final String charset; + + /** + * Constructor for the <code>AcceptToken</code> object. This is + * to create an object that can generate a token from the client + * key available from the <code>Sec-WebSocket-Key</code> header. + * + * @param request this is the session initiating request + */ + public AcceptToken(Request request) throws Exception { + this(request, "SHA-1"); + } + + /** + * Constructor for the <code>AcceptToken</code> object. This is + * to create an object that can generate a token from the client + * key available from the <code>Sec-WebSocket-Key</code> header. + * + * @param request this is the session initiating request + * @param algorithm the algorithm used to create the token + */ + public AcceptToken(Request request, String algorithm) throws Exception { + this(request, algorithm, "UTF-8"); + } + + /** + * Constructor for the <code>AcceptToken</code> object. This is + * to create an object that can generate a token from the client + * key available from the <code>Sec-WebSocket-Key</code> header. + * + * @param request this is the session initiating request + * @param algorithm the algorithm used to create the token + * @param charset the encoding used to decode the client key + */ + public AcceptToken(Request request, String algorithm, String charset) throws Exception { + this.digest = MessageDigest.getInstance(algorithm); + this.request = request; + this.charset = charset; + } + + /** + * This is used to create the required accept token for the session + * initiating response. The resulting token is a SHA-1 digest of + * the <code>Sec-WebSocket-Key</code> a globally unique identifier + * defined in RFC 6455 all encoded in base64. + * + * @return the accept token for the session initiating response + */ + public String create() throws IOException { + String value = request.getValue(SEC_WEBSOCKET_KEY); + byte[] data = value.getBytes(charset); + + if (data.length > 0) { + digest.update(data); + digest.update(MAGIC); + } + byte[] digested = digest.digest(); + char[] text = Base64Encoder.encode(digested); + + return new String(text); + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/DirectRouter.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/DirectRouter.java new file mode 100644 index 0000000..0c09063 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/DirectRouter.java @@ -0,0 +1,107 @@ +/* + * DirectRouter.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import static org.simpleframework.http.Protocol.SEC_WEBSOCKET_PROTOCOL; +import static org.simpleframework.http.Protocol.SEC_WEBSOCKET_VERSION; +import static org.simpleframework.http.Protocol.UPGRADE; +import static org.simpleframework.http.Protocol.WEBSOCKET; + +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; + +/** + * The <code>DirectRouter</code> object is used to create a router + * that uses a single service. Typically this is used by simpler + * servers that wish to expose a single sub-protocol to clients. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.service.RouterContainer + */ +public class DirectRouter implements Router { + + /** + * The service used by this router instance. + */ + private final Service service; + + /** + * The protocol used or null if none was specified. + */ + private final String protocol; + + /** + * Constructor for the <code>DirectRouter</code> object. This + * is used to create an object that will select a single service. + * Creating an instance with this constructor means that the + * protocol header will not be set. + * + * @param service this is the service used by this instance + * @param protocol the protocol used by this router or null + */ + public DirectRouter(Service service) { + this(service, null); + } + + /** + * Constructor for the <code>DirectRouter</code> object. This + * is used to create an object that will select a single service. + * If the protocol specified is null then the response to the + * session initiation will contain null for the protocol header. + * + * @param service this is the service used by this instance + * @param protocol the protocol used by this router or null + */ + public DirectRouter(Service service, String protocol) { + this.protocol = protocol; + this.service = service; + } + + /** + * This is used to route an incoming request to a service if + * the request represents a WebSocket handshake as defined by + * RFC 6455. If the request is not a session initiating handshake + * then this will return a null value to allow it to be processed + * by some other part of the server. + * + * @param request this is the request to use for routing + * @param response this is the response to establish the session + * + * @return a service that can be used to process the session + */ + public Service route(Request request, Response response) { + String token = request.getValue(UPGRADE); + + if(token != null) { + if(token.equalsIgnoreCase(WEBSOCKET)) { + String version = request.getValue(SEC_WEBSOCKET_VERSION); + + if(version != null) { + response.setValue(SEC_WEBSOCKET_VERSION, version); + } + if(protocol != null) { + response.setValue(SEC_WEBSOCKET_PROTOCOL, protocol); + } + return service; + } + } + return null; + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameBuilder.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameBuilder.java new file mode 100644 index 0000000..6ab224a --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameBuilder.java @@ -0,0 +1,118 @@ +/* + * FrameBuilder.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import java.util.Arrays; + +import org.simpleframework.http.socket.DataConverter; +import org.simpleframework.http.socket.DataFrame; +import org.simpleframework.http.socket.Frame; +import org.simpleframework.http.socket.FrameType; + +/** + * The <code>FrameBuilder</code> object is used to create an object + * that interprets a frame header to produce frame objects. For + * efficiency this converts binary data to the native frame data + * type, which avoids memory churn. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.service.FrameConsumer + */ +class FrameBuilder { + + /** + * This converts binary data to a UTF-8 string for text frames. + */ + private final DataConverter converter; + + /** + * This is used to determine the type of frames to create. + */ + private final FrameHeader header; + + /** + * Constructor for the <code>FrameBuilder</code> object. This acts + * as a factory for frame objects by using the provided header to + * determine the frame type to be created. + * + * @param header the header used to determine the frame type + */ + public FrameBuilder(FrameHeader header) { + this.converter = new DataConverter(); + this.header = header; + } + + /** + * This is used to create a frame object to represent the data that + * has been consumed. The frame created will contain either a copy of + * the provided byte buffer or a text string encoded in UTF-8. To + * avoid memory churn this method should be used sparingly. + * + * @return this returns a frame created from the consumed bytes + */ + public Frame create(byte[] data, int count) { + FrameType type = header.getType(); + + if(type.isText()) { + return createText(data, count); + } + return createBinary(data, count); + } + + /** + * This is used to create a frame object from the provided data. + * The resulting frame will contain a UTF-8 encoding of the data + * to ensure that data conversion needs to be performed only once. + * + * @param data this is the data to convert to a new frame + * @param count this is the number of bytes in the frame + * + * @return a new frame containing the text + */ + private Frame createText(byte[] data, int count) { + FrameType type = header.getType(); + String text = converter.convert(data, 0, count); + + if(header.isFinal()) { + return new DataFrame(type, text, true); + } + return new DataFrame(type, text, false); + } + + /** + * This is used to create a frame object from the provided data. + * The resulting frame will contain a copy of the data to ensure + * that the frame is immutable. + * + * @param data this is the data to convert to a new frame + * @param count this is the number of bytes in the frame + * + * @return a new frame containing a copy of the provided data + */ + private Frame createBinary(byte[] data, int count) { + FrameType type = header.getType(); + byte[] copy = Arrays.copyOf(data, count); + + if(header.isFinal()) { + return new DataFrame(type, copy, true); + } + return new DataFrame(type, copy, false); + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameCollector.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameCollector.java new file mode 100644 index 0000000..8987620 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameCollector.java @@ -0,0 +1,179 @@ +/* + * FrameCollector.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import static org.simpleframework.http.socket.service.ServiceEvent.ERROR; + +import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; + +import org.simpleframework.http.Request; +import org.simpleframework.http.socket.FrameListener; +import org.simpleframework.http.socket.Session; +import org.simpleframework.transport.Channel; +import org.simpleframework.transport.ByteCursor; +import org.simpleframework.transport.reactor.Operation; +import org.simpleframework.transport.reactor.Reactor; +import org.simpleframework.transport.trace.Trace; + +/** + * The <code>FrameCollector</code> operation is used to collect frames + * from a channel and dispatch them to a <code>FrameListener</code>. + * To ensure that stale connections do not linger any connection that + * does not send a control ping or pong frame within two minutes will + * be terminated and the close control frame will be sent. + * + * @author Niall Gallagher + */ +class FrameCollector implements Operation { + + /** + * This decodes the frame bytes from the channel and processes it. + */ + private final FrameProcessor processor; + + /** + * This is the cursor used to maintain a stream seek position. + */ + private final ByteCursor cursor; + + /** + * This is the underlying channel for this frame collector. + */ + private final Channel channel; + + /** + * This is the reactor used to schedule this operation for reads. + */ + private final Reactor reactor; + + /** + * This is the tracer that is used to trace the frame collection. + */ + private final Trace trace; + + /** + * Constructor for the <code>FrameCollector</code> object. This is + * used to create a collector that will process and dispatch web + * socket frames as defined by RFC 6455. + * + * @param encoder this is the encoder used to send messages + * @param session this is the web socket session + * @param channel this is the underlying TCP communication channel + * @param reactor this is the reactor used for read notifications + */ + public FrameCollector(FrameEncoder encoder, Session session, Request request, Reactor reactor) { + this.processor = new FrameProcessor(encoder, session, request); + this.channel = request.getChannel(); + this.cursor = channel.getCursor(); + this.trace = channel.getTrace(); + this.reactor = reactor; + } + + /** + * This is used to acquire the trace object that is associated + * with the operation. A trace object is used to collection details + * on what operations are being performed. For instance it may + * contain information relating to I/O events or errors. + * + * @return this returns the trace associated with this operation + */ + public Trace getTrace() { + return trace; + } + + /** + * This is the channel associated with this collector. This is used + * to register for notification of read events. If at any time the + * remote endpoint is closed then this will cause the collector + * to perform a final execution before closing. + * + * @return this returns the selectable TCP channel + */ + public SelectableChannel getChannel() { + return channel.getSocket(); + } + + /** + * This is used to register a <code>FrameListener</code> to this + * instance. The registered listener will receive all user frames + * and control frames sent from the client. Also, when the frame + * is closed or when an unexpected error occurs the listener is + * notified. Any number of listeners can be registered at any time. + * + * @param listener this is the listener that is to be registered + */ + public void register(FrameListener listener) { + processor.register(listener); + } + + /** + * This is used to remove a <code>FrameListener</code> from this + * instance. After removal the listener will no longer receive + * any user frames or control messages from this specific instance. + * + * @param listener this is the listener to be removed + */ + public void remove(FrameListener listener) { + processor.remove(listener); + } + + /** + * This is used to execute the collection operation. Collection is + * done by reading the frame header from the incoming data, once + * consumed the remainder of the frame is collected until such + * time as it has been fully consumed. When consumed it will be + * dispatched to the registered frame listeners. + */ + public void run() { + try { + processor.process(); + + if(cursor.isOpen()) { + reactor.process(this, SelectionKey.OP_READ); + } else { + processor.close(); + } + } catch(Exception cause) { + trace.trace(ERROR, cause); + + try { + processor.failure(cause); + } catch(Exception fatal) { + trace.trace(ERROR, fatal); + } finally { + channel.close(); + } + } + } + + /** + * This is called when a read operation has timed out. To ensure + * that stale channels do not remain registered they are cleared + * out with this method and a close frame is sent if possible. + */ + public void cancel() { + try{ + processor.close(); + } catch(Exception cause) { + trace.trace(ERROR, cause); + channel.close(); + } + } +}
\ No newline at end of file diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameConnection.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameConnection.java new file mode 100644 index 0000000..b904130 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameConnection.java @@ -0,0 +1,214 @@ +/* + * FrameConnection.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import static org.simpleframework.http.socket.CloseCode.NORMAL_CLOSURE; +import static org.simpleframework.http.socket.service.ServiceEvent.OPEN_SOCKET; + +import java.io.IOException; + +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; +import org.simpleframework.http.socket.Frame; +import org.simpleframework.http.socket.FrameListener; +import org.simpleframework.http.socket.Reason; +import org.simpleframework.http.socket.Session; +import org.simpleframework.http.socket.FrameChannel; +import org.simpleframework.transport.Channel; +import org.simpleframework.transport.ByteWriter; +import org.simpleframework.transport.reactor.Reactor; +import org.simpleframework.transport.trace.Trace; + +/** + * The <code>FrameConnection</code> represents a connection that can + * send and receivd WebSocket frames. Any instance of this will provide + * a means to perform asynchronous writes and reads to a remote client + * using a lightweight framing protocol. A frame is a finite length + * sequence of bytes that can hold either text or binary data. Also, + * control frames are used to perform heartbeat monitoring and closure. + * <p> + * For convenience frames can be consumed from the socket via a + * callback to a registered listener. This avoids having to poll each + * socket for data and provides a asynchronous event driven model of + * communication, which greatly reduces overhead and complication. + * + * @author Niall Gallagher + */ +class FrameConnection implements FrameChannel { + + /** + * The collector is used to collect frames from the TCP channel. + */ + private final FrameCollector operation; + + /** + * This encoder is used to encode data as RFC 6455 frames. + */ + private final FrameEncoder encoder; + + /** + * This is the sender used to send frames over the channel. + */ + private final ByteWriter writer; + + /** + * This is the session object that has a synchronized channel. + */ + private final Session session; + + /** + * This is the underlying TCP channel that frames are sent over. + */ + private final Channel channel; + + /** + * The reason that is sent if at any time the channel is closed. + */ + private final Reason reason; + + /** + * This is used to trace all events that occur on the channel. + */ + private final Trace trace; + + /** + * Constructor for the <code>FrameConnection</code> object. This is used + * to create a channel that can read and write frames over a TCP + * channel. For asynchronous read and dispatch operations this will + * produce an operation to collect and process RFC 6455 frames. + * + * @param request this is the initiating request for the WebSocket + * @param response this is the initiating response for the WebSocket + * @param reactor this is the reactor used to process frames + */ + public FrameConnection(Request request, Response response, Reactor reactor) { + this.encoder = new FrameEncoder(request); + this.session = new ServiceSession(this, request, response); + this.operation = new FrameCollector(encoder, session, request, reactor); + this.reason = new Reason(NORMAL_CLOSURE); + this.channel = request.getChannel(); + this.writer = channel.getWriter(); + this.trace = channel.getTrace(); + } + + /** + * This is used to open the channel and begin consuming frames. This + * will also return the session that contains the details for the + * created WebSocket such as the initiating request and response as + * well as the <code>FrameChannel</code> object. + * + * @return the session associated with the WebSocket + */ + public Session open() throws IOException { + trace.trace(OPEN_SOCKET); + operation.run(); + return session; + } + + /** + * This is used to register a <code>FrameListener</code> to this + * instance. The registered listener will receive all user frames + * and control frames sent from the client. Also, when the frame + * is closed or when an unexpected error occurs the listener is + * notified. Any number of listeners can be registered at any time. + * + * @param listener this is the listener that is to be registered + */ + public void register(FrameListener listener) throws IOException { + operation.register(listener); + } + + /** + * This is used to remove a <code>FrameListener</code> from this + * instance. After removal the listener will no longer receive + * any user frames or control messages from this specific instance. + * + * @param listener this is the listener to be removed + */ + public void remove(FrameListener listener) throws IOException { + operation.remove(listener); + } + + /** + * This is used to send data to the connected client. To prevent + * an application code from causing resource issues this will block + * as soon as a configured linked list of mapped memory buffers has + * been exhausted. Caution should be taken when writing a broadcast + * implementation that can write to multiple sockets as a badly + * behaving socket that has filled its output buffering capacity + * can cause congestion. + * + * @param data this is the data that is to be sent + */ + public void send(byte[] data) throws IOException { + encoder.encode(data); + } + + /** + * This is used to send text to the connected client. To prevent + * an application code from causing resource issues this will block + * as soon as a configured linked list of mapped memory buffers has + * been exhausted. Caution should be taken when writing a broadcast + * implementation that can write to multiple sockets as a badly + * behaving socket that has filled its output buffering capacity + * can cause congestion. + * + * @param text this is the text that is to be sent + */ + public void send(String text) throws IOException { + encoder.encode(text); + } + + /** + * This is used to send data to the connected client. To prevent + * an application code from causing resource issues this will block + * as soon as a configured linked list of mapped memory buffers has + * been exhausted. Caution should be taken when writing a broadcast + * implementation that can write to multiple sockets as a badly + * behaving socket that has filled its output buffering capacity + * can cause congestion. + * + * @param frame this is the frame that is to be sent + */ + public void send(Frame frame) throws IOException { + encoder.encode(frame); + } + + /** + * This is used to close the connection with a specific reason. + * The close reason will be sent as a control frame before the + * TCP connection is terminated. + * + * @param reason the reason for closing the connection + */ + public void close(Reason reason) throws IOException { + encoder.encode(reason); + writer.close(); + } + + /** + * This is used to close the connection without a specific reason. + * The close reason will be sent as a control frame before the + * TCP connection is terminated. + */ + public void close() throws IOException { + encoder.encode(reason); + writer.close(); + } +}
\ No newline at end of file diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameConsumer.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameConsumer.java new file mode 100644 index 0000000..579d6ef --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameConsumer.java @@ -0,0 +1,162 @@ +/* + * FrameConsumer.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import java.io.IOException; + +import org.simpleframework.http.socket.Frame; +import org.simpleframework.http.socket.FrameType; +import org.simpleframework.transport.ByteCursor; + +/** + * The <code>FrameConsumer</code> object is used to read a WebSocket + * frame as defined by RFC 6455. This is a state machine that can read + * the data one byte at a time until the entire frame has been consumed. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.service.FrameCollector + */ +class FrameConsumer { + + /** + * This is used to consume the header part of the frame. + */ + private FrameHeaderConsumer header; + + /** + * This is used to interpret the header and create a frame. + */ + private FrameBuilder builder; + + /** + * This is used to buffer the bytes that form the frame. + */ + private byte[] buffer; + + /** + * This is a count of the payload bytes currently consumed. + */ + private int count; + + /** + * Constructor for the <code>FrameConsumer</code> object. This is + * used to create a consumer to read the bytes that form the frame + * from an underlying TCP connection. Internally a buffer is created + * to allow bytes to be consumed and collected in chunks. + */ + public FrameConsumer() { + this.header = new FrameHeaderConsumer(); + this.builder = new FrameBuilder(header); + this.buffer = new byte[2048]; + } + + /** + * This is used to determine the type of frame. Interpretation of + * this type is outlined in RFC 6455 and can be loosely categorised + * as control frames and either data or binary frames. + * + * @return this returns the type of frame that this represents + */ + public FrameType getType() { + return header.getType(); + } + + /** + * This is used to create a frame object to represent the data that + * has been consumed. The frame created will make a copy of the + * internal byte buffer so this method should be used sparingly. + * + * @return this returns a frame created from the consumed bytes + */ + public Frame getFrame() { + return builder.create(buffer, count); + } + + /** + * This consumes frame bytes using the provided cursor. The consumer + * acts as a state machine by consuming the data as that data + * becomes available, this allows it to consume data asynchronously + * and dispatch once the whole frame has been consumed. + * + * @param cursor the cursor to consume the frame data from + */ + public void consume(ByteCursor cursor) throws IOException { + while (cursor.isReady()) { + if(!header.isFinished()) { + header.consume(cursor); + } + if(header.isFinished()) { + int length = header.getLength(); + + if(count <= length) { + if(buffer.length < length) { + buffer = new byte[length]; + } + if(count < length) { + int size = cursor.read(buffer, count, length - count); + + if(size == -1) { + throw new IOException("Could only read " + count + " of length " + length); + } + count += size; + } + if(count == length) { + if(header.isMasked()) { + byte[] mask = header.getMask(); + + for (int i = 0; i < count; i++) { + buffer[i] ^= mask[i % 4]; + } + } + break; + } + } + } + } + } + + /** + * This is used to determine if the collector has finished. If it + * is not finished the collector will be registered to listen for + * an I/O interrupt to read further bytes of the frame. + * + * @return true if the collector has finished consuming + */ + public boolean isFinished() { + if(header.isFinished()) { + int length = header.getLength(); + + if(count == length) { + return true; + } + } + return false; + } + + /** + * This resets the collector to its original state so that it can + * be reused. Reusing the collector has obvious benefits as it will + * reduce the amount of memory churn for the server. + */ + public void clear() { + header.clear(); + count = 0; + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameEncoder.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameEncoder.java new file mode 100644 index 0000000..1a99d30 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameEncoder.java @@ -0,0 +1,229 @@ +/* + * FrameEncoder.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import static org.simpleframework.http.socket.FrameType.BINARY; +import static org.simpleframework.http.socket.FrameType.CLOSE; +import static org.simpleframework.http.socket.FrameType.TEXT; +import static org.simpleframework.http.socket.service.ServiceEvent.WRITE_FRAME; + +import java.io.IOException; + +import org.simpleframework.http.Request; +import org.simpleframework.http.socket.CloseCode; +import org.simpleframework.http.socket.Frame; +import org.simpleframework.http.socket.FrameType; +import org.simpleframework.http.socket.Reason; +import org.simpleframework.transport.Channel; +import org.simpleframework.transport.trace.Trace; + +/** + * The <code>FrameEncoder</code> is used to encode data as frames as + * defined by RFC 6455. This can encode binary, and text frames as + * well as control frames. All frames generated are written to the + * underlying channel but are not flushed so that multiple frames + * can be buffered before the final flush is made. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.service.FrameConnection + */ +class FrameEncoder { + + /** + * This is the underlying sender used to send the frames. + */ + private final OutputBarrier barrier; + + /** + * This is the TCP channel the frames are delivered over. + */ + private final Channel channel; + + /** + * This is used to trace the traffic on the channel. + */ + private final Trace trace; + + /** + * This is the charset used to encode the text frames with. + */ + private final String charset; + + /** + * Constructor for the <code>FrameEncoder</code> object. This is + * used to create an encoder to sending frames over the provided + * channel. Frames send remain unflushed so they can be batched + * on a single output buffer. + * + * @param request contains the opening handshake information + */ + public FrameEncoder(Request request) { + this(request, "UTF-8"); + } + + /** + * Constructor for the <code>FrameEncoder</code> object. This is + * used to create an encoder to sending frames over the provided + * channel. Frames send remain unflushed so they can be batched + * on a single output buffer. + * + * @param request contains the opening handshake information + * @param charset this is the character encoding to encode with + */ + public FrameEncoder(Request request, String charset) { + this.barrier = new OutputBarrier(request, 5000); + this.channel = request.getChannel(); + this.trace = channel.getTrace(); + this.charset = charset; + } + + /** + * This is used to encode the provided data as a WebSocket frame as + * of RFC 6455. The encoded data is written to the underlying socket + * and the number of bytes generated is returned. + * + * @param text this is the data used to encode the frame + * + * @return the size of the generated frame including the header + */ + public int encode(String text) throws IOException { + byte[] data = text.getBytes(charset); + return encode(TEXT, data, true); + } + + /** + * This is used to encode the provided data as a WebSocket frame as + * of RFC 6455. The encoded data is written to the underlying socket + * and the number of bytes generated is returned. + * + * @param data this is the data used to encode the frame + * + * @return the size of the generated frame including the header + */ + public int encode(byte[] data) throws IOException { + return encode(BINARY, data, true); + } + + /** + * This is used to encode the provided data as a WebSocket frame as + * of RFC 6455. The encoded data is written to the underlying socket + * and the number of bytes generated is returned. A close frame with + * a reason is similar to a text frame with the exception that the + * first two bytes of the frame payload contains the close code as + * a two byte integer in network byte order. The body of the close + * frame may contain UTF-8 encoded data with a reason, the + * interpretation of which is not defined by RFC 6455. + * + * @param reason this is the data used to encode the frame + * + * @return the size of the generated frame including the header + */ + public int encode(Reason reason) throws IOException { + CloseCode code = reason.getCode(); + String text = reason.getText(); + byte[] header = code.getData(); + + if(text != null) { + byte[] data = text.getBytes(charset); + byte[] message = new byte[data.length + 2]; + + message[0] = header[0]; + message[1] = header[1]; + + for(int i = 0; i < data.length; i++) { + message[i + 2] = data[i]; + } + return encode(CLOSE, message, true); + } + return encode(CLOSE, header, true); + } + + /** + * This is used to encode the provided frame as a WebSocket frame as + * of RFC 6455. The encoded data is written to the underlying socket + * and the number of bytes generated is returned. + * + * @param frame this is frame that is to be send over the channel + * + * @return the size of the generated frame including the header + */ + public int encode(Frame frame) throws IOException { + FrameType code = frame.getType(); + byte[] data = frame.getBinary(); + boolean last = frame.isFinal(); + + return encode(code, data, last); + } + + /** + * This is used to encode the provided frame as a WebSocket frame as + * of RFC 6455. The encoded data is written to the underlying socket + * and the number of bytes generated is returned. + * + * @param type this is the type of frame that is to be encoded + * @param data this is the data used to create the frame + * @param last determines if the is the last frame in a sequence + * + * @return the size of the generated frame including the header + */ + private int encode(FrameType type, byte[] data, boolean last) throws IOException { + byte[] header = new byte[10]; + long length = data.length; + int count = 0; + + if (last) { + header[0] |= 1 << 7; + } + header[0] |= type.code % 128; + + if (length <= 125) { + header[1] = (byte) length; + count = 2; + } else if (length >= 126 && length <= 65535) { + header[1] = (byte) 126; + header[2] = (byte) ((length >>> 8) & 0xff); + header[3] = (byte) (length & 0xff); + count = 4; + } else { + header[1] = (byte) 127; + header[2] = (byte) ((length >>> 56) & 0xff); + header[3] = (byte) ((length >>> 48) & 0xff); + header[4] = (byte) ((length >>> 40) & 0xff); + header[5] = (byte) ((length >>> 32) & 0xff); + header[6] = (byte) ((length >>> 24) & 0xff); + header[7] = (byte) ((length >>> 16) & 0xff); + header[8] = (byte) ((length >>> 8) & 0xff); + header[9] = (byte) (length & 0xff); + count = 10; + } + byte[] reply = new byte[count + data.length]; + + for (int i = 0; i < count; i++) { + reply[i] = header[i]; + } + for (int i = 0; i < length; i++) { + reply[i + count] = data[i]; + } + trace.trace(WRITE_FRAME, type); + barrier.send(reply); + + return reply.length; + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameHeader.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameHeader.java new file mode 100644 index 0000000..a246451 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameHeader.java @@ -0,0 +1,80 @@ +/* + * FrameHeader.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import org.simpleframework.http.socket.FrameType; + +/** + * The <code>FrameHeader</code> represents the variable length header + * used for a WebSocket frame. It is used to determine the number of + * bytes that need to be consumed to successfully process a frame + * from the connected client. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.service.FrameConsumer + */ +interface FrameHeader { + + /** + * This is used to determine the type of frame. Interpretation of + * this type is outlined in RFC 6455 and can be loosely categorised + * as control frames and either data or binary frames. + * + * @return this returns the type of frame that this represents + */ + FrameType getType(); + + /** + * This provides the client mask send with the request. The mask is + * a 32 bit value that is used as an XOR bitmask of the client + * payload. Masking applies only in the client to server direction. + * + * @return this returns the 32 bit mask used for this frame + */ + byte[] getMask(); + + /** + * This provides the length of the payload within the frame. It + * is used to determine how much data to consume from the underlying + * TCP stream in order to recreate the frame to dispatch. + * + * @return the number of bytes used in the frame + */ + int getLength(); + + /** + * This is used to determine if the frame is masked. All client + * frames should be masked according to RFC 6455. If masked the + * payload will have its contents bitmasked with a 32 bit value. + * + * @return this returns true if the payload has been masked + */ + boolean isMasked(); + + /** + * This is used to determine if the frame is the final frame in + * a sequence of fragments or a whole frame. If this returns false + * then the frame is a continuation from from a sequence of + * fragments, otherwise it is a whole frame or the last fragment. + * + * @return this returns false if the frame is a fragment + */ + boolean isFinal(); +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameHeaderConsumer.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameHeaderConsumer.java new file mode 100644 index 0000000..d651ea9 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameHeaderConsumer.java @@ -0,0 +1,235 @@ +/* + * FrameHeaderConsumer.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import java.io.IOException; + +import org.simpleframework.http.socket.FrameType; +import org.simpleframework.transport.ByteCursor; + +/** + * The <code>FrameHeaderConsumer</code> is used to consume frames from + * a connected TCP channel. This is a state machine that can consume + * the data one byte at a time until the entire header has been consumed. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.service.FrameConsumer + */ +class FrameHeaderConsumer implements FrameHeader { + + /** + * This is the frame type which represents the opcode. + */ + private FrameType type; + + /** + * If header consumed was from a client frame the data is masked. + */ + private boolean masked; + + /** + * Determines if this frame is part of a larger sequence. + */ + private boolean last; + + /** + * This is the mask that is used to obfuscate client frames. + */ + private byte[] mask; + + /** + * This is the octet that is used to read one byte at a time. + */ + private byte[] octet; + + /** + * Required number of bytes within the frame header. + */ + private int required; + + /** + * This represents the length of the frame payload. + */ + private int length; + + /** + * This determines the count of the mask bytes read. + */ + private int count; + + /** + * Constructor for the <code>FrameHeaderConsumer</code> object. This + * is used to create a consumer to read the bytes that form the + * frame header from an underlying TCP connection. + */ + public FrameHeaderConsumer() { + this.octet = new byte[1]; + this.mask = new byte[4]; + this.length = -1; + } + + /** + * This provides the length of the payload within the frame. It + * is used to determine how much data to consume from the underlying + * TCP stream in order to recreate the frame to dispatch. + * + * @return the number of bytes used in the frame + */ + public int getLength() { + return length; + } + + /** + * This provides the client mask send with the request. The mask is + * a 32 bit value that is used as an XOR bitmask of the client + * payload. Masking applies only in the client to server direction. + * + * @return this returns the 32 bit mask used for this frame + */ + public byte[] getMask() { + return mask; + } + + /** + * This is used to determine the type of frame. Interpretation of + * this type is outlined in RFC 6455 and can be loosely categorised + * as control frames and either data or binary frames. + * + * @return this returns the type of frame that this represents + */ + public FrameType getType() { + return type; + } + + /** + * This is used to determine if the frame is masked. All client + * frames should be masked according to RFC 6455. If masked the + * payload will have its contents bitmasked with a 32 bit value. + * + * @return this returns true if the payload has been masked + */ + public boolean isMasked() { + return masked; + } + + /** + * This is used to determine if the frame is the final frame in + * a sequence of fragments or a whole frame. If this returns false + * then the frame is a continuation from from a sequence of + * fragments, otherwise it is a whole frame or the last fragment. + * + * @return this returns false if the frame is a fragment + */ + public boolean isFinal() { + return last; + } + + /** + * This consumes frame bytes using the provided cursor. The consumer + * acts as a state machine by consuming the data as that data + * becomes available, this allows it to consume data asynchronously + * and dispatch once the whole frame has been consumed. + * + * @param cursor the cursor to consume the frame data from + */ + public void consume(ByteCursor cursor) throws IOException { + if (cursor.isReady()) { + if (type == null) { + int count = cursor.read(octet); + + if (count <= 0) { + throw new IOException("Ready cursor produced no data"); + } + type = FrameType.resolveType(octet[0] & 0x0f); + + if(type == null) { + throw new IOException("Frame type code not supported"); + } + last = (octet[0] & 0x80) != 0; + } else { + if (length < 0) { + int count = cursor.read(octet); + + if (count <= 0) { + throw new IOException("Ready cursor produced no data"); + } + masked = (octet[0] & 0x80) != 0; + length = (octet[0] & 0x7F); + + if (length == 0x7F) { // 8 byte extended payload length + required = 8; + length = 0; + } else if (length == 0x7E) { // 2 bytes extended payload length + required = 2; + length = 0; + } + } else if (required > 0) { + int count = cursor.read(octet); + + if (count == -1) { + throw new IOException("Could not read length"); + } + length |= (octet[0] & 0xFF) << (8 * --required); + } else { + if (masked && count < mask.length) { + int size = cursor.read(mask, count, mask.length - count); + + if (size == -1) { + throw new IOException("Could not read mask"); + } + count += size; + } + } + } + } + } + + /** + * This is used to determine if the collector has finished. If it + * is not finished the collector will be registered to listen for + * an I/O intrrupt to read further bytes of the frame. + * + * @return true if the collector has finished consuming + */ + public boolean isFinished() { + if(type != null) { + if(length >= 0 && required == 0) { + if(masked) { + return count == mask.length; + } + return true; + } + } + return false; + } + + /** + * This resets the collector to its original state so that it can + * be reused. Reusing the collector has obvious benefits as it will + * reduce the amount of memory churn for the server. + */ + public void clear() { + type = null; + length = -1; + required = 0; + masked = false; + count = 0; + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameProcessor.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameProcessor.java new file mode 100644 index 0000000..c7528d4 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/FrameProcessor.java @@ -0,0 +1,255 @@ +/* + * FrameProcessor.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import static org.simpleframework.http.socket.CloseCode.NORMAL_CLOSURE; +import static org.simpleframework.http.socket.service.ServiceEvent.ERROR; +import static org.simpleframework.http.socket.service.ServiceEvent.READ_FRAME; +import static org.simpleframework.http.socket.service.ServiceEvent.READ_PING; +import static org.simpleframework.http.socket.service.ServiceEvent.READ_PONG; +import static org.simpleframework.http.socket.service.ServiceEvent.WRITE_PONG; + +import java.io.IOException; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.simpleframework.http.Request; +import org.simpleframework.http.socket.Frame; +import org.simpleframework.http.socket.FrameListener; +import org.simpleframework.http.socket.FrameType; +import org.simpleframework.http.socket.Reason; +import org.simpleframework.http.socket.Session; +import org.simpleframework.transport.Channel; +import org.simpleframework.transport.ByteCursor; +import org.simpleframework.transport.trace.Trace; + +/** + * The <code>FrameProcessor</code> object is used to process incoming + * data and dispatch that data as WebSocket frames. Dispatching of the + * frames is done by making a callback to <code>FrameListener</code> + * objects registered. In addition to frames this will also notify of + * any errors that occur or on connection closure. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.service.FrameConsumer + */ +class FrameProcessor { + + /** + * This is the set of listeners to dispatch frames to. + */ + private final Set<FrameListener> listeners; + + /** + * This is used to extract the reason description from a frame. + */ + private final ReasonExtractor extractor; + + /** + * This is used to consume the frames from the underling channel. + */ + private final FrameConsumer consumer; + + /** + * This is the encoder that is used to send control messages. + */ + private final FrameEncoder encoder; + + /** + * This is used to determine if a close notification was sent. + */ + private final AtomicBoolean closed; + + /** + * This is the cursor used to maintain a read seek position. + */ + private final ByteCursor cursor; + + /** + * This is the session associated with the WebSocket connection. + */ + private final Session session; + + /** + * This is the underlying TCP channel this reads frames from. + */ + private final Channel channel; + + /** + * This is the reason message used for a normal closure. + */ + private final Reason normal; + + /** + * This is used to trace the events that occur on the channel. + */ + private final Trace trace; + + /** + * Constructor for the <code>FrameProcessor</code> object. This is + * used to create a processor that can consume and dispatch frames + * as defined by RFC 6455 to a set of registered listeners. + * + * @param encoder this is the encoder used to send control frames + * @param session this is the session associated with the channel + * @param channel this is the channel to read frames from + */ + public FrameProcessor(FrameEncoder encoder, Session session, Request request) { + this.listeners = new CopyOnWriteArraySet<FrameListener>(); + this.normal = new Reason(NORMAL_CLOSURE); + this.extractor = new ReasonExtractor(); + this.consumer = new FrameConsumer(); + this.closed = new AtomicBoolean(); + this.channel = request.getChannel(); + this.cursor = channel.getCursor(); + this.trace = channel.getTrace(); + this.encoder = encoder; + this.session = session; + } + + /** + * This is used to register a <code>FrameListener</code> to this + * instance. The registered listener will receive all user frames + * and control frames sent from the client. Also, when the frame + * is closed or when an unexpected error occurs the listener is + * notified. Any number of listeners can be registered at any time. + * + * @param listener this is the listener that is to be registered + */ + public void register(FrameListener listener) { + listeners.add(listener); + } + + /** + * This is used to remove a <code>FrameListener</code> from this + * instance. After removal the listener will no longer receive + * any user frames or control messages from this specific instance. + * + * @param listener this is the listener to be removed + */ + public void remove(FrameListener listener) { + listeners.remove(listener); + } + + /** + * This is used to process frames consumed from the underlying TCP + * connection. It will respond to control frames such as pings and + * will also handle close frames. Each frame, regardless of its + * type will be dispatched to any <code>FrameListener</code> objects + * that are registered with the processor. If an a close frame is + * received it will echo that close frame, with the same close code + * and back to the sender as suggested by RFC 6455 section 5.5.1. + */ + public void process() throws IOException { + if(cursor.isReady()) { + consumer.consume(cursor); + + if(consumer.isFinished()) { + Frame frame = consumer.getFrame(); + FrameType type = frame.getType(); + + trace.trace(READ_FRAME, type); + + if(type.isPong()) { + trace.trace(READ_PONG); + } + if(type.isPing()){ + Frame response = frame.getFrame(FrameType.PONG); + + trace.trace(READ_PING); + encoder.encode(response); + trace.trace(WRITE_PONG); + } + for(FrameListener listener : listeners) { + listener.onFrame(session, frame); + } + if(type.isClose()){ + Reason reason = extractor.extract(frame); + + if(reason != null) { + close(reason); + } else { + close(); + } + } + consumer.clear(); + } + } + } + + /** + * This is used to report failures back to the client. Any I/O + * or frame processing exception is reported back to all of the + * registered listeners so that they can take action. The + * underlying TCP connection is closed after any failure. + * + * @param reason this is the cause of the failure + */ + public void failure(Exception reason) throws IOException { + if(!closed.getAndSet(true)) { + for(FrameListener listener : listeners) { + try { + listener.onError(session, reason); + } catch(Exception cause) { + trace.trace(ERROR, cause); + } + } + } + } + + /** + * This is used to close the connection without a specific reason. + * The close reason will be sent as a control frame before the + * TCP connection is terminated. All registered listeners will be + * notified of the close event. + * + * @param reason this is the reason for the connection closure + */ + public void close(Reason reason) throws IOException{ + if(!closed.getAndSet(true)) { + for(FrameListener listener : listeners) { + try { + listener.onClose(session, reason); + } catch(Exception cause) { + trace.trace(ERROR, cause); + } + } + } + } + + /** + * This is used to close the connection when it has not responded + * to any activity for a configured period of time. It may be + * possible to send up a control frame, however if the TCP channel + * is closed this will just notify the listeners. + */ + public void close() throws IOException{ + if(!closed.getAndSet(true)) { + try { + for(FrameListener listener : listeners) { + listener.onClose(session, normal); + } + } catch(Exception cause) { + trace.trace(ERROR, cause); + } + } + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/OutputBarrier.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/OutputBarrier.java new file mode 100644 index 0000000..3da2635 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/OutputBarrier.java @@ -0,0 +1,99 @@ +/* + * OutputBarrier.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import java.io.IOException; +import java.util.concurrent.locks.ReentrantLock; + +import org.simpleframework.http.Request; +import org.simpleframework.transport.Channel; +import org.simpleframework.transport.ByteWriter; + +/** + * The <code>OutputBarrier</code> is used to ensure that control + * frames and data frames do not get sent at the same time. Sending + * both at the same time could lead to the status checking thread + * being blocked and this could eventually exhaust the thread pool. + * + * @author Niall Gallagher + */ +class OutputBarrier { + + /** + * This is used to check if there is an operation in progress. + */ + private final ReentrantLock lock; + + /** + * This is the underlying sender used to send the frames. + */ + private final ByteWriter writer; + + /** + * This is the TCP channel the frames are delivered over. + */ + private final Channel channel; + + /** + * This is the length of time to wait before failing to lock. + */ + private final long duration; + + /** + * Constructor for the <code>OutputBarrier</code> object. This + * is used to ensure that if there is currently a blocking write + * in place that the <code>SessionChecker</code> will not end up + * being blocked if it attempts to send a control frame. + * + * @param request this is the request to get the TCP channel from + * @param duration this is the length of time to wait for the lock + */ + public OutputBarrier(Request request, long duration) { + this.lock = new ReentrantLock(); + this.channel = request.getChannel(); + this.writer = channel.getWriter(); + this.duration = duration; + } + + /** + * This method is used to send all frames. It is important that + * a lock is used to protect this so that if there is an attempt + * to send out a control frame while the connection is blocked + * there is an exception thrown. + * + * @param frame this is the frame to send over the TCP channel + */ + public void send(byte[] frame) throws IOException { + try { + if(!lock.tryLock(duration, MILLISECONDS)) { + throw new IOException("Transport lock could not be acquired"); + } + try { + writer.write(frame); + writer.flush(); // less throughput, better latency + } finally { + lock.unlock(); + } + } catch(Exception e) { + throw new IOException("Error writing to transport", e); + } + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/PathRouter.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/PathRouter.java new file mode 100644 index 0000000..9deb66a --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/PathRouter.java @@ -0,0 +1,111 @@ +/* + * PathRouter.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import static org.simpleframework.http.Protocol.SEC_WEBSOCKET_PROTOCOL; +import static org.simpleframework.http.Protocol.SEC_WEBSOCKET_VERSION; +import static org.simpleframework.http.Protocol.UPGRADE; +import static org.simpleframework.http.Protocol.WEBSOCKET; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import org.simpleframework.http.Path; +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; + +/** + * The <code>PathRouter</code> is used when there are multiple + * services that can be used. Each service is selected based on the + * path sent in the initiating request. If a match cannot be made + * based on the request then a default service us chosen. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.service.RouterContainer + */ +public class PathRouter implements Router { + + /** + * This is the set of services that can be selected. + */ + private final Map<String, Service> registry; + + /** + * This is the default service chosen if there is no match. + */ + private final Service primary; + + /** + * Constructor for the <code>PathRouter</code> object. This is used + * to create a router using a selection of services that can be + * selected using the path provided in the initiating request. + * + * @param registry this is the registry of available services + * @param primary this is the default service to use + */ + public PathRouter(Map<String, Service> registry, Service primary) throws IOException { + this.registry = registry; + this.primary = primary; + } + + /** + * This is used to route an incoming request to a service if + * the request represents a WebSocket handshake as defined by + * RFC 6455. If the request is not a session initiating handshake + * then this will return a null value to allow it to be processed + * by some other part of the server. + * + * @param request this is the request to use for routing + * @param response this is the response to establish the session + * + * @return a service that can be used to process the session + */ + public Service route(Request request, Response response) { + String token = request.getValue(UPGRADE); + + if(token != null) { + if(token.equalsIgnoreCase(WEBSOCKET)) { + List<String> protocols = request.getValues(SEC_WEBSOCKET_PROTOCOL); + String version = request.getValue(SEC_WEBSOCKET_VERSION); + Path path = request.getPath(); + String normal = path.getPath(); + + if(version != null) { + response.setValue(SEC_WEBSOCKET_VERSION, version); + } + for(String protocol : protocols) { + String original = response.getValue(SEC_WEBSOCKET_PROTOCOL); + + if(original == null) { + response.setValue(SEC_WEBSOCKET_PROTOCOL, protocol); + } + } + Service service = registry.get(normal); + + if(service != null) { + return service; + } + return primary; + } + } + return null; + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ProtocolRouter.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ProtocolRouter.java new file mode 100644 index 0000000..54060c9 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ProtocolRouter.java @@ -0,0 +1,105 @@ +/* + * ProtocolRouter.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import static org.simpleframework.http.Protocol.SEC_WEBSOCKET_PROTOCOL; +import static org.simpleframework.http.Protocol.SEC_WEBSOCKET_VERSION; +import static org.simpleframework.http.Protocol.UPGRADE; +import static org.simpleframework.http.Protocol.WEBSOCKET; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; + +/** + * The <code>ProtocolRouter</code> is used when there are multiple + * services that can be used. Each service is selected based on the + * protocol sent in the initiating request. If a match cannot be + * made based on the request then a default service us chosen. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.service.RouterContainer + */ +public class ProtocolRouter implements Router { + + /** + * This is the set of services that can be selected. + */ + private final Map<String, Service> registry; + + /** + * This is the default service chosen if there is no match. + */ + private final Service primary; + + /** + * Constructor for the <code>ProtocolRouter</code> object. This is + * used to create a router using a selection of services that can + * be selected using the <code>Sec-WebSocket-Protocol</code> header + * sent in the initiating request by the client. + * + * @param registry this is the registry of available services + * @param primary this is the default service to use + */ + public ProtocolRouter(Map<String, Service> registry, Service primary) throws IOException { + this.registry = registry; + this.primary = primary; + } + + /** + * This is used to route an incoming request to a service if + * the request represents a WebSocket handshake as defined by + * RFC 6455. If the request is not a session initiating handshake + * then this will return a null value to allow it to be processed + * by some other part of the server. + * + * @param request this is the request to use for routing + * @param response this is the response to establish the session + * + * @return a service that can be used to process the session + */ + public Service route(Request request, Response response) { + String token = request.getValue(UPGRADE); + + if(token != null) { + if(token.equalsIgnoreCase(WEBSOCKET)) { + List<String> protocols = request.getValues(SEC_WEBSOCKET_PROTOCOL); + String version = request.getValue(SEC_WEBSOCKET_VERSION); + + if(version != null) { + response.setValue(SEC_WEBSOCKET_VERSION, version); + } + for(String protocol : protocols) { + Service service = registry.get(protocol); + + if(service != null) { + response.setValue(SEC_WEBSOCKET_PROTOCOL, protocol); + return service; + } + } + return primary; + } + } + return null; + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ReasonExtractor.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ReasonExtractor.java new file mode 100644 index 0000000..fb6ce88 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ReasonExtractor.java @@ -0,0 +1,114 @@ +/* + * ReasonExtractor.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import static org.simpleframework.http.socket.CloseCode.NO_STATUS_CODE; + +import org.simpleframework.http.socket.CloseCode; +import org.simpleframework.http.socket.DataConverter; +import org.simpleframework.http.socket.Frame; +import org.simpleframework.http.socket.Reason; + +/** + * The <code>ReasonExtractor</code> object is used to extract the close + * reason from a frame payload. If their is no close reason this will + * return a <code>Reason</code> with just the close code. Finally in + * the event of a botched frame being sent with no close code then the + * close code 1005 is used to indicate no reason. + * + * @author Niall Gallagher + */ +class ReasonExtractor { + + /** + * This is the data converter object used to convert data. + */ + private final DataConverter converter; + + /** + * Constructor for the <code>ReasonExtractor</code> object. This + * is used to create an extractor for close code and the close + * reason descriptions. All descriptions are decoded using the + * UTF-8 character encoding. + */ + public ReasonExtractor() { + this.converter = new DataConverter(); + } + + /** + * This is used to extract a reason from the provided frame. The + * close reason is taken from the first two bytes of the frame + * payload and the UTF-8 string that follows is the description. + * + * @param frame this is the frame to extract the reason from + * + * @return a reason containing the close code and reason + */ + public Reason extract(Frame frame) { + byte[] data = frame.getBinary(); + + if(data.length > 0) { + CloseCode code = extractCode(data); + String text = extractText(data); + + return new Reason(code, text); + } + return new Reason(NO_STATUS_CODE); + } + + /** + * This method is used to extract the UTF-8 description from the + * frame payload. If there are only two bytes within the payload + * then this will return null for the reason. + * + * @param data the frame payload to extract the description from + * + * @return returns the description within the payload + */ + private String extractText(byte[] data) { + int length = data.length - 2; + + if(length > 0) { + return converter.convert(data, 2, length); + } + return null; + } + + /** + * This method is used to extract the close code. The close code + * is an two byte integer in network byte order at the start + * of the close frame payload. This code is required by RFC 6455 + * however if not code is available code 1005 is returned. + * + * @param data the frame payload to extract the description from + * + * @return returns the description within the payload + */ + private CloseCode extractCode(byte[] data) { + int length = data.length; + + if(length > 0) { + int high = data[0]; + int low = data[1]; + + return CloseCode.resolveCode(high, low); + } + return NO_STATUS_CODE; + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/RequestValidator.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/RequestValidator.java new file mode 100644 index 0000000..7e47dc3 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/RequestValidator.java @@ -0,0 +1,137 @@ +/* + * RequestValidator.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import static org.simpleframework.http.Protocol.CONNECTION; +import static org.simpleframework.http.Protocol.SEC_WEBSOCKET_KEY; +import static org.simpleframework.http.Protocol.SEC_WEBSOCKET_VERSION; +import static org.simpleframework.http.Protocol.UPGRADE; +import static org.simpleframework.http.Protocol.WEBSOCKET; + +import java.util.List; + +import org.simpleframework.http.Request; + +/** + * The <code>RequestValidator</code> object is used to ensure requests + * for confirm to RFC 6455 section 4.2.1. The client opening handshake + * must consist of several parts, including a version of 13 referring + * to RFC 6455, a WebSocket key, and the required HTTP connection + * details. If any of these are missing the server is obliged to + * respond with a HTTP 400 response indicating a bad request. + * + * @author Niall Gallagher + */ +class RequestValidator { + + /** + * This is the request forming the client part of the handshake. + */ + private final Request request; + + /** + * This is the version referring to the required client version. + */ + private final String version; + + /** + * Constructor for the <code>RequestValidator</code> object. This + * is used to create a plain vanilla validator that uses version + * 13 as dictated by RFC 6455 section 4.2.1. + * + * @param request this is the handshake request from the client + */ + public RequestValidator(Request request) { + this(request, "13"); + } + + /** + * Constructor for the <code>RequestValidator</code> object. This + * is used to create a plain vanilla validator that uses version + * 13 as dictated by RFC 6455 section 4.2.1. + * + * @param request this is the handshake request from the client + * @param version a version other than 13 if desired + */ + public RequestValidator(Request request, String version) { + this.request = request; + this.version = version; + } + + /** + * This is used to determine if the client handshake request had + * all the required headers as dictated by RFC 6455 section 4.2.1. + * If the request does not contain any of these parts then this + * will return false, indicating a HTTP 400 response should be + * sent to the client. + * + * @return true if the request was a valid handshake + */ + public boolean isValid() { + if(!isProtocol()) { + return false; + } + if(!isUpgrade()) { + return false; + } + return true; + } + + /** + * This is used to determine if the request is a valid WebSocket + * handshake of the correct version. This also checks to see if + * the request contained the required handshake token. + * + * @return this returns true if the request is a valid handshake + */ + private boolean isProtocol() { + String protocol = request.getValue(SEC_WEBSOCKET_VERSION); + String token = request.getValue(SEC_WEBSOCKET_KEY); + + if(token != null) { + return version.equals(protocol); + } + return false; + } + + /** + * Here we check to ensure that there is a HTTP connection header + * with the required upgrade token. The upgrade token may be + * one of many, so all must be checked. Finally to ensure that + * the upgrade is for a WebSocket the upgrade header is checked. + * + * @return this returns true if the request is an upgrade + */ + private boolean isUpgrade() { + List<String> tokens = request.getValues(CONNECTION); + + for(String token : tokens) { + if(token.equalsIgnoreCase(UPGRADE)) { + String upgrade = request.getValue(UPGRADE); + + if(upgrade != null) { + return upgrade.equalsIgnoreCase(WEBSOCKET); + } + return false; + } + } + return false; + } + +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ResponseBuilder.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ResponseBuilder.java new file mode 100644 index 0000000..0ba780e --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ResponseBuilder.java @@ -0,0 +1,159 @@ +/* + * ResponseBuilder.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import static org.simpleframework.http.Protocol.CLOSE; +import static org.simpleframework.http.Protocol.CONNECTION; +import static org.simpleframework.http.Protocol.DATE; +import static org.simpleframework.http.Protocol.SEC_WEBSOCKET_ACCEPT; +import static org.simpleframework.http.Protocol.UPGRADE; +import static org.simpleframework.http.Protocol.WEBSOCKET; +import static org.simpleframework.http.socket.service.ServiceEvent.WRITE_HEADER; + +import java.io.IOException; + +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; +import org.simpleframework.http.Status; +import org.simpleframework.transport.Channel; +import org.simpleframework.transport.ByteWriter; +import org.simpleframework.transport.trace.Trace; + +/** + * The <code>ResponseBuilder</code> object is used to build a response + * to a WebSocket handshake. In order for a successful handshake to + * complete a HTTP request must have a version of 13 referring + * to RFC 6455, a WebSocket key, and the required HTTP connection + * details. If any of these are missing the server is obliged to + * respond with a HTTP 400 response indicating a bad request. + * + * @author Niall Gallagher + */ +class ResponseBuilder { + + /** + * This is used to validate the initiating WebSocket request. + */ + private final RequestValidator validator; + + /** + * This is the accept token generated for the request. + */ + private final AcceptToken token; + + /** + * This is the sender used to send the WebSocket response. + */ + private final ByteWriter writer; + + /** + * This is the response to the WebSocket handshake. + */ + private final Response response; + + /** + * This is the underlying TCP channel for the request. + */ + private final Channel channel; + + /** + * This is used to trace the activity for the handshake. + */ + private final Trace trace; + + /** + * Constructor for the <code>ResponseBuilder</code> object. In order + * to process the WebSocket handshake this requires the original + * request and the response as well as the underlying TCP channel + * which forms the basis of the WebSocket connection. + * + * @param request this is the request that initiated the handshake + * @param response this is the response for the handshake + */ + public ResponseBuilder(Request request, Response response) throws Exception { + this.validator = new RequestValidator(request); + this.token = new AcceptToken(request); + this.channel = request.getChannel(); + this.writer = channel.getWriter(); + this.trace = channel.getTrace(); + this.response = response; + } + + /** + * This is used to determine if the client handshake request had + * all the required headers as dictated by RFC 6455 section 4.2.1. + * If the request does not contain any of these parts then this + * will return false, indicating a HTTP 400 response is sent to + * the client, otherwise a HTTP 101 response is sent. + */ + public void commit() throws IOException { + if(validator.isValid()) { + accept(); + } else { + reject(); + } + } + + /** + * This is used to respond to the client with a HTTP 400 response + * indicating the WebSocket handshake failed. No response body is + * sent with the rejection message and the underlying TCP channel + * is closed to prevent further use of the connection. + */ + private void reject() throws IOException { + long time = System.currentTimeMillis(); + + response.setStatus(Status.BAD_REQUEST); + response.setValue(CONNECTION, CLOSE); + response.setDate(DATE, time); + + String header = response.toString(); + byte[] message = header.getBytes("UTF-8"); + + trace.trace(WRITE_HEADER, header); + writer.write(message); + writer.flush(); + writer.close(); + } + + /** + * This is used to respond to the client with a HTTP 101 response + * to indicate that the WebSocket handshake succeeeded. Once this + * response has been sent all traffic between the client and + * server will be with WebSocket frames as defined by RFC 6455. + */ + private void accept() throws IOException { + long time = System.currentTimeMillis(); + String accept = token.create(); + + response.setStatus(Status.SWITCHING_PROTOCOLS); + response.setDescription(UPGRADE); + response.setValue(CONNECTION, UPGRADE); + response.setDate(DATE, time); + response.setValue(SEC_WEBSOCKET_ACCEPT, accept); + response.setValue(UPGRADE, WEBSOCKET); + + String header = response.toString(); + byte[] message = header.getBytes("UTF-8"); + + trace.trace(WRITE_HEADER, header); + writer.write(message); + writer.flush(); + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/Router.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/Router.java new file mode 100644 index 0000000..3b466f5 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/Router.java @@ -0,0 +1,59 @@ +/* + * Router.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; + +/** + * The <code>Router</code> interface represents a means of routing + * a session initiating request to the correct service. Typically + * a service is chosen based on the sub-protocol provided in the + * initiating request, however it can be chosen on any criteria + * available in the request. An initiating request must contain + * a <code>Connection</code> header with the <code>websocket</code> + * token according to RFC 6455 section 4.2.1. If the request does + * not contain this token it is treated as a normal request and + * a <code>Service</code> will not be resolved. + * <p> + * If a service has been successfully chosen from the initiating + * request the the value of <code>Sec-WebSocket-Protocol</code> will + * contain either the chosen protocol if a match was made with the + * initiating request or null to indicate a default choice. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.service.RouterContainer + */ +public interface Router { + + /** + * This is used to route an incoming request to a service if + * the request represents a WebSocket handshake as defined by + * RFC 6455. If the request is not a session initiating handshake + * then this must return a null value to allow it to be processed + * by some other part of the server. + * + * @param request this is the request to use for routing + * @param response this is the response to establish the session + * + * @return a service that can be used to process the session + */ + Service route(Request request, Response response); +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/RouterContainer.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/RouterContainer.java new file mode 100644 index 0000000..3b018a9 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/RouterContainer.java @@ -0,0 +1,109 @@ +/* + * RouterContainer.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import java.io.IOException; + +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; +import org.simpleframework.http.core.Container; + +/** + * The <code>RouterContainer</code> is used to route requests that + * satisfy a WebSocket opening handshake to a specific service. Each + * request intercepted by this <code>Container</code> implementation + * is examined for opening handshake criteria as specified by RFC 6455, + * and if it contains the required information it is router to a + * specific service using a <code>Router</code> implementation. If the + * request does not contain the required criteria it is handled by + * an internal container delegate. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.service.Router + */ +public class RouterContainer implements Container { + + /** + * This is the service dispatcher used to dispatch requests. + */ + private final ServiceDispatcher dispatcher; + + /** + * This is the container used to handle traditional requests. + */ + private final Container container; + + /** + * This is the router used to select specific services. + */ + private final Router router; + + /** + * Constructor for the <code>RouterContainer</code> object. This + * requires a container to delegate traditional requests to and + * a <code>Router</code> implementation which can be used to + * select a service to dispatch a WebSocket session to. + * + * @param container this is the container to delegate to + * @param router this is the router used to select services + * @param threads this contains the number of threads to use + */ + public RouterContainer(Container container, Router router, int threads) throws IOException { + this(container, router, threads, 10000); + } + + /** + * Constructor for the <code>RouterContainer</code> object. This + * requires a container to delegate traditional requests to and + * a <code>Router</code> implementation which can be used to + * select a service to dispatch a WebSocket session to. + * + * @param container this is the container to delegate to + * @param router this is the router used to select services + * @param threads this contains the number of threads to use + * @param ping this is the frequency to send ping frames with + */ + public RouterContainer(Container container, Router router, int threads, long ping) throws IOException { + this.dispatcher = new ServiceDispatcher(router, threads, ping); + this.container = container; + this.router = router; + } + + /** + * This method is used to create a dispatch a <code>Session</code> to + * a specific service selected by a router. If the session initiating + * handshake fails for any reason this will close the underlying TCP + * connection and send a HTTP 400 response back to the client. All + * traditional requests that do not represent an WebSocket opening + * handshake are dispatched to the internal container. + * + * @param req the request that contains the client HTTP message + * @param resp the response used to deliver the server response + */ + public void handle(Request req, Response resp) { + Service service = router.route(req, resp); + + if(service != null) { + dispatcher.dispatch(req, resp); + } else { + container.handle(req, resp); + } + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/Service.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/Service.java new file mode 100644 index 0000000..d95c01f --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/Service.java @@ -0,0 +1,44 @@ +/* + * Service.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import org.simpleframework.http.socket.Session; + +/** + * The <code>Service</code> interface represents a service that can be + * used to communicate with the WebSocket protocol defined in RFC 6455. + * Typically a service will implement a sub-protocol negotiated from + * the initiating HTTP request. The service should be considered a + * hand off point rather than an place to implement business logic. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.FrameChannel + */ +public interface Service { + + /** + * This method connects a new session with a service implementation. + * Connecting a session with a service in this way should not block + * as it could cause starvation of the servicing thread pool. + * + * @param session the new session to connect to the service + */ + void connect(Session session); +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ServiceChannel.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ServiceChannel.java new file mode 100644 index 0000000..ad5325c --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ServiceChannel.java @@ -0,0 +1,149 @@ +/* + * ServiceChannel.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import java.io.IOException; + +import org.simpleframework.http.socket.Frame; +import org.simpleframework.http.socket.FrameListener; +import org.simpleframework.http.socket.Reason; +import org.simpleframework.http.socket.FrameChannel; + +/** + * The <code>ServiceChannel</code> represents a full duplex communication + * channel as defined by RFC 6455. Any instance of this will provide + * a means to perform asynchronous writes and reads to a remote client + * using a lightweight framing protocol. A frame is a finite length + * sequence of bytes that can hold either text or binary data. Also, + * control frames are used to perform heartbeat monitoring and closure. + * <p> + * For convenience frames can be consumed from the socket via a + * callback to a registered listener. This avoids having to poll each + * socket for data and provides a asynchronous event driven model of + * communication, which greatly reduces overhead and complication. + * + * @author Niall Gallagher + */ +class ServiceChannel implements FrameChannel { + + /** + * This is the internal channel for full duplex communication. + */ + private final FrameChannel channel; + + /** + * Constructor for the <code>ServiceChannel</code> object. This is + * used to create a channel that is given to the application. This + * is synchronized so only one frame can be dispatched at a time. + * + * @param channel this is the channel to delegate to + */ + public ServiceChannel(FrameChannel channel) { + this.channel = channel; + } + + /** + * This is used to send data to the connected client. To prevent + * an application code from causing resource issues this will block + * as soon as a configured linked list of mapped memory buffers has + * been exhausted. Caution should be taken when writing a broadcast + * implementation that can write to multiple sockets as a badly + * behaving socket that has filled its output buffering capacity + * can cause congestion. + * + * @param data this is the data that is to be sent + */ + public synchronized void send(byte[] data) throws IOException { + channel.send(data); + } + + /** + * This is used to send text to the connected client. To prevent + * an application code from causing resource issues this will block + * as soon as a configured linked list of mapped memory buffers has + * been exhausted. Caution should be taken when writing a broadcast + * implementation that can write to multiple sockets as a badly + * behaving socket that has filled its output buffering capacity + * can cause congestion. + * + * @param text this is the text that is to be sent + */ + public synchronized void send(String text) throws IOException { + channel.send(text); + } + + /** + * This is used to send data to the connected client. To prevent + * an application code from causing resource issues this will block + * as soon as a configured linked list of mapped memory buffers has + * been exhausted. Caution should be taken when writing a broadcast + * implementation that can write to multiple sockets as a badly + * behaving socket that has filled its output buffering capacity + * can cause congestion. + * + * @param frame this is the frame that is to be sent + */ + public synchronized void send(Frame frame) throws IOException { + channel.send(frame); + } + + /** + * This is used to register a <code>FrameListener</code> to this + * instance. The registered listener will receive all user frames + * and control frames sent from the client. Also, when the frame + * is closed or when an unexpected error occurs the listener is + * notified. Any number of listeners can be registered at any time. + * + * @param listener this is the listener that is to be registered + */ + public synchronized void register(FrameListener listener) throws IOException { + channel.register(listener); + } + + /** + * This is used to remove a <code>FrameListener</code> from this + * instance. After removal the listener will no longer receive + * any user frames or control messages from this specific instance. + * + * @param listener this is the listener to be removed + */ + public synchronized void remove(FrameListener listener) throws IOException { + channel.remove(listener); + } + + /** + * This is used to close the connection with a specific reason. + * The close reason will be sent as a control frame before the + * TCP connection is terminated. + * + * @param reason the reason for closing the connection + */ + public synchronized void close(Reason reason) throws IOException { + channel.close(reason); + } + + /** + * This is used to close the connection without a specific reason. + * The close reason will be sent as a control frame before the + * TCP connection is terminated. + */ + public void close() throws IOException { + channel.close(); + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ServiceDispatcher.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ServiceDispatcher.java new file mode 100644 index 0000000..509495f --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ServiceDispatcher.java @@ -0,0 +1,101 @@ +/* + * ServiceDispatcher.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import java.io.IOException; + +import org.simpleframework.common.thread.ConcurrentScheduler; +import org.simpleframework.common.thread.Scheduler; +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; +import org.simpleframework.transport.reactor.ExecutorReactor; +import org.simpleframework.transport.reactor.Reactor; + +/** + * The <code>ServiceDispatcher</code> object is used to perform the + * opening handshake for a WebSocket session. Once the session has been + * established it is connected to a <code>Service</code> where frames + * can be sent and received. If for any reason the handshake fails + * this will terminated the connection with a HTTP 400 response. + * + * @author Niall Gallagher + */ +class ServiceDispatcher { + + /** + * This is the session dispatcher used to dispatch the session. + */ + private final SessionDispatcher dispatcher; + + /** + * This is used to build the sessions from the handshake request. + */ + private final SessionBuilder builder; + + /** + * This is used asynchronously read frames from the TCP channel. + */ + private final Scheduler scheduler; + + /** + * This is used to notify of read events on the TCP channel. + */ + private final Reactor reactor; + + /** + * Constructor for the <code>ServiceDispatcher</code> object. The + * dispatcher created will dispatch WebSocket sessions to a service + * using the provided <code>Router</code> instance. + * + * @param router this is the router used to select a service + * @param threads this is the number of threads to use + */ + public ServiceDispatcher(Router router, int threads) throws IOException { + this(router, threads, 10000); + } + + /** + * Constructor for the <code>ServiceDispatcher</code> object. The + * dispatcher created will dispatch WebSocket sessions to a service + * using the provided <code>Router</code> instance. + * + * @param router this is the router used to select a service + * @param threads this is the number of threads to use + * @param ping this is the frequency used to send ping frames + */ + public ServiceDispatcher(Router router, int threads, long ping) throws IOException { + this.scheduler = new ConcurrentScheduler(FrameCollector.class, threads); + this.reactor = new ExecutorReactor(scheduler); + this.builder = new SessionBuilder(scheduler, reactor, ping); + this.dispatcher = new SessionDispatcher(builder, router); + } + + /** + * This method is used to create a dispatch a <code>Session</code> to + * a specific service selected by a router. If the session initiating + * handshake fails for any reason this will close the underlying TCP + * connection and send a HTTP 400 response back to the client. + * + * @param request this is the session initiating request + * @param response this is the session initiating response + */ + public void dispatch(Request request, Response response) { + dispatcher.dispatch(request, response); + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ServiceEvent.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ServiceEvent.java new file mode 100644 index 0000000..a5d0079 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ServiceEvent.java @@ -0,0 +1,97 @@ +/* + * ServiceEvent.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +/** + * The <code>ServiceEvent</code> enumeration contains the events that + * are dispatched processing a WebSocket. To see how a WebSocket is + * behaving and to gather performance statistics the service events + * can be intercepted using a custom <code>TraceAnalyzer</code> object. + * + * @author Niall Gallagher + * + * @see org.simpleframework.transport.trace.TraceAnalyzer + */ +public enum ServiceEvent { + + /** + * This event is dispatched when a WebSocket is connected. + */ + OPEN_SOCKET, + + /** + * This event is dispatched when a WebSocket is dispatched. + */ + DISPATCH_SOCKET, + + /** + * This event is dispatched when a WebSocket channel is closed. + */ + TERMINATE_SOCKET, + + /** + * This event is dispatched when the response handshake is sent. + */ + WRITE_HEADER, + + /** + * This event is dispatched when the WebSocket receives a ping. + */ + READ_PING, + + /** + * This event is dispatched when a ping is sent over a WebSocket. + */ + WRITE_PING, + + /** + * This event is dispatched when the WebSocket receives a pong. + */ + READ_PONG, + + /** + * This event is dispatched when a pong is sent over a WebSocket. + */ + WRITE_PONG, + + /** + * This event is dispatched when a frame is read from a WebSocket. + */ + READ_FRAME, + + /** + * This event is dispatched when a frame is sent over a WebSocket. + */ + WRITE_FRAME, + + /** + * This indicates that there has been no response to a ping. + */ + PING_EXPIRED, + + /** + * This indicates that there has been no response to a ping. + */ + PONG_RECEIVED, + + /** + * This event is dispatched when an error occurs with a WebSocket. + */ + ERROR; +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ServiceSession.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ServiceSession.java new file mode 100644 index 0000000..b8fc083 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/ServiceSession.java @@ -0,0 +1,139 @@ +/* + * ServiceSession.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import java.util.Map; + +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; +import org.simpleframework.http.socket.FrameChannel; +import org.simpleframework.http.socket.Session; + +/** + * The <code>ServiceSession</code> represents a simple WebSocket session + * that contains the connection handshake details and the actual socket. + * In order to determine how the session should be interacted with the + * protocol is conveniently exposed, however all attributes of the + * original HTTP request are available. + * + * @author Niall Gallagher + * + * @see org.simpleframework.http.socket.FrameChannel + */ +class ServiceSession implements Session { + + /** + * The WebSocket used for asynchronous full duplex communication. + */ + private final FrameChannel channel; + + /** + * This is the initiating response associated with the session. + */ + private final Response response; + + /** + * This is the initiating request associated with the session. + */ + private final Request request; + + /** + * This is the bag of attributes used by this session. + */ + private final Map attributes; + + /** + * Constructor for the <code>ServiceSession</code> object. This is used + * to create the session that will be used by a <code>Service</code> to + * send and receive WebSocket frames. + * + * @param channel this is the actual WebSocket for the session + * @param request this is the session initiating request + * @param response this is the session initiating response + */ + public ServiceSession(FrameChannel channel, Request request, Response response) { + this.channel = new ServiceChannel(channel); + this.attributes = request.getAttributes(); + this.response = response; + this.request = request; + } + + /** + * This can be used to retrieve the response attributes. These can + * be used to keep state with the response when it is passed to + * other systems for processing. Attributes act as a convenient + * model for storing objects associated with the response. This + * also inherits attributes associated with the client connection. + * + * @return the attributes of that have been set on the request + */ + public Map getAttributes() { + return attributes; + } + + /** + * This is used as a shortcut for acquiring attributes for the + * response. This avoids acquiring the attribute <code>Map</code> + * in order to retrieve the attribute directly from that object. + * The attributes contain data specific to the response. + * + * @param key this is the key of the attribute to acquire + * + * @return this returns the attribute for the specified name + */ + public Object getAttribute(Object key) { + return attributes.get(key); + } + + /** + * Provides a <code>WebSocket</code> that can be used to communicate + * with the connected client. Communication is full duplex and also + * asynchronous through the use of a <code>FrameListener</code> that + * can be registered with the socket. + * + * @return a web socket for full duplex communication + */ + public FrameChannel getChannel() { + return channel; + } + + /** + * Provides the <code>Request</code> used to initiate the session. + * This is useful in establishing the identity of the user, acquiring + * an security information and also for determining the request path + * that was used, which be used to establish context. + * + * @return the request used to initiate the session + */ + public Request getRequest() { + return request; + } + + /** + * Provides the <code>Response</code> used to establish the session + * with the remote client. This is useful in establishing the protocol + * used to create the session and also for determining various other + * useful contextual information. + * + * @return the response used to establish the session + */ + public Response getResponse() { + return response; + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/SessionBuilder.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/SessionBuilder.java new file mode 100644 index 0000000..59864ee --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/SessionBuilder.java @@ -0,0 +1,93 @@ +/* + * SessionBuilder.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import java.io.IOException; + +import org.simpleframework.common.thread.Scheduler; +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; +import org.simpleframework.http.socket.Session; +import org.simpleframework.transport.reactor.Reactor; + +/** + * The <code>SessionBuilder</code> object is used to create sessions + * for connected WebSockets. Before the session is created a response + * is sent back to the connected client. If for some reason the session + * is not valid or does not conform to the requirements of RFC 6455 + * then a HTTP 400 response code is sent and the TCP channel is closed. + * + * @author Niall Gallagher + */ +class SessionBuilder { + + /** + * This is the scheduler that is used to ping WebSocket sessions. + */ + private final Scheduler scheduler; + + /** + * This is the reactor used to register for I/O notifications. + */ + private final Reactor reactor; + + /** + * This is the frequency the server should send out ping frames. + */ + private final long ping; + + /** + * Constructor for the <code>SessionBuilder</code> object. This is + * used to create sessions using the request and response associated + * with the WebSocket opening handshake. + * + * @param scheduler this is the shared thread pool used for pinging + * @param reactor this is used to check for I/O notifications + * @param ping this is the frequency to send out ping frames + */ + public SessionBuilder(Scheduler scheduler, Reactor reactor, long ping) { + this.scheduler = scheduler; + this.reactor = reactor; + this.ping = ping; + } + + /** + * This is used to create a WebSocket session. If at any point there + * is an error creating the session the underlying TCP connection is + * closed and a <code>Session</code> is returned regardless. + * + * @param request this is the request associated with this session + * @param response this is the response associated with this session + * + * @return this returns the session associated with the WebSocket + */ + public Session create(Request request, Response response) throws Exception { + FrameConnection connection = new FrameConnection(request, response, reactor); + ResponseBuilder builder = new ResponseBuilder(request, response); + StatusChecker checker = new StatusChecker(connection, request, scheduler, ping); + + try { + builder.commit(); + checker.start(); + } catch(Exception e) { + throw new IOException("Could not send response", e); + } + return connection.open(); + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/SessionDispatcher.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/SessionDispatcher.java new file mode 100644 index 0000000..6543be0 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/SessionDispatcher.java @@ -0,0 +1,111 @@ +/* + * SessionDispatcher.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import static org.simpleframework.http.socket.service.ServiceEvent.DISPATCH_SOCKET; +import static org.simpleframework.http.socket.service.ServiceEvent.ERROR; +import static org.simpleframework.http.socket.service.ServiceEvent.TERMINATE_SOCKET; + +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; +import org.simpleframework.http.socket.Session; +import org.simpleframework.transport.Channel; +import org.simpleframework.transport.trace.Trace; + +/** + * The <code>SessionDispatcher</code> object is used to perform the + * opening handshake for a WebSocket session. Once the session has been + * established it is connected to a <code>Service</code> where frames + * can be sent and received. If for any reason the handshake fails + * this will terminated the connection with a HTTP 400 response. + * + * @author Niall Gallagher + */ +class SessionDispatcher { + + /** + * This is used to create the session for the WebSocket. + */ + private final SessionBuilder builder; + + /** + * This is used to select the service to dispatch to. + */ + private final Router router; + + /** + * Constructor for the <code>SessionDispatcher</code> object. The + * dispatcher created will dispatch WebSocket sessions to a service + * using the provided <code>Router</code> instance. + * + * @param builder this is used to build the WebSocket session + * @param router this is used to select the service + */ + public SessionDispatcher(SessionBuilder builder, Router router) { + this.builder = builder; + this.router = router; + } + + /** + * This method is used to create a dispatch a <code>Session</code> to + * a specific service selected by a router. If the session initiating + * handshake fails for any reason this will close the underlying TCP + * connection and send a HTTP 400 response back to the client. + * + * @param request this is the session initiating request + * @param response this is the session initiating response + */ + public void dispatch(Request request, Response response) { + Channel channel = request.getChannel(); + Trace trace = channel.getTrace(); + + try { + Service service = router.route(request, response); + Session session = builder.create(request, response); + + trace.trace(DISPATCH_SOCKET); + service.connect(session); + } catch(Exception cause) { + trace.trace(ERROR, cause); + terminate(request, response); + } + } + + /** + * This method is used to terminate the connection and commit the + * response. Terminating the session before it has been dispatched + * is done when there is a protocol or an unexpected I/O error with + * the underlying TCP channel. + * + * @param request this is the session initiating request + * @param response this is the session initiating response + */ + public void terminate(Request request, Response response) { + Channel channel = request.getChannel(); + Trace trace = channel.getTrace(); + + try { + response.close(); + channel.close(); + trace.trace(TERMINATE_SOCKET); + } catch(Exception cause) { + trace.trace(ERROR, cause); + } + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/StatusChecker.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/StatusChecker.java new file mode 100644 index 0000000..1f4f0d7 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/StatusChecker.java @@ -0,0 +1,220 @@ +/* + * StatusChecker.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import static org.simpleframework.http.socket.CloseCode.INTERNAL_SERVER_ERROR; +import static org.simpleframework.http.socket.CloseCode.NORMAL_CLOSURE; +import static org.simpleframework.http.socket.FrameType.PING; +import static org.simpleframework.http.socket.service.ServiceEvent.ERROR; +import static org.simpleframework.http.socket.service.ServiceEvent.PING_EXPIRED; +import static org.simpleframework.http.socket.service.ServiceEvent.PONG_RECEIVED; +import static org.simpleframework.http.socket.service.ServiceEvent.WRITE_PING; + +import java.util.concurrent.atomic.AtomicLong; + +import org.simpleframework.common.thread.Scheduler; +import org.simpleframework.http.Request; +import org.simpleframework.http.socket.DataFrame; +import org.simpleframework.http.socket.Frame; +import org.simpleframework.http.socket.Reason; +import org.simpleframework.transport.Channel; +import org.simpleframework.transport.trace.Trace; + +/** + * The <code>StatusChecker</code> object is used to perform health + * checks on connected sessions. Health is determined using the ping + * pong protocol defined in RFC 6455. The ping pong protocol requires + * that any endpoint must respond to a ping control frame with a pong + * control frame containing the same payload. This session checker + * will send out out ping controls frames and wait for a pong frame. + * If it does not receive a pong frame after a configured expiry time + * then it will close the associated session. + * + * @author Niall Gallagher + */ +class StatusChecker implements Runnable{ + + /** + * This is used to perform the monitoring of the sessions. + */ + private final StatusResultListener listener; + + /** + * This is the WebSocket this this pinger will be monitoring. + */ + private final FrameConnection connection; + + /** + * This is the shared scheduler used to execute this checker. + */ + private final Scheduler scheduler; + + /** + * This is a count of the number of unacknowledged ping frames. + */ + private final AtomicLong counter; + + /** + * This is the underling TCP channel that is being checked. + */ + private final Channel channel; + + /** + * The only reason for a close is for an unexpected error. + */ + private final Reason normal; + + /** + * The only reason for a close is for an unexpected error. + */ + private final Reason error; + + /** + * This is used to trace various events for this pinger. + */ + private final Trace trace; + + /** + * This is the frame that contains the ping to send. + */ + private final Frame frame; + + /** + * This is the frequency with which the checker should run. + */ + private final long frequency; + + /** + * Constructor for the <code>StatusChecker</code> object. This + * is used to create a pinger that will send out ping frames at + * a specified interval. If a session does not respond within + * three times the duration of the ping the connection is reset. + * + * @param connection this is the WebSocket to send the frames + * @param request this is the associated request + * @param scheduler this is the scheduler used to execute this + * @param frequency this is the frequency with which to ping + */ + public StatusChecker(FrameConnection connection, Request request, Scheduler scheduler, long frequency) { + this.listener = new StatusResultListener(this); + this.error = new Reason(INTERNAL_SERVER_ERROR); + this.normal = new Reason(NORMAL_CLOSURE); + this.frame = new DataFrame(PING); + this.counter = new AtomicLong(); + this.channel = request.getChannel(); + this.trace = channel.getTrace(); + this.connection = connection; + this.scheduler = scheduler; + this.frequency = frequency; + } + + /** + * This is used to kick of the status checking. Here an initial + * ping is sent over the socket and the task is then scheduled to + * check the result after the frequency period has expired. If + * this method fails for any reason the TCP channel is closed. + */ + public void start() { + try { + connection.register(listener); + trace.trace(WRITE_PING); + connection.send(frame); + counter.getAndIncrement(); + scheduler.execute(this, frequency); + } catch(Exception cause) { + trace.trace(ERROR, cause); + channel.close(); + } + } + + /** + * This method is used to check to see if a session has expired. + * If there have been three unacknowledged ping events then this + * will force a closure of the WebSocket connection. This is done + * to ensure only healthy connections are maintained within the + * server, also RFC 6455 recommends using the ping pong protocol. + */ + public void run() { + long count = counter.get(); + + try { + if(count < 3) { + trace.trace(WRITE_PING); + connection.send(frame); + counter.getAndIncrement(); + scheduler.execute(this, frequency); // schedule the next one + } else { + trace.trace(PING_EXPIRED); + connection.close(normal); + } + } catch (Exception cause) { + trace.trace(ERROR, cause); + channel.close(); + } + } + + /** + * If the connection gets a response to its ping message then this + * will reset the internal counter. This ensure that the connection + * does not time out. If after three pings there is not response + * from the other side then the connection will be terminated. + */ + public void refresh() { + try { + trace.trace(PONG_RECEIVED); + counter.set(0); + } catch(Exception cause) { + trace.trace(ERROR, cause); + channel.close(); + } + } + + /** + * This is used to close the session and send a 1011 close code + * to the client indicating an internal server error. Closing + * of the session in this manner only occurs if there is an + * expiry of the session or an I/O error, both of which are + * unexpected and violate the behaviour as defined in RFC 6455. + */ + public void failure() { + try { + connection.close(error); + channel.close(); + } catch(Exception cause) { + trace.trace(ERROR, cause); + channel.close(); + } + } + + /** + * This is used to close the session and send a 1000 close code + * to the client indicating a normal closure. This will be called + * when there is a close notification dispatched to the status + * listener. Typically here a graceful closure is best. + */ + public void close() { + try { + connection.close(normal); + channel.close(); + } catch(Exception cause) { + trace.trace(ERROR, cause); + channel.close(); + } + } +}
\ No newline at end of file diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/StatusResultListener.java b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/StatusResultListener.java new file mode 100644 index 0000000..2b2a049 --- /dev/null +++ b/simple/simple-http/src/main/java/org/simpleframework/http/socket/service/StatusResultListener.java @@ -0,0 +1,93 @@ +/* + * StatusResultListener.java February 2014 + * + * Copyright (C) 2014, Niall Gallagher <niallg@users.sf.net> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.http.socket.service; + +import org.simpleframework.http.socket.Frame; +import org.simpleframework.http.socket.FrameListener; +import org.simpleframework.http.socket.FrameType; +import org.simpleframework.http.socket.Reason; +import org.simpleframework.http.socket.Session; + +/** + * The <code>StatusResultListener</code> is used to listen for responses + * to ping frames sent out by the server. A response to the ping frame + * is a pong frame. When a pong is received it allows the session to + * be scheduled to receive another ping. + * + * @author Niall Gallagher + */ +class StatusResultListener implements FrameListener { + + /** + * This is used to ping sessions to check for health. + */ + private final StatusChecker checker; + + /** + * Constructor for the <code>StatusResultListener</code> object. + * This requires the session health checker that performs the pings + * so that it can reschedule the session for multiple pings if + * the connection responds with a pong. + * + * @param checker this is the session health checker + */ + public StatusResultListener(StatusChecker checker) { + this.checker = checker; + } + + /** + * This is called when a new frame arrives on the WebSocket. If + * the frame is a pong then this will reschedule the the session + * to receive another ping frame. + * + * @param session this is the associated session + * @param frame this is the frame that has been received + */ + public void onFrame(Session session, Frame frame) { + FrameType type = frame.getType(); + + if(type.isPong()) { + checker.refresh(); + } + } + + /** + * This is called when there is an error with the connection. + * When called the session is removed from the checker and no + * more ping frames are sent. + * + * @param session this is the associated session + * @param cause this is the cause of the error + */ + public void onError(Session session, Exception cause) { + checker.failure(); + } + + /** + * This is called when the connection is closed from the other + * side. When called the session is removed from the checker + * and no more ping frames are sent. + * + * @param session this is the associated session + * @param reason this is the reason the connection was closed + */ + public void onClose(Session session, Reason reason) { + checker.close(); + } +}
\ No newline at end of file |