File Coverage

lib/Hypersonic/Session.pm
Criterion Covered Total %
statement 119 164 72.5
branch 35 78 44.8
condition 28 73 38.3
subroutine 23 28 82.1
pod 2 12 16.6
total 207 355 58.3


line stmt bran cond sub pod time code
1             package Hypersonic::Session;
2              
3 1     1   1603 use strict;
  1         3  
  1         45  
4 1     1   6 use warnings;
  1         2  
  1         53  
5 1     1   20 use 5.010;
  1         4  
6              
7             our $VERSION = '0.15';
8              
9             # Session management for Hypersonic
10             # Uses signed cookies for session ID, memory store for data
11             # Digest::SHA provides fast C-based HMAC-SHA256
12             # Only activated when session_config() is called
13              
14 1     1   859 use Digest::SHA qw(hmac_sha256_hex);
  1         5196  
  1         165  
15 1     1   11 use Hypersonic::JIT::Util;
  1         8  
  1         63  
16              
17             # Session slot constants (used in Request object)
18             use constant {
19 1         134 SLOT_SESSION => 12,
20             SLOT_SESSION_ID => 13,
21             SLOT_SESSION_MODIFIED => 14,
22 1     1   6 };
  1         7  
23              
24             # Export slot constants
25 1     1   8 use Exporter 'import';
  1         2  
  1         4178  
26             our @EXPORT_OK = qw(
27             SLOT_SESSION SLOT_SESSION_ID SLOT_SESSION_MODIFIED
28             );
29              
30             # Global session store - memory-based (survives across requests)
31             my %SESSION_STORE;
32             my $SESSION_CONFIG;
33              
34             # JIT compilation state
35             my $COMPILED = 0;
36             my $MODULE_ID = 0;
37              
38             # ============================================================
39             # JIT Compilation of Cryptographic Operations
40             # ============================================================
41              
42             # Unified compile interface
43             sub compile {
44 0     0 0 0 my ($class, %opts) = @_;
45 0         0 return $class->compile_session_ops(%opts);
46             }
47              
48             sub compile_session_ops {
49 5     5 1 22 my ($class, %opts) = @_;
50              
51 5 50       14 return 1 if $COMPILED;
52              
53             # Check if OpenSSL is available (via TLS module)
54 5         12 eval { require Hypersonic::TLS };
  5         42  
55 5 50       15 if ($@) {
56             warn "Hypersonic::Session: Cannot load TLS module, using pure Perl crypto: $@\n"
57 0 0       0 if $ENV{HYPERSONIC_DEBUG};
58 0         0 return 0;
59             }
60              
61 5         31 my $has_openssl = Hypersonic::TLS::check_openssl();
62 5 50       12 unless ($has_openssl) {
63             warn "Hypersonic::Session: OpenSSL not available, using pure Perl crypto\n"
64 5 50       16 if $ENV{HYPERSONIC_DEBUG};
65 5         12 return 0;
66             }
67              
68             # Get OpenSSL flags - must be non-empty for compilation to work
69 0   0     0 my $extra_cflags = Hypersonic::TLS::get_extra_cflags() // '';
70 0   0     0 my $extra_ldflags = Hypersonic::TLS::get_extra_ldflags() // '';
71              
72 0 0       0 unless ($extra_cflags =~ /-I/) {
73             warn "Hypersonic::Session: OpenSSL headers not found, using pure Perl crypto\n"
74 0 0       0 if $ENV{HYPERSONIC_DEBUG};
75 0         0 return 0;
76             }
77              
78 0   0     0 my $cache_dir = $opts{cache_dir} // '_hypersonic_cache/session';
79 0         0 my $module_name = 'Hypersonic::Session::Ops_' . $MODULE_ID++;
80              
81 0         0 my $builder = XS::JIT::Builder->new;
82              
83             # Add required includes via centralized utility
84 0         0 Hypersonic::JIT::Util->add_standard_includes($builder,
85             qw(unistd fcntl openssl));
86              
87             # --------------------------------------------------------
88             # jit_hmac_sha256_hex: Generate HMAC-SHA256 signature
89             # Input: data (SV*), key (SV*), output_len (IV, optional, default 32)
90             # Output: hex-encoded signature (SV*)
91             # --------------------------------------------------------
92 0         0 $builder->xs_function('jit_hmac_sha256_hex')
93             ->xs_preamble
94             ->line('int i;')
95             ->line('if (items < 2) croak("Usage: _jit_hmac_sha256_hex(data, key, [output_len])");')
96             ->line('STRLEN data_len, key_len;')
97             ->line('const unsigned char* data = (const unsigned char*)SvPV(ST(0), data_len);')
98             ->line('const unsigned char* key = (const unsigned char*)SvPV(ST(1), key_len);')
99             ->line('IV out_len = items > 2 ? SvIV(ST(2)) : 32;')
100             ->line('if (out_len > 64) out_len = 64;')
101             ->line('if (out_len < 1) out_len = 1;')
102             ->blank
103             ->line('unsigned char digest[EVP_MAX_MD_SIZE];')
104             ->line('unsigned int digest_len = 0;')
105             ->line('HMAC(EVP_sha256(), key, (int)key_len, data, data_len, digest, &digest_len);')
106             ->blank
107             ->line('char hex[129];')
108             ->line('int hex_bytes = (int)(out_len / 2);')
109             ->line('if (hex_bytes > 32) hex_bytes = 32;')
110             ->line('for (i = 0; i < hex_bytes; i++) {')
111             ->line(' sprintf(hex + i*2, "%02x", digest[i]);')
112             ->line('}')
113             ->line('hex[out_len] = \'\\0\';')
114             ->blank
115             ->line('ST(0) = sv_2mortal(newSVpv(hex, out_len));')
116             ->xs_return('1')
117             ->xs_end;
118              
119             # --------------------------------------------------------
120             # jit_constant_time_compare: Timing-attack resistant compare
121             # Input: s1 (SV*), s2 (SV*)
122             # Output: 1 if equal, 0 if not (IV)
123             # --------------------------------------------------------
124 0         0 $builder->xs_function('jit_constant_time_compare')
125             ->xs_preamble
126             ->line('STRLEN i;')
127             ->line('if (items != 2) croak("Usage: _jit_constant_time_compare(s1, s2)");')
128             ->line('STRLEN len1, len2;')
129             ->line('const unsigned char* s1 = (const unsigned char*)SvPV(ST(0), len1);')
130             ->line('const unsigned char* s2 = (const unsigned char*)SvPV(ST(1), len2);')
131             ->blank
132             ->comment('Length mismatch - still do constant-time work')
133             ->line('STRLEN max_len = len1 > len2 ? len1 : len2;')
134             ->line('unsigned char diff = (len1 != len2) ? 1 : 0;')
135             ->blank
136             ->line('for (i = 0; i < max_len; i++) {')
137             ->line(' unsigned char c1 = (i < len1) ? s1[i] : 0;')
138             ->line(' unsigned char c2 = (i < len2) ? s2[i] : 0;')
139             ->line(' diff |= c1 ^ c2;')
140             ->line('}')
141             ->blank
142             ->line('ST(0) = sv_2mortal(newSViv(diff == 0 ? 1 : 0));')
143             ->xs_return('1')
144             ->xs_end;
145              
146             # --------------------------------------------------------
147             # jit_generate_session_id: Generate secure random session ID
148             # Input: (none)
149             # Output: 32-char hex string (SV*)
150             # --------------------------------------------------------
151 0         0 $builder->xs_function('jit_generate_session_id')
152             ->xs_preamble
153             ->line('int i;')
154             ->line('unsigned char bytes[16];')
155             ->blank
156             ->comment('Try /dev/urandom first (most portable)')
157             ->line('int fd = open("/dev/urandom", O_RDONLY);')
158             ->if('fd >= 0')
159             ->line('ssize_t n = read(fd, bytes, 16);')
160             ->line('close(fd);')
161             ->if('n != 16')
162             ->line('croak("Failed to read 16 bytes from /dev/urandom");')
163             ->endif
164             ->else
165             ->comment('Fallback to OpenSSL RAND_bytes')
166             ->if('RAND_bytes(bytes, 16) != 1')
167             ->line('croak("Failed to generate random bytes");')
168             ->endif
169             ->endif
170             ->blank
171             ->comment('Convert to hex')
172             ->line('char hex[33];')
173             ->line('for (i = 0; i < 16; i++) {')
174             ->line(' sprintf(hex + i*2, "%02x", bytes[i]);')
175             ->line('}')
176             ->line('hex[32] = \'\\0\';')
177             ->blank
178             ->line('ST(0) = sv_2mortal(newSVpv(hex, 32));')
179             ->xs_return('1')
180             ->xs_end;
181              
182             # --------------------------------------------------------
183             # jit_verify_signature: Combined verify operation
184             # Input: signed_cookie (SV*), secret (SV*)
185             # Output: session_id if valid (SV*), undef if invalid
186             # Format: 32-char-hex-id.16-char-hex-sig (49 chars total)
187             # --------------------------------------------------------
188 0         0 $builder->xs_function('jit_verify_signature')
189             ->xs_preamble
190             ->line('int i;')
191             ->line('if (items != 2) croak("Usage: _jit_verify_signature(signed_cookie, secret)");')
192             ->line('STRLEN cookie_len, secret_len;')
193             ->line('const char* cookie = SvPV(ST(0), cookie_len);')
194             ->line('const char* secret = SvPV(ST(1), secret_len);')
195             ->blank
196             ->comment('Validate format: 32-char-hex-id.16-char-hex-sig')
197             ->if('cookie_len != 49 || cookie[32] != \'.\'')
198             ->line('ST(0) = &PL_sv_undef;')
199             ->line('XSRETURN(1);')
200             ->endif
201             ->blank
202             ->comment('Validate hex characters in session_id')
203             ->line('for (i = 0; i < 32; i++) {')
204             ->line(' char c = cookie[i];')
205             ->line(' if (!((c >= \'0\' && c <= \'9\') || (c >= \'a\' && c <= \'f\'))) {')
206             ->line(' ST(0) = &PL_sv_undef;')
207             ->line(' XSRETURN(1);')
208             ->line(' }')
209             ->line('}')
210             ->blank
211             ->comment('Extract session_id')
212             ->line('char session_id[33];')
213             ->line('memcpy(session_id, cookie, 32);')
214             ->line('session_id[32] = \'\\0\';')
215             ->blank
216             ->comment('Get provided signature')
217             ->line('const char* provided_sig = cookie + 33;')
218             ->blank
219             ->comment('Compute expected HMAC signature')
220             ->line('unsigned char digest[EVP_MAX_MD_SIZE];')
221             ->line('unsigned int digest_len = 0;')
222             ->line('HMAC(EVP_sha256(), secret, (int)secret_len, (unsigned char*)session_id, 32, digest, &digest_len);')
223             ->blank
224             ->comment('Convert first 8 bytes to hex (16 chars)')
225             ->line('char expected[17];')
226             ->line('for (i = 0; i < 8; i++) {')
227             ->line(' sprintf(expected + i*2, "%02x", digest[i]);')
228             ->line('}')
229             ->line('expected[16] = \'\\0\';')
230             ->blank
231             ->comment('Constant-time comparison')
232             ->line('unsigned char diff = 0;')
233             ->line('for (i = 0; i < 16; i++) {')
234             ->line(' diff |= expected[i] ^ provided_sig[i];')
235             ->line('}')
236             ->blank
237             ->if('diff != 0')
238             ->line('ST(0) = &PL_sv_undef;')
239             ->line('XSRETURN(1);')
240             ->endif
241             ->blank
242             ->line('ST(0) = sv_2mortal(newSVpv(session_id, 32));')
243             ->xs_return('1')
244             ->xs_end;
245              
246             # --------------------------------------------------------
247             # jit_session_get_set: Get or set session value
248             # Input: req (AV*), key (SV*), [value (SV*)]
249             # Output: value (SV*) or undef
250             # --------------------------------------------------------
251 0         0 $builder->xs_function('jit_session_get_set')
252             ->xs_preamble
253             ->line('if (items < 2) croak("Usage: _jit_session_get_set(req, key, [value])");')
254             ->line('SV** ary = AvARRAY((AV*)SvRV(ST(0)));')
255             ->line('SV* session_sv = ary[' . SLOT_SESSION . '];')
256             ->blank
257             ->comment('Check if session data exists')
258             ->if('!session_sv || !SvROK(session_sv) || SvTYPE(SvRV(session_sv)) != SVt_PVHV')
259             ->line('ST(0) = &PL_sv_undef;')
260             ->line('XSRETURN(1);')
261             ->endif
262             ->blank
263             ->line('HV* session = (HV*)SvRV(session_sv);')
264             ->line('STRLEN klen;')
265             ->line('const char* key = SvPV(ST(1), klen);')
266             ->blank
267             ->if('items == 2')
268             ->comment('Getter mode')
269             ->line('SV** val = hv_fetch(session, key, klen, 0);')
270             ->line('ST(0) = (val && *val) ? *val : &PL_sv_undef;')
271             ->else
272             ->comment('Setter mode')
273             ->line('hv_store(session, key, klen, newSVsv(ST(2)), 0);')
274             ->line('ary[' . SLOT_SESSION_MODIFIED . '] = newSViv(1);')
275             ->line('ST(0) = ST(2);')
276             ->endif
277             ->xs_return('1')
278             ->xs_end;
279              
280             # --------------------------------------------------------
281             # jit_session_get_all: Get all session data
282             # Input: req (AV*)
283             # Output: session hashref (SV*) or undef
284             # --------------------------------------------------------
285 0         0 $builder->xs_function('jit_session_get_all')
286             ->xs_preamble
287             ->line('if (items < 1) croak("Usage: _jit_session_get_all(req)");')
288             ->line('SV** ary = AvARRAY((AV*)SvRV(ST(0)));')
289             ->line('SV* session_sv = ary[' . SLOT_SESSION . '];')
290             ->if('session_sv && SvROK(session_sv)')
291             ->line('ST(0) = session_sv;')
292             ->else
293             ->line('ST(0) = &PL_sv_undef;')
294             ->endif
295             ->xs_return('1')
296             ->xs_end;
297              
298             # Compile via XS::JIT
299 0         0 eval {
300 0         0 XS::JIT->compile(
301             code => $builder->code,
302             name => $module_name,
303             cache_dir => $cache_dir,
304             extra_cflags => $extra_cflags,
305             extra_ldflags => $extra_ldflags,
306             functions => {
307             'Hypersonic::Session::_jit_hmac_sha256_hex' => { source => 'jit_hmac_sha256_hex', is_xs_native => 1 },
308             'Hypersonic::Session::_jit_constant_time_compare' => { source => 'jit_constant_time_compare', is_xs_native => 1 },
309             'Hypersonic::Session::_jit_generate_session_id' => { source => 'jit_generate_session_id', is_xs_native => 1 },
310             'Hypersonic::Session::_jit_verify_signature' => { source => 'jit_verify_signature', is_xs_native => 1 },
311             'Hypersonic::Session::_jit_session_get_set' => { source => 'jit_session_get_set', is_xs_native => 1 },
312             'Hypersonic::Session::_jit_session_get_all' => { source => 'jit_session_get_all', is_xs_native => 1 },
313             },
314             );
315 0         0 $COMPILED = 1;
316             };
317 0 0       0 if ($@) {
318             warn "Hypersonic::Session: JIT compilation failed, using pure Perl: $@\n"
319 0 0       0 if $ENV{HYPERSONIC_DEBUG};
320             }
321              
322 0         0 return $COMPILED;
323             }
324              
325             # Check if JIT is compiled
326 0     0 1 0 sub is_jit_compiled { $COMPILED }
327              
328             # ============================================================
329             # Core Cryptographic Functions (JIT or Perl fallback)
330             # ============================================================
331              
332             # Session ID generation using secure random
333             sub _generate_session_id {
334             # Use JIT-compiled version if available and actually defined
335 6 50 33 6   166395 if ($COMPILED && defined &_jit_generate_session_id) {
336 0         0 return _jit_generate_session_id();
337             }
338              
339             # Perl fallback
340 6         17 my $bytes;
341              
342             # Try /dev/urandom first (Unix)
343 6 50       166 if (-r '/dev/urandom') {
344 6 50       363 open my $fh, '<:raw', '/dev/urandom' or die "Cannot open /dev/urandom: $!";
345 6         494 read($fh, $bytes, 16);
346 6         93 close $fh;
347             } else {
348             # Fallback to Perl's rand (less secure, but works everywhere)
349 0         0 $bytes = pack('L*', map { int(rand(2**32)) } 1..4);
  0         0  
350             }
351              
352 6         39 return unpack('H*', $bytes);
353             }
354              
355             # Sign a session ID with HMAC-SHA256
356             sub _sign {
357 4     4   5779 my ($session_id, $secret) = @_;
358              
359             # Use JIT-compiled HMAC if available and actually defined
360 4 50 33     31 if ($COMPILED && defined &_jit_hmac_sha256_hex) {
361 0         0 my $sig = _jit_hmac_sha256_hex($session_id, $secret, 16);
362 0         0 return "$session_id.$sig";
363             }
364              
365             # Perl fallback
366 4         82 my $sig = substr(hmac_sha256_hex($session_id, $secret), 0, 16);
367 4         29 return "$session_id.$sig";
368             }
369              
370             # Verify and extract session ID from signed cookie
371             sub _verify {
372 5     5   2817 my ($signed_cookie, $secret) = @_;
373              
374             # Use JIT-compiled combined verify if available and actually defined
375 5 50 33     26 if ($COMPILED && defined &_jit_verify_signature) {
376 0         0 return _jit_verify_signature($signed_cookie, $secret);
377             }
378              
379             # Perl fallback
380 5 100 66     132 return unless $signed_cookie && $signed_cookie =~ /^([a-f0-9]{32})\.([a-f0-9]{16})$/;
381              
382 4         33 my ($session_id, $sig) = ($1, $2);
383 4         45 my $expected = substr(hmac_sha256_hex($session_id, $secret), 0, 16);
384              
385             # Constant-time comparison to prevent timing attacks
386 4 50       20 return unless length($sig) == length($expected);
387 4         6 my $diff = 0;
388 4         71 $diff |= ord(substr($sig, $_, 1)) ^ ord(substr($expected, $_, 1)) for 0 .. length($sig) - 1;
389              
390 4 100       25 return $diff == 0 ? $session_id : undef;
391             }
392              
393             # Constant-time string comparison
394             sub _constant_time_compare {
395 0     0   0 my ($s1, $s2) = @_;
396              
397             # Use JIT-compiled version if available and actually defined
398 0 0 0     0 if ($COMPILED && defined &_jit_constant_time_compare) {
399 0         0 return _jit_constant_time_compare($s1, $s2);
400             }
401              
402             # Perl fallback
403 0 0 0     0 return 0 unless defined $s1 && defined $s2;
404 0 0       0 return 0 unless length($s1) == length($s2);
405              
406 0         0 my $diff = 0;
407 0         0 $diff |= ord(substr($s1, $_, 1)) ^ ord(substr($s2, $_, 1)) for 0 .. length($s1) - 1;
408              
409 0 0       0 return $diff == 0 ? 1 : 0;
410             }
411              
412             # ============================================================
413             # Session Configuration and Middleware
414             # ============================================================
415              
416             # Configure session handling
417             sub configure {
418 7     7 0 3855 my ($class, %opts) = @_;
419              
420 7 100 100     82 die "Session secret is required" unless $opts{secret} && length($opts{secret}) >= 16;
421              
422             # Try to compile JIT ops on first configuration
423 5 50       52 $class->compile_session_ops(cache_dir => $opts{cache_dir}) unless $COMPILED;
424              
425             $SESSION_CONFIG = {
426             secret => $opts{secret},
427             cookie_name => $opts{cookie_name} // 'hsid',
428             max_age => $opts{max_age} // 86400, # 1 day default
429             path => $opts{path} // '/',
430             httponly => $opts{httponly} // 1,
431             secure => $opts{secure} // 0,
432 5   100     129 samesite => $opts{samesite} // 'Lax',
      100        
      50        
      50        
      50        
      50        
433             };
434              
435 5         15 return $SESSION_CONFIG;
436             }
437              
438             # Get session config
439 0     0 0 0 sub config { $SESSION_CONFIG }
440              
441             # Generate before middleware for session loading
442             sub before_middleware {
443             return sub {
444 4     4   752 my ($req) = @_;
445              
446 4 50       14 my $config = $SESSION_CONFIG or return;
447              
448             # Get session cookie
449 4         20 my $signed_cookie = $req->cookie($config->{cookie_name});
450              
451 4         9 my ($session_id, $data);
452              
453 4 100       11 if ($signed_cookie) {
454             # Verify signature and get session ID
455 1         15 $session_id = _verify($signed_cookie, $config->{secret});
456              
457 1 50 33     15 if ($session_id && exists $SESSION_STORE{$session_id}) {
458 1         3 $data = $SESSION_STORE{$session_id};
459             }
460             }
461              
462             # Create new session if needed
463 4 100 66     46 unless ($session_id && $data) {
464 3         11 $session_id = _generate_session_id();
465 3         21 $data = { _created => time(), _new => 1 };
466 3         9 $SESSION_STORE{$session_id} = $data;
467             }
468              
469             # Store in request (use the session slot)
470 4         14 $req->[SLOT_SESSION] = $data;
471 4         9 $req->[SLOT_SESSION_ID] = $session_id;
472 4         11 $req->[SLOT_SESSION_MODIFIED] = 0;
473              
474 4         16 return; # Continue to handler
475 4     4 0 59 };
476             }
477              
478             # Generate after middleware for session saving
479             sub after_middleware {
480             return sub {
481 2     2   16 my ($req, $res) = @_;
482              
483 2 50       9 my $config = $SESSION_CONFIG or return $res;
484              
485 2         6 my $session_id = $req->[SLOT_SESSION_ID];
486 2         5 my $data = $req->[SLOT_SESSION];
487 2         5 my $modified = $req->[SLOT_SESSION_MODIFIED];
488 2   33     17 my $is_new = $data && $data->{_new};
489              
490             # Only set cookie if session is new or modified
491 2 50 33     12 if ($session_id && ($is_new || $modified)) {
      33        
492 2 50       9 delete $data->{_new} if $data;
493              
494             # Sign the session ID
495 2         10 my $signed = _sign($session_id, $config->{secret});
496              
497             # Set cookie on response
498             $res->cookie($config->{cookie_name}, $signed,
499             path => $config->{path},
500             max_age => $config->{max_age},
501             httponly => $config->{httponly},
502             secure => $config->{secure},
503             samesite => $config->{samesite},
504 2         28 );
505              
506             # Save session data
507 2 50       9 $SESSION_STORE{$session_id} = $data if $data;
508             }
509              
510 2         7 return $res;
511 3     3 0 22 };
512             }
513              
514             # ============================================================
515             # Session Data Access Methods
516             # ============================================================
517              
518             # Session accessor - get or set session values
519             # Called as: $req->session('key') or $req->session('key', $value)
520             sub get_set {
521 5     5 0 40 my ($req, $key, $value) = @_;
522              
523             # Use JIT-compiled version if available
524 5 50 33     23 if ($COMPILED && defined &_jit_session_get_set) {
525 0 0       0 return @_ == 2
526             ? _jit_session_get_set($req, $key)
527             : _jit_session_get_set($req, $key, $value);
528             }
529              
530             # Perl fallback
531 5         9 my $data = $req->[SLOT_SESSION];
532 5 50       11 return unless $data;
533              
534 5 100       14 if (@_ == 2) {
535             # Getter
536 4         24 return $data->{$key};
537             } else {
538             # Setter
539 1         6 $data->{$key} = $value;
540 1         3 $req->[SLOT_SESSION_MODIFIED] = 1;
541 1         2 return $value;
542             }
543             }
544              
545             # Get all session data
546             sub get_all {
547 1     1 0 2 my ($req) = @_;
548              
549             # Use JIT-compiled version if available
550 1 50 33     3 if ($COMPILED && defined &_jit_session_get_all) {
551 0         0 return _jit_session_get_all($req);
552             }
553              
554 1         2 return $req->[SLOT_SESSION];
555             }
556              
557             # Clear session
558             sub clear {
559 1     1 0 4 my ($req) = @_;
560              
561 1         3 my $session_id = $req->[SLOT_SESSION_ID];
562 1 50       6 delete $SESSION_STORE{$session_id} if $session_id;
563              
564 1         4 $req->[SLOT_SESSION] = {};
565 1         4 $req->[SLOT_SESSION_MODIFIED] = 1;
566             }
567              
568             # Regenerate session ID (for security, e.g., after login)
569             sub regenerate {
570 1     1 0 3 my ($req) = @_;
571              
572 1         4 my $old_id = $req->[SLOT_SESSION_ID];
573 1   50     6 my $data = $req->[SLOT_SESSION] || {};
574              
575             # Delete old session
576 1 50       6 delete $SESSION_STORE{$old_id} if $old_id;
577              
578             # Create new session ID
579 1         3 my $new_id = _generate_session_id();
580 1         5 $SESSION_STORE{$new_id} = $data;
581              
582 1         3 $req->[SLOT_SESSION_ID] = $new_id;
583 1         3 $req->[SLOT_SESSION_MODIFIED] = 1;
584              
585 1         6 return $new_id;
586             }
587              
588             # Cleanup expired sessions (call periodically)
589             sub cleanup {
590 1     1 0 5 my ($max_age) = @_;
591 1   0     5 $max_age //= $SESSION_CONFIG->{max_age} // 86400;
      33        
592              
593 1         3 my $cutoff = time() - $max_age;
594              
595 1         13 for my $id (keys %SESSION_STORE) {
596 1         5 my $data = $SESSION_STORE{$id};
597 1 50 33     16 if ($data && $data->{_created} && $data->{_created} < $cutoff) {
      33        
598 1         6 delete $SESSION_STORE{$id};
599             }
600             }
601             }
602              
603             # For testing - get store size
604 4     4   870 sub _store_size { scalar keys %SESSION_STORE }
605              
606             # For testing - clear entire store
607 5     5   20294 sub _clear_store { %SESSION_STORE = () }
608              
609             # For testing - reset JIT state
610 0     0     sub _reset_jit { $COMPILED = 0; $MODULE_ID = 0; }
  0            
611              
612             1;
613              
614             __END__