benches/markdowm: adds README
[nit.git] / lib / websocket / websocket.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2014 Lucas Bajolet <r4pass@hotmail.com>
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 # Adds support for a websocket connection in Nit
18 # Uses standard sockets
19 module websocket
20
21 import socket
22 import sha1
23 import base64
24
25 intrude import standard::stream
26
27 # Websocket compatible server, works as an extra layer to the original Sockets
28 class WebSocket
29 super BufferedIStream
30 super OStream
31 super PollableIStream
32
33 # Client connection to the server
34 var client: TCPStream
35
36 # Socket listening to connections on a defined port
37 var listener: TCPServer
38
39 # Creates a new Websocket server listening on given port with `max_clients` slots available
40 init(port: Int, max_clients: Int)
41 do
42 _buffer = new FlatBuffer
43 _buffer_pos = 0
44 listener = new TCPServer(port)
45 listener.listen max_clients
46 end
47
48 # Accept an incoming connection and initializes the handshake
49 fun accept
50 do
51 assert not listener.closed
52
53 var client = listener.accept
54 assert client != null
55 self.client = client
56
57 var headers = parse_handshake
58 var resp = handshake_response(headers)
59
60 client.write(resp)
61 end
62
63 # Disconnect from a client
64 fun disconnect_client
65 do
66 client.close
67 end
68
69 # Disconnects the client if one is connected
70 # And stops the server
71 redef fun close
72 do
73 client.close
74 listener.close
75 end
76
77 # Parses the input handshake sent by the client
78 # See RFC 6455 for information
79 private fun parse_handshake: Map[String,String]
80 do
81 var recved = read_http_frame(new FlatBuffer)
82 var headers = recved.split("\r\n")
83 var headmap = new HashMap[String,String]
84 for i in headers do
85 var temp_head = i.split(" ")
86 var head = temp_head.shift
87 if head.is_empty or head.length == 1 then continue
88 if head.chars.last == ':' then
89 head = head.substring(0, head.length - 1)
90 end
91 var body = temp_head.join(" ")
92 headmap[head] = body
93 end
94 return headmap
95 end
96
97 # Generates the handshake
98 private fun handshake_response(heads: Map[String,String]): String
99 do
100 var resp_map = new HashMap[String,String]
101 resp_map["HTTP/1.1"] = "101 Switching Protocols"
102 resp_map["Upgrade:"] = "websocket"
103 resp_map["Connection:"] = "Upgrade"
104 var key = heads["Sec-WebSocket-Key"]
105 key += "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
106 key = key.sha1.encode_base64
107 resp_map["Sec-WebSocket-Accept:"] = key
108 var resp = resp_map.join("\r\n", " ")
109 resp += "\r\n\r\n"
110 return resp
111 end
112
113 # Frames a text message to be sent to a client
114 private fun frame_message(msg: String): String
115 do
116 var ans_buffer = new FlatBuffer
117 # Flag for final frame set to 1
118 # opcode set to 1 (for text)
119 ans_buffer.add(129.ascii)
120 if msg.length < 126 then
121 ans_buffer.add(msg.length.ascii)
122 end
123 if msg.length >= 126 and msg.length <= 65535 then
124 ans_buffer.add(126.ascii)
125 ans_buffer.add(msg.length.rshift(8).ascii)
126 ans_buffer.add(msg.length.ascii)
127 end
128 ans_buffer.append(msg)
129 return ans_buffer.to_s
130 end
131
132 # Reads an HTTP frame
133 protected fun read_http_frame(buf: Buffer): String
134 do
135 client.append_line_to(buf)
136 buf.chars.add('\n')
137 if buf.has_substring("\r\n\r\n", buf.length - 4) then return buf.to_s
138 return read_http_frame(buf)
139 end
140
141 # Gets the message from the client, unpads it and reconstitutes the message
142 private fun unpad_message do
143 var fin = false
144 while not fin do
145 var fst_char = client.read_char
146 var snd_char = client.read_char
147 # First byte in msg is formatted this way :
148 # |(fin - 1bit)|(RSV1 - 1bit)|(RSV2 - 1bit)|(RSV3 - 1bit)|(opcode - 4bits)
149 # fin = Flag indicating if current frame is the last one
150 # RSV1/2/3 = Extension flags, unsupported
151 # Opcode values :
152 # %x0 denotes a continuation frame
153 # %x1 denotes a text frame
154 # %x2 denotes a binary frame
155 # %x3-7 are reserved for further non-control frames
156 # %x8 denotes a connection close
157 # %x9 denotes a ping
158 # %xA denotes a pong
159 # %xB-F are reserved for further control frames
160 var fin_flag = fst_char.bin_and(128)
161 if fin_flag != 0 then fin = true
162 var opcode = fst_char.bin_and(15)
163 if opcode == 9 then
164 _buffer.add(138.ascii)
165 _buffer.add('\0')
166 client.write(_buffer.to_s)
167 _buffer_pos += 2
168 return
169 end
170 if opcode == 8 then
171 self.client.close
172 return
173 end
174 # Second byte is formatted this way :
175 # |(mask - 1bit)|(payload length - 7 bits)
176 # As specified, if the payload length is 126 or 127
177 # The next 16 or 64 bits contain an extended payload length
178 var mask_flag = snd_char.bin_and(128)
179 var len = snd_char.bin_and(127)
180 var payload_ext_len = 0
181 if len == 126 then
182 payload_ext_len = client.read_char.lshift(8)
183 payload_ext_len += client.read_char
184 else if len == 127 then
185 # 64 bits for length are not supported,
186 # only the last 32 will be interpreted as a Nit Integer
187 for i in [0..4[ do client.read_char
188 payload_ext_len = client.read_char.lshift(24)
189 payload_ext_len += client.read_char.lshift(16)
190 payload_ext_len += client.read_char.lshift(8)
191 payload_ext_len += client.read_char
192 end
193 if mask_flag != 0 then
194 if payload_ext_len != 0 then
195 var msg = client.read(payload_ext_len+4)
196 var mask = msg.substring(0,4)
197 _buffer.append(unmask_message(mask, msg.substring(4, payload_ext_len)))
198 else
199 if len == 0 then
200 return
201 end
202 var msg = client.read(len+4)
203 var mask = msg.substring(0,4)
204 _buffer.append(unmask_message(mask, msg.substring(4, len)))
205 end
206 end
207 end
208 end
209
210 # Unmasks a message sent by a client
211 private fun unmask_message(key: String, message: String): String
212 do
213 var return_message = new FlatBuffer.with_capacity(message.length)
214 var msg_iter = message.chars.iterator
215
216 while msg_iter.is_ok do
217 return_message.chars[msg_iter.index] = msg_iter.item.ascii.bin_xor(key.chars[msg_iter.index%4].ascii).ascii
218 msg_iter.next
219 end
220
221 return return_message.to_s
222 end
223
224 # Checks if a connection to a client is available
225 fun connected: Bool do return client.connected
226
227 redef fun write(msg: Text)
228 do
229 client.write(frame_message(msg.to_s))
230 end
231
232 redef fun is_writable do return client.connected
233
234 redef fun fill_buffer
235 do
236 _buffer.clear
237 _buffer_pos = 0
238 unpad_message
239 end
240
241 redef fun end_reached do return _buffer_pos >= _buffer.length and client.eof
242
243 # Is there some data available to be read ?
244 fun can_read(timeout: Int): Bool do return client.ready_to_read(timeout)
245
246 redef fun poll_in do return client.poll_in
247 end