/
P6W.pm6
167 lines (131 loc) · 4.7 KB
/
P6W.pm6
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
use v6;
unit class WebSocket::P6W;
use MIME::Base64;
use WebSocket::Handle;
use WebSocket::Handshake;
use WebSocket::Frame::Grammar;
constant WS_DEBUG=so %*ENV<WS_DEBUG>;
sub debug($msg) {
say "WS:S: [DEBUG] $msg";
}
sub ws-psgi(%env, Callable :$on-ready, Callable :$on-text, Callable :$on-binary, Callable :$on-close) is export {
# use socket directly is bad idea. But HTTP/2 deprecates `connection: upgrade`. Then, this code may not
# break on feature HTTP updates.
my $sock = %env<p6wx.io>;
debug(%env.perl) if WS_DEBUG;
die 'no p6wx.io in psgi env' unless $sock;
die 'p6wx.io must contain instance of IO::Socket::Async' unless $sock ~~ IO::Socket::Async;
unless %env<HTTP_UPGRADE> ~~ 'websocket' {
warn 'no upgrade header in HTTP request';
return bad-request
}
unless %env<HTTP_SEC_WEBSOCKET_VERSION> ~~ /^\d+$/ {
warn "invalid websocket version... we don't support draft version of websocket.";
return bad-request
}
my $ws-key = %env<HTTP_SEC_WEBSOCKET_KEY>;
unless $ws-key {
warn 'no HTTP_SEC_WEBSOCKET_KEY';
return bad-request
}
my $accept = make-sec-websocket-accept($ws-key);
debug 'return 101' if WS_DEBUG;
return 101, [
Connection => 'Upgrade',
Upgrade => 'websocket',
Sec-WebSocket-Accept => $accept,
], supply {
debug("handshake succeeded") if WS_DEBUG;
my $handle = WebSocket::Handle.new(socket => $sock);
$on-ready($handle);
my $buf;
whenever $sock.Supply(:bin) -> $got {
$buf ~= $got.decode('latin1');
loop {
my $m = WebSocket::Frame::Grammar.subparse($buf);
if $m {
my $frame = $m.made;
debug "got frame {$frame.opcode}, {$frame.fin.Str}" if WS_DEBUG;
$buf = $buf.substr($m.to);
given $frame.opcode {
when (WebSocket::Frame::TEXT) {
debug "got text frame" if WS_DEBUG;
$on-text($handle, $frame.payload.encode('latin1').decode('utf-8')) if $on-text;
}
when (WebSocket::Frame::BINARY) {
debug "got binary frame" if WS_DEBUG;
$on-binary($handle, $frame.payload) if $on-binary;
}
when (WebSocket::Frame::DOCLOSE) {
debug "got close frame" if WS_DEBUG;
$on-close($handle);
try $handle.close;
done;
}
when (WebSocket::Frame::PING) {
debug "got ping frame" if WS_DEBUG;
$handle.pong;
}
when (WebSocket::Frame::PONG) {
debug "got pong frame" if WS_DEBUG;
# nop
}
default {
debug "GOT $_";
}
}
} else {
# maybe, frame is partial. maybe...
debug 'frame is partial' if WS_DEBUG;
last;
}
};
CATCH { default {
say $_;
%env<p6w.errors>.print: "error in websocket processing: $_\n{.backtrace.full}";
done;
} }
};
(); # on requires a callable that returns a list of pairs with Supply keys
};
}
sub internal-server-error {
return 500, [], ['Internal Server Error'];
}
sub bad-request() {
return 400, [], ['Bad Request'];
}
=begin pod
=head1 NAME
WebSocket::P6W - P6W utility for WebSocket
=head1 SYNOPSIS
=begin code
use HTTP::Server::Tiny;
use WebSocket::P6W;
-> %env {
ws-psgi(%env,
on-ready => -> $ws {
$ws.send('hoge');
},
on-text => -> $ws, $txt {
$ws.send-text(uc $txt);
if $txt eq 'quit' {
$ws.close();
}
},
on-binary => -> $ws, $binary {
$ws.send-binary($binary);
},
on-close => -> $ws {
say "closing socket";
},
);
}
=end code
=head1 DESCRIPTION
This module provides utility functions to create P6W application.
=head1 FUNCTIONS
=item C<ws-psgi(%env, Callable :$on-ready, Callable :$on-text, Callable :$on-binary, Callable :$on-close)>
Create new P6W application from arguments.
You can pass return value to P6W server.
=end pod