From 7b1cff9f5ea66a4b8d13e8a36b0f6e1d928a6d62 Mon Sep 17 00:00:00 2001 From: Lucas Bajolet Date: Fri, 4 Apr 2014 14:23:08 -0400 Subject: [PATCH] lib/websockets: Added basic support for Websockets Signed-off-by: Lucas Bajolet --- lib/websocket.nit | 240 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 240 insertions(+) create mode 100644 lib/websocket.nit diff --git a/lib/websocket.nit b/lib/websocket.nit new file mode 100644 index 0000000..5730c9d --- /dev/null +++ b/lib/websocket.nit @@ -0,0 +1,240 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2014 Lucas Bajolet +# +# 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 + -- 1.7.9.5