File Coverage

lib/Hypersonic/Protocol/WebSocket.pm
Criterion Covered Total %
statement 68 94 72.3
branch 17 20 85.0
condition 4 8 50.0
subroutine 10 15 66.6
pod 0 13 0.0
total 99 150 66.0


line stmt bran cond sub pod time code
1             package Hypersonic::Protocol::WebSocket;
2 2     2   219639 use strict;
  2         3  
  2         57  
3 2     2   6 use warnings;
  2         1  
  2         3779  
4              
5             # Hypersonic::Protocol::WebSocket - JIT code generation for WebSocket handshake
6             #
7             # Implements RFC 6455 WebSocket opening handshake:
8             # - Parse Upgrade/Connection headers
9             # - Extract Sec-WebSocket-Key, Version, Protocol
10             # - Calculate Sec-WebSocket-Accept (SHA1 + Base64)
11             # - Generate 101 Switching Protocols response
12             #
13             # All methods return C code strings for JIT compilation.
14             # Zero runtime overhead - handshake code compiled only when websocket routes exist.
15              
16             our $VERSION = '0.15';
17              
18             # RFC 6455 magic GUID for accept key calculation
19             our $WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
20              
21             # Protocol identifier
22 0     0 0 0 sub protocol_id { 'WebSocket' }
23              
24             # Generate OpenSSL includes for SHA1/Base64
25             sub gen_includes {
26 0     0 0 0 my ($class, $builder) = @_;
27            
28 0         0 $builder->line('#include ')
29             ->line('#include ')
30             ->line('#include ')
31             ->line('#include ')
32             ->line('#include ')
33             ->blank;
34 0         0 return $builder;
35             }
36              
37             # Generate WebSocket handshake struct
38             sub gen_handshake_struct {
39 0     0 0 0 my ($class, $builder) = @_;
40            
41 0         0 $builder->comment('WebSocket handshake data from client request')
42             ->line('typedef struct {')
43             ->line(' char ws_key[64];')
44             ->line(' char ws_protocol[128];')
45             ->line(' int ws_version;')
46             ->line(' int is_websocket;')
47             ->line('} WSHandshake;')
48             ->blank;
49 0         0 return $builder;
50             }
51              
52             # Generate Sec-WebSocket-Accept key calculation
53             # Accept = base64(SHA1(key + GUID))
54             sub gen_accept_key {
55 1     1 0 3188 my ($class, $builder) = @_;
56            
57 1         24 $builder->comment('Calculate Sec-WebSocket-Accept from client key')
58             ->comment('RFC 6455 Section 4.2.2, Step 4')
59             ->comment('Returns base64(SHA1(key + GUID))')
60             ->line('static void calc_websocket_accept(const char* client_key, char* accept_out) {')
61             ->line(' static const char* WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";')
62             ->blank
63             ->comment(' Concatenate key + GUID')
64             ->line(' char concat[256];')
65             ->line(' size_t key_len = strlen(client_key);')
66             ->line(' size_t guid_len = 36;')
67             ->blank;
68            
69 1         31 $builder->if('key_len + guid_len >= sizeof(concat)')
70             ->line('accept_out[0] = \'\\0\';')
71             ->line('return;')
72             ->endif
73             ->blank
74             ->line(' memcpy(concat, client_key, key_len);')
75             ->line(' memcpy(concat + key_len, WS_GUID, guid_len);')
76             ->line(' concat[key_len + guid_len] = \'\\0\';')
77             ->blank
78             ->comment(' SHA1 hash')
79             ->line(' unsigned char sha1_hash[SHA_DIGEST_LENGTH];')
80             ->line(' SHA1((unsigned char*)concat, key_len + guid_len, sha1_hash);')
81             ->blank
82             ->comment(' Base64 encode using OpenSSL BIO')
83             ->line(' BIO* b64 = BIO_new(BIO_f_base64());')
84             ->line(' BIO* mem = BIO_new(BIO_s_mem());')
85             ->line(' b64 = BIO_push(b64, mem);')
86             ->line(' BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);')
87             ->line(' BIO_write(b64, sha1_hash, SHA_DIGEST_LENGTH);')
88             ->line(' BIO_flush(b64);')
89             ->blank
90             ->line(' BUF_MEM* buf;')
91             ->line(' BIO_get_mem_ptr(b64, &buf);')
92             ->blank;
93            
94 1         12 $builder->if('buf->length < 64')
95             ->line('memcpy(accept_out, buf->data, buf->length);')
96             ->line('accept_out[buf->length] = \'\\0\';')
97             ->else
98             ->line('accept_out[0] = \'\\0\';')
99             ->endif
100             ->blank
101             ->line(' BIO_free_all(b64);')
102             ->line('}')
103             ->blank;
104            
105 1         2 return $builder;
106             }
107              
108             # Generate handshake request parser
109             sub gen_handshake_parser {
110 1     1 0 4967 my ($class, $builder) = @_;
111            
112             # Case-insensitive strstr helper
113 1         11 $builder->comment('Case-insensitive strstr for header matching')
114             ->line('static const char* ws_strcasestr(const char* haystack, const char* needle) {')
115             ->line(' if (!*needle) return haystack;')
116             ->blank;
117            
118 1         15 $builder->for('', '*haystack', 'haystack++')
119             ->line('const char* h = haystack;')
120             ->line('const char* n = needle;')
121             ->blank
122             ->while('*h && *n && (tolower((unsigned char)*h) == tolower((unsigned char)*n))')
123             ->line('h++;')
124             ->line('n++;')
125             ->endloop
126             ->blank;
127            
128 1         9 $builder->if('!*n')
129             ->line('return haystack;')
130             ->endif
131             ->endfor
132             ->line(' return NULL;')
133             ->line('}')
134             ->blank;
135            
136             # Main parser function
137 1         13 $builder->comment('Parse WebSocket upgrade request headers')
138             ->comment('Returns 1 if valid WebSocket upgrade, 0 otherwise')
139             ->line('static int parse_ws_handshake(const char* request, size_t len, WSHandshake* hs) {')
140             ->line(' hs->is_websocket = 0;')
141             ->line(' hs->ws_version = 0;')
142             ->line(' hs->ws_key[0] = \'\\0\';')
143             ->line(' hs->ws_protocol[0] = \'\\0\';')
144             ->blank
145             ->comment(' Check for Upgrade: websocket header')
146             ->line(' const char* upgrade = ws_strcasestr(request, "Upgrade:");');
147            
148 1         20 $builder->if('!upgrade')
149             ->line('return 0;')
150             ->endif
151             ->blank
152             ->line(' const char* ws = ws_strcasestr(upgrade, "websocket");');
153            
154 1         12 $builder->if('!ws || ws > upgrade + 64')
155             ->line('return 0;')
156             ->endif
157             ->blank
158             ->comment(' Check for Connection: Upgrade header')
159             ->line(' const char* conn = ws_strcasestr(request, "Connection:");');
160            
161 1         11 $builder->if('!conn')
162             ->line('return 0;')
163             ->endif
164             ->blank
165             ->line(' const char* upg = ws_strcasestr(conn, "Upgrade");');
166            
167 1         11 $builder->if('!upg || upg > conn + 128')
168             ->line('return 0;')
169             ->endif
170             ->blank
171             ->comment(' Extract Sec-WebSocket-Key')
172             ->line(' const char* key = ws_strcasestr(request, "Sec-WebSocket-Key:");');
173            
174 1         22 $builder->if('key')
175             ->line('key += 18;')
176             ->while('*key == \' \' || *key == \'\\t\'')
177             ->line('key++;')
178             ->endloop
179             ->blank
180             ->line('const char* end = key;')
181             ->while('*end && *end != \'\\r\' && *end != \'\\n\'')
182             ->line('end++;')
183             ->endloop
184             ->blank
185             ->comment('Trim trailing whitespace')
186             ->while('end > key && (*(end-1) == \' \' || *(end-1) == \'\\t\')')
187             ->line('end--;')
188             ->endloop
189             ->blank
190             ->line('size_t key_len = end - key;');
191            
192 1         20 $builder->if('key_len > 0 && key_len < sizeof(hs->ws_key)')
193             ->line('memcpy(hs->ws_key, key, key_len);')
194             ->line('hs->ws_key[key_len] = \'\\0\';')
195             ->endif
196             ->endif
197             ->blank
198             ->comment(' Extract Sec-WebSocket-Version')
199             ->line(' const char* ver = ws_strcasestr(request, "Sec-WebSocket-Version:");');
200            
201 1         10 $builder->if('ver')
202             ->line('ver += 22;')
203             ->while('*ver == \' \' || *ver == \'\\t\'')
204             ->line('ver++;')
205             ->endloop
206             ->line('hs->ws_version = atoi(ver);')
207             ->endif
208             ->blank
209             ->comment(' Extract Sec-WebSocket-Protocol (optional)')
210             ->line(' const char* proto = ws_strcasestr(request, "Sec-WebSocket-Protocol:");');
211            
212 1         12 $builder->if('proto')
213             ->line('proto += 23;')
214             ->while('*proto == \' \' || *proto == \'\\t\'')
215             ->line('proto++;')
216             ->endloop
217             ->blank
218             ->line('const char* end = proto;')
219             ->while('*end && *end != \'\\r\' && *end != \'\\n\'')
220             ->line('end++;')
221             ->endloop
222             ->blank
223             ->line('size_t proto_len = end - proto;');
224            
225 1         16 $builder->if('proto_len > 0 && proto_len < sizeof(hs->ws_protocol)')
226             ->line('memcpy(hs->ws_protocol, proto, proto_len);')
227             ->line('hs->ws_protocol[proto_len] = \'\\0\';')
228             ->endif
229             ->endif
230             ->blank
231             ->comment(' Valid if we have key and version 13')
232             ->line(' hs->is_websocket = (hs->ws_key[0] != \'\\0\' && hs->ws_version == 13);')
233             ->line(' return hs->is_websocket;')
234             ->line('}')
235             ->blank;
236            
237 1         3 return $builder;
238             }
239              
240             # Generate 101 Switching Protocols response builder
241             sub gen_handshake_response {
242 1     1 0 5515 my ($class, $builder) = @_;
243            
244 1         21 $builder->comment('Build and send WebSocket handshake response')
245             ->comment('Returns bytes sent, or -1 on error')
246             ->line('static int send_ws_handshake_response(int fd, const char* client_key, const char* protocol) {')
247             ->line(' char accept_key[64];')
248             ->line(' calc_websocket_accept(client_key, accept_key);')
249             ->blank;
250            
251 1         13 $builder->if('accept_key[0] == \'\\0\'')
252             ->line('return -1;')
253             ->endif
254             ->blank
255             ->line(' char response[512];')
256             ->line(' int len;')
257             ->blank;
258            
259 1         30 $builder->if('protocol && protocol[0]')
260             ->comment('Include negotiated subprotocol')
261             ->line('len = snprintf(response, sizeof(response),')
262             ->line(' "HTTP/1.1 101 Switching Protocols\\r\\n"')
263             ->line(' "Upgrade: websocket\\r\\n"')
264             ->line(' "Connection: Upgrade\\r\\n"')
265             ->line(' "Sec-WebSocket-Accept: %s\\r\\n"')
266             ->line(' "Sec-WebSocket-Protocol: %s\\r\\n"')
267             ->line(' "\\r\\n",')
268             ->line(' accept_key, protocol);')
269             ->else
270             ->line('len = snprintf(response, sizeof(response),')
271             ->line(' "HTTP/1.1 101 Switching Protocols\\r\\n"')
272             ->line(' "Upgrade: websocket\\r\\n"')
273             ->line(' "Connection: Upgrade\\r\\n"')
274             ->line(' "Sec-WebSocket-Accept: %s\\r\\n"')
275             ->line(' "\\r\\n",')
276             ->line(' accept_key);')
277             ->endif
278             ->blank;
279            
280 1         8 $builder->if('len >= (int)sizeof(response)')
281             ->line('return -1;')
282             ->endif
283             ->blank
284             ->line(' return send(fd, response, len, 0);')
285             ->line('}')
286             ->blank;
287            
288 1         2 return $builder;
289             }
290              
291             # Generate error response functions
292             sub gen_error_responses {
293 1     1 0 4951 my ($class, $builder) = @_;
294            
295 1         41 $builder->comment('Send 400 Bad Request for malformed WebSocket requests')
296             ->line('static int send_ws_bad_request(int fd) {')
297             ->line(' static const char resp[] =')
298             ->line(' "HTTP/1.1 400 Bad Request\\r\\n"')
299             ->line(' "Content-Type: text/plain\\r\\n"')
300             ->line(' "Content-Length: 11\\r\\n"')
301             ->line(' "Connection: close\\r\\n"')
302             ->line(' "\\r\\n"')
303             ->line(' "Bad Request";')
304             ->line(' return send(fd, resp, sizeof(resp) - 1, 0);')
305             ->line('}')
306             ->blank
307             ->comment('Send 426 Upgrade Required for wrong WebSocket version')
308             ->line('static int send_ws_upgrade_required(int fd) {')
309             ->line(' static const char resp[] =')
310             ->line(' "HTTP/1.1 426 Upgrade Required\\r\\n"')
311             ->line(' "Sec-WebSocket-Version: 13\\r\\n"')
312             ->line(' "Content-Type: text/plain\\r\\n"')
313             ->line(' "Content-Length: 26\\r\\n"')
314             ->line(' "Connection: close\\r\\n"')
315             ->line(' "\\r\\n"')
316             ->line(' "WebSocket version 13 only";')
317             ->line(' return send(fd, resp, sizeof(resp) - 1, 0);')
318             ->line('}')
319             ->blank
320             ->comment('Send 403 Forbidden for origin check failure')
321             ->line('static int send_ws_forbidden(int fd) {')
322             ->line(' static const char resp[] =')
323             ->line(' "HTTP/1.1 403 Forbidden\\r\\n"')
324             ->line(' "Content-Type: text/plain\\r\\n"')
325             ->line(' "Content-Length: 14\\r\\n"')
326             ->line(' "Connection: close\\r\\n"')
327             ->line(' "\\r\\n"')
328             ->line(' "Origin denied";')
329             ->line(' return send(fd, resp, sizeof(resp) - 1, 0);')
330             ->line('}')
331             ->blank;
332            
333 1         2 return $builder;
334             }
335              
336             # Generate protocol negotiation helper
337             sub gen_protocol_negotiation {
338 0     0 0 0 my ($class, $builder) = @_;
339            
340 0         0 $builder->comment('Check if requested protocol is in comma-separated list')
341             ->comment('Returns pointer to matched protocol, or NULL')
342             ->line('static const char* ws_negotiate_protocol(const char* requested, const char* supported) {');
343            
344 0         0 $builder->if('!requested || !requested[0] || !supported || !supported[0]')
345             ->line('return NULL;')
346             ->endif
347             ->blank
348             ->comment(' Parse comma-separated requested protocols')
349             ->line(' char buf[128];')
350             ->line(' strncpy(buf, requested, sizeof(buf) - 1);')
351             ->line(' buf[sizeof(buf) - 1] = \'\\0\';')
352             ->blank
353             ->line(' char* token = strtok(buf, ", ");');
354            
355 0         0 $builder->while('token')
356             ->comment('Skip whitespace')
357             ->while('*token == \' \'')
358             ->line('token++;')
359             ->endloop
360             ->blank
361             ->comment('Check if this protocol is supported');
362            
363 0         0 $builder->if('strstr(supported, token)')
364             ->line('return token;')
365             ->endif
366             ->blank
367             ->line('token = strtok(NULL, ", ");')
368             ->endloop
369             ->blank
370             ->line(' return NULL;')
371             ->line('}')
372             ->blank;
373            
374 0         0 $builder->comment('Extract first protocol from list (for simple cases)')
375             ->line('static void ws_first_protocol(const char* list, char* out, size_t out_size) {');
376            
377 0         0 $builder->if('!list || !list[0]')
378             ->line('out[0] = \'\\0\';')
379             ->line('return;')
380             ->endif
381             ->blank
382             ->line(' const char* end = list;')
383             ->while('*end && *end != \',\' && *end != \' \'')
384             ->line('end++;')
385             ->endloop
386             ->blank
387             ->line(' size_t len = end - list;');
388            
389 0         0 $builder->if('len >= out_size')
390             ->line('len = out_size - 1;')
391             ->endif
392             ->blank
393             ->line(' memcpy(out, list, len);')
394             ->line(' out[len] = \'\\0\';')
395             ->line('}')
396             ->blank;
397            
398 0         0 return $builder;
399             }
400              
401             # Generate all WebSocket handshake C code
402             sub generate_c_code {
403 0     0 0 0 my ($class, $builder, $opts) = @_;
404 0   0     0 $opts //= {};
405            
406 0         0 $class->gen_includes($builder);
407 0         0 $class->gen_handshake_struct($builder);
408 0         0 $class->gen_accept_key($builder);
409 0         0 $class->gen_handshake_parser($builder);
410 0         0 $class->gen_handshake_response($builder);
411 0         0 $class->gen_error_responses($builder);
412 0         0 $class->gen_protocol_negotiation($builder);
413            
414 0         0 return $builder;
415             }
416              
417             #
418             # Perl-side helpers for testing and fallback
419             #
420              
421             # Calculate accept key in pure Perl (for testing)
422             sub calc_accept_key {
423 30     30 0 9331 my ($class, $client_key) = @_;
424            
425 30         1165 require Digest::SHA;
426 30         6043 require MIME::Base64;
427            
428 30         1879 my $concat = $client_key . $WS_GUID;
429 30         178 my $sha1 = Digest::SHA::sha1($concat);
430 30         95 my $accept = MIME::Base64::encode_base64($sha1, '');
431            
432 30         72 return $accept;
433             }
434              
435             # Parse WebSocket headers from request string (Perl fallback)
436             sub parse_handshake {
437 6     6 0 18518 my ($class, $request) = @_;
438            
439 6         28 my %result = (
440             is_websocket => 0,
441             ws_key => '',
442             ws_version => 0,
443             ws_protocol => '',
444             );
445            
446             # Check Upgrade: websocket
447 6 100       45 return \%result unless $request =~ /Upgrade:\s*websocket/i;
448            
449             # Check Connection: Upgrade
450 5 100       19 return \%result unless $request =~ /Connection:.*Upgrade/i;
451            
452             # Extract Sec-WebSocket-Key
453 4 50       19 if ($request =~ /Sec-WebSocket-Key:\s*(\S+)/i) {
454 4         11 $result{ws_key} = $1;
455             }
456            
457             # Extract Sec-WebSocket-Version
458 4 50       15 if ($request =~ /Sec-WebSocket-Version:\s*(\d+)/i) {
459 4         10 $result{ws_version} = int($1);
460             }
461            
462             # Extract Sec-WebSocket-Protocol
463 4 100       14 if ($request =~ /Sec-WebSocket-Protocol:\s*([^\r\n]+)/i) {
464 1         3 $result{ws_protocol} = $1;
465 1         16 $result{ws_protocol} =~ s/\s+$//; # Trim trailing
466             }
467            
468             # Valid if we have key and version 13
469 4 100 66     25 $result{is_websocket} = ($result{ws_key} && $result{ws_version} == 13) ? 1 : 0;
470            
471 4         10 return \%result;
472             }
473              
474             # Build handshake response in Perl
475             sub build_response {
476 27     27 0 2173652 my ($class, %args) = @_;
477            
478 27 100       93 my $client_key = $args{key} or return '';
479 26         41 my $protocol = $args{protocol};
480            
481 26         67 my $accept = $class->calc_accept_key($client_key);
482            
483 26         59 my $response = "HTTP/1.1 101 Switching Protocols\r\n"
484             . "Upgrade: websocket\r\n"
485             . "Connection: Upgrade\r\n"
486             . "Sec-WebSocket-Accept: $accept\r\n";
487            
488 26 100       49 if ($protocol) {
489 1         3 $response .= "Sec-WebSocket-Protocol: $protocol\r\n";
490             }
491            
492 26         52 $response .= "\r\n";
493            
494 26         133 return $response;
495             }
496              
497             # Validate WebSocket key format (16 bytes base64 = 24 chars)
498             sub validate_key {
499 7     7 0 5532 my ($class, $key) = @_;
500            
501 7 100 66     52 return 0 unless defined $key && length($key) == 24;
502 3 50       23 return 0 unless $key =~ m{^[A-Za-z0-9+/]{22}==$};
503            
504 3         11 return 1;
505             }
506              
507             1;
508              
509             __END__