-
Notifications
You must be signed in to change notification settings - Fork 0
/
websocket.reb
270 lines (261 loc) · 7.74 KB
/
websocket.reb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
Rebol [
Title: "WebSocket scheme and codec"
Type: module
Name: websocket
Date: 02-Jan-2024
Version: 0.2.0
Author: @Oldes
Home: https://github.com/Oldes/Rebol-WebSocket
Rights: http://opensource.org/licenses/Apache-2.0
Purpose: {Communicate with a server over WebSocket's connection.}
History: [
01-Jan-2024 "Oldes" {Initial version}
]
Needs: [3.11.0] ;; used bit/hexadecimal integer syntax
]
;--- WebSocket Codec --------------------------------------------------
append system/options/log [ws: 4]
register-codec [
name: 'ws
type: 'text
title: "WebSocket"
encode: function/with [
"Encodes one WebSocket message."
data [binary! any-string! word! map!]
/no-mask
][
case [
data = 'ping [return #{81801B1F519C}]
data = 'close [return #{888260D19A196338}]
map? data [data: to-json data]
word? data [data: form data]
]
out: clear #{}
;; first byte has FIN bit and an opcode (if data are string or binary data)
byte1: either binary? data [2#10000010][2#10000001] ;; final binary/string
unless binary? data [data: to binary! data]
len: length? data
either no-mask [
binary/write out case [
len < 0#007E [[UI8 :byte1 UI8 :len :data]]
len <= 0#FFFF [[UI8 :byte1 UI8 126 UI16 :len :data]]
'else [[UI8 :byte1 UI8 127 UI64 :len :data]]
]
][
;; update a mask...
repeat i 4 [mask/:i: 1 + random 254] ;; avoiding zero
data: data xor mask
binary/write out case [
len < 0#007E [byte2: 2#10000000 | len [UI8 :byte1 UI8 :byte2 :mask :data]]
len <= 0#FFFF [[UI8 :byte1 UI8 254 UI16 :len :mask :data]]
'else [[UI8 :byte1 UI8 255 UI64 :len :mask :data]]
]
]
out
][
mask: #{00000000}
out: make binary! 100
]
decode: function [
"Decodes WebSocket messages from a given input."
data [binary!] "Consumed data are removed! (modified)"
][
out: copy []
;; minimal WebSocket message has 2 bytes at least (when no masking involved)
while [2 < length? data][
final?: data/1 & 2#10000000 = 2#10000000
opcode: data/1 & 2#00001111
mask?: data/2 & 2#10000000 = 2#10000000
len: data/2 & 2#01111111
data: skip data 2
sys/log/debug 'WS ["Length:" len "opcode:" opcode "final?" final? "data-length?" length? data]
;@@ Not removing bytes until we make sure, that there is enough data!
case [
len = 126 [
;; there must be at least 2 bytes for the message length
if 2 >= length? data [break]
len: binary/read data 'UI16
sys/log/debug 'WS ["Real length:" len]
data: skip data 2
]
len = 127 [
if 8 >= length? data [break]
len: binary/read data 'UI64
sys/log/debug 'WS ["Real length:" len]
data: skip data 8
]
]
if (4 + length? data) < len [break]
data: truncate data ;; removes already processed bytes from the head
either mask? [
masks: take/part data 4
temp: masks xor take/part data len
if len < 4 [truncate/part temp len] ;; the mask was longer then the message
][ temp: take/part data len ]
if all [final? opcode = 1] [try [temp: to string! temp]]
append append append out :final? :opcode :temp
]
out
]
]
ws-encode: :codecs/ws/encode
ws-decode: :codecs/ws/decode
;--- WebSocket Scheme -------------------------------------------------
ws-conn-awake: func [event /local port extra parent spec temp] [
port: event/port
unless parent: port/parent [return true]
extra: parent/extra
sys/log/debug 'WS ["==TCP-event:" as-red event/type]
either extra/handshake [
switch event/type [
read [
append extra/buffer port/data
clear port/data
]
]
insert system/ports/system make event! [ type: event/type port: parent ]
port
][
switch/default event/type [
;- Upgrading from HTTP to WS...
read [
;print ["^/read:" length? port/data]
append extra/buffer port/data
clear port/data
;probe to string! parent/data
either find extra/buffer #{0D0A0D0A} [
;; parse response header...
try/with [
;; skip the first line and construct response fields
extra/fields: temp: construct find/tail extra/buffer #{0D0A}
unless all [
"websocket" = select temp 'Upgrade
"Upgrade" = select temp 'Connection
extra/key = select temp 'Sec-WebSocket-Accept
][
insert system/ports/system make event! [ type: 'error port: parent ]
return true
]
] :print
clear port/data
clear extra/buffer
extra/handshake: true
insert system/ports/system make event! [ type: 'connect port: parent ]
][
;; missing end of the response header...
read port ;; keep reading...
]
]
wrote [read port]
lookup [open port]
connect [
spec: parent/spec
extra/key: enbase/part checksum form now/precise 'sha1 64 16
write port ajoin [
{GET } spec/path spec/target { HTTP/1.1} CRLF
{Host: } spec/host if spec/port [join #":" spec/port] CRLF
{Upgrade: websocket} CRLF
{Connection: Upgrade} CRLF
{Sec-WebSocket-Key: } extra/key CRLF
{Sec-WebSocket-Protocol: chat, superchat} CRLF
{Sec-WebSocket-Version: 13} CRLF
CRLF
]
extra/key: enbase checksum join extra/key "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" 'sha1 64
]
][true]
]
]
sys/make-scheme [
name: 'ws
title: "Websocket"
spec: make system/standard/port-spec-net []
awake: func [event /local port ctx raw temp] [
;; This is just a default awake handler...
;; one may want to redefine it for a real life use!
port: event/port
ctx: port/extra
raw: ctx/buffer ;; used to store unprocessed raw data
sys/log/more 'WS ["== WS-event:" as-red event/type port/spec/ref]
switch event/type [
read [
sys/log/debug 'WS ["== raw-data:" as-blue mold/flat/part raw 100]
if empty? temp: ws-decode raw [
sys/log/debug 'WS "data not complete..."
read port ;; keep reading...
return false ;; don't wake up yet...
]
;; there may be more then one message in the decoded data
foreach [fin op data] temp [
;; store only decoded messages...
if fin [append port/data data] ;@@ what if the data are just partial (not final)?
]
sys/log/more 'WS ["Have" as-yellow length? port/data "messages."]
;?? port/data
;insert system/ports/system make event! [ type: 'read port: parent ]
]
wrote [
;; don't wake up and instead wait for a response...
read port
return false
]
connect [
;; optional validation of response headers
sys/log/debug 'WS ["Connect response:" mold/flat ctx/fields]
]
error [
sys/log/info 'WS "Closing..."
try [close ctx/connection]
;wait port/extra/connection
]
]
true
]
actor: [
open: func [port [port!] /local spec host conn port-spec][
spec: port/spec
port/extra: context [
connection:
key:
handshake:
fields: none
buffer: make binary! 200 ;; used to hold undecoded raw websocket data
]
port/data: copy [] ;; used to hold decoeded packets
;; `ref` is used in logging and errors
conn: make port/spec [ref: none]
conn/scheme: 'tcp
port-spec: if spec/port [join #":" spec/port]
conn/ref: as url! ajoin [conn/scheme "://" spec/host port-spec]
unless url? spec/ref [
spec/ref: as url! ajoin ["ws://" spec/host port-spec spec/path spec/target]
]
port/extra/connection: conn: make port! conn
conn/parent: port
conn/awake: :ws-conn-awake
open conn
port
]
open?: func[port /local ctx][
all [
ctx: port/extra
ctx/handshake
open? ctx/connection
]
]
close: func[port][
close port/extra/connection
]
write: func[port data][
sys/log/debug 'WS ["Write:" as-green mold/flat data]
either open? port [
write port/extra/connection ws-encode data
][ sys/log/error 'WS "Not open!"]
]
read: func[port][
either open? port [
read port/extra/connection
][ sys/log/error 'WS "Not open!"]
]
]
]