Skip to content

Commit bae6cff

Browse files
committed
FEAT: Minimalistic WebSocket support in the httpd scheme
1 parent f96f3c6 commit bae6cff

File tree

3 files changed

+319
-38
lines changed

3 files changed

+319
-38
lines changed

src/modules/httpd.reb

+169-29
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,9 @@ Rebol [
3434
10-May-2020 "Oldes" {Implemented directory listing, logging and multipart POST processing}
3535
02-Jul-2020 "Oldes" {Added possibility to stop server and return data from client (useful for OAuth2)}
3636
]
37+
needs: [mime-types]
3738
]
3839

39-
import 'mime-types
40-
4140
append system/options/log [httpd: 1]
4241

4342
;------------------------------------------------------------------------
@@ -343,6 +342,21 @@ sys/make-scheme [
343342
]
344343
]
345344

345+
On-Read-Websocket: func[
346+
"Process READ action on client's port using websocket"
347+
ctx [object!]
348+
final? [logic!] "Indicates that this is the final fragment in a message."
349+
opcode [integer!] "Defines the interpretation of the 'Payload data'."
350+
][
351+
;@@ this is just a placeholder!
352+
]
353+
On-Close-Websocket: func[
354+
"Process READ action on client's port using websocket"
355+
ctx [object!] code [integer!]
356+
][
357+
;@@ this is just a placeholder!
358+
]
359+
346360
On-List-Dir: func[
347361
ctx [object!] target [object!]
348362
/local path dir out size date files dirs
@@ -399,10 +413,25 @@ sys/make-scheme [
399413
sys/log/more 'HTTPD ["Target not found:^[[1m" mold target/file]
400414
ctx/out/status: 404
401415
]
416+
417+
WS-handshake: func[ctx /local key][
418+
if all [
419+
"websocket" = select ctx/inp/header 'Upgrade
420+
key: select ctx/inp/header 'Sec-WebSocket-Key
421+
][
422+
ctx/out/status: 101
423+
ctx/out/header/Upgrade: "websocket"
424+
ctx/out/header/Connection: "Upgrade"
425+
ctx/out/header/Sec-WebSocket-Accept: enbase checksum join key "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" 'sha1 64
426+
;? ctx/out/header
427+
;ctx/out/content: ""
428+
]
429+
]
402430
]
403431

404432
Status-Codes: make map! [
405433
100 "Continue"
434+
101 "Switching Protocols"
406435
200 "OK"
407436
201 "Created"
408437
202 "Accepted"
@@ -471,38 +500,44 @@ sys/make-scheme [
471500
buffer: make binary! 1024
472501
append buffer ajoin ["HTTP/" ctx/inp/version #" " out/status #" " status-codes/(out/status) CRLF]
473502

474-
unless out/header/Content-Type [
475-
if out/target [
476-
out/header/Content-Type: mime-type? out/target
477-
]
478-
if all [
479-
none? out/header/Content-Type ; no mime found above
480-
string? out/content
481-
][
482-
out/header/Content-Type: "text/html; charset=UTF-8"
483-
]
484-
]
503+
either "websocket" = out/header/upgrade [
504+
ctx/inp/method: "websocket"
505+
try [ctx/inp/version: to integer! ctx/inp/header/Sec-WebSocket-Version]
506+
port/awake: :Awake-Websocket
507+
][
508+
unless out/header/Content-Type [
509+
if out/target [
510+
out/header/Content-Type: mime-type? out/target
511+
]
512+
if all [
513+
none? out/header/Content-Type ; no mime found above
514+
string? out/content
515+
][
516+
out/header/Content-Type: "text/html; charset=UTF-8"
517+
]
518+
]
485519

486-
out/header/Content-Length: either out/content [
487-
if string? out/content [
488-
; must be converted to binary to have proper length if not ascii
489-
out/content: to binary! out/content
520+
out/header/Content-Length: either out/content [
521+
if string? out/content [
522+
; must be converted to binary to have proper length if not ascii
523+
out/content: to binary! out/content
524+
]
525+
length? out/content
526+
][
527+
0
490528
]
491-
length? out/content
492-
][
493-
0
494-
]
495529

496-
if keep-alive: ctx/config/keep-alive [
497-
if logic? keep-alive [
498-
; using defaults
499-
ctx/config/keep-alive:
500-
keep-alive: [15 100] ; [timeout max-requests]
530+
if keep-alive: ctx/config/keep-alive [
531+
if logic? keep-alive [
532+
; using defaults
533+
ctx/config/keep-alive:
534+
keep-alive: [15 100] ; [timeout max-requests]
535+
]
536+
ctx/out/header/Connection: "keep-alive"
537+
ctx/out/header/Keep-Alive: ajoin ["timeout=" keep-alive/1 ", max=" keep-alive/2]
501538
]
502-
ctx/out/header/Connection: "keep-alive"
503-
ctx/out/header/Keep-Alive: ajoin ["timeout=" keep-alive/1 ", max=" keep-alive/2]
539+
out/header/Server: ctx/config/server-name
504540
]
505-
out/header/Server: ctx/config/server-name
506541

507542
;probe out/header
508543
foreach [name value] out/header [
@@ -680,6 +715,111 @@ sys/make-scheme [
680715
true
681716
]
682717

718+
Awake-Websocket: function [
719+
event [event!]
720+
][
721+
port: event/port
722+
ctx: port/extra
723+
724+
sys/log/more 'HTTPD ["Awake Websocket:^[[1m" ctx/remote "^[[22m" event/type]
725+
726+
ctx/timeout: now + 0:0:30
727+
728+
switch event/type [
729+
READ [
730+
ready?: false
731+
data: head port/data
732+
sys/log/more 'HTTPD ["bytes:^[[1m" length? data]
733+
try/except [
734+
while [2 < length? data][
735+
final?: data/1 & 128 = 128
736+
opcode: data/1 & 15
737+
mask?: data/2 & 128 = 128
738+
len: data/2 & 127
739+
data: skip data 2
740+
;? final? ? opcode ? len
741+
case [
742+
len = 126 [
743+
if 2 >= length? data [break]
744+
len: binary/read data 'UI16
745+
data: skip data 2
746+
]
747+
len = 127 [
748+
if 8 >= length? data [break]
749+
len: binary/read data 'UI64
750+
data: skip data 8
751+
]
752+
]
753+
if (4 + length? data) < len [break]
754+
remove/part head data data
755+
data: head data
756+
either mask? [
757+
request-data: make binary! len
758+
masks: take/part data 4
759+
payload: take/part data len
760+
while [not tail? payload][
761+
append request-data masks xor take/part payload 4
762+
]
763+
][
764+
request-data: take/part data len
765+
]
766+
ready?: true
767+
clear skip request-data len
768+
ctx/inp/content: request-data
769+
if opcode = 8 [
770+
sys/log/more 'HTTPD "WS Connection Close Frame!"
771+
code: 0
772+
if all [
773+
2 <= len
774+
2 <= length? request-data
775+
][
776+
code: to integer! take/part request-data 2
777+
sys/log/more 'HTTPD ["WS Close reason:" as-red code]
778+
]
779+
actor/On-Close-Websocket ctx code
780+
event/type: 'CLOSE
781+
Awake-Websocket event
782+
exit
783+
]
784+
actor/On-Read-Websocket ctx final? opcode
785+
]
786+
][
787+
print system/state/last-error
788+
]
789+
either ready? [
790+
;; there was complete input...
791+
write port either all [
792+
series? content: ctx/out/content
793+
not empty? content
794+
][
795+
content: to binary! content
796+
clear ctx/out/content
797+
len: length? content
798+
;print len
799+
;prin "out: " ? content
800+
bin: binary len
801+
binary/write bin case [
802+
len < 127 [ [UI8 129 UI8 :len :content] ]
803+
all [ len > 126 len <= 65535 ][ [UI8 129 UI8 126 UI16 :len :content] ]
804+
len > 65535 [ [UI8 129 UI8 127 UI64 :len :content] ]
805+
]
806+
head bin/buffer
807+
][ "" ]
808+
][
809+
;; needs more data!
810+
read port
811+
]
812+
]
813+
WROTE [
814+
read port
815+
]
816+
CLOSE [
817+
sys/log/info 'HTTPD ["Closing:^[[22m" ctx/remote]
818+
if pos: find ctx/parent/extra/clients port [ remove pos ]
819+
close port
820+
]
821+
]
822+
]
683823

684824

685825
New-Client: func[port [port!] /local client info err][

src/tests/httpd-root/websocket.html

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>WebSocket Test Page</title>
5+
<script>
6+
var socket = null;
7+
var msg = "Thank you for accepting this Web Socket request.";
8+
var timerID = 0;
9+
const timeout = 20000;
10+
11+
var log = function(s) {
12+
if (document.readyState !== "complete") {
13+
log.buffer.push(s);
14+
} else {
15+
document.getElementById("output").innerHTML += (s + "\n")
16+
}
17+
}
18+
log.buffer = [];
19+
20+
function connect() {
21+
url = "ws://localhost:8081/echo";
22+
socket = new WebSocket(url);
23+
socket.onopen = function() {
24+
log("open");
25+
socket.send(msg);
26+
}
27+
socket.onmessage = function(e) {
28+
if(e.data != '') log(e.data);
29+
}
30+
socket.onclose = function(e) {
31+
cancelKeepAlive();
32+
log("closed");
33+
}
34+
keepAlive();
35+
}
36+
37+
function keepAlive() {
38+
// sends empty message each 20s to keep the connection open
39+
if (socket.readyState == socket.OPEN) {
40+
socket.send('');
41+
}
42+
timerId = setTimeout(keepAlive, timeout);
43+
}
44+
function cancelKeepAlive() {
45+
if (timerId) {
46+
clearTimeout(timerId);
47+
timerId = 0;
48+
}
49+
}
50+
function resetKeepAlive() {
51+
if (timerId) { clearTimeout(timerId); }
52+
timerId = setTimeout(keepAlive, timeout);
53+
}
54+
55+
window.onload = function() {
56+
log(log.buffer.join("\n"));
57+
connect();
58+
document.getElementById("sendButton").onclick = function() {
59+
const value = document.getElementById("inputMessage").value;
60+
if(socket == null || socket.readyState >= socket.CLOSING) {
61+
msg = value;
62+
connect();
63+
}
64+
else if (socket.readyState == socket.CONNECTING) {
65+
msg = value;
66+
} else {
67+
socket.send(value);
68+
}
69+
resetKeepAlive();
70+
}
71+
document.getElementById("closeButton").onclick = function() {
72+
socket.close(1000, "I'm done!");
73+
}
74+
document.getElementById("inputMessage").addEventListener("keypress", function(event) {
75+
if (event.key === "Enter") {
76+
event.preventDefault();
77+
document.getElementById("sendButton").click();
78+
}
79+
});
80+
}
81+
</script>
82+
</head>
83+
<body>
84+
<input type="text" id="inputMessage" value="Hello, Web Socket!">
85+
<button id="sendButton">Send</button>
86+
<button id="closeButton">Close</button>
87+
<pre id="output"></pre>
88+
</body>
89+
</html>
90+
91+
92+

0 commit comments

Comments
 (0)