File Coverage

blib/lib/Hypersonic/TLS.pm
Criterion Covered Total %
statement 18 64 28.1
branch 2 10 20.0
condition 0 14 0.0
subroutine 6 16 37.5
pod 9 11 81.8
total 35 115 30.4


line stmt bran cond sub pod time code
1             package Hypersonic::TLS;
2              
3 35     35   243 use strict;
  35         63  
  35         1213  
4 35     35   152 use warnings;
  35         55  
  35         1973  
5 35     35   614 use 5.010;
  35         104  
6              
7             our $VERSION = '0.15';
8              
9             # JIT-compiled TLS/HTTPS support for Hypersonic
10             # Uses OpenSSL for TLS support (via Alien::OpenSSL when available)
11             # Generates XS code via XS::JIT::Builder
12              
13 35     35   192 use XS::JIT;
  35         52  
  35         638  
14 35     35   167 use XS::JIT::Builder;
  35         139  
  35         51327  
15              
16             my $COMPILED = 0;
17             my $MODULE_ID = 0;
18              
19             # Cache for OpenSSL detection result
20             my $OPENSSL_DETECTION;
21              
22             # Check if OpenSSL is available (uses centralized detection)
23             sub check_openssl {
24 42 100   42 1 142180 return $OPENSSL_DETECTION->{available} if defined $OPENSSL_DETECTION;
25              
26 35         195 require Hypersonic::JIT::Util;
27 35         229 $OPENSSL_DETECTION = Hypersonic::JIT::Util->detect_openssl();
28 35         492 return $OPENSSL_DETECTION->{available};
29             }
30              
31             # Unified compile interface
32             sub compile {
33 0     0 0   my ($class, %opts) = @_;
34 0           return $class->compile_tls_ops(%opts);
35             }
36              
37             # Compile TLS ops using XS::JIT::Builder
38             sub compile_tls_ops {
39 0     0 1   my ($class, %opts) = @_;
40              
41 0 0         return 1 if $COMPILED;
42              
43 0   0       my $cache_dir = $opts{cache_dir} // '_hypersonic_cache/tls';
44 0           my $module_name = 'Hypersonic::TLS::Ops_' . $MODULE_ID++;
45              
46 0           my $builder = XS::JIT::Builder->new;
47 0           my $inline = Hypersonic::JIT::Util->inline_keyword;
48              
49             # Add OpenSSL includes
50 0           $builder->include('')
51             ->include('')
52             ->include('');
53              
54             # Add TLS global state and structures
55 0           $builder->line('/* Global SSL context - initialized once */')
56             ->line('static SSL_CTX* g_ssl_ctx = NULL;')
57             ->line('')
58             ->line('/* Connection state for TLS */')
59             ->line('typedef struct {')
60             ->line(' int fd;')
61             ->line(' SSL* ssl;')
62             ->line(' time_t last_activity;')
63             ->line(' int handshake_complete;')
64             ->line('} TLSConnection;')
65             ->line('')
66             ->line('#define MAX_TLS_CONNECTIONS 10000')
67             ->line('static TLSConnection g_tls_connections[MAX_TLS_CONNECTIONS];');
68              
69             # Helper to get TLS connection
70 0           $builder->line('')
71             ->line("static $inline TLSConnection* get_tls_connection(int fd) {")
72             ->line(' int i;')
73             ->line(' for (i = 0; i < MAX_TLS_CONNECTIONS; i++) {')
74             ->line(' if (g_tls_connections[i].fd == fd) {')
75             ->line(' return &g_tls_connections[i];')
76             ->line(' }')
77             ->line(' }')
78             ->line(' return NULL;')
79             ->line('}');
80              
81             # Helper to allocate TLS connection
82 0           $builder->line('')
83             ->line("static $inline TLSConnection* alloc_tls_connection(int fd, SSL* ssl) {")
84             ->line(' int i;')
85             ->line(' for (i = 0; i < MAX_TLS_CONNECTIONS; i++) {')
86             ->line(' if (g_tls_connections[i].fd == 0) {')
87             ->line(' g_tls_connections[i].fd = fd;')
88             ->line(' g_tls_connections[i].ssl = ssl;')
89             ->line(' g_tls_connections[i].last_activity = time(NULL);')
90             ->line(' g_tls_connections[i].handshake_complete = 0;')
91             ->line(' return &g_tls_connections[i];')
92             ->line(' }')
93             ->line(' }')
94             ->line(' return NULL;')
95             ->line('}');
96              
97             # Helper to free TLS connection
98 0           $builder->line('')
99             ->line("static $inline void free_tls_connection(int fd) {")
100             ->line(' int i;')
101             ->line(' for (i = 0; i < MAX_TLS_CONNECTIONS; i++) {')
102             ->line(' if (g_tls_connections[i].fd == fd) {')
103             ->line(' if (g_tls_connections[i].ssl) {')
104             ->line(' SSL_shutdown(g_tls_connections[i].ssl);')
105             ->line(' SSL_free(g_tls_connections[i].ssl);')
106             ->line(' }')
107             ->line(' g_tls_connections[i].fd = 0;')
108             ->line(' g_tls_connections[i].ssl = NULL;')
109             ->line(' return;')
110             ->line(' }')
111             ->line(' }')
112             ->line('}');
113              
114             # SSL context initialization
115 0           $builder->line('')
116             ->line('static int init_ssl_ctx(const char* cert_file, const char* key_file) {')
117             ->line(' SSL_library_init();')
118             ->line(' SSL_load_error_strings();')
119             ->line(' OpenSSL_add_all_algorithms();')
120             ->line('')
121             ->line(' g_ssl_ctx = SSL_CTX_new(TLS_server_method());')
122             ->line(' if (!g_ssl_ctx) {')
123             ->line(' return -1;')
124             ->line(' }')
125             ->line('')
126             ->line(' /* Set minimum TLS version to 1.2 for security */')
127             ->line(' SSL_CTX_set_min_proto_version(g_ssl_ctx, TLS1_2_VERSION);')
128             ->line('')
129             ->line(' /* Load certificate and key */')
130             ->line(' if (SSL_CTX_use_certificate_file(g_ssl_ctx, cert_file, SSL_FILETYPE_PEM) <= 0) {')
131             ->line(' SSL_CTX_free(g_ssl_ctx);')
132             ->line(' g_ssl_ctx = NULL;')
133             ->line(' return -2;')
134             ->line(' }')
135             ->line('')
136             ->line(' if (SSL_CTX_use_PrivateKey_file(g_ssl_ctx, key_file, SSL_FILETYPE_PEM) <= 0) {')
137             ->line(' SSL_CTX_free(g_ssl_ctx);')
138             ->line(' g_ssl_ctx = NULL;')
139             ->line(' return -3;')
140             ->line(' }')
141             ->line('')
142             ->line(' /* Verify private key matches certificate */')
143             ->line(' if (!SSL_CTX_check_private_key(g_ssl_ctx)) {')
144             ->line(' SSL_CTX_free(g_ssl_ctx);')
145             ->line(' g_ssl_ctx = NULL;')
146             ->line(' return -4;')
147             ->line(' }')
148             ->line('')
149             ->line(' return 0;')
150             ->line('}');
151              
152             # TLS accept
153 0           $builder->line('')
154             ->line('/* Accept TLS connection - non-blocking */')
155             ->line('static int tls_accept(int client_fd) {')
156             ->line(' if (!g_ssl_ctx) return -1;')
157             ->line('')
158             ->line(' SSL* ssl = SSL_new(g_ssl_ctx);')
159             ->line(' if (!ssl) return -2;')
160             ->line('')
161             ->line(' SSL_set_fd(ssl, client_fd);')
162             ->line(' SSL_set_accept_state(ssl);')
163             ->line('')
164             ->line(' TLSConnection* conn = alloc_tls_connection(client_fd, ssl);')
165             ->line(' if (!conn) {')
166             ->line(' SSL_free(ssl);')
167             ->line(' return -3;')
168             ->line(' }')
169             ->line('')
170             ->line(' /* Non-blocking handshake will complete on first read */')
171             ->line(' return 0;')
172             ->line('}');
173              
174             # TLS handshake
175 0           $builder->line('')
176             ->line('/* Complete TLS handshake - may need multiple calls */')
177             ->line('static int tls_handshake(TLSConnection* conn) {')
178             ->line(' if (conn->handshake_complete) return 1;')
179             ->line('')
180             ->line(' int ret = SSL_accept(conn->ssl);')
181             ->line(' if (ret == 1) {')
182             ->line(' conn->handshake_complete = 1;')
183             ->line(' return 1;')
184             ->line(' }')
185             ->line('')
186             ->line(' int err = SSL_get_error(conn->ssl, ret);')
187             ->line(' if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) {')
188             ->line(' return 0; /* Need more data, try again later */')
189             ->line(' }')
190             ->line('')
191             ->line(' return -1; /* Fatal error */')
192             ->line('}');
193              
194             # TLS recv
195 0           $builder->line('')
196             ->line('/* TLS read - returns bytes read, 0 for EAGAIN, -1 for error/close */')
197             ->line('static ssize_t tls_recv(TLSConnection* conn, char* buf, size_t len) {')
198             ->line(' if (!conn->handshake_complete) {')
199             ->line(' int hs = tls_handshake(conn);')
200             ->line(' if (hs <= 0) return hs;')
201             ->line(' }')
202             ->line('')
203             ->line(' int ret = SSL_read(conn->ssl, buf, (int)len);')
204             ->line(' if (ret > 0) {')
205             ->line(' conn->last_activity = time(NULL);')
206             ->line(' return ret;')
207             ->line(' }')
208             ->line('')
209             ->line(' int err = SSL_get_error(conn->ssl, ret);')
210             ->line(' if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) {')
211             ->line(' return 0; /* Would block, try again */')
212             ->line(' }')
213             ->line('')
214             ->line(' return -1; /* Error or connection closed */')
215             ->line('}');
216              
217             # TLS send
218 0           $builder->line('')
219             ->line('/* TLS write - returns bytes written, 0 for EAGAIN, -1 for error */')
220             ->line('static ssize_t tls_send(TLSConnection* conn, const char* buf, size_t len) {')
221             ->line(' int ret = SSL_write(conn->ssl, buf, (int)len);')
222             ->line(' if (ret > 0) {')
223             ->line(' conn->last_activity = time(NULL);')
224             ->line(' return ret;')
225             ->line(' }')
226             ->line('')
227             ->line(' int err = SSL_get_error(conn->ssl, ret);')
228             ->line(' if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) {')
229             ->line(' return 0; /* Would block, try again */')
230             ->line(' }')
231             ->line('')
232             ->line(' return -1; /* Error */')
233             ->line('}');
234              
235             # TLS close
236 0           $builder->line('')
237             ->line('/* Close TLS connection */')
238             ->line('static void tls_close(int fd) {')
239             ->line(' free_tls_connection(fd);')
240             ->line(' close(fd);')
241             ->line('}');
242              
243             # XS wrapper for init_ssl_ctx
244 0           $builder->xs_function('jit_init_ssl_ctx')
245             ->xs_preamble
246             ->line('STRLEN cert_len, key_len;')
247             ->line('const char* cert_file;')
248             ->line('const char* key_file;')
249             ->line('int result;')
250             ->line('if (items != 2) {')
251             ->line(' croak("init_ssl_ctx requires cert_file and key_file");')
252             ->line('}')
253             ->line('cert_file = SvPV(ST(0), cert_len);')
254             ->line('key_file = SvPV(ST(1), key_len);')
255             ->line('result = init_ssl_ctx(cert_file, key_file);')
256             ->line('ST(0) = sv_2mortal(newSViv(result));')
257             ->xs_return('1')
258             ->xs_end;
259              
260             # XS wrapper for tls_accept
261 0           $builder->xs_function('jit_tls_accept')
262             ->xs_preamble
263             ->line('int client_fd;')
264             ->line('int result;')
265             ->line('if (items != 1) {')
266             ->line(' croak("tls_accept requires client_fd");')
267             ->line('}')
268             ->line('client_fd = SvIV(ST(0));')
269             ->line('result = tls_accept(client_fd);')
270             ->line('ST(0) = sv_2mortal(newSViv(result));')
271             ->xs_return('1')
272             ->xs_end;
273              
274             # XS wrapper for tls_close
275 0           $builder->xs_function('jit_tls_close')
276             ->xs_preamble
277             ->line('int fd;')
278             ->line('if (items != 1) {')
279             ->line(' croak("tls_close requires fd");')
280             ->line('}')
281             ->line('fd = SvIV(ST(0));')
282             ->line('tls_close(fd);')
283             ->xs_return('0')
284             ->xs_end;
285              
286             # Compile via XS::JIT with OpenSSL flags
287 0           XS::JIT->compile(
288             code => $builder->code,
289             name => $module_name,
290             cache_dir => $cache_dir,
291             extra_cflags => get_extra_cflags(),
292             extra_ldflags => get_extra_ldflags(),
293             functions => {
294             'Hypersonic::TLS::init_ssl_ctx' => { source => 'jit_init_ssl_ctx', is_xs_native => 1 },
295             'Hypersonic::TLS::tls_accept' => { source => 'jit_tls_accept', is_xs_native => 1 },
296             'Hypersonic::TLS::tls_close' => { source => 'jit_tls_close', is_xs_native => 1 },
297             },
298             );
299              
300 0           $COMPILED = 1;
301 0           return 1;
302             }
303              
304             # Generate OpenSSL includes (for use by Hypersonic.pm code generation)
305             sub gen_includes {
306 0     0 1   return <<'C';
307             #include
308             #include
309             #include
310             C
311             }
312              
313             # Generate SSL context initialization (for use by Hypersonic.pm code generation)
314             sub gen_ssl_ctx_init {
315 0     0 1   my (%opts) = @_;
316 0   0       my $enable_http2 = $opts{http2} // 0;
317 0           my $inline = Hypersonic::JIT::Util->inline_keyword;
318              
319 0           my $alpn_code = '';
320 0 0         if ($enable_http2) {
321 0           $alpn_code = <<'ALPN';
322              
323             /* ALPN protocol list for HTTP/2 negotiation */
324             static const unsigned char alpn_protos[] = {
325             2, 'h', '2', /* HTTP/2 */
326             8, 'h', 't', 't', 'p', '/', '1', '.', '1' /* HTTP/1.1 fallback */
327             };
328              
329             /* ALPN selection callback - called during TLS handshake */
330             static int alpn_select_cb(SSL* ssl,
331             const unsigned char** out,
332             unsigned char* outlen,
333             const unsigned char* in,
334             unsigned int inlen,
335             void* arg) {
336             (void)ssl; (void)arg;
337            
338             /* Use OpenSSL's helper to select preferred protocol */
339             if (SSL_select_next_proto((unsigned char**)out, outlen,
340             alpn_protos, sizeof(alpn_protos),
341             in, inlen) == OPENSSL_NPN_NEGOTIATED) {
342             return SSL_TLSEXT_ERR_OK;
343             }
344            
345             /* No matching protocol - allow connection anyway (HTTP/1.1) */
346             return SSL_TLSEXT_ERR_NOACK;
347             }
348             ALPN
349             }
350            
351 0 0         my $alpn_setup = $enable_http2
352             ? "\n /* Enable ALPN for HTTP/2 negotiation */\n SSL_CTX_set_alpn_select_cb(g_ssl_ctx, alpn_select_cb, NULL);\n"
353             : '';
354            
355 0           return <<"C";
356             /* Global SSL context - initialized once */
357             static SSL_CTX* g_ssl_ctx = NULL;
358              
359             /* Connection state for TLS */
360             typedef struct {
361             int fd;
362             SSL* ssl;
363             time_t last_activity;
364             int handshake_complete;
365             int protocol; /* PROTO_HTTP1=1, PROTO_HTTP2=2 */
366             } TLSConnection;
367              
368             #define PROTO_HTTP1 1
369             #define PROTO_HTTP2 2
370             #define MAX_TLS_CONNECTIONS 10000
371             static TLSConnection g_tls_connections[MAX_TLS_CONNECTIONS];
372             $alpn_code
373             static $inline TLSConnection* get_tls_connection(int fd) {
374             int i;
375             for (i = 0; i < MAX_TLS_CONNECTIONS; i++) {
376             if (g_tls_connections[i].fd == fd) {
377             return &g_tls_connections[i];
378             }
379             }
380             return NULL;
381             }
382              
383             static $inline TLSConnection* alloc_tls_connection(int fd, SSL* ssl) {
384             int i;
385             for (i = 0; i < MAX_TLS_CONNECTIONS; i++) {
386             if (g_tls_connections[i].fd == 0) {
387             g_tls_connections[i].fd = fd;
388             g_tls_connections[i].ssl = ssl;
389             g_tls_connections[i].last_activity = time(NULL);
390             g_tls_connections[i].handshake_complete = 0;
391             g_tls_connections[i].protocol = PROTO_HTTP1; /* Default */
392             return &g_tls_connections[i];
393             }
394             }
395             return NULL;
396             }
397              
398             static $inline void free_tls_connection(int fd) {
399             int i;
400             for (i = 0; i < MAX_TLS_CONNECTIONS; i++) {
401             if (g_tls_connections[i].fd == fd) {
402             if (g_tls_connections[i].ssl) {
403             SSL_shutdown(g_tls_connections[i].ssl);
404             SSL_free(g_tls_connections[i].ssl);
405             }
406             g_tls_connections[i].fd = 0;
407             g_tls_connections[i].ssl = NULL;
408             return;
409             }
410             }
411             }
412              
413             /* Check negotiated protocol after TLS handshake */
414             static $inline int get_negotiated_protocol(TLSConnection* conn) {
415             const unsigned char* alpn = NULL;
416             unsigned int alpn_len = 0;
417             SSL_get0_alpn_selected(conn->ssl, &alpn, &alpn_len);
418            
419             if (alpn_len == 2 && memcmp(alpn, "h2", 2) == 0) {
420             conn->protocol = PROTO_HTTP2;
421             return PROTO_HTTP2;
422             }
423             conn->protocol = PROTO_HTTP1;
424             return PROTO_HTTP1;
425             }
426              
427             static int init_ssl_ctx(const char* cert_file, const char* key_file) {
428             SSL_library_init();
429             SSL_load_error_strings();
430             OpenSSL_add_all_algorithms();
431              
432             g_ssl_ctx = SSL_CTX_new(TLS_server_method());
433             if (!g_ssl_ctx) {
434             return -1;
435             }
436              
437             /* Set minimum TLS version to 1.2 for security */
438             SSL_CTX_set_min_proto_version(g_ssl_ctx, TLS1_2_VERSION);
439             $alpn_setup
440             /* Load certificate and key */
441             if (SSL_CTX_use_certificate_file(g_ssl_ctx, cert_file, SSL_FILETYPE_PEM) <= 0) {
442             SSL_CTX_free(g_ssl_ctx);
443             g_ssl_ctx = NULL;
444             return -2;
445             }
446              
447             if (SSL_CTX_use_PrivateKey_file(g_ssl_ctx, key_file, SSL_FILETYPE_PEM) <= 0) {
448             SSL_CTX_free(g_ssl_ctx);
449             g_ssl_ctx = NULL;
450             return -3;
451             }
452              
453             /* Verify private key matches certificate */
454             if (!SSL_CTX_check_private_key(g_ssl_ctx)) {
455             SSL_CTX_free(g_ssl_ctx);
456             g_ssl_ctx = NULL;
457             return -4;
458             }
459              
460             return 0;
461             }
462             C
463             }
464              
465             # Generate TLS accept code
466             sub gen_ssl_accept {
467 0     0 1   return <<'C';
468             /* Accept TLS connection - non-blocking */
469             static int tls_accept(int client_fd) {
470             if (!g_ssl_ctx) return -1;
471              
472             SSL* ssl = SSL_new(g_ssl_ctx);
473             if (!ssl) return -2;
474              
475             SSL_set_fd(ssl, client_fd);
476             SSL_set_accept_state(ssl);
477              
478             TLSConnection* conn = alloc_tls_connection(client_fd, ssl);
479             if (!conn) {
480             SSL_free(ssl);
481             return -3;
482             }
483              
484             /* Non-blocking handshake will complete on first read */
485             return 0;
486             }
487              
488             /* Complete TLS handshake - may need multiple calls */
489             static int tls_handshake(TLSConnection* conn) {
490             if (conn->handshake_complete) return 1;
491              
492             int ret = SSL_accept(conn->ssl);
493             if (ret == 1) {
494             conn->handshake_complete = 1;
495             return 1;
496             }
497              
498             int err = SSL_get_error(conn->ssl, ret);
499             if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) {
500             return 0; /* Need more data, try again later */
501             }
502              
503             return -1; /* Fatal error */
504             }
505             C
506             }
507              
508             # Generate TLS read/write wrappers
509             sub gen_ssl_io {
510 0     0 1   return <<'C';
511             /* TLS read - returns bytes read, 0 for EAGAIN, -1 for error/close */
512             static ssize_t tls_recv(TLSConnection* conn, char* buf, size_t len) {
513             if (!conn->handshake_complete) {
514             int hs = tls_handshake(conn);
515             if (hs <= 0) return hs;
516             }
517              
518             int ret = SSL_read(conn->ssl, buf, (int)len);
519             if (ret > 0) {
520             conn->last_activity = time(NULL);
521             return ret;
522             }
523              
524             int err = SSL_get_error(conn->ssl, ret);
525             if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) {
526             return 0; /* Would block, try again */
527             }
528              
529             return -1; /* Error or connection closed */
530             }
531              
532             /* TLS write - returns bytes written, 0 for EAGAIN, -1 for error */
533             static ssize_t tls_send(TLSConnection* conn, const char* buf, size_t len) {
534             int ret = SSL_write(conn->ssl, buf, (int)len);
535             if (ret > 0) {
536             conn->last_activity = time(NULL);
537             return ret;
538             }
539              
540             int err = SSL_get_error(conn->ssl, ret);
541             if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) {
542             return 0; /* Would block, try again */
543             }
544              
545             return -1; /* Error */
546             }
547             C
548             }
549              
550             # Generate TLS close
551             sub gen_ssl_close {
552 0     0 1   return <<'C';
553             /* Close TLS connection */
554             static void tls_close(int fd) {
555             free_tls_connection(fd);
556             close(fd);
557             }
558             C
559             }
560              
561             # Return all TLS C code (for Hypersonic.pm to embed in generated code)
562             sub generate_tls_code {
563 0     0 0   return join("\n\n",
564             gen_includes(),
565             gen_ssl_ctx_init(),
566             gen_ssl_accept(),
567             gen_ssl_io(),
568             gen_ssl_close(),
569             );
570             }
571              
572             # Get extra compiler flags for OpenSSL (uses centralized detection)
573             sub get_extra_cflags {
574 0     0 1   require Hypersonic::JIT::Util;
575 0   0       $OPENSSL_DETECTION //= Hypersonic::JIT::Util->detect_openssl();
576 0   0       return $OPENSSL_DETECTION->{cflags} // '';
577             }
578              
579             sub get_extra_ldflags {
580 0     0 1   require Hypersonic::JIT::Util;
581 0   0       $OPENSSL_DETECTION //= Hypersonic::JIT::Util->detect_openssl();
582             # Need both -lssl and -lcrypto for full OpenSSL
583 0   0       my $ldflags = $OPENSSL_DETECTION->{ldflags} // '';
584 0 0         $ldflags .= ' -lcrypto' unless $ldflags =~ /-lcrypto/;
585 0           return $ldflags;
586             }
587              
588             1;
589              
590             __END__