1 # This file is part of NIT ( http://www.nitlanguage.org ).
3 # Copyright 2014 Lucas Bajolet <r4pass@hotmail.com>
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
9 # http://www.apache.org/licenses/LICENSE-2.0
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.
17 # Adds support for a websocket connection in Nit
18 # Uses standard sockets
25 intrude import standard
::stream
26 intrude import standard
::bytes
28 # Websocket compatible listener
30 # Produces Websocket client-server connections
31 class WebSocketListener
34 # Socket listening to connections on a defined port
35 var listener
: TCPServer
37 # Creates a new Websocket server listening on given port with `max_clients` slots available
38 init(port
: Int, max_clients
: Int)
40 listener
= new TCPServer(port
)
41 listener
.listen max_clients
44 # Accepts an incoming connection
45 fun accept
: WebsocketConnection
47 assert not listener
.closed
49 var client
= listener
.accept
52 return new WebsocketConnection(listener
.port
, "", client
)
55 # Stop listening for incoming connections
62 # Connection to a websocket client
64 # Can be used to communicate with a client
65 class WebsocketConnection
69 _buffer
= new NativeString(1024)
71 _buffer_capacity
= 1024
73 var headers
= parse_handshake
74 var resp
= handshake_response
(headers
)
79 # Client connection to the server
82 # Disconnect from a client
88 # Parses the input handshake sent by the client
89 # See RFC 6455 for information
90 private fun parse_handshake
: Map[String,String]
92 var recved
= read_http_frame
(new FlatBuffer)
93 var headers
= recved
.split
("\r\n")
94 var headmap
= new HashMap[String,String]
96 var temp_head
= i
.split
(" ")
97 var head
= temp_head
.shift
98 if head
.is_empty
or head
.length
== 1 then continue
99 if head
.chars
.last
== ':' then
100 head
= head
.substring
(0, head
.length
- 1)
102 var body
= temp_head
.join
(" ")
108 # Generates the handshake
109 private fun handshake_response
(heads
: Map[String,String]): String
111 var resp_map
= new HashMap[String,String]
112 resp_map
["HTTP/1.1"] = "101 Switching Protocols"
113 resp_map
["Upgrade:"] = "websocket"
114 resp_map
["Connection:"] = "Upgrade"
115 var key
= heads
["Sec-WebSocket-Key"]
116 key
+= "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
117 key
= key
.sha1
.encode_base64
118 resp_map
["Sec-WebSocket-Accept:"] = key
119 var resp
= resp_map
.join
("\r\n", " ")
124 # Frames a text message to be sent to a client
125 private fun frame_message
(msg
: String): Bytes
127 var ans_buffer
= new Bytes.with_capacity
(msg
.length
)
128 # Flag for final frame set to 1
129 # opcode set to 1 (for text)
131 if msg
.length
< 126 then
132 ans_buffer
.add
(msg
.length
)
134 if msg
.length
>= 126 and msg
.length
<= 65535 then
136 ans_buffer
.add
(msg
.length
.rshift
(8))
137 ans_buffer
.add
(msg
.length
)
139 if msg
isa FlatString then
140 ans_buffer
.append_ns_from
(msg
.items
, msg
.length
, msg
.index_from
)
142 for i
in msg
.substrings
do
143 ans_buffer
.append_ns_from
(i
.as(FlatString).items
, i
.length
, i
.as(FlatString).index_from
)
149 # Reads an HTTP frame
150 protected fun read_http_frame
(buf
: Buffer): String
152 buf
.append client
.read_line
154 if buf
.has_suffix
("\r\n\r\n") then return buf
.to_s
155 return read_http_frame
(buf
)
158 # Gets the message from the client, unpads it and reconstitutes the message
159 private fun unpad_message
do
161 var bf
= new Bytes.empty
163 var fst_byte
= client
.read_byte
164 var snd_byte
= client
.read_byte
165 if fst_byte
== null or snd_byte
== null then
166 last_error
= new IOError("Error: bad frame")
170 # First byte in msg is formatted this way :
171 # |(fin - 1bit)|(RSV1 - 1bit)|(RSV2 - 1bit)|(RSV3 - 1bit)|(opcode - 4bits)
172 # fin = Flag indicating if current frame is the last one
173 # RSV1/2/3 = Extension flags, unsupported
175 # %x0 denotes a continuation frame
176 # %x1 denotes a text frame
177 # %x2 denotes a binary frame
178 # %x3-7 are reserved for further non-control frames
179 # %x8 denotes a connection close
182 # %xB-F are reserved for further control frames
183 var fin_flag
= fst_byte
.bin_and
(128)
184 if fin_flag
!= 0 then fin
= true
185 var opcode
= fst_byte
.bin_and
(15)
189 client
.write
(bf
.to_s
)
190 _buffer_pos
= _buffer_length
197 # Second byte is formatted this way :
198 # |(mask - 1bit)|(payload length - 7 bits)
199 # As specified, if the payload length is 126 or 127
200 # The next 16 or 64 bits contain an extended payload length
201 var mask_flag
= snd_byte
.bin_and
(128)
202 var len
= snd_byte
.bin_and
(127)
203 var payload_ext_len
= 0
205 var tmp
= client
.read_bytes
(2)
206 if tmp
.length
!= 2 then
207 last_error
= new IOError("Error: received interrupted frame")
211 payload_ext_len
= tmp
[1] + tmp
[0].lshift
(8)
212 else if len
== 127 then
213 # 64 bits for length are not supported,
214 # only the last 32 will be interpreted as a Nit Integer
215 var tmp
= client
.read_bytes
(8)
216 if tmp
.length
!= 8 then
217 last_error
= new IOError("Error: received interrupted frame")
221 for pos
in [0 .. tmp
.length
[ do
223 payload_ext_len
+= i
.lshift
(8 * (7 - pos
))
226 if mask_flag
!= 0 then
227 var mask
= client
.read_bytes
(4).items
228 if payload_ext_len
!= 0 then
229 len
= payload_ext_len
231 var msg
= client
.read_bytes
(len
).items
232 bf
.append_ns
(unmask_message
(mask
, msg
, len
), len
)
236 _buffer_length
= bf
.length
239 # Unmasks a message sent by a client
240 private fun unmask_message
(key
: NativeString, message
: NativeString, len
: Int): NativeString
242 var return_message
= new NativeString(len
)
244 for i
in [0 .. len
[ do
245 return_message
[i
] = message
[i
].ascii
.bin_xor
(key
[i
%4].ascii
).ascii
248 return return_message
251 # Checks if a connection to a client is available
252 redef fun connected
do return client
.connected
254 redef fun write_bytes
(s
) do client
.write_bytes
(frame_message
(s
.to_s
))
256 redef fun write
(msg
) do client
.write
(frame_message
(msg
.to_s
).to_s
)
258 redef fun is_writable
do return client
.connected
260 redef fun fill_buffer
266 redef fun end_reached
do return client
._buffer_pos
>= client
._buffer_length
and client
.end_reached
268 # Is there some data available to be read ?
269 fun can_read
(timeout
: Int): Bool do return client
.ready_to_read
(timeout
)
271 redef fun poll_in
do return client
.poll_in