File Coverage

blib/lib/Hypersonic/UA/WebSocket.pm
Criterion Covered Total %
statement 79 85 92.9
branch n/a
condition 1 2 50.0
subroutine 25 27 92.5
pod 0 19 0.0
total 105 133 78.9


line stmt bran cond sub pod time code
1             package Hypersonic::UA::WebSocket;
2              
3 1     1   206027 use strict;
  1         2  
  1         43  
4 1     1   3 use warnings;
  1         2  
  1         49  
5 1     1   12 use 5.010;
  1         4  
6              
7             our $VERSION = '0.15';
8              
9 1     1   4 use constant MAX_WS_CLIENT_CONNS => 1024;
  1         1  
  1         58  
10              
11             use constant {
12 1         69 STATE_CONNECTING => 0,
13             STATE_OPEN => 1,
14             STATE_CLOSING => 2,
15             STATE_CLOSED => 3,
16 1     1   4 };
  1         4  
17              
18             use constant {
19 1         60 OP_CONTINUATION => 0x00,
20             OP_TEXT => 0x01,
21             OP_BINARY => 0x02,
22             OP_CLOSE => 0x08,
23             OP_PING => 0x09,
24             OP_PONG => 0x0A,
25 1     1   4 };
  1         1  
26              
27             use constant {
28 1         2077 SLOT_FD => 0,
29             SLOT_UA => 1,
30             SLOT_URL => 2,
31             SLOT_CALLBACKS => 3,
32             SLOT_PROTOCOLS => 4,
33 1     1   3 };
  1         1  
34              
35             sub generate_c_code {
36 1     1 0 9115 my ($class, $builder, $opts) = @_;
37              
38 1   50     6 my $max_conns = $opts->{max_ws_client_conns} // MAX_WS_CLIENT_CONNS;
39              
40 1         19 $builder->line('#include ')
41             ->line('#include ')
42             ->line('#include ')
43             ->line('#include ')
44             ->line('#include ')
45             ->line('#include ')
46             ->line('#include ')
47             ->line('#include ')
48             ->line('#include ')
49             ->line('#include ')
50             ->line('#include ')
51             ->blank;
52              
53 1         3 $class->gen_websocket_registry($builder, $max_conns);
54 1         3 $class->gen_base64_codec($builder);
55 1         3 $class->gen_frame_encoder($builder);
56 1         5 $class->gen_frame_decoder($builder);
57 1         3 $class->gen_xs_new($builder);
58 1         2 $class->gen_xs_connect($builder);
59 1         3 $class->gen_xs_send($builder);
60 1         3 $class->gen_xs_send_binary($builder);
61 1         3 $class->gen_xs_ping($builder);
62 1         3 $class->gen_xs_pong($builder);
63 1         2 $class->gen_xs_close($builder);
64 1         10 $class->gen_xs_recv_frame($builder);
65 1         3 $class->gen_xs_state($builder);
66 1         2 $class->gen_xs_is_open($builder);
67 1         4 $class->gen_xs_fd($builder);
68 1         2 $class->gen_xs_cleanup($builder);
69             }
70              
71             sub get_xs_functions {
72             return {
73 1     1 0 9478 'Hypersonic::UA::WebSocket::new' => { source => 'xs_ws_client_new', is_xs_native => 1 },
74             'Hypersonic::UA::WebSocket::connect' => { source => 'xs_ws_client_connect', is_xs_native => 1 },
75             'Hypersonic::UA::WebSocket::send' => { source => 'xs_ws_client_send', is_xs_native => 1 },
76             'Hypersonic::UA::WebSocket::send_binary' => { source => 'xs_ws_client_send_binary', is_xs_native => 1 },
77             'Hypersonic::UA::WebSocket::ping' => { source => 'xs_ws_client_ping', is_xs_native => 1 },
78             'Hypersonic::UA::WebSocket::pong' => { source => 'xs_ws_client_pong', is_xs_native => 1 },
79             'Hypersonic::UA::WebSocket::close' => { source => 'xs_ws_client_close', is_xs_native => 1 },
80             'Hypersonic::UA::WebSocket::recv_frame' => { source => 'xs_ws_client_recv_frame', is_xs_native => 1 },
81             'Hypersonic::UA::WebSocket::state' => { source => 'xs_ws_client_state', is_xs_native => 1 },
82             'Hypersonic::UA::WebSocket::is_open' => { source => 'xs_ws_client_is_open', is_xs_native => 1 },
83             'Hypersonic::UA::WebSocket::fd' => { source => 'xs_ws_client_fd', is_xs_native => 1 },
84             'Hypersonic::UA::WebSocket::cleanup' => { source => 'xs_ws_client_cleanup', is_xs_native => 1 },
85             };
86             }
87              
88             sub gen_websocket_registry {
89 1     1 0 2 my ($class, $builder, $max_conns) = @_;
90              
91 1         17 $builder->line("#define MAX_WS_CLIENT_CONNS $max_conns")
92             ->line('#define WS_STATE_CONNECTING 0')
93             ->line('#define WS_STATE_OPEN 1')
94             ->line('#define WS_STATE_CLOSING 2')
95             ->line('#define WS_STATE_CLOSED 3')
96             ->blank
97             ->line('#define WS_OP_CONTINUATION 0x00')
98             ->line('#define WS_OP_TEXT 0x01')
99             ->line('#define WS_OP_BINARY 0x02')
100             ->line('#define WS_OP_CLOSE 0x08')
101             ->line('#define WS_OP_PING 0x09')
102             ->line('#define WS_OP_PONG 0x0A')
103             ->blank
104             ->line('#define WS_GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"')
105             ->blank;
106              
107 1         8 $builder->line('typedef struct {')
108             ->line(' int fd;')
109             ->line(' SSL *ssl;')
110             ->line(' int state;')
111             ->line(' uint16_t close_code;')
112             ->line(' char *recv_buffer;')
113             ->line(' size_t recv_buffer_len;')
114             ->line(' size_t recv_buffer_cap;')
115             ->line(' int in_use;')
116             ->line('} WSClientConnection;')
117             ->blank;
118              
119 1         5 $builder->line("static WSClientConnection ws_client_registry[MAX_WS_CLIENT_CONNS];")
120             ->blank;
121              
122 1         23 $builder->line('static int ws_client_alloc_slot(void) {')
123             ->line(' int i;')
124             ->line(' for (i = 0; i < MAX_WS_CLIENT_CONNS; i++) {')
125             ->line(' if (!ws_client_registry[i].in_use) {')
126             ->line(' memset(&ws_client_registry[i], 0, sizeof(WSClientConnection));')
127             ->line(' ws_client_registry[i].in_use = 1;')
128             ->line(' ws_client_registry[i].fd = -1;')
129             ->line(' ws_client_registry[i].state = WS_STATE_CONNECTING;')
130             ->line(' return i;')
131             ->line(' }')
132             ->line(' }')
133             ->line(' return -1;')
134             ->line('}')
135             ->blank;
136              
137 1         7 $builder->line('static void ws_client_free_slot(int slot) {')
138             ->line(' if (slot < 0 || slot >= MAX_WS_CLIENT_CONNS) return;')
139             ->line(' WSClientConnection *conn = &ws_client_registry[slot];')
140             ->line(' if (conn->recv_buffer) { free(conn->recv_buffer); conn->recv_buffer = NULL; }')
141             ->line(' if (conn->ssl) { SSL_shutdown(conn->ssl); SSL_free(conn->ssl); conn->ssl = NULL; }')
142             ->line(' if (conn->fd >= 0) { close(conn->fd); conn->fd = -1; }')
143             ->line(' conn->in_use = 0;')
144             ->line('}')
145             ->blank;
146             }
147              
148             sub gen_base64_codec {
149 1     1 0 2 my ($class, $builder) = @_;
150              
151 1         3 $builder->line('static const char base64_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";')
152             ->blank;
153              
154 1         23 $builder->line('static size_t ws_base64_encode(const unsigned char *in, size_t in_len, char *out) {')
155             ->line(' size_t i, j;')
156             ->line(' for (i = 0, j = 0; i < in_len; ) {')
157             ->line(' uint32_t octet_a = i < in_len ? in[i++] : 0;')
158             ->line(' uint32_t octet_b = i < in_len ? in[i++] : 0;')
159             ->line(' uint32_t octet_c = i < in_len ? in[i++] : 0;')
160             ->line(' uint32_t triple = (octet_a << 16) | (octet_b << 8) | octet_c;')
161             ->line(' out[j++] = base64_chars[(triple >> 18) & 0x3F];')
162             ->line(' out[j++] = base64_chars[(triple >> 12) & 0x3F];')
163             ->line(' out[j++] = base64_chars[(triple >> 6) & 0x3F];')
164             ->line(' out[j++] = base64_chars[triple & 0x3F];')
165             ->line(' }')
166             ->line(' int mod = in_len % 3;')
167             ->line(' if (mod > 0) {')
168             ->line(' out[j - 1] = \'=\';')
169             ->line(' if (mod == 1) out[j - 2] = \'=\';')
170             ->line(' }')
171             ->line(' out[j] = \'\\0\';')
172             ->line(' return j;')
173             ->line('}')
174             ->blank;
175             }
176              
177             sub gen_frame_encoder {
178 1     1 0 2 my ($class, $builder) = @_;
179              
180 1         34 $builder->line('static size_t ws_client_encode_frame(unsigned char *out, const unsigned char *data, size_t data_len, int opcode, int fin) {')
181             ->line(' size_t offset = 0;')
182             ->line(' size_t i;')
183             ->blank
184             ->line(' out[offset++] = (fin ? 0x80 : 0x00) | (opcode & 0x0F);')
185             ->blank
186             ->line(' if (data_len < 126) {')
187             ->line(' out[offset++] = 0x80 | data_len;')
188             ->line(' } else if (data_len < 65536) {')
189             ->line(' out[offset++] = 0x80 | 126;')
190             ->line(' out[offset++] = (data_len >> 8) & 0xFF;')
191             ->line(' out[offset++] = data_len & 0xFF;')
192             ->line(' } else {')
193             ->line(' out[offset++] = 0x80 | 127;')
194             ->line(' out[offset++] = 0; out[offset++] = 0;')
195             ->line(' out[offset++] = 0; out[offset++] = 0;')
196             ->line(' out[offset++] = (data_len >> 24) & 0xFF;')
197             ->line(' out[offset++] = (data_len >> 16) & 0xFF;')
198             ->line(' out[offset++] = (data_len >> 8) & 0xFF;')
199             ->line(' out[offset++] = data_len & 0xFF;')
200             ->line(' }')
201             ->blank
202             ->line(' unsigned char mask[4];')
203             ->line(' RAND_bytes(mask, 4);')
204             ->line(' memcpy(out + offset, mask, 4);')
205             ->line(' offset += 4;')
206             ->blank
207             ->line(' for (i = 0; i < data_len; i++) {')
208             ->line(' out[offset + i] = data[i] ^ mask[i & 3];')
209             ->line(' }')
210             ->blank
211             ->line(' return offset + data_len;')
212             ->line('}')
213             ->blank;
214              
215 1         5 $builder->line('static size_t ws_client_frame_size(size_t data_len) {')
216             ->line(' if (data_len < 126) return 2 + 4 + data_len;')
217             ->line(' if (data_len < 65536) return 4 + 4 + data_len;')
218             ->line(' return 10 + 4 + data_len;')
219             ->line('}')
220             ->blank;
221             }
222              
223             sub gen_frame_decoder {
224 1     1 0 1 my ($class, $builder) = @_;
225              
226 1         10 $builder->line('typedef struct {')
227             ->line(' int fin;')
228             ->line(' int opcode;')
229             ->line(' unsigned char *payload;')
230             ->line(' size_t payload_len;')
231             ->line(' size_t consumed;')
232             ->line('} WSFrame;')
233             ->blank;
234              
235 1         47 $builder->line('static int ws_client_decode_frame(const unsigned char *buf, size_t buf_len, WSFrame *frame) {')
236             ->line(' size_t i;')
237             ->line(' if (buf_len < 2) return 0;')
238             ->blank
239             ->line(' frame->fin = (buf[0] >> 7) & 1;')
240             ->line(' frame->opcode = buf[0] & 0x0F;')
241             ->line(' int masked = (buf[1] >> 7) & 1;')
242             ->line(' size_t payload_len = buf[1] & 0x7F;')
243             ->line(' size_t header_len = 2;')
244             ->blank
245             ->line(' if (payload_len == 126) {')
246             ->line(' if (buf_len < 4) return 0;')
247             ->line(' payload_len = ((size_t)buf[2] << 8) | buf[3];')
248             ->line(' header_len = 4;')
249             ->line(' } else if (payload_len == 127) {')
250             ->line(' if (buf_len < 10) return 0;')
251             ->line(' payload_len = ((size_t)buf[6] << 24) | ((size_t)buf[7] << 16) |')
252             ->line(' ((size_t)buf[8] << 8) | (size_t)buf[9];')
253             ->line(' header_len = 10;')
254             ->line(' }')
255             ->blank
256             ->line(' unsigned char *mask_key = NULL;')
257             ->line(' if (masked) {')
258             ->line(' if (buf_len < header_len + 4) return 0;')
259             ->line(' mask_key = (unsigned char *)buf + header_len;')
260             ->line(' header_len += 4;')
261             ->line(' }')
262             ->blank
263             ->line(' if (buf_len < header_len + payload_len) return 0;')
264             ->blank
265             ->line(' frame->payload = (unsigned char *)buf + header_len;')
266             ->line(' frame->payload_len = payload_len;')
267             ->line(' frame->consumed = header_len + payload_len;')
268             ->blank
269             ->line(' if (masked && payload_len > 0) {')
270             ->line(' for (i = 0; i < payload_len; i++) {')
271             ->line(' frame->payload[i] ^= mask_key[i & 3];')
272             ->line(' }')
273             ->line(' }')
274             ->blank
275             ->line(' return 1;')
276             ->line('}')
277             ->blank;
278             }
279              
280             sub gen_xs_new {
281 1     1 0 4 my ($class, $builder) = @_;
282              
283 1         20 $builder->comment('Create new WebSocket client object')
284             ->xs_function('xs_ws_client_new')
285             ->xs_preamble
286             ->line('if (items < 3) croak("Usage: Hypersonic::UA::WebSocket->new(ua, url)");')
287             ->blank
288             ->line('int slot = ws_client_alloc_slot();')
289             ->line('if (slot < 0) croak("Too many WebSocket connections");')
290             ->blank
291             ->line('AV *self = newAV();')
292             ->line('av_extend(self, 4);')
293             ->line('av_store(self, 0, newSViv(slot));')
294             ->line('av_store(self, 1, SvREFCNT_inc(ST(1)));')
295             ->line('av_store(self, 2, SvREFCNT_inc(ST(2)));')
296             ->line('av_store(self, 3, (SV *)newHV());')
297             ->line('av_store(self, 4, &PL_sv_undef);')
298             ->blank
299             ->line('SV *rv = newRV_noinc((SV *)self);')
300             ->line('sv_bless(rv, gv_stashpv("Hypersonic::UA::WebSocket", GV_ADD));')
301             ->line('ST(0) = sv_2mortal(rv);')
302             ->xs_return('1')
303             ->xs_end
304             ->blank;
305             }
306              
307             sub gen_xs_connect {
308 1     1 0 2 my ($class, $builder) = @_;
309              
310 1         93 $builder->comment('Connect WebSocket client')
311             ->xs_function('xs_ws_client_connect')
312             ->xs_preamble
313             ->line('if (items != 1) croak("Usage: $ws->connect()");')
314             ->blank
315             ->line('AV *self = (AV *)SvRV(ST(0));')
316             ->line('int slot = SvIV(*av_fetch(self, 0, 0));')
317             ->line('SV *url_sv = *av_fetch(self, 2, 0);')
318             ->line('STRLEN url_len;')
319             ->line('const char *url = SvPV(url_sv, url_len);')
320             ->blank
321             ->line('WSClientConnection *conn = &ws_client_registry[slot];')
322             ->blank
323             ->line('int tls = 0;')
324             ->line('const char *host_start;')
325             ->line('if (strncmp(url, "wss://", 6) == 0) { tls = 1; host_start = url + 6; }')
326             ->line('else if (strncmp(url, "ws://", 5) == 0) { host_start = url + 5; }')
327             ->line('else croak("Invalid WebSocket URL scheme");')
328             ->blank
329             ->line('char host[256];')
330             ->line('int port = tls ? 443 : 80;')
331             ->line('char path[1024] = "/";')
332             ->blank
333             ->line('const char *port_start = strchr(host_start, \':\');')
334             ->line('const char *path_start = strchr(host_start, \'/\');')
335             ->line('if (!path_start) path_start = host_start + strlen(host_start);')
336             ->blank
337             ->line('if (port_start && port_start < path_start) {')
338             ->line(' size_t host_len = port_start - host_start;')
339             ->line(' strncpy(host, host_start, host_len);')
340             ->line(' host[host_len] = \'\\0\';')
341             ->line(' port = atoi(port_start + 1);')
342             ->line('} else {')
343             ->line(' size_t host_len = path_start - host_start;')
344             ->line(' strncpy(host, host_start, host_len);')
345             ->line(' host[host_len] = \'\\0\';')
346             ->line('}')
347             ->line('if (*path_start == \'/\') strncpy(path, path_start, sizeof(path) - 1);')
348             ->blank
349             ->line('struct addrinfo hints = {0}, *res;')
350             ->line('hints.ai_family = AF_INET;')
351             ->line('hints.ai_socktype = SOCK_STREAM;')
352             ->line('char port_str[16];')
353             ->line('snprintf(port_str, sizeof(port_str), "%d", port);')
354             ->blank
355             ->line('if (getaddrinfo(host, port_str, &hints, &res) != 0) croak("DNS lookup failed");')
356             ->blank
357             ->line('int fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);')
358             ->line('if (fd < 0) { freeaddrinfo(res); croak("Socket creation failed"); }')
359             ->line('if (connect(fd, res->ai_addr, res->ai_addrlen) < 0) { freeaddrinfo(res); close(fd); croak("Connect failed"); }')
360             ->line('freeaddrinfo(res);')
361             ->line('conn->fd = fd;')
362             ->blank
363             ->line('if (tls) {')
364             ->line(' SSL_CTX *ctx = SSL_CTX_new(TLS_client_method());')
365             ->line(' if (!ctx) croak("SSL_CTX_new failed");')
366             ->line(' SSL *ssl = SSL_new(ctx);')
367             ->line(' SSL_set_fd(ssl, fd);')
368             ->line(' SSL_set_tlsext_host_name(ssl, host);')
369             ->line(' if (SSL_connect(ssl) != 1) { SSL_free(ssl); SSL_CTX_free(ctx); close(fd); croak("TLS handshake failed"); }')
370             ->line(' conn->ssl = ssl;')
371             ->line(' SSL_CTX_free(ctx);')
372             ->line('}')
373             ->blank
374             ->line('unsigned char key_bytes[16];')
375             ->line('RAND_bytes(key_bytes, 16);')
376             ->line('char key_b64[32];')
377             ->line('ws_base64_encode(key_bytes, 16, key_b64);')
378             ->blank
379             ->line('char request[4096];')
380             ->line('int len = snprintf(request, sizeof(request),')
381             ->line(' "GET %s HTTP/1.1\\r\\nHost: %s\\r\\nUpgrade: websocket\\r\\nConnection: Upgrade\\r\\nSec-WebSocket-Key: %s\\r\\nSec-WebSocket-Version: 13\\r\\n\\r\\n",')
382             ->line(' path, host, key_b64);')
383             ->blank
384             ->line('ssize_t sent = conn->ssl ? SSL_write(conn->ssl, request, len) : send(fd, request, len, 0);')
385             ->line('if (sent != len) { ws_client_free_slot(slot); croak("Failed to send handshake"); }')
386             ->blank
387             ->line('char response[4096];')
388             ->line('ssize_t received = conn->ssl ? SSL_read(conn->ssl, response, sizeof(response) - 1) : recv(fd, response, sizeof(response) - 1, 0);')
389             ->line('if (received <= 0) { ws_client_free_slot(slot); croak("Failed to receive handshake"); }')
390             ->line('response[received] = \'\\0\';')
391             ->blank
392             ->line('if (strstr(response, "101") == NULL) { ws_client_free_slot(slot); croak("WebSocket handshake failed"); }')
393             ->blank
394             ->line('conn->recv_buffer_cap = 65536;')
395             ->line('conn->recv_buffer = (char *)malloc(conn->recv_buffer_cap);')
396             ->line('conn->recv_buffer_len = 0;')
397             ->line('conn->state = WS_STATE_OPEN;')
398             ->blank
399             ->line('ST(0) = sv_2mortal(newSViv(1));')
400             ->xs_return('1')
401             ->xs_end
402             ->blank;
403             }
404              
405             sub gen_xs_send {
406 1     1 0 2 my ($class, $builder) = @_;
407              
408 1         18 $builder->comment('Send text frame')
409             ->xs_function('xs_ws_client_send')
410             ->xs_preamble
411             ->line('if (items != 2) croak("Usage: $ws->send(data)");')
412             ->blank
413             ->line('AV *self = (AV *)SvRV(ST(0));')
414             ->line('int slot = SvIV(*av_fetch(self, 0, 0));')
415             ->line('WSClientConnection *conn = &ws_client_registry[slot];')
416             ->blank
417             ->line('if (conn->state != WS_STATE_OPEN) croak("WebSocket not connected");')
418             ->blank
419             ->line('STRLEN data_len;')
420             ->line('const unsigned char *data = (const unsigned char *)SvPV(ST(1), data_len);')
421             ->blank
422             ->line('size_t frame_size = ws_client_frame_size(data_len);')
423             ->line('unsigned char *frame = (unsigned char *)malloc(frame_size);')
424             ->line('size_t frame_len = ws_client_encode_frame(frame, data, data_len, WS_OP_TEXT, 1);')
425             ->blank
426             ->line('ssize_t sent = conn->ssl ? SSL_write(conn->ssl, frame, frame_len) : send(conn->fd, frame, frame_len, 0);')
427             ->line('free(frame);')
428             ->blank
429             ->line('if (sent != (ssize_t)frame_len) croak("Failed to send frame");')
430             ->blank
431             ->line('ST(0) = sv_2mortal(newSViv(1));')
432             ->xs_return('1')
433             ->xs_end
434             ->blank;
435             }
436              
437             sub gen_xs_send_binary {
438 1     1 0 1 my ($class, $builder) = @_;
439              
440 1         43 $builder->comment('Send binary frame')
441             ->xs_function('xs_ws_client_send_binary')
442             ->xs_preamble
443             ->line('if (items != 2) croak("Usage: $ws->send_binary(data)");')
444             ->blank
445             ->line('AV *self = (AV *)SvRV(ST(0));')
446             ->line('int slot = SvIV(*av_fetch(self, 0, 0));')
447             ->line('WSClientConnection *conn = &ws_client_registry[slot];')
448             ->blank
449             ->line('if (conn->state != WS_STATE_OPEN) croak("WebSocket not connected");')
450             ->blank
451             ->line('STRLEN data_len;')
452             ->line('const unsigned char *data = (const unsigned char *)SvPV(ST(1), data_len);')
453             ->blank
454             ->line('size_t frame_size = ws_client_frame_size(data_len);')
455             ->line('unsigned char *frame = (unsigned char *)malloc(frame_size);')
456             ->line('size_t frame_len = ws_client_encode_frame(frame, data, data_len, WS_OP_BINARY, 1);')
457             ->blank
458             ->line('ssize_t sent = conn->ssl ? SSL_write(conn->ssl, frame, frame_len) : send(conn->fd, frame, frame_len, 0);')
459             ->line('free(frame);')
460             ->blank
461             ->line('ST(0) = sv_2mortal(newSViv(sent == (ssize_t)frame_len ? 1 : 0));')
462             ->xs_return('1')
463             ->xs_end
464             ->blank;
465             }
466              
467             sub gen_xs_ping {
468 1     1 0 1 my ($class, $builder) = @_;
469              
470 1         30 $builder->comment('Send ping frame')
471             ->xs_function('xs_ws_client_ping')
472             ->xs_preamble
473             ->line('if (items < 1) croak("Usage: $ws->ping([data])");')
474             ->blank
475             ->line('AV *self = (AV *)SvRV(ST(0));')
476             ->line('int slot = SvIV(*av_fetch(self, 0, 0));')
477             ->line('WSClientConnection *conn = &ws_client_registry[slot];')
478             ->blank
479             ->line('if (conn->state != WS_STATE_OPEN) croak("WebSocket not connected");')
480             ->blank
481             ->line('STRLEN data_len = 0;')
482             ->line('const unsigned char *data = (const unsigned char *)"";')
483             ->line('if (items > 1 && SvOK(ST(1))) data = (const unsigned char *)SvPV(ST(1), data_len);')
484             ->blank
485             ->line('size_t frame_size = ws_client_frame_size(data_len);')
486             ->line('unsigned char *frame = (unsigned char *)malloc(frame_size);')
487             ->line('size_t frame_len = ws_client_encode_frame(frame, data, data_len, WS_OP_PING, 1);')
488             ->blank
489             ->line('ssize_t sent = conn->ssl ? SSL_write(conn->ssl, frame, frame_len) : send(conn->fd, frame, frame_len, 0);')
490             ->line('free(frame);')
491             ->blank
492             ->line('ST(0) = sv_2mortal(newSViv(sent == (ssize_t)frame_len ? 1 : 0));')
493             ->xs_return('1')
494             ->xs_end
495             ->blank;
496             }
497              
498             sub gen_xs_pong {
499 1     1 0 1 my ($class, $builder) = @_;
500              
501 1         18 $builder->comment('Send pong frame')
502             ->xs_function('xs_ws_client_pong')
503             ->xs_preamble
504             ->line('if (items < 1) croak("Usage: $ws->pong([data])");')
505             ->blank
506             ->line('AV *self = (AV *)SvRV(ST(0));')
507             ->line('int slot = SvIV(*av_fetch(self, 0, 0));')
508             ->line('WSClientConnection *conn = &ws_client_registry[slot];')
509             ->blank
510             ->line('if (conn->state != WS_STATE_OPEN) croak("WebSocket not connected");')
511             ->blank
512             ->line('STRLEN data_len = 0;')
513             ->line('const unsigned char *data = (const unsigned char *)"";')
514             ->line('if (items > 1 && SvOK(ST(1))) data = (const unsigned char *)SvPV(ST(1), data_len);')
515             ->blank
516             ->line('size_t frame_size = ws_client_frame_size(data_len);')
517             ->line('unsigned char *frame = (unsigned char *)malloc(frame_size);')
518             ->line('size_t frame_len = ws_client_encode_frame(frame, data, data_len, WS_OP_PONG, 1);')
519             ->blank
520             ->line('ssize_t sent = conn->ssl ? SSL_write(conn->ssl, frame, frame_len) : send(conn->fd, frame, frame_len, 0);')
521             ->line('free(frame);')
522             ->blank
523             ->line('ST(0) = sv_2mortal(newSViv(sent == (ssize_t)frame_len ? 1 : 0));')
524             ->xs_return('1')
525             ->xs_end
526             ->blank;
527             }
528              
529             sub gen_xs_close {
530 1     1 0 2 my ($class, $builder) = @_;
531              
532 1         31 $builder->comment('Send close frame')
533             ->xs_function('xs_ws_client_close')
534             ->xs_preamble
535             ->line('if (items < 1) croak("Usage: $ws->close([code], [reason])");')
536             ->blank
537             ->line('AV *self = (AV *)SvRV(ST(0));')
538             ->line('int slot = SvIV(*av_fetch(self, 0, 0));')
539             ->line('WSClientConnection *conn = &ws_client_registry[slot];')
540             ->blank
541             ->line('if (conn->state >= WS_STATE_CLOSING) {')
542             ->line(' ST(0) = sv_2mortal(newSViv(0));')
543             ->line(' XSRETURN(1);')
544             ->line('}')
545             ->blank
546             ->line('conn->state = WS_STATE_CLOSING;')
547             ->line('int code = (items > 1) ? SvIV(ST(1)) : 1000;')
548             ->line('conn->close_code = code;')
549             ->blank
550             ->line('unsigned char payload[128];')
551             ->line('payload[0] = (code >> 8) & 0xFF;')
552             ->line('payload[1] = code & 0xFF;')
553             ->line('size_t payload_len = 2;')
554             ->blank
555             ->line('if (items > 2 && SvOK(ST(2))) {')
556             ->line(' STRLEN reason_len;')
557             ->line(' const char *reason = SvPV(ST(2), reason_len);')
558             ->line(' if (reason_len > 123) reason_len = 123;')
559             ->line(' memcpy(payload + 2, reason, reason_len);')
560             ->line(' payload_len += reason_len;')
561             ->line('}')
562             ->blank
563             ->line('size_t frame_size = ws_client_frame_size(payload_len);')
564             ->line('unsigned char *frame = (unsigned char *)malloc(frame_size);')
565             ->line('size_t frame_len = ws_client_encode_frame(frame, payload, payload_len, WS_OP_CLOSE, 1);')
566             ->blank
567             ->line('ssize_t sent = conn->ssl ? SSL_write(conn->ssl, frame, frame_len) : send(conn->fd, frame, frame_len, 0);')
568             ->line('free(frame);')
569             ->blank
570             ->line('ST(0) = sv_2mortal(newSViv(sent > 0 ? 1 : 0));')
571             ->xs_return('1')
572             ->xs_end
573             ->blank;
574             }
575              
576             sub gen_xs_recv_frame {
577 1     1 0 1 my ($class, $builder) = @_;
578              
579 1         41 $builder->comment('Receive WebSocket frame')
580             ->xs_function('xs_ws_client_recv_frame')
581             ->xs_preamble
582             ->line('if (items != 1) croak("Usage: $ws->recv_frame()");')
583             ->blank
584             ->line('AV *self = (AV *)SvRV(ST(0));')
585             ->line('int slot = SvIV(*av_fetch(self, 0, 0));')
586             ->line('WSClientConnection *conn = &ws_client_registry[slot];')
587             ->blank
588             ->line('if (conn->state == WS_STATE_CLOSED) {')
589             ->line(' ST(0) = &PL_sv_undef;')
590             ->line(' XSRETURN(1);')
591             ->line('}')
592             ->blank
593             ->line('if (conn->recv_buffer_len < conn->recv_buffer_cap) {')
594             ->line(' ssize_t received = conn->ssl')
595             ->line(' ? SSL_read(conn->ssl, conn->recv_buffer + conn->recv_buffer_len, conn->recv_buffer_cap - conn->recv_buffer_len)')
596             ->line(' : recv(conn->fd, conn->recv_buffer + conn->recv_buffer_len, conn->recv_buffer_cap - conn->recv_buffer_len, MSG_DONTWAIT);')
597             ->line(' if (received > 0) conn->recv_buffer_len += received;')
598             ->line(' else if (received == 0) { conn->state = WS_STATE_CLOSED; ST(0) = &PL_sv_undef; XSRETURN(1); }')
599             ->line('}')
600             ->blank
601             ->line('WSFrame frame;')
602             ->line('if (!ws_client_decode_frame((unsigned char *)conn->recv_buffer, conn->recv_buffer_len, &frame)) {')
603             ->line(' ST(0) = &PL_sv_undef;')
604             ->line(' XSRETURN(1);')
605             ->line('}')
606             ->blank
607             ->line('memmove(conn->recv_buffer, conn->recv_buffer + frame.consumed, conn->recv_buffer_len - frame.consumed);')
608             ->line('conn->recv_buffer_len -= frame.consumed;')
609             ->blank
610             ->line('HV *result = newHV();')
611             ->line('hv_stores(result, "fin", newSViv(frame.fin));')
612             ->line('hv_stores(result, "opcode", newSViv(frame.opcode));')
613             ->line('hv_stores(result, "payload", newSVpvn((char *)frame.payload, frame.payload_len));')
614             ->blank
615             ->line('ST(0) = sv_2mortal(newRV_noinc((SV *)result));')
616             ->xs_return('1')
617             ->xs_end
618             ->blank;
619             }
620              
621             sub gen_xs_state {
622 1     1 0 1 my ($class, $builder) = @_;
623              
624 1         7 $builder->comment('Get connection state')
625             ->xs_function('xs_ws_client_state')
626             ->xs_preamble
627             ->line('if (items != 1) croak("Usage: $ws->state()");')
628             ->line('AV *self = (AV *)SvRV(ST(0));')
629             ->line('int slot = SvIV(*av_fetch(self, 0, 0));')
630             ->line('ST(0) = sv_2mortal(newSViv(ws_client_registry[slot].state));')
631             ->xs_return('1')
632             ->xs_end
633             ->blank;
634             }
635              
636             sub gen_xs_is_open {
637 1     1 0 1 my ($class, $builder) = @_;
638              
639 1         7 $builder->comment('Check if open')
640             ->xs_function('xs_ws_client_is_open')
641             ->xs_preamble
642             ->line('if (items != 1) croak("Usage: $ws->is_open()");')
643             ->line('AV *self = (AV *)SvRV(ST(0));')
644             ->line('int slot = SvIV(*av_fetch(self, 0, 0));')
645             ->line('ST(0) = ws_client_registry[slot].state == WS_STATE_OPEN ? &PL_sv_yes : &PL_sv_no;')
646             ->xs_return('1')
647             ->xs_end
648             ->blank;
649             }
650              
651             sub gen_xs_fd {
652 1     1 0 1 my ($class, $builder) = @_;
653              
654 1         28 $builder->comment('Get file descriptor')
655             ->xs_function('xs_ws_client_fd')
656             ->xs_preamble
657             ->line('if (items != 1) croak("Usage: $ws->fd()");')
658             ->line('AV *self = (AV *)SvRV(ST(0));')
659             ->line('int slot = SvIV(*av_fetch(self, 0, 0));')
660             ->line('ST(0) = sv_2mortal(newSViv(ws_client_registry[slot].fd));')
661             ->xs_return('1')
662             ->xs_end
663             ->blank;
664             }
665              
666             sub gen_xs_cleanup {
667 1     1 0 2 my ($class, $builder) = @_;
668              
669 1         11 $builder->comment('Cleanup connection')
670             ->xs_function('xs_ws_client_cleanup')
671             ->xs_preamble
672             ->line('if (items != 1) croak("Usage: $ws->cleanup()");')
673             ->line('AV *self = (AV *)SvRV(ST(0));')
674             ->line('int slot = SvIV(*av_fetch(self, 0, 0));')
675             ->line('ws_client_free_slot(slot);')
676             ->line('ST(0) = &PL_sv_yes;')
677             ->xs_return('1')
678             ->xs_end
679             ->blank;
680             }
681              
682             # Perl callback methods
683             sub on {
684 0     0 0   my ($self, $event, $callback) = @_;
685 0           my $callbacks = $self->[SLOT_CALLBACKS];
686 0           $callbacks->{$event} = $callback;
687 0           return $self;
688             }
689              
690             sub _get_callbacks {
691 0     0     my ($self) = @_;
692 0           return $self->[SLOT_CALLBACKS];
693             }
694              
695             1;