lib/websockets: Added basic support for Websockets
authorLucas Bajolet <r4pass@hotmail.com>
Fri, 4 Apr 2014 18:23:08 +0000 (14:23 -0400)
committerLucas Bajolet <r4pass@hotmail.com>
Mon, 7 Apr 2014 17:57:01 +0000 (13:57 -0400)
Signed-off-by: Lucas Bajolet <r4pass@hotmail.com>

lib/websocket.nit [new file with mode: 0644]

diff --git a/lib/websocket.nit b/lib/websocket.nit
new file mode 100644 (file)
index 0000000..5730c9d
--- /dev/null
@@ -0,0 +1,240 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2014 Lucas Bajolet <r4pass@hotmail.com>
+#
+# 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.
+
+# Adds support for a websocket connection in Nit
+# Uses standard sockets
+module websocket
+
+import socket
+import sha1
+import base64
+
+# Websocket compatible server, works as an extra layer to the original Sockets
+class WebSocket
+
+       # Client connection to the server
+       var client: Socket
+
+       # Socket listening to connections on a defined port
+       var listener: Socket
+
+       # Creates a new Websocket server listening on given port with `max_clients` slots available
+       init(port: Int, max_clients: Int)
+       do
+               listener = new Socket.stream_with_port(port)
+
+               if not listener.bind then
+                       return
+               end
+
+               if not listener.listen(1) then
+                       return
+               end
+       end
+
+       # Accept an incoming connection and initializes the handshake
+       fun accept
+       do
+               assert listener.still_alive
+
+               client = listener.accept
+
+               var headers = parse_handshake
+               var resp = handshake_response(headers)
+
+               client.write(resp)
+       end
+
+       # Disconnect from a client
+       fun disconnect_client
+       do
+               client.close
+       end
+
+       # Disconnects the client if one is connected
+       # And stops the server
+       fun stop_server
+       do
+               client.close
+               listener.close
+       end
+
+       # Parses the input handshake sent by the client
+       # See RFC 6455 for information
+       private fun parse_handshake: Map[String,String]
+       do
+               var recved = client.read
+               var headers = recved.split("\r\n")
+               var headmap = new HashMap[String,String]
+               for i in headers do
+                       var temp_head = i.split(" ")
+                       var head = temp_head.shift
+                       if head.is_empty or head.length == 1 then continue
+                       if head.chars.last == ':' then
+                               head = head.substring(0, head.length - 1)
+                       end
+                       var body = temp_head.join(" ")
+                       headmap[head] = body
+               end
+               return headmap
+       end
+
+       # Generates the handshake
+       private fun handshake_response(heads: Map[String,String]): String
+       do
+               var resp_map = new HashMap[String,String]
+               resp_map["HTTP/1.1"] = "101 Switching Protocols"
+               resp_map["Upgrade:"] = "websocket"
+               resp_map["Connection:"] = "Upgrade"
+               var key = heads["Sec-WebSocket-Key"]
+               key += "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
+               key = key.sha1.encode_base64
+               resp_map["Sec-WebSocket-Accept:"] = key
+               var resp = resp_map.join("\r\n", " ")
+               resp += "\r\n\r\n"
+               return resp
+       end
+
+       # Frames a text message to be sent to a client
+       private fun frame_message(msg: String): String
+       do
+               var ans_buffer = new FlatBuffer
+               # Flag for final frame set to 1
+               # opcode set to 1 (for text)
+               ans_buffer.add(129.ascii)
+               if msg.length < 126 then
+                       ans_buffer.add(msg.length.ascii)
+               end
+               if msg.length >= 126 and msg.length <= 65535 then
+                       ans_buffer.add(126.ascii)
+                       ans_buffer.add(msg.length.rshift(8).ascii)
+                       ans_buffer.add(msg.length.ascii)
+               end
+               ans_buffer.append(msg)
+               return ans_buffer.to_s
+       end
+
+       # Gets the message from the client, unpads it and reconstitutes the message
+       private fun unpad_message: String do
+               var fin = false
+               var ret_buffer = new FlatBuffer
+               while not fin do
+                       var msg = client.read
+                       if msg.length == 0 then return ""
+                       var iter = msg.chars.iterator
+                       # First byte in msg is formatted this way :
+                       # |(fin - 1bit)|(RSV1 - 1bit)|(RSV2 - 1bit)|(RSV3 - 1bit)|(opcode - 4bits)
+                       # fin = Flag indicating if current frame is the last one
+                       # RSV1/2/3 = Extension flags, unsupported
+                       # Opcode values :
+                       #       %x0 denotes a continuation frame
+                       #       %x1 denotes a text frame
+                       #       %x2 denotes a binary frame
+                       #       %x3-7 are reserved for further non-control frames
+                       #       %x8 denotes a connection close
+                       #       %x9 denotes a ping
+                       #       %xA denotes a pong
+                       #       %xB-F are reserved for further control frames
+                       var fin_flag = iter.item.ascii.bin_and(128)
+                       if fin_flag != 0 then fin = true
+                       var opcode = iter.item.ascii.bin_and(15)
+                       if opcode == 9 then
+                               ret_buffer.add(138.ascii)
+                               ret_buffer.add(0.ascii)
+                               client.write(ret_buffer.to_s)
+                               return ""
+                       end
+                       if opcode == 8 then
+                               self.client.close
+                               return ""
+                       end
+                       iter.next
+                       # Second byte is formatted this way :
+                       # |(mask - 1bit)|(payload length - 7 bits)
+                       # As specified, if the payload length is 126 or 127
+                       # The next 16 or 64 bits contain an extended payload length
+                       var mask_flag = iter.item.ascii.bin_and(128)
+                       var mask: String
+                       var len = iter.item.ascii.bin_and(127)
+                       var payload_ext_len = 0
+                       if len == 126 then
+                               iter.next
+                               payload_ext_len = iter.item.ascii.lshift(8)
+                               iter.next
+                               payload_ext_len += iter.item.ascii
+                       else if len == 127 then
+                               # 64 bits for length are not supported,
+                               # only the last 32 will be interpreted as a Nit Integer
+                               for i in [0..4[ do iter.next
+                               payload_ext_len = iter.item.ascii.lshift(24)
+                               iter.next
+                               payload_ext_len += iter.item.ascii.lshift(16)
+                               iter.next
+                               payload_ext_len += iter.item.ascii.lshift(8)
+                               iter.next
+                               payload_ext_len += iter.item.ascii
+                       end
+                       if mask_flag != 0 then
+                               iter.next
+                               var startindex = iter.index
+                               mask = msg.substring(startindex,4)
+                               if payload_ext_len != 0 then
+                                       ret_buffer.append(unmask_message(mask, msg.substring(startindex+4, payload_ext_len)))
+                               else
+                                       if len == 0 then
+                                               return ret_buffer.to_s
+                                       end
+                                       ret_buffer.append(unmask_message(mask, msg.substring(startindex+4, len)))
+                               end
+                       end
+               end
+               return ret_buffer.to_s
+       end
+
+       # Unmasks a message sent by a client
+       private fun unmask_message(key: String, message: String): String
+       do
+               var return_message = new FlatBuffer.with_capacity(message.length)
+               var msg_iter = message.chars.iterator
+
+               while msg_iter.is_ok do
+                       return_message.chars[msg_iter.index] = msg_iter.item.ascii.bin_xor(key.chars[msg_iter.index%4].ascii).ascii
+                       msg_iter.next
+               end
+
+               return return_message.to_s
+       end
+
+       # Checks if a connection to a client is available
+       fun connected: Bool do return client.connected
+
+       # Writes a text message to a client
+       fun write(msg: String)
+       do
+               client.write(frame_message(msg))
+       end
+
+       # Reads data from a Websocket client
+       fun read: String
+       do
+               return unpad_message
+       end
+
+       # Is there some data available to be read ?
+       fun can_read(timeout: Int): Bool do return client.connected and client.ready_to_read(timeout)
+
+end
+