| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
package Hypersonic; |
|
2
|
|
|
|
|
|
|
|
|
3
|
35
|
|
|
35
|
|
4426742
|
use strict; |
|
|
35
|
|
|
|
|
58
|
|
|
|
35
|
|
|
|
|
1137
|
|
|
4
|
35
|
|
|
35
|
|
151
|
use warnings; |
|
|
35
|
|
|
|
|
51
|
|
|
|
35
|
|
|
|
|
1810
|
|
|
5
|
35
|
|
|
35
|
|
553
|
use 5.010; |
|
|
35
|
|
|
|
|
105
|
|
|
6
|
|
|
|
|
|
|
|
|
7
|
|
|
|
|
|
|
our $VERSION = '0.15'; |
|
8
|
|
|
|
|
|
|
|
|
9
|
35
|
|
|
35
|
|
171
|
use Scalar::Util qw(blessed); |
|
|
35
|
|
|
|
|
92
|
|
|
|
35
|
|
|
|
|
1887
|
|
|
10
|
35
|
|
|
35
|
|
13359
|
use XS::JIT; |
|
|
35
|
|
|
|
|
31407
|
|
|
|
35
|
|
|
|
|
1179
|
|
|
11
|
35
|
|
|
35
|
|
18208
|
use XS::JIT::Builder; |
|
|
35
|
|
|
|
|
43697
|
|
|
|
35
|
|
|
|
|
1810
|
|
|
12
|
35
|
|
|
35
|
|
16413
|
use Hypersonic::Socket; |
|
|
35
|
|
|
|
|
75
|
|
|
|
35
|
|
|
|
|
212
|
|
|
13
|
35
|
|
|
35
|
|
17150
|
use Hypersonic::Protocol::HTTP1; |
|
|
35
|
|
|
|
|
87
|
|
|
|
35
|
|
|
|
|
1114
|
|
|
14
|
35
|
|
|
35
|
|
174
|
use Hypersonic::JIT::Util; |
|
|
35
|
|
|
|
|
43
|
|
|
|
35
|
|
|
|
|
147847
|
|
|
15
|
|
|
|
|
|
|
|
|
16
|
|
|
|
|
|
|
# Cache deparser instance for handler analysis (B::Deparse lazy-loaded) |
|
17
|
|
|
|
|
|
|
my $DEPARSER; |
|
18
|
|
|
|
|
|
|
|
|
19
|
|
|
|
|
|
|
# Protocol module for HTTP/1.1 (extensible for HTTP/2 in future) |
|
20
|
|
|
|
|
|
|
my $PROTOCOL = 'Hypersonic::Protocol::HTTP1'; |
|
21
|
|
|
|
|
|
|
|
|
22
|
|
|
|
|
|
|
# Optional TLS support |
|
23
|
|
|
|
|
|
|
my $HAS_TLS = 0; |
|
24
|
|
|
|
|
|
|
eval { require Hypersonic::TLS; $HAS_TLS = Hypersonic::TLS::check_openssl(); }; |
|
25
|
|
|
|
|
|
|
|
|
26
|
|
|
|
|
|
|
# Check for HTTP/2 support (nghttp2) |
|
27
|
|
|
|
|
|
|
my $HAS_HTTP2 = 0; |
|
28
|
|
|
|
|
|
|
eval { require Hypersonic::Protocol::HTTP2; $HAS_HTTP2 = Hypersonic::Protocol::HTTP2::check_nghttp2() ? 1 : 0; }; |
|
29
|
|
|
|
|
|
|
|
|
30
|
|
|
|
|
|
|
sub new { |
|
31
|
48
|
|
|
48
|
1
|
1975776
|
my ($class, %opts) = @_; |
|
32
|
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
# Validate TLS options |
|
34
|
48
|
50
|
|
|
|
797
|
if ($opts{tls}) { |
|
35
|
0
|
0
|
|
|
|
0
|
die "TLS support not available (OpenSSL not found)" unless $HAS_TLS; |
|
36
|
0
|
0
|
|
|
|
0
|
die "cert_file required for TLS" unless $opts{cert_file}; |
|
37
|
0
|
0
|
|
|
|
0
|
die "key_file required for TLS" unless $opts{key_file}; |
|
38
|
0
|
0
|
|
|
|
0
|
die "cert_file not found: $opts{cert_file}" unless -f $opts{cert_file}; |
|
39
|
0
|
0
|
|
|
|
0
|
die "key_file not found: $opts{key_file}" unless -f $opts{key_file}; |
|
40
|
|
|
|
|
|
|
} |
|
41
|
|
|
|
|
|
|
|
|
42
|
|
|
|
|
|
|
# Validate HTTP/2 options |
|
43
|
48
|
50
|
|
|
|
491
|
if ($opts{http2}) { |
|
44
|
0
|
0
|
|
|
|
0
|
die "HTTP/2 requires TLS (set tls => 1)" unless $opts{tls}; |
|
45
|
0
|
0
|
|
|
|
0
|
die "HTTP/2 not available (nghttp2 not found)" unless $HAS_HTTP2; |
|
46
|
|
|
|
|
|
|
} |
|
47
|
|
|
|
|
|
|
|
|
48
|
|
|
|
|
|
|
# Security headers configuration |
|
49
|
48
|
|
100
|
|
|
972
|
my $security_headers = $opts{security_headers} // {}; |
|
50
|
|
|
|
|
|
|
|
|
51
|
|
|
|
|
|
|
return bless { |
|
52
|
|
|
|
|
|
|
routes => [], |
|
53
|
|
|
|
|
|
|
compiled => 0, |
|
54
|
|
|
|
|
|
|
cache_dir => $opts{cache_dir} // '_hypersonic_cache', |
|
55
|
|
|
|
|
|
|
id => int(rand(100000)), |
|
56
|
|
|
|
|
|
|
# Server options |
|
57
|
|
|
|
|
|
|
host => $opts{host} // '0.0.0.0', |
|
58
|
|
|
|
|
|
|
port => $opts{port} // 8080, |
|
59
|
|
|
|
|
|
|
# TLS options |
|
60
|
|
|
|
|
|
|
tls => $opts{tls} // 0, |
|
61
|
|
|
|
|
|
|
cert_file => $opts{cert_file}, |
|
62
|
|
|
|
|
|
|
key_file => $opts{key_file}, |
|
63
|
|
|
|
|
|
|
# HTTP/2 support |
|
64
|
|
|
|
|
|
|
http2 => $opts{http2} // 0, |
|
65
|
|
|
|
|
|
|
# Security hardening options |
|
66
|
|
|
|
|
|
|
max_connections => $opts{max_connections} // 10000, |
|
67
|
|
|
|
|
|
|
max_request_size => $opts{max_request_size} // 8192, |
|
68
|
|
|
|
|
|
|
keepalive_timeout => $opts{keepalive_timeout} // 30, |
|
69
|
|
|
|
|
|
|
recv_timeout => $opts{recv_timeout} // 30, |
|
70
|
|
|
|
|
|
|
# WebSocket JIT options - granular control |
|
71
|
|
|
|
|
|
|
websocket_rooms => $opts{websocket_rooms} // 0, # Enable Room support |
|
72
|
|
|
|
|
|
|
max_rooms => $opts{max_rooms} // 1000, |
|
73
|
|
|
|
|
|
|
max_clients_per_room => $opts{max_clients_per_room} // 10000, |
|
74
|
|
|
|
|
|
|
# Graceful shutdown |
|
75
|
|
|
|
|
|
|
drain_timeout => $opts{drain_timeout} // 5, |
|
76
|
|
|
|
|
|
|
# JIT extension points |
|
77
|
|
|
|
|
|
|
c_helpers => $opts{c_helpers}, # User C helper functions |
|
78
|
|
|
|
|
|
|
# Security headers (JIT optimized - pre-computed at compile time) |
|
79
|
|
|
|
|
|
|
security_headers => { |
|
80
|
|
|
|
|
|
|
'X-Frame-Options' => $security_headers->{'X-Frame-Options'} // 'DENY', |
|
81
|
|
|
|
|
|
|
'X-Content-Type-Options' => $security_headers->{'X-Content-Type-Options'} // 'nosniff', |
|
82
|
|
|
|
|
|
|
'X-XSS-Protection' => $security_headers->{'X-XSS-Protection'} // '1; mode=block', |
|
83
|
|
|
|
|
|
|
'Referrer-Policy' => $security_headers->{'Referrer-Policy'} // 'strict-origin-when-cross-origin', |
|
84
|
|
|
|
|
|
|
'Content-Security-Policy' => $security_headers->{'Content-Security-Policy'}, # User must set this |
|
85
|
|
|
|
|
|
|
'Strict-Transport-Security' => ($opts{tls} ? ($security_headers->{'Strict-Transport-Security'} // 'max-age=31536000; includeSubDomains') : undef), |
|
86
|
|
|
|
|
|
|
'Permissions-Policy' => $security_headers->{'Permissions-Policy'}, # User can optionally set |
|
87
|
|
|
|
|
|
|
}, |
|
88
|
|
|
|
|
|
|
enable_security_headers => $opts{enable_security_headers} // 1, # Enabled by default |
|
89
|
|
|
|
|
|
|
# Middleware support |
|
90
|
|
|
|
|
|
|
before_middleware => [], # Global before hooks |
|
91
|
|
|
|
|
|
|
after_middleware => [], # Global after hooks |
|
92
|
|
|
|
|
|
|
# Event backend (optional override) |
|
93
|
|
|
|
|
|
|
event_backend => $opts{event_backend}, |
|
94
|
48
|
50
|
100
|
|
|
9523
|
}, $class; |
|
|
|
|
50
|
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
95
|
|
|
|
|
|
|
} |
|
96
|
|
|
|
|
|
|
|
|
97
|
|
|
|
|
|
|
# Route registration methods |
|
98
|
56
|
|
|
56
|
1
|
1719
|
sub get { shift->_add_route('GET', @_) } |
|
99
|
8
|
|
|
8
|
1
|
242
|
sub post { shift->_add_route('POST', @_) } |
|
100
|
1
|
|
|
1
|
1
|
10
|
sub put { shift->_add_route('PUT', @_) } |
|
101
|
1
|
|
|
1
|
1
|
18
|
sub del { shift->_add_route('DELETE', @_) } |
|
102
|
0
|
|
|
0
|
1
|
0
|
sub patch { shift->_add_route('PATCH', @_) } |
|
103
|
0
|
|
|
0
|
1
|
0
|
sub head { shift->_add_route('HEAD', @_) } |
|
104
|
0
|
|
|
0
|
1
|
0
|
sub options { shift->_add_route('OPTIONS', @_) } |
|
105
|
|
|
|
|
|
|
|
|
106
|
|
|
|
|
|
|
# Health check endpoint - built-in route for load balancer / k8s probes |
|
107
|
|
|
|
|
|
|
sub health_check { |
|
108
|
1
|
|
|
1
|
0
|
391
|
my ($self, $path, $handler) = @_; |
|
109
|
1
|
|
50
|
|
|
15
|
$path //= '/health'; |
|
110
|
|
|
|
|
|
|
|
|
111
|
|
|
|
|
|
|
# Default handler returns JSON string (JIT compiled as constant) |
|
112
|
|
|
|
|
|
|
$handler //= sub { |
|
113
|
0
|
|
|
0
|
|
0
|
return '{"status":"ok"}'; |
|
114
|
1
|
|
33
|
|
|
11
|
}; |
|
115
|
|
|
|
|
|
|
|
|
116
|
1
|
|
|
|
|
5
|
return $self->get($path => $handler); |
|
117
|
|
|
|
|
|
|
} |
|
118
|
|
|
|
|
|
|
|
|
119
|
|
|
|
|
|
|
# Readiness check endpoint - separate from health for k8s |
|
120
|
|
|
|
|
|
|
sub ready_check { |
|
121
|
1
|
|
|
1
|
0
|
286
|
my ($self, $path, $handler) = @_; |
|
122
|
1
|
|
50
|
|
|
6
|
$path //= '/ready'; |
|
123
|
|
|
|
|
|
|
|
|
124
|
|
|
|
|
|
|
$handler //= sub { |
|
125
|
0
|
|
|
0
|
|
0
|
return '{"ready":true}'; |
|
126
|
1
|
|
33
|
|
|
5
|
}; |
|
127
|
|
|
|
|
|
|
|
|
128
|
1
|
|
|
|
|
3
|
return $self->get($path => $handler); |
|
129
|
|
|
|
|
|
|
} |
|
130
|
|
|
|
|
|
|
|
|
131
|
|
|
|
|
|
|
# WebSocket route registration |
|
132
|
|
|
|
|
|
|
sub websocket { |
|
133
|
4
|
|
|
4
|
1
|
50
|
my ($self, $path, $handler) = @_; |
|
134
|
|
|
|
|
|
|
|
|
135
|
4
|
50
|
|
|
|
17
|
die "WebSocket path must start with /" unless $path =~ m{^/}; |
|
136
|
4
|
50
|
|
|
|
12
|
die "WebSocket handler must be a coderef" unless ref($handler) eq 'CODE'; |
|
137
|
|
|
|
|
|
|
|
|
138
|
4
|
|
100
|
|
|
2
|
push @{$self->{websocket_routes} //= []}, { |
|
|
4
|
|
|
|
|
31
|
|
|
139
|
|
|
|
|
|
|
path => $path, |
|
140
|
|
|
|
|
|
|
handler => $handler, |
|
141
|
|
|
|
|
|
|
pattern => $self->_compile_path_pattern($path), |
|
142
|
|
|
|
|
|
|
}; |
|
143
|
|
|
|
|
|
|
|
|
144
|
4
|
|
|
|
|
11
|
return $self; |
|
145
|
|
|
|
|
|
|
} |
|
146
|
|
|
|
|
|
|
|
|
147
|
|
|
|
|
|
|
# Check if any WebSocket routes are registered |
|
148
|
|
|
|
|
|
|
sub _has_websocket_routes { |
|
149
|
21
|
|
|
21
|
|
64
|
my ($self) = @_; |
|
150
|
21
|
|
100
|
|
|
38
|
return scalar @{$self->{websocket_routes} // []}; |
|
|
21
|
|
|
|
|
279
|
|
|
151
|
|
|
|
|
|
|
} |
|
152
|
|
|
|
|
|
|
|
|
153
|
|
|
|
|
|
|
# Match a path against WebSocket routes |
|
154
|
|
|
|
|
|
|
sub _match_websocket_route { |
|
155
|
3
|
|
|
3
|
|
2381
|
my ($self, $path) = @_; |
|
156
|
|
|
|
|
|
|
|
|
157
|
3
|
|
50
|
|
|
6
|
for my $route (@{$self->{websocket_routes} // []}) { |
|
|
3
|
|
|
|
|
13
|
|
|
158
|
5
|
|
|
|
|
7
|
my $pattern = $route->{pattern}; |
|
159
|
5
|
100
|
|
|
|
36
|
if ($path =~ $pattern) { |
|
160
|
|
|
|
|
|
|
# Extract params |
|
161
|
2
|
|
|
|
|
2
|
my %params; |
|
162
|
2
|
|
|
|
|
8
|
my @captures = ($path =~ $pattern); |
|
163
|
2
|
|
|
|
|
13
|
my @param_names = $route->{path} =~ /:(\w+)/g; |
|
164
|
2
|
|
|
|
|
6
|
for my $i (0..$#param_names) { |
|
165
|
1
|
50
|
|
|
|
7
|
$params{$param_names[$i]} = $captures[$i] if defined $captures[$i]; |
|
166
|
|
|
|
|
|
|
} |
|
167
|
2
|
|
|
|
|
16
|
return ($route->{handler}, \%params); |
|
168
|
|
|
|
|
|
|
} |
|
169
|
|
|
|
|
|
|
} |
|
170
|
1
|
|
|
|
|
2
|
return; |
|
171
|
|
|
|
|
|
|
} |
|
172
|
|
|
|
|
|
|
|
|
173
|
|
|
|
|
|
|
# Compile path pattern (reuse for HTTP routes) |
|
174
|
|
|
|
|
|
|
sub _compile_path_pattern { |
|
175
|
6
|
|
|
6
|
|
1075
|
my ($self, $path) = @_; |
|
176
|
|
|
|
|
|
|
|
|
177
|
6
|
|
|
|
|
6
|
my $pattern = $path; |
|
178
|
6
|
|
|
|
|
21
|
$pattern =~ s{:(\w+)}{([^/]+)}g; # :param -> capture group |
|
179
|
6
|
|
|
|
|
9
|
$pattern =~ s{\*}{(.+)}g; # * -> greedy capture |
|
180
|
6
|
|
|
|
|
157
|
return qr{^$pattern$}; |
|
181
|
|
|
|
|
|
|
} |
|
182
|
|
|
|
|
|
|
|
|
183
|
|
|
|
|
|
|
# Static file serving - JIT-compiled for maximum performance |
|
184
|
|
|
|
|
|
|
# Files are read at compile time and baked into C constants |
|
185
|
|
|
|
|
|
|
sub static { |
|
186
|
0
|
|
|
0
|
1
|
0
|
my ($self, $url_prefix, $directory, $opts) = @_; |
|
187
|
|
|
|
|
|
|
|
|
188
|
0
|
|
0
|
|
|
0
|
$opts //= {}; |
|
189
|
0
|
0
|
|
|
|
0
|
die "URL prefix must start with /" unless $url_prefix =~ m{^/}; |
|
190
|
0
|
0
|
|
|
|
0
|
die "Directory does not exist: $directory" unless -d $directory; |
|
191
|
|
|
|
|
|
|
|
|
192
|
|
|
|
|
|
|
# Normalize paths |
|
193
|
0
|
|
|
|
|
0
|
$url_prefix =~ s{/$}{}; # Remove trailing slash |
|
194
|
0
|
|
|
|
|
0
|
$directory =~ s{/$}{}; |
|
195
|
|
|
|
|
|
|
|
|
196
|
|
|
|
|
|
|
# Store static config for compile-time processing |
|
197
|
0
|
|
0
|
|
|
0
|
push @{$self->{static_dirs} //= []}, { |
|
198
|
|
|
|
|
|
|
prefix => $url_prefix, |
|
199
|
|
|
|
|
|
|
directory => $directory, |
|
200
|
|
|
|
|
|
|
# Options |
|
201
|
|
|
|
|
|
|
max_age => $opts->{max_age} // 3600, # Cache-Control max-age |
|
202
|
|
|
|
|
|
|
index => $opts->{index} // 'index.html', # Directory index file |
|
203
|
|
|
|
|
|
|
etag => $opts->{etag} // 1, # Generate ETags |
|
204
|
0
|
|
0
|
|
|
0
|
gzip => $opts->{gzip} // 0, # Serve .gz files if available |
|
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
205
|
|
|
|
|
|
|
}; |
|
206
|
|
|
|
|
|
|
|
|
207
|
0
|
|
|
|
|
0
|
return $self; |
|
208
|
|
|
|
|
|
|
} |
|
209
|
|
|
|
|
|
|
|
|
210
|
|
|
|
|
|
|
# MIME type lookup - JIT baked into C at compile time |
|
211
|
|
|
|
|
|
|
my %MIME_TYPES = ( |
|
212
|
|
|
|
|
|
|
# Text |
|
213
|
|
|
|
|
|
|
html => 'text/html; charset=utf-8', |
|
214
|
|
|
|
|
|
|
htm => 'text/html; charset=utf-8', |
|
215
|
|
|
|
|
|
|
css => 'text/css; charset=utf-8', |
|
216
|
|
|
|
|
|
|
js => 'application/javascript; charset=utf-8', |
|
217
|
|
|
|
|
|
|
mjs => 'application/javascript; charset=utf-8', |
|
218
|
|
|
|
|
|
|
json => 'application/json; charset=utf-8', |
|
219
|
|
|
|
|
|
|
xml => 'application/xml; charset=utf-8', |
|
220
|
|
|
|
|
|
|
txt => 'text/plain; charset=utf-8', |
|
221
|
|
|
|
|
|
|
csv => 'text/csv; charset=utf-8', |
|
222
|
|
|
|
|
|
|
md => 'text/markdown; charset=utf-8', |
|
223
|
|
|
|
|
|
|
# Images |
|
224
|
|
|
|
|
|
|
png => 'image/png', |
|
225
|
|
|
|
|
|
|
jpg => 'image/jpeg', |
|
226
|
|
|
|
|
|
|
jpeg => 'image/jpeg', |
|
227
|
|
|
|
|
|
|
gif => 'image/gif', |
|
228
|
|
|
|
|
|
|
svg => 'image/svg+xml', |
|
229
|
|
|
|
|
|
|
ico => 'image/x-icon', |
|
230
|
|
|
|
|
|
|
webp => 'image/webp', |
|
231
|
|
|
|
|
|
|
avif => 'image/avif', |
|
232
|
|
|
|
|
|
|
# Fonts |
|
233
|
|
|
|
|
|
|
woff => 'font/woff', |
|
234
|
|
|
|
|
|
|
woff2 => 'font/woff2', |
|
235
|
|
|
|
|
|
|
ttf => 'font/ttf', |
|
236
|
|
|
|
|
|
|
otf => 'font/otf', |
|
237
|
|
|
|
|
|
|
eot => 'application/vnd.ms-fontobject', |
|
238
|
|
|
|
|
|
|
# Media |
|
239
|
|
|
|
|
|
|
mp3 => 'audio/mpeg', |
|
240
|
|
|
|
|
|
|
mp4 => 'video/mp4', |
|
241
|
|
|
|
|
|
|
webm => 'video/webm', |
|
242
|
|
|
|
|
|
|
ogg => 'audio/ogg', |
|
243
|
|
|
|
|
|
|
wav => 'audio/wav', |
|
244
|
|
|
|
|
|
|
# Documents |
|
245
|
|
|
|
|
|
|
pdf => 'application/pdf', |
|
246
|
|
|
|
|
|
|
zip => 'application/zip', |
|
247
|
|
|
|
|
|
|
gz => 'application/gzip', |
|
248
|
|
|
|
|
|
|
tar => 'application/x-tar', |
|
249
|
|
|
|
|
|
|
# Web |
|
250
|
|
|
|
|
|
|
wasm => 'application/wasm', |
|
251
|
|
|
|
|
|
|
map => 'application/json', |
|
252
|
|
|
|
|
|
|
); |
|
253
|
|
|
|
|
|
|
|
|
254
|
|
|
|
|
|
|
sub _get_mime_type { |
|
255
|
0
|
|
|
0
|
|
0
|
my ($path) = @_; |
|
256
|
0
|
|
|
|
|
0
|
my ($ext) = $path =~ /\.(\w+)$/; |
|
257
|
0
|
|
0
|
|
|
0
|
return $MIME_TYPES{lc($ext // '')} // 'application/octet-stream'; |
|
|
|
|
0
|
|
|
|
|
|
258
|
|
|
|
|
|
|
} |
|
259
|
|
|
|
|
|
|
|
|
260
|
|
|
|
|
|
|
# Middleware registration methods |
|
261
|
|
|
|
|
|
|
# before() - runs before route handler, can short-circuit by returning a response |
|
262
|
|
|
|
|
|
|
# after() - runs after route handler, can modify response |
|
263
|
|
|
|
|
|
|
# Accepts either: |
|
264
|
|
|
|
|
|
|
# - CODE ref (traditional Perl middleware, called via call_sv at runtime) |
|
265
|
|
|
|
|
|
|
# - Builder object (has build_before/build_after methods, generates inline C at compile time) |
|
266
|
|
|
|
|
|
|
sub before { |
|
267
|
0
|
|
|
0
|
1
|
0
|
my ($self, $handler) = @_; |
|
268
|
0
|
|
0
|
|
|
0
|
my $is_builder = blessed($handler) && ($handler->can('build_before') || $handler->can('build_after')); |
|
269
|
0
|
0
|
0
|
|
|
0
|
die "Middleware must be a code ref or builder object" |
|
270
|
|
|
|
|
|
|
unless ref($handler) eq 'CODE' || $is_builder; |
|
271
|
0
|
|
|
|
|
0
|
push @{$self->{before_middleware}}, $handler; |
|
|
0
|
|
|
|
|
0
|
|
|
272
|
0
|
|
|
|
|
0
|
return $self; |
|
273
|
|
|
|
|
|
|
} |
|
274
|
|
|
|
|
|
|
|
|
275
|
|
|
|
|
|
|
sub after { |
|
276
|
0
|
|
|
0
|
1
|
0
|
my ($self, $handler) = @_; |
|
277
|
0
|
|
0
|
|
|
0
|
my $is_builder = blessed($handler) && ($handler->can('build_before') || $handler->can('build_after')); |
|
278
|
0
|
0
|
0
|
|
|
0
|
die "Middleware must be a code ref or builder object" |
|
279
|
|
|
|
|
|
|
unless ref($handler) eq 'CODE' || $is_builder; |
|
280
|
0
|
|
|
|
|
0
|
push @{$self->{after_middleware}}, $handler; |
|
|
0
|
|
|
|
|
0
|
|
|
281
|
0
|
|
|
|
|
0
|
return $self; |
|
282
|
|
|
|
|
|
|
} |
|
283
|
|
|
|
|
|
|
|
|
284
|
|
|
|
|
|
|
# Enable request ID tracing - adds X-Request-ID header |
|
285
|
|
|
|
|
|
|
# Only loads middleware module when called (JIT philosophy) |
|
286
|
|
|
|
|
|
|
sub enable_request_id { |
|
287
|
1
|
|
|
1
|
0
|
343
|
my ($self, %opts) = @_; |
|
288
|
|
|
|
|
|
|
|
|
289
|
1
|
|
|
|
|
6
|
require Hypersonic::Middleware::RequestId; |
|
290
|
|
|
|
|
|
|
|
|
291
|
|
|
|
|
|
|
# Builder pattern: middleware() returns a builder object, not a coderef |
|
292
|
|
|
|
|
|
|
# The builder generates inline C at compile time - zero Perl in hot path |
|
293
|
1
|
|
|
|
|
2
|
push @{$self->{before_middleware}}, Hypersonic::Middleware::RequestId::middleware(%opts); |
|
|
1
|
|
|
|
|
4
|
|
|
294
|
1
|
|
|
|
|
2
|
push @{$self->{after_middleware}}, Hypersonic::Middleware::RequestId::after_middleware(%opts); |
|
|
1
|
|
|
|
|
2
|
|
|
295
|
|
|
|
|
|
|
|
|
296
|
1
|
|
|
|
|
2
|
return $self; |
|
297
|
|
|
|
|
|
|
} |
|
298
|
|
|
|
|
|
|
|
|
299
|
|
|
|
|
|
|
# Session configuration - enables session support with signed cookies |
|
300
|
|
|
|
|
|
|
# Only loads session module when called (JIT philosophy) |
|
301
|
|
|
|
|
|
|
sub session_config { |
|
302
|
1
|
|
|
1
|
1
|
16
|
my ($self, %opts) = @_; |
|
303
|
|
|
|
|
|
|
|
|
304
|
1
|
|
|
|
|
6
|
require Hypersonic::Session; |
|
305
|
|
|
|
|
|
|
|
|
306
|
|
|
|
|
|
|
# Configure the session module |
|
307
|
1
|
|
|
|
|
5
|
my $config = Hypersonic::Session->configure(%opts); |
|
308
|
|
|
|
|
|
|
|
|
309
|
|
|
|
|
|
|
# Inject session middleware |
|
310
|
|
|
|
|
|
|
# Before: Load session from cookie/store |
|
311
|
1
|
|
|
|
|
2
|
unshift @{$self->{before_middleware}}, Hypersonic::Session::before_middleware(); |
|
|
1
|
|
|
|
|
30
|
|
|
312
|
|
|
|
|
|
|
|
|
313
|
|
|
|
|
|
|
# After: Save session to cookie/store |
|
314
|
1
|
|
|
|
|
2
|
push @{$self->{after_middleware}}, Hypersonic::Session::after_middleware(); |
|
|
1
|
|
|
|
|
4
|
|
|
315
|
|
|
|
|
|
|
|
|
316
|
|
|
|
|
|
|
# Mark that we need cookies parsed for all dynamic routes |
|
317
|
1
|
|
|
|
|
2
|
$self->{_session_enabled} = 1; |
|
318
|
|
|
|
|
|
|
|
|
319
|
1
|
|
|
|
|
3
|
return $self; |
|
320
|
|
|
|
|
|
|
} |
|
321
|
|
|
|
|
|
|
|
|
322
|
|
|
|
|
|
|
# Async/Future Pool configuration - JIT-compiled thread pool for async operations |
|
323
|
|
|
|
|
|
|
# Only loads Future/Pool when called (JIT philosophy) |
|
324
|
|
|
|
|
|
|
sub async_pool { |
|
325
|
0
|
|
|
0
|
1
|
0
|
my ($self, %opts) = @_; |
|
326
|
|
|
|
|
|
|
|
|
327
|
0
|
|
|
|
|
0
|
require Hypersonic::Future; |
|
328
|
0
|
|
|
|
|
0
|
require Hypersonic::Future::Pool; |
|
329
|
|
|
|
|
|
|
|
|
330
|
|
|
|
|
|
|
# Create a Pool instance (OO - allows multiple pools) |
|
331
|
|
|
|
|
|
|
my $pool = Hypersonic::Future::Pool->new( |
|
332
|
|
|
|
|
|
|
workers => $opts{workers} // 8, |
|
333
|
0
|
|
0
|
|
|
0
|
queue_size => $opts{queue_size} // 4096, |
|
|
|
|
0
|
|
|
|
|
|
334
|
|
|
|
|
|
|
); |
|
335
|
|
|
|
|
|
|
|
|
336
|
|
|
|
|
|
|
# Store in array for event loop registration |
|
337
|
0
|
|
0
|
|
|
0
|
push @{$self->{_async_pools} //= []}, $pool; |
|
|
0
|
|
|
|
|
0
|
|
|
338
|
|
|
|
|
|
|
|
|
339
|
|
|
|
|
|
|
# Mark that async pool is enabled - JIT code gen will include thread pool |
|
340
|
0
|
|
|
|
|
0
|
$self->{_async_enabled} = 1; |
|
341
|
|
|
|
|
|
|
|
|
342
|
|
|
|
|
|
|
# Return the Pool object (not $self anymore) |
|
343
|
0
|
|
|
|
|
0
|
return $pool; |
|
344
|
|
|
|
|
|
|
} |
|
345
|
|
|
|
|
|
|
|
|
346
|
|
|
|
|
|
|
# Compression configuration - JIT-compiled gzip compression in C |
|
347
|
|
|
|
|
|
|
# Only loads compression module when called (JIT philosophy) |
|
348
|
|
|
|
|
|
|
sub compress { |
|
349
|
0
|
|
|
0
|
1
|
0
|
my ($self, %opts) = @_; |
|
350
|
|
|
|
|
|
|
|
|
351
|
0
|
|
|
|
|
0
|
require Hypersonic::Compress; |
|
352
|
|
|
|
|
|
|
|
|
353
|
|
|
|
|
|
|
# Check for zlib |
|
354
|
0
|
0
|
|
|
|
0
|
unless (Hypersonic::Compress::check_zlib()) { |
|
355
|
0
|
|
|
|
|
0
|
warn "Warning: zlib not found, compression disabled\n"; |
|
356
|
0
|
|
|
|
|
0
|
return $self; |
|
357
|
|
|
|
|
|
|
} |
|
358
|
|
|
|
|
|
|
|
|
359
|
|
|
|
|
|
|
# Configure compression |
|
360
|
0
|
|
|
|
|
0
|
my $config = Hypersonic::Compress->configure(%opts); |
|
361
|
|
|
|
|
|
|
|
|
362
|
|
|
|
|
|
|
# Mark that compression is enabled - JIT code gen will include zlib code |
|
363
|
0
|
|
|
|
|
0
|
$self->{_compression_enabled} = 1; |
|
364
|
0
|
|
|
|
|
0
|
$self->{_compression_config} = $config; |
|
365
|
|
|
|
|
|
|
|
|
366
|
0
|
|
|
|
|
0
|
return $self; |
|
367
|
|
|
|
|
|
|
} |
|
368
|
|
|
|
|
|
|
|
|
369
|
|
|
|
|
|
|
sub _add_route { |
|
370
|
66
|
|
|
66
|
|
402
|
my ($self, $method, $path, $handler, $opts) = @_; |
|
371
|
|
|
|
|
|
|
|
|
372
|
66
|
100
|
|
|
|
629
|
die "Path must start with /" unless $path =~ m{^/}; |
|
373
|
65
|
100
|
|
|
|
276
|
die "Handler must be a code ref" unless ref($handler) eq 'CODE'; |
|
374
|
|
|
|
|
|
|
|
|
375
|
|
|
|
|
|
|
# Check for dynamic option and feature flags |
|
376
|
64
|
|
|
|
|
125
|
my $dynamic = 0; |
|
377
|
64
|
|
|
|
|
810
|
my %features = ( |
|
378
|
|
|
|
|
|
|
parse_query => 0, # Parse ?key=value query strings |
|
379
|
|
|
|
|
|
|
parse_headers => 0, # Parse HTTP headers |
|
380
|
|
|
|
|
|
|
parse_cookies => 0, # Parse Cookie header |
|
381
|
|
|
|
|
|
|
parse_json => 0, # Parse JSON body (requires Cpanel::JSON::XS) |
|
382
|
|
|
|
|
|
|
parse_form => 0, # Parse form-urlencoded body |
|
383
|
|
|
|
|
|
|
response_helpers => 0, # JIT compile response helper methods |
|
384
|
|
|
|
|
|
|
streaming => 0, # Streaming response handler |
|
385
|
|
|
|
|
|
|
need_xs_builder => 0, # Handler receives XS::JIT::Builder |
|
386
|
|
|
|
|
|
|
); |
|
387
|
|
|
|
|
|
|
|
|
388
|
64
|
100
|
|
|
|
188
|
if (ref($opts) eq 'HASH') { |
|
389
|
18
|
100
|
|
|
|
62
|
$dynamic = $opts->{dynamic} ? 1 : 0; |
|
390
|
|
|
|
|
|
|
# Copy feature flags from options |
|
391
|
18
|
|
|
|
|
90
|
for my $feat (keys %features) { |
|
392
|
144
|
50
|
|
|
|
249
|
$features{$feat} = $opts->{$feat} ? 1 : 0 if exists $opts->{$feat}; |
|
|
|
100
|
|
|
|
|
|
|
393
|
|
|
|
|
|
|
} |
|
394
|
|
|
|
|
|
|
} |
|
395
|
|
|
|
|
|
|
|
|
396
|
|
|
|
|
|
|
# Parse path parameters (supports multiple: /users/:user_id/posts/:post_id) |
|
397
|
64
|
|
|
|
|
197
|
my @params; |
|
398
|
64
|
|
|
|
|
510
|
my @segments = split '/', $path; |
|
399
|
64
|
|
|
|
|
133
|
shift @segments; # Remove leading empty string |
|
400
|
|
|
|
|
|
|
|
|
401
|
64
|
|
|
|
|
208
|
for my $i (0 .. $#segments) { |
|
402
|
80
|
100
|
|
|
|
266
|
if ($segments[$i] =~ /^:(\w+)$/) { |
|
403
|
5
|
|
|
|
|
23
|
push @params, { name => $1, position => $i }; |
|
404
|
5
|
|
|
|
|
9
|
$dynamic = 1; # Path params imply dynamic |
|
405
|
|
|
|
|
|
|
} |
|
406
|
|
|
|
|
|
|
} |
|
407
|
|
|
|
|
|
|
|
|
408
|
|
|
|
|
|
|
# Streaming handlers are always dynamic |
|
409
|
64
|
100
|
|
|
|
213
|
if ($features{streaming}) { |
|
410
|
7
|
|
|
|
|
8
|
$dynamic = 1; |
|
411
|
|
|
|
|
|
|
} |
|
412
|
|
|
|
|
|
|
|
|
413
|
|
|
|
|
|
|
# need_xs_builder handlers are always dynamic (but handled specially at compile time) |
|
414
|
64
|
50
|
|
|
|
196
|
if ($features{need_xs_builder}) { |
|
415
|
0
|
|
|
|
|
0
|
$dynamic = 1; |
|
416
|
|
|
|
|
|
|
} |
|
417
|
|
|
|
|
|
|
|
|
418
|
64
|
|
|
|
|
1681
|
push @{$self->{routes}}, { |
|
419
|
|
|
|
|
|
|
method => $method, |
|
420
|
|
|
|
|
|
|
path => $path, |
|
421
|
|
|
|
|
|
|
handler => $handler, |
|
422
|
|
|
|
|
|
|
dynamic => $dynamic, |
|
423
|
|
|
|
|
|
|
streaming => $features{streaming}, |
|
424
|
|
|
|
|
|
|
need_xs_builder => $features{need_xs_builder}, |
|
425
|
|
|
|
|
|
|
params => \@params, |
|
426
|
|
|
|
|
|
|
segments => \@segments, |
|
427
|
|
|
|
|
|
|
features => \%features, |
|
428
|
|
|
|
|
|
|
# Per-route middleware (optional) |
|
429
|
|
|
|
|
|
|
before => $opts->{before} // [], |
|
430
|
64
|
|
50
|
|
|
91
|
after => $opts->{after} // [], |
|
|
|
|
50
|
|
|
|
|
|
431
|
|
|
|
|
|
|
}; |
|
432
|
|
|
|
|
|
|
|
|
433
|
64
|
|
|
|
|
281
|
return $self; |
|
434
|
|
|
|
|
|
|
} |
|
435
|
|
|
|
|
|
|
|
|
436
|
|
|
|
|
|
|
sub compile { |
|
437
|
23
|
|
|
23
|
1
|
1275
|
my ($self) = @_; |
|
438
|
|
|
|
|
|
|
|
|
439
|
23
|
100
|
50
|
|
|
93
|
die "No routes defined" unless @{$self->{routes}} || @{$self->{static_dirs} // []}; |
|
|
23
|
|
66
|
|
|
183
|
|
|
|
1
|
|
|
|
|
17
|
|
|
440
|
22
|
100
|
|
|
|
104
|
die "Already compiled" if $self->{compiled}; |
|
441
|
|
|
|
|
|
|
|
|
442
|
|
|
|
|
|
|
# ============================================================ |
|
443
|
|
|
|
|
|
|
# STATIC FILE PROCESSING - bake files into C at compile time |
|
444
|
|
|
|
|
|
|
# ============================================================ |
|
445
|
21
|
50
|
|
|
|
96
|
if (my $static_dirs = $self->{static_dirs}) { |
|
446
|
0
|
|
|
|
|
0
|
$self->_compile_static_files($static_dirs); |
|
447
|
|
|
|
|
|
|
} |
|
448
|
|
|
|
|
|
|
|
|
449
|
|
|
|
|
|
|
# ============================================================ |
|
450
|
|
|
|
|
|
|
# ROUTE ANALYSIS - determine what code to generate (JIT philosophy) |
|
451
|
|
|
|
|
|
|
# ============================================================ |
|
452
|
|
|
|
|
|
|
my %analysis = ( |
|
453
|
|
|
|
|
|
|
methods_used => {}, # GET => 1, POST => 1, etc. |
|
454
|
|
|
|
|
|
|
has_dynamic => 0, # Any dynamic routes? |
|
455
|
|
|
|
|
|
|
has_static => 0, # Any static routes? |
|
456
|
|
|
|
|
|
|
has_path_params => 0, # Any routes with :param? |
|
457
|
|
|
|
|
|
|
has_body_access => 0, # Any routes that need body? |
|
458
|
21
|
|
|
|
|
90
|
route_count => scalar(@{$self->{routes}}), |
|
459
|
|
|
|
|
|
|
all_same_prefix => undef, # Common prefix like /api/* |
|
460
|
|
|
|
|
|
|
single_method => undef, # Only one HTTP method used? |
|
461
|
|
|
|
|
|
|
# JIT feature flags - only generate code for features actually used |
|
462
|
|
|
|
|
|
|
needs_query => 0, # Any route needs query string parsing? |
|
463
|
|
|
|
|
|
|
needs_headers => 0, # Any route needs header access? |
|
464
|
|
|
|
|
|
|
needs_cookies => 0, # Any route needs cookie parsing? |
|
465
|
|
|
|
|
|
|
needs_json => 0, # Any route needs JSON body parsing? |
|
466
|
|
|
|
|
|
|
needs_form => 0, # Any route needs form data parsing? |
|
467
|
|
|
|
|
|
|
needs_response_helpers => 0, # Any route needs response helper methods? |
|
468
|
|
|
|
|
|
|
needs_streaming => 0, # Any route uses streaming responses? |
|
469
|
|
|
|
|
|
|
needs_xs_builder => 0, # Any route uses need_xs_builder? |
|
470
|
|
|
|
|
|
|
# Middleware flags - JIT: only generate middleware code if actually used |
|
471
|
21
|
|
|
|
|
67
|
has_global_before => scalar(@{$self->{before_middleware}}) > 0, |
|
472
|
21
|
|
|
|
|
59
|
has_global_after => scalar(@{$self->{after_middleware}}) > 0, |
|
|
21
|
|
|
|
|
881
|
|
|
473
|
|
|
|
|
|
|
has_route_middleware => 0, # Any route has before/after hooks? |
|
474
|
|
|
|
|
|
|
has_any_middleware => 0, # Global OR per-route middleware? |
|
475
|
|
|
|
|
|
|
); |
|
476
|
|
|
|
|
|
|
|
|
477
|
|
|
|
|
|
|
# First pass: collect method usage and route characteristics |
|
478
|
21
|
|
|
|
|
57
|
for my $route (@{$self->{routes}}) { |
|
|
21
|
|
|
|
|
196
|
|
|
479
|
47
|
|
|
|
|
163
|
$analysis{methods_used}{$route->{method}} = 1; |
|
480
|
|
|
|
|
|
|
|
|
481
|
47
|
100
|
|
|
|
192
|
if ($route->{dynamic}) { |
|
482
|
11
|
|
|
|
|
24
|
$analysis{has_dynamic} = 1; |
|
483
|
|
|
|
|
|
|
# Dynamic routes might need body access (POST/PUT/PATCH typically do) |
|
484
|
11
|
100
|
|
|
|
42
|
if ($route->{method} =~ /^(POST|PUT|PATCH)$/) { |
|
485
|
3
|
|
|
|
|
6
|
$analysis{has_body_access} = 1; |
|
486
|
|
|
|
|
|
|
} |
|
487
|
|
|
|
|
|
|
|
|
488
|
|
|
|
|
|
|
# JIT FEATURE DETECTION: Analyze handler code to detect what request |
|
489
|
|
|
|
|
|
|
# features it actually uses. This avoids generating unused parsing code. |
|
490
|
11
|
|
50
|
|
|
45
|
my $f = $route->{features} // {}; |
|
491
|
|
|
|
|
|
|
|
|
492
|
|
|
|
|
|
|
# Explicit flags take precedence |
|
493
|
11
|
50
|
|
|
|
75
|
$analysis{needs_query} = 1 if $f->{parse_query}; |
|
494
|
11
|
50
|
|
|
|
26
|
$analysis{needs_headers} = 1 if $f->{parse_headers}; |
|
495
|
11
|
50
|
|
|
|
35
|
$analysis{needs_cookies} = 1 if $f->{parse_cookies}; |
|
496
|
11
|
100
|
|
|
|
26
|
$analysis{needs_json} = 1 if $f->{parse_json}; |
|
497
|
11
|
50
|
|
|
|
23
|
$analysis{needs_form} = 1 if $f->{parse_form}; |
|
498
|
11
|
50
|
|
|
|
147
|
$analysis{needs_response_helpers} = 1 if $f->{response_helpers}; |
|
499
|
11
|
100
|
|
|
|
33
|
$analysis{needs_streaming} = 1 if $f->{streaming}; |
|
500
|
11
|
50
|
|
|
|
21
|
$analysis{needs_xs_builder} = 1 if $f->{need_xs_builder}; |
|
501
|
|
|
|
|
|
|
|
|
502
|
|
|
|
|
|
|
# Auto-detect by analyzing handler code |
|
503
|
11
|
|
|
|
|
59
|
my $handler_code = _deparse_handler($route->{handler}); |
|
504
|
11
|
50
|
|
|
|
47
|
if ($handler_code) { |
|
505
|
|
|
|
|
|
|
# Look for $req->{query} or ->{query} access patterns |
|
506
|
11
|
50
|
|
|
|
53
|
$analysis{needs_query} = 1 if $handler_code =~ /\{['"]*query['"]*\}/; |
|
507
|
11
|
50
|
|
|
|
49
|
$analysis{needs_headers} = 1 if $handler_code =~ /\{['"]*headers['"]*\}/; |
|
508
|
11
|
50
|
|
|
|
45
|
$analysis{needs_cookies} = 1 if $handler_code =~ /\{['"]*cookies['"]*\}/; |
|
509
|
11
|
50
|
|
|
|
78
|
$analysis{needs_json} = 1 if $handler_code =~ /\{['"]*json['"]*\}/; |
|
510
|
11
|
50
|
|
|
|
57
|
$analysis{needs_form} = 1 if $handler_code =~ /\{['"]*form['"]*\}/; |
|
511
|
|
|
|
|
|
|
} |
|
512
|
|
|
|
|
|
|
} else { |
|
513
|
36
|
|
|
|
|
68
|
$analysis{has_static} = 1; |
|
514
|
|
|
|
|
|
|
} |
|
515
|
|
|
|
|
|
|
|
|
516
|
47
|
50
|
|
|
|
65
|
if (@{$route->{params}}) { |
|
|
47
|
|
|
|
|
156
|
|
|
517
|
0
|
|
|
|
|
0
|
$analysis{has_path_params} = 1; |
|
518
|
|
|
|
|
|
|
} |
|
519
|
|
|
|
|
|
|
|
|
520
|
|
|
|
|
|
|
# Check for per-route middleware |
|
521
|
47
|
50
|
33
|
|
|
65
|
if (@{$route->{before}} || @{$route->{after}}) { |
|
|
47
|
|
|
|
|
263
|
|
|
|
47
|
|
|
|
|
209
|
|
|
522
|
0
|
|
|
|
|
0
|
$analysis{has_route_middleware} = 1; |
|
523
|
|
|
|
|
|
|
} |
|
524
|
|
|
|
|
|
|
} |
|
525
|
|
|
|
|
|
|
|
|
526
|
|
|
|
|
|
|
# Session support: force cookie parsing when sessions are enabled |
|
527
|
21
|
100
|
|
|
|
98
|
if ($self->{_session_enabled}) { |
|
528
|
1
|
|
|
|
|
4
|
$analysis{needs_cookies} = 1; |
|
529
|
|
|
|
|
|
|
} |
|
530
|
|
|
|
|
|
|
|
|
531
|
|
|
|
|
|
|
# Compression support: force header parsing to check Accept-Encoding |
|
532
|
21
|
50
|
|
|
|
83
|
if ($self->{_compression_enabled}) { |
|
533
|
0
|
|
|
|
|
0
|
$analysis{needs_headers} = 1; |
|
534
|
|
|
|
|
|
|
} |
|
535
|
|
|
|
|
|
|
|
|
536
|
|
|
|
|
|
|
# Async Pool support: enable thread pool integration. |
|
537
|
|
|
|
|
|
|
# Disabled on Windows - Pool uses pthread + eventfd, neither of |
|
538
|
|
|
|
|
|
|
# which exists there. (Future/Pool deserve a Win32 port; not in |
|
539
|
|
|
|
|
|
|
# the 0.14 minimum-viable Windows scope.) |
|
540
|
21
|
50
|
|
|
|
76
|
if ($self->{_async_enabled}) { |
|
541
|
0
|
0
|
|
|
|
0
|
if ($^O eq 'MSWin32') { |
|
542
|
0
|
|
|
|
|
0
|
warn "Hypersonic: async pool not supported on Windows; " |
|
543
|
|
|
|
|
|
|
. "ignoring.\n"; |
|
544
|
|
|
|
|
|
|
} else { |
|
545
|
0
|
|
|
|
|
0
|
$analysis{needs_async_pool} = 1; |
|
546
|
|
|
|
|
|
|
} |
|
547
|
|
|
|
|
|
|
} |
|
548
|
|
|
|
|
|
|
|
|
549
|
|
|
|
|
|
|
# Classify middleware as builder (inline C) or Perl (call_sv) |
|
550
|
|
|
|
|
|
|
# Builder middleware has build_before/build_after methods and generates C at compile time |
|
551
|
21
|
|
|
|
|
175
|
my (@builder_before, @perl_before, @builder_after, @perl_after); |
|
552
|
21
|
|
|
|
|
94
|
for my $mw (@{$self->{before_middleware}}) { |
|
|
21
|
|
|
|
|
98
|
|
|
553
|
1
|
50
|
0
|
|
|
4
|
if (blessed($mw) && ($mw->can('build_before') || $mw->can('build_after'))) { |
|
|
|
|
33
|
|
|
|
|
|
554
|
0
|
|
|
|
|
0
|
push @builder_before, $mw; |
|
555
|
|
|
|
|
|
|
} else { |
|
556
|
1
|
|
|
|
|
3
|
push @perl_before, $mw; |
|
557
|
|
|
|
|
|
|
} |
|
558
|
|
|
|
|
|
|
} |
|
559
|
21
|
|
|
|
|
65
|
for my $mw (@{$self->{after_middleware}}) { |
|
|
21
|
|
|
|
|
53
|
|
|
560
|
1
|
50
|
0
|
|
|
3
|
if (blessed($mw) && ($mw->can('build_before') || $mw->can('build_after'))) { |
|
|
|
|
33
|
|
|
|
|
|
561
|
0
|
|
|
|
|
0
|
push @builder_after, $mw; |
|
562
|
|
|
|
|
|
|
} else { |
|
563
|
1
|
|
|
|
|
2
|
push @perl_after, $mw; |
|
564
|
|
|
|
|
|
|
} |
|
565
|
|
|
|
|
|
|
} |
|
566
|
|
|
|
|
|
|
|
|
567
|
21
|
|
|
|
|
59
|
$analysis{builder_before} = \@builder_before; |
|
568
|
21
|
|
|
|
|
95
|
$analysis{builder_after} = \@builder_after; |
|
569
|
21
|
|
|
|
|
78
|
$analysis{perl_before} = \@perl_before; |
|
570
|
21
|
|
|
|
|
48
|
$analysis{perl_after} = \@perl_after; |
|
571
|
21
|
|
|
|
|
194
|
$analysis{has_builder_before} = @builder_before > 0; |
|
572
|
21
|
|
|
|
|
148
|
$analysis{has_builder_after} = @builder_after > 0; |
|
573
|
|
|
|
|
|
|
# Update flags: has_global_* now refers to Perl middleware only (for call_sv) |
|
574
|
21
|
|
|
|
|
152
|
$analysis{has_global_before} = @perl_before > 0; |
|
575
|
21
|
|
|
|
|
226
|
$analysis{has_global_after} = @perl_after > 0; |
|
576
|
|
|
|
|
|
|
|
|
577
|
|
|
|
|
|
|
# Allocate slots for builder middleware |
|
578
|
|
|
|
|
|
|
# Request slots 0-15 are reserved for core fields (see Hypersonic::Request) |
|
579
|
|
|
|
|
|
|
# Middleware slots start at 16 |
|
580
|
21
|
|
|
|
|
42
|
my $next_slot = 16; |
|
581
|
21
|
|
|
|
|
48
|
my %middleware_slots; |
|
582
|
21
|
|
|
|
|
78
|
for my $mw (@builder_before, @builder_after) { |
|
583
|
0
|
0
|
|
|
|
0
|
next unless $mw->can('slot_requirements'); |
|
584
|
0
|
|
|
|
|
0
|
my $reqs = $mw->slot_requirements; |
|
585
|
0
|
|
|
|
|
0
|
for my $name (keys %$reqs) { |
|
586
|
0
|
0
|
|
|
|
0
|
next if exists $middleware_slots{$name}; # Deduplicate |
|
587
|
0
|
|
|
|
|
0
|
$middleware_slots{$name} = $next_slot; |
|
588
|
0
|
|
|
|
|
0
|
$next_slot += $reqs->{$name}; |
|
589
|
|
|
|
|
|
|
} |
|
590
|
|
|
|
|
|
|
} |
|
591
|
21
|
|
|
|
|
61
|
$analysis{middleware_slots} = \%middleware_slots; |
|
592
|
|
|
|
|
|
|
|
|
593
|
|
|
|
|
|
|
# Determine if any middleware is present (global or per-route) |
|
594
|
|
|
|
|
|
|
$analysis{has_any_middleware} = $analysis{has_global_before} || |
|
595
|
|
|
|
|
|
|
$analysis{has_global_after} || |
|
596
|
|
|
|
|
|
|
$analysis{has_builder_before} || |
|
597
|
|
|
|
|
|
|
$analysis{has_builder_after} || |
|
598
|
21
|
|
33
|
|
|
362
|
$analysis{has_route_middleware}; |
|
599
|
|
|
|
|
|
|
|
|
600
|
|
|
|
|
|
|
# Check if only one method is used |
|
601
|
21
|
|
|
|
|
50
|
my @methods = keys %{$analysis{methods_used}}; |
|
|
21
|
|
|
|
|
224
|
|
|
602
|
21
|
100
|
|
|
|
70
|
if (@methods == 1) { |
|
603
|
16
|
|
|
|
|
30
|
$analysis{single_method} = $methods[0]; |
|
604
|
|
|
|
|
|
|
} |
|
605
|
|
|
|
|
|
|
|
|
606
|
|
|
|
|
|
|
# Check for common prefix (e.g., all routes start with /api) |
|
607
|
21
|
|
|
|
|
45
|
my @paths = map { $_->{path} } @{$self->{routes}}; |
|
|
47
|
|
|
|
|
176
|
|
|
|
21
|
|
|
|
|
88
|
|
|
608
|
21
|
100
|
|
|
|
84
|
if (@paths > 1) { |
|
609
|
11
|
|
|
|
|
164
|
my $prefix = _find_common_prefix(@paths); |
|
610
|
11
|
50
|
|
|
|
49
|
if (length($prefix) > 1) { # More than just "/" |
|
611
|
0
|
|
|
|
|
0
|
$analysis{all_same_prefix} = $prefix; |
|
612
|
|
|
|
|
|
|
} |
|
613
|
|
|
|
|
|
|
} |
|
614
|
|
|
|
|
|
|
|
|
615
|
|
|
|
|
|
|
# Store analysis for use in code generation |
|
616
|
21
|
|
|
|
|
47
|
$self->{route_analysis} = \%analysis; |
|
617
|
|
|
|
|
|
|
|
|
618
|
|
|
|
|
|
|
# Compile JIT request accessors (after analysis so we know what features are needed) |
|
619
|
21
|
|
|
|
|
22551
|
require Hypersonic::Request; |
|
620
|
|
|
|
|
|
|
Hypersonic::Request->compile_accessors( |
|
621
|
|
|
|
|
|
|
cache_dir => $self->{cache_dir}, |
|
622
|
|
|
|
|
|
|
response_helpers => $analysis{needs_response_helpers}, |
|
623
|
21
|
|
|
|
|
233
|
); |
|
624
|
|
|
|
|
|
|
|
|
625
|
|
|
|
|
|
|
# Pre-evaluate all static handlers and build FULL HTTP responses |
|
626
|
21
|
|
|
|
|
159
|
my @full_responses; |
|
627
|
|
|
|
|
|
|
my @dynamic_handlers; # Store CV refs for dynamic routes |
|
628
|
21
|
|
|
|
|
0
|
my @route_param_info; # Store param info for dynamic routes |
|
629
|
|
|
|
|
|
|
|
|
630
|
21
|
|
|
|
|
167
|
for my $i (0 .. $#{$self->{routes}}) { |
|
|
21
|
|
|
|
|
163
|
|
|
631
|
47
|
|
|
|
|
158
|
my $route = $self->{routes}[$i]; |
|
632
|
|
|
|
|
|
|
|
|
633
|
47
|
100
|
|
|
|
202
|
if (!$route->{dynamic}) { |
|
634
|
|
|
|
|
|
|
# Check if this is a pre-built static file response |
|
635
|
36
|
50
|
|
|
|
130
|
if ($route->{_static_file}) { |
|
636
|
0
|
|
|
|
|
0
|
push @full_responses, $route->{_static_response}; |
|
637
|
0
|
|
|
|
|
0
|
$route->{response_idx} = $#full_responses; |
|
638
|
0
|
|
|
|
|
0
|
next; |
|
639
|
|
|
|
|
|
|
} |
|
640
|
|
|
|
|
|
|
|
|
641
|
|
|
|
|
|
|
# need_xs_builder routes are handled later in code generation |
|
642
|
36
|
50
|
|
|
|
106
|
if ($route->{need_xs_builder}) { |
|
643
|
0
|
|
|
|
|
0
|
$route->{dynamic} = 1; # Treat as dynamic for dispatch |
|
644
|
0
|
|
|
|
|
0
|
push @dynamic_handlers, $route->{handler}; |
|
645
|
0
|
|
|
|
|
0
|
push @route_param_info, $route->{params}; |
|
646
|
0
|
|
|
|
|
0
|
$route->{handler_idx} = $#dynamic_handlers; |
|
647
|
0
|
|
|
|
|
0
|
next; |
|
648
|
|
|
|
|
|
|
} |
|
649
|
|
|
|
|
|
|
|
|
650
|
|
|
|
|
|
|
# RUN THE HANDLER ONCE - this is the magic |
|
651
|
36
|
|
|
|
|
176
|
my $result = $route->{handler}->(); |
|
652
|
|
|
|
|
|
|
|
|
653
|
|
|
|
|
|
|
# Support both string and [status, headers, body] format |
|
654
|
36
|
|
|
|
|
390
|
my ($status, $headers, $body); |
|
655
|
36
|
100
|
|
|
|
142
|
if (ref($result) eq 'ARRAY') { |
|
|
|
100
|
|
|
|
|
|
|
656
|
1
|
|
|
|
|
3
|
($status, $headers, $body) = @$result; |
|
657
|
1
|
|
50
|
|
|
4
|
$status //= 200; |
|
658
|
1
|
|
50
|
|
|
5
|
$headers //= {}; |
|
659
|
|
|
|
|
|
|
} elsif (ref($result) eq 'HASH') { |
|
660
|
8
|
|
100
|
|
|
23
|
$status = $result->{status} // 200; |
|
661
|
8
|
|
100
|
|
|
38
|
$headers = $result->{headers} // {}; |
|
662
|
8
|
|
50
|
|
|
16
|
$body = $result->{body} // ''; |
|
663
|
|
|
|
|
|
|
} else { |
|
664
|
27
|
|
|
|
|
70
|
$status = 200; |
|
665
|
27
|
|
|
|
|
52
|
$headers = {}; |
|
666
|
27
|
|
|
|
|
47
|
$body = $result; |
|
667
|
|
|
|
|
|
|
} |
|
668
|
|
|
|
|
|
|
|
|
669
|
36
|
100
|
66
|
|
|
348
|
die "Handler for $route->{method} $route->{path} must return a string or response structure" |
|
670
|
|
|
|
|
|
|
unless defined $body && !ref($body); |
|
671
|
|
|
|
|
|
|
|
|
672
|
|
|
|
|
|
|
# Build COMPLETE HTTP response via Protocol module (JIT at compile time) |
|
673
|
|
|
|
|
|
|
my $security_hdrs = $self->{enable_security_headers} |
|
674
|
35
|
50
|
|
|
|
387
|
? $self->_get_security_headers_string() |
|
675
|
|
|
|
|
|
|
: ''; |
|
676
|
|
|
|
|
|
|
|
|
677
|
35
|
|
|
|
|
573
|
my $full_response = $PROTOCOL->build_response( |
|
678
|
|
|
|
|
|
|
status => $status, |
|
679
|
|
|
|
|
|
|
headers => $headers, |
|
680
|
|
|
|
|
|
|
body => $body, |
|
681
|
|
|
|
|
|
|
keep_alive => 1, |
|
682
|
|
|
|
|
|
|
security_headers => $security_hdrs, |
|
683
|
|
|
|
|
|
|
); |
|
684
|
|
|
|
|
|
|
|
|
685
|
35
|
|
|
|
|
75
|
push @full_responses, $full_response; |
|
686
|
35
|
|
|
|
|
200
|
$route->{response_idx} = $#full_responses; |
|
687
|
|
|
|
|
|
|
} else { |
|
688
|
|
|
|
|
|
|
# Dynamic route - store handler index and param info |
|
689
|
11
|
|
|
|
|
27
|
push @dynamic_handlers, $route->{handler}; |
|
690
|
11
|
|
|
|
|
18
|
push @route_param_info, $route->{params}; |
|
691
|
11
|
|
|
|
|
28
|
$route->{handler_idx} = $#dynamic_handlers; |
|
692
|
|
|
|
|
|
|
} |
|
693
|
|
|
|
|
|
|
} |
|
694
|
|
|
|
|
|
|
|
|
695
|
|
|
|
|
|
|
# Store dynamic handlers and param info for runtime access |
|
696
|
20
|
|
|
|
|
69
|
$self->{dynamic_handlers} = \@dynamic_handlers; |
|
697
|
20
|
|
|
|
|
62
|
$self->{route_param_info} = \@route_param_info; |
|
698
|
|
|
|
|
|
|
|
|
699
|
|
|
|
|
|
|
# JIT: Build streaming handlers lookup table (only if streaming is enabled) |
|
700
|
20
|
100
|
|
|
|
101
|
if ($self->{route_analysis}{needs_streaming}) { |
|
701
|
2
|
|
|
|
|
3
|
my @streaming_flags; |
|
702
|
2
|
|
|
|
|
3
|
for my $route (@{$self->{routes}}) { |
|
|
2
|
|
|
|
|
6
|
|
|
703
|
4
|
100
|
|
|
|
12
|
next unless $route->{dynamic}; |
|
704
|
2
|
50
|
|
|
|
6
|
push @streaming_flags, $route->{streaming} ? 1 : 0; |
|
705
|
|
|
|
|
|
|
} |
|
706
|
2
|
|
|
|
|
7
|
$self->{_streaming_flags} = \@streaming_flags; |
|
707
|
|
|
|
|
|
|
} |
|
708
|
|
|
|
|
|
|
|
|
709
|
|
|
|
|
|
|
# JIT: Build WebSocket handlers lookup table |
|
710
|
20
|
50
|
|
|
|
276
|
if ($self->_has_websocket_routes()) { |
|
711
|
0
|
|
|
|
|
0
|
my @ws_handlers; |
|
712
|
|
|
|
|
|
|
my @ws_paths; |
|
713
|
0
|
|
|
|
|
0
|
for my $route (@{$self->{websocket_routes}}) { |
|
|
0
|
|
|
|
|
0
|
|
|
714
|
0
|
|
|
|
|
0
|
push @ws_handlers, $route->{handler}; |
|
715
|
0
|
|
|
|
|
0
|
push @ws_paths, $route->{path}; |
|
716
|
|
|
|
|
|
|
# Check for Room usage in route options |
|
717
|
0
|
0
|
|
|
|
0
|
if ($route->{opts}{rooms}) { |
|
718
|
0
|
|
|
|
|
0
|
$self->{route_analysis}{needs_websocket_rooms} = 1; |
|
719
|
|
|
|
|
|
|
} |
|
720
|
|
|
|
|
|
|
} |
|
721
|
0
|
|
|
|
|
0
|
$self->{_websocket_handlers} = \@ws_handlers; |
|
722
|
0
|
|
|
|
|
0
|
$self->{_websocket_paths} = \@ws_paths; |
|
723
|
0
|
|
|
|
|
0
|
$self->{route_analysis}{needs_websocket} = 1; |
|
724
|
|
|
|
|
|
|
# Handler is needed if we have websocket routes |
|
725
|
0
|
|
|
|
|
0
|
$self->{route_analysis}{needs_websocket_handler} = 1; |
|
726
|
|
|
|
|
|
|
# WebSocket uses Stream for connection handling |
|
727
|
0
|
|
|
|
|
0
|
$self->{route_analysis}{needs_streaming} = 1; |
|
728
|
|
|
|
|
|
|
} |
|
729
|
|
|
|
|
|
|
|
|
730
|
|
|
|
|
|
|
# JIT: Explicit opt-in for Room support (can also be set in new()) |
|
731
|
20
|
50
|
|
|
|
97
|
if ($self->{websocket_rooms}) { |
|
732
|
0
|
|
|
|
|
0
|
$self->{route_analysis}{needs_websocket_rooms} = 1; |
|
733
|
|
|
|
|
|
|
} |
|
734
|
|
|
|
|
|
|
|
|
735
|
|
|
|
|
|
|
# JIT: Build per-route middleware arrays (only if route middleware is present) |
|
736
|
20
|
|
|
|
|
44
|
my $analysis = $self->{route_analysis}; |
|
737
|
20
|
50
|
|
|
|
69
|
if ($analysis->{has_route_middleware}) { |
|
738
|
0
|
|
|
|
|
0
|
my @route_before_mw; |
|
739
|
|
|
|
|
|
|
my @route_after_mw; |
|
740
|
0
|
|
|
|
|
0
|
for my $route (@{$self->{routes}}) { |
|
|
0
|
|
|
|
|
0
|
|
|
741
|
0
|
0
|
|
|
|
0
|
next unless $route->{dynamic}; |
|
742
|
|
|
|
|
|
|
# Store arrays of middleware handlers per route (by handler_idx) |
|
743
|
0
|
|
|
|
|
0
|
push @route_before_mw, $route->{before}; |
|
744
|
0
|
|
|
|
|
0
|
push @route_after_mw, $route->{after}; |
|
745
|
|
|
|
|
|
|
} |
|
746
|
0
|
|
|
|
|
0
|
$self->{_route_before_mw} = \@route_before_mw; |
|
747
|
0
|
|
|
|
|
0
|
$self->{_route_after_mw} = \@route_after_mw; |
|
748
|
|
|
|
|
|
|
} |
|
749
|
|
|
|
|
|
|
|
|
750
|
|
|
|
|
|
|
# JIT: Store Perl-only middleware for runtime call_sv dispatch |
|
751
|
|
|
|
|
|
|
# Builder middleware is handled at compile time (inline C), not runtime |
|
752
|
20
|
100
|
66
|
|
|
248
|
if ($analysis->{has_global_before} || $analysis->{has_global_after}) { |
|
753
|
1
|
|
|
|
|
7
|
$self->{_perl_before_mw} = $analysis->{perl_before}; |
|
754
|
1
|
|
|
|
|
4
|
$self->{_perl_after_mw} = $analysis->{perl_after}; |
|
755
|
|
|
|
|
|
|
} |
|
756
|
|
|
|
|
|
|
|
|
757
|
|
|
|
|
|
|
# Generate C code with pure C event loop |
|
758
|
20
|
|
|
|
|
206
|
my $c_code = $self->_generate_server_code(\@full_responses); |
|
759
|
|
|
|
|
|
|
|
|
760
|
|
|
|
|
|
|
# Compile via XS::JIT |
|
761
|
20
|
|
|
|
|
150
|
my $module_name = 'Hypersonic::_Server_' . $self->{id}; |
|
762
|
|
|
|
|
|
|
|
|
763
|
|
|
|
|
|
|
# Build compile options - add TLS flags if enabled |
|
764
|
20
|
|
|
|
|
318
|
my %functions = ( |
|
765
|
|
|
|
|
|
|
"${module_name}::run_event_loop" => { |
|
766
|
|
|
|
|
|
|
source => 'hypersonic_run_event_loop', |
|
767
|
|
|
|
|
|
|
is_xs_native => 1, |
|
768
|
|
|
|
|
|
|
}, |
|
769
|
|
|
|
|
|
|
"${module_name}::dispatch" => { |
|
770
|
|
|
|
|
|
|
source => 'hypersonic_dispatch', |
|
771
|
|
|
|
|
|
|
is_xs_native => 1, |
|
772
|
|
|
|
|
|
|
}, |
|
773
|
|
|
|
|
|
|
); |
|
774
|
|
|
|
|
|
|
|
|
775
|
|
|
|
|
|
|
# Add Stream and SSE XS functions if streaming is enabled |
|
776
|
20
|
100
|
|
|
|
95
|
if ($self->{route_analysis}{needs_streaming}) { |
|
777
|
2
|
|
|
|
|
5
|
%functions = (%functions, %{Hypersonic::Stream->get_xs_functions()}); |
|
|
2
|
|
|
|
|
20
|
|
|
778
|
2
|
|
|
|
|
15
|
%functions = (%functions, %{Hypersonic::SSE->get_xs_functions()}); |
|
|
2
|
|
|
|
|
9
|
|
|
779
|
|
|
|
|
|
|
} |
|
780
|
|
|
|
|
|
|
|
|
781
|
|
|
|
|
|
|
# Add WebSocket XS functions if WebSocket routes are registered |
|
782
|
20
|
50
|
|
|
|
114
|
if ($self->{route_analysis}{needs_websocket}) { |
|
783
|
0
|
|
|
|
|
0
|
require Hypersonic::WebSocket; |
|
784
|
0
|
|
|
|
|
0
|
%functions = (%functions, %{Hypersonic::WebSocket->get_xs_functions()}); |
|
|
0
|
|
|
|
|
0
|
|
|
785
|
|
|
|
|
|
|
} |
|
786
|
|
|
|
|
|
|
|
|
787
|
|
|
|
|
|
|
# Add WebSocket Handler XS functions |
|
788
|
20
|
50
|
|
|
|
71
|
if ($self->{route_analysis}{needs_websocket_handler}) { |
|
789
|
0
|
|
|
|
|
0
|
require Hypersonic::WebSocket::Handler; |
|
790
|
0
|
|
|
|
|
0
|
%functions = (%functions, %{Hypersonic::WebSocket::Handler->get_xs_functions()}); |
|
|
0
|
|
|
|
|
0
|
|
|
791
|
|
|
|
|
|
|
} |
|
792
|
|
|
|
|
|
|
|
|
793
|
|
|
|
|
|
|
# Add WebSocket Room XS functions |
|
794
|
20
|
50
|
|
|
|
221
|
if ($self->{route_analysis}{needs_websocket_rooms}) { |
|
795
|
0
|
|
|
|
|
0
|
require Hypersonic::WebSocket::Room; |
|
796
|
0
|
|
|
|
|
0
|
%functions = (%functions, %{Hypersonic::WebSocket::Room->get_xs_functions()}); |
|
|
0
|
|
|
|
|
0
|
|
|
797
|
|
|
|
|
|
|
} |
|
798
|
|
|
|
|
|
|
|
|
799
|
|
|
|
|
|
|
# Add Future/Pool XS functions if async pool is enabled |
|
800
|
20
|
50
|
|
|
|
71
|
if ($self->{route_analysis}{needs_async_pool}) { |
|
801
|
0
|
|
|
|
|
0
|
require Hypersonic::Future; |
|
802
|
0
|
|
|
|
|
0
|
require Hypersonic::Future::Pool; |
|
803
|
0
|
|
|
|
|
0
|
%functions = (%functions, %{Hypersonic::Future->get_xs_functions()}); |
|
|
0
|
|
|
|
|
0
|
|
|
804
|
0
|
|
|
|
|
0
|
%functions = (%functions, %{Hypersonic::Future::Pool->get_xs_functions()}); |
|
|
0
|
|
|
|
|
0
|
|
|
805
|
|
|
|
|
|
|
} |
|
806
|
|
|
|
|
|
|
|
|
807
|
|
|
|
|
|
|
# Add need_xs_builder route additional XS functions (if any) |
|
808
|
|
|
|
|
|
|
# Note: The main handler functions are C functions called by call_xs_builder_handler, |
|
809
|
|
|
|
|
|
|
# NOT XS functions callable from Perl, so we don't register them. |
|
810
|
20
|
50
|
|
|
|
115
|
if (my $xsr = $self->{_xs_builder_routes}) { |
|
811
|
0
|
|
|
|
|
0
|
for my $entry (@$xsr) { |
|
812
|
0
|
|
|
|
|
0
|
my $result = $entry->{result}; |
|
813
|
|
|
|
|
|
|
|
|
814
|
|
|
|
|
|
|
# Add any additional XS functions the handler defined |
|
815
|
0
|
0
|
|
|
|
0
|
if ($result->{xs_functions}) { |
|
816
|
0
|
|
|
|
|
0
|
%functions = (%functions, %{$result->{xs_functions}}); |
|
|
0
|
|
|
|
|
0
|
|
|
817
|
|
|
|
|
|
|
} |
|
818
|
|
|
|
|
|
|
} |
|
819
|
|
|
|
|
|
|
} |
|
820
|
|
|
|
|
|
|
|
|
821
|
|
|
|
|
|
|
my %compile_opts = ( |
|
822
|
|
|
|
|
|
|
code => $c_code, |
|
823
|
|
|
|
|
|
|
name => $module_name, |
|
824
|
|
|
|
|
|
|
cache_dir => $self->{cache_dir}, |
|
825
|
20
|
|
|
|
|
522
|
functions => \%functions, |
|
826
|
|
|
|
|
|
|
); |
|
827
|
|
|
|
|
|
|
|
|
828
|
|
|
|
|
|
|
# Add OpenSSL flags for TLS support |
|
829
|
20
|
50
|
|
|
|
96
|
if ($self->{tls}) { |
|
830
|
0
|
|
|
|
|
0
|
$compile_opts{extra_cflags} = Hypersonic::TLS::get_extra_cflags(); |
|
831
|
0
|
|
|
|
|
0
|
$compile_opts{extra_ldflags} = Hypersonic::TLS::get_extra_ldflags(); |
|
832
|
|
|
|
|
|
|
} |
|
833
|
|
|
|
|
|
|
|
|
834
|
|
|
|
|
|
|
# Add nghttp2 flags for HTTP/2 support |
|
835
|
20
|
50
|
|
|
|
81
|
if ($self->{http2}) { |
|
836
|
0
|
|
|
|
|
0
|
require Hypersonic::Protocol::HTTP2; |
|
837
|
0
|
|
|
|
|
0
|
my $h2_cflags = Hypersonic::Protocol::HTTP2::get_extra_cflags(); |
|
838
|
0
|
|
|
|
|
0
|
my $h2_ldflags = Hypersonic::Protocol::HTTP2::get_extra_ldflags(); |
|
839
|
0
|
|
0
|
|
|
0
|
$compile_opts{extra_cflags} = ($compile_opts{extra_cflags} // '') . " $h2_cflags"; |
|
840
|
0
|
|
0
|
|
|
0
|
$compile_opts{extra_ldflags} = ($compile_opts{extra_ldflags} // '') . " $h2_ldflags"; |
|
841
|
|
|
|
|
|
|
} |
|
842
|
|
|
|
|
|
|
|
|
843
|
|
|
|
|
|
|
# Add zlib flags for compression support |
|
844
|
20
|
50
|
|
|
|
72
|
if ($self->{_compression_enabled}) { |
|
845
|
0
|
|
|
|
|
0
|
require Hypersonic::Compress; |
|
846
|
0
|
|
|
|
|
0
|
my ($cflags, $ldflags) = Hypersonic::Compress::get_zlib_flags(); |
|
847
|
0
|
|
0
|
|
|
0
|
$compile_opts{extra_cflags} = ($compile_opts{extra_cflags} // '') . " $cflags"; |
|
848
|
0
|
|
0
|
|
|
0
|
$compile_opts{extra_ldflags} = ($compile_opts{extra_ldflags} // '') . " $ldflags"; |
|
849
|
|
|
|
|
|
|
} |
|
850
|
|
|
|
|
|
|
|
|
851
|
|
|
|
|
|
|
# Add pthread flags for async pool (thread pool) |
|
852
|
20
|
50
|
|
|
|
124
|
if ($self->{route_analysis}{needs_async_pool}) { |
|
853
|
0
|
|
0
|
|
|
0
|
$compile_opts{extra_cflags} = ($compile_opts{extra_cflags} // '') . " -pthread"; |
|
854
|
0
|
|
0
|
|
|
0
|
$compile_opts{extra_ldflags} = ($compile_opts{extra_ldflags} // '') . " -lpthread"; |
|
855
|
|
|
|
|
|
|
} |
|
856
|
|
|
|
|
|
|
|
|
857
|
|
|
|
|
|
|
# Add event backend flags (e.g., io_uring needs -luring) |
|
858
|
20
|
50
|
|
|
|
65
|
if ($self->{_event_backend}) { |
|
859
|
20
|
|
|
|
|
147
|
my $backend = $self->{_event_backend}; |
|
860
|
20
|
50
|
|
|
|
532
|
if ($backend->can('extra_cflags')) { |
|
861
|
20
|
|
50
|
|
|
82
|
my $ev_cflags = $backend->extra_cflags // ''; |
|
862
|
20
|
50
|
0
|
|
|
78
|
$compile_opts{extra_cflags} = ($compile_opts{extra_cflags} // '') . " $ev_cflags" |
|
863
|
|
|
|
|
|
|
if $ev_cflags; |
|
864
|
|
|
|
|
|
|
} |
|
865
|
20
|
50
|
|
|
|
98
|
if ($backend->can('extra_ldflags')) { |
|
866
|
20
|
|
50
|
|
|
63
|
my $ev_ldflags = $backend->extra_ldflags // ''; |
|
867
|
20
|
50
|
0
|
|
|
58
|
$compile_opts{extra_ldflags} = ($compile_opts{extra_ldflags} // '') . " $ev_ldflags" |
|
868
|
|
|
|
|
|
|
if $ev_ldflags; |
|
869
|
|
|
|
|
|
|
} |
|
870
|
|
|
|
|
|
|
} |
|
871
|
|
|
|
|
|
|
|
|
872
|
|
|
|
|
|
|
# Windows: link Winsock so socket()/recv()/send()/etc. resolve in |
|
873
|
|
|
|
|
|
|
# the JIT-compiled .so. The select backend already adds this, but |
|
874
|
|
|
|
|
|
|
# belt-and-braces - the main compile uses these symbols too. |
|
875
|
20
|
50
|
|
|
|
184
|
if ($^O eq 'MSWin32') { |
|
876
|
0
|
|
0
|
|
|
0
|
my $ld = $compile_opts{extra_ldflags} // ''; |
|
877
|
0
|
0
|
|
|
|
0
|
$compile_opts{extra_ldflags} = $ld . ' -lws2_32' |
|
878
|
|
|
|
|
|
|
unless $ld =~ /-lws2_32\b/; |
|
879
|
|
|
|
|
|
|
} |
|
880
|
|
|
|
|
|
|
|
|
881
|
20
|
|
|
|
|
12896796
|
my $ok = XS::JIT->compile(%compile_opts); |
|
882
|
|
|
|
|
|
|
die "XS::JIT->compile failed for $module_name (check liburing/zlib/openssl " |
|
883
|
|
|
|
|
|
|
. "are installed and linkable; extra_ldflags='" |
|
884
|
20
|
50
|
0
|
|
|
356
|
. ($compile_opts{extra_ldflags} // '') . "')" |
|
885
|
|
|
|
|
|
|
unless $ok; |
|
886
|
|
|
|
|
|
|
|
|
887
|
|
|
|
|
|
|
# Store function references - confirm the boot xsub actually installed |
|
888
|
|
|
|
|
|
|
# them. If we somehow got past compile() with the .so loaded but the |
|
889
|
|
|
|
|
|
|
# symbols missing, fail loudly here rather than crash later in run(). |
|
890
|
|
|
|
|
|
|
{ |
|
891
|
35
|
|
|
35
|
|
318
|
no strict 'refs'; |
|
|
35
|
|
|
|
|
60
|
|
|
|
35
|
|
|
|
|
364356
|
|
|
|
20
|
|
|
|
|
48
|
|
|
892
|
20
|
|
|
|
|
42
|
$self->{run_loop_fn} = \&{"${module_name}::run_event_loop"}; |
|
|
20
|
|
|
|
|
259
|
|
|
893
|
20
|
|
|
|
|
53
|
$self->{dispatch_fn} = \&{"${module_name}::dispatch"}; |
|
|
20
|
|
|
|
|
133
|
|
|
894
|
|
|
|
|
|
|
die "XS::JIT loaded $module_name but ::dispatch is not defined " |
|
895
|
|
|
|
|
|
|
. "(stale cache_dir? link line was: ldflags='" |
|
896
|
|
|
|
|
|
|
. ($compile_opts{extra_ldflags} // '') . "')" |
|
897
|
20
|
50
|
0
|
|
|
46
|
unless defined &{"${module_name}::dispatch"}; |
|
|
20
|
|
|
|
|
119
|
|
|
898
|
|
|
|
|
|
|
} |
|
899
|
|
|
|
|
|
|
|
|
900
|
|
|
|
|
|
|
# Mark Future/Pool as compiled if async pool is enabled |
|
901
|
|
|
|
|
|
|
# (prevents them from trying to compile separately) |
|
902
|
20
|
50
|
|
|
|
118
|
if ($self->{route_analysis}{needs_async_pool}) { |
|
903
|
0
|
|
|
|
|
0
|
$Hypersonic::Future::COMPILED = 1; |
|
904
|
0
|
|
|
|
|
0
|
$Hypersonic::Future::Pool::COMPILED = 1; |
|
905
|
|
|
|
|
|
|
# Register custom ops for Future after compilation |
|
906
|
0
|
|
|
|
|
0
|
Hypersonic::Future->_register_ops(); |
|
907
|
|
|
|
|
|
|
} |
|
908
|
|
|
|
|
|
|
|
|
909
|
20
|
|
|
|
|
60
|
$self->{compiled} = 1; |
|
910
|
20
|
|
|
|
|
728
|
return $self; |
|
911
|
|
|
|
|
|
|
} |
|
912
|
|
|
|
|
|
|
|
|
913
|
|
|
|
|
|
|
sub _generate_server_code { |
|
914
|
20
|
|
|
20
|
|
92
|
my ($self, $full_responses) = @_; |
|
915
|
|
|
|
|
|
|
|
|
916
|
|
|
|
|
|
|
# Load event backend module |
|
917
|
20
|
|
|
|
|
6818
|
require Hypersonic::Event; |
|
918
|
20
|
|
33
|
|
|
667
|
my $backend_name = $self->{event_backend} // Hypersonic::Event->best_backend; |
|
919
|
20
|
|
|
|
|
196
|
my $backend = Hypersonic::Event->backend($backend_name); |
|
920
|
|
|
|
|
|
|
|
|
921
|
|
|
|
|
|
|
# Store backend for use in event loop generation |
|
922
|
20
|
|
|
|
|
340
|
$self->{_event_backend} = $backend; |
|
923
|
20
|
|
|
|
|
160
|
$self->{_event_backend_name} = $backend_name; |
|
924
|
|
|
|
|
|
|
|
|
925
|
20
|
|
|
|
|
426
|
my $builder = XS::JIT::Builder->new; |
|
926
|
|
|
|
|
|
|
|
|
927
|
|
|
|
|
|
|
# C99 detection for inline keyword |
|
928
|
20
|
|
|
|
|
461
|
my $inline = Hypersonic::JIT::Util->inline_keyword; |
|
929
|
|
|
|
|
|
|
|
|
930
|
|
|
|
|
|
|
# Check if we have any dynamic routes |
|
931
|
20
|
|
|
|
|
8107
|
my $has_dynamic = grep { $_->{dynamic} } @{$self->{routes}}; |
|
|
46
|
|
|
|
|
251
|
|
|
|
20
|
|
|
|
|
546
|
|
|
932
|
|
|
|
|
|
|
|
|
933
|
|
|
|
|
|
|
# Common includes - portable across POSIX and Windows. On Windows |
|
934
|
|
|
|
|
|
|
# we substitute Winsock for netinet/socket/unistd, and define a |
|
935
|
|
|
|
|
|
|
# couple of compatibility macros so the rest of the codegen can |
|
936
|
|
|
|
|
|
|
# stay POSIX-shaped. |
|
937
|
20
|
|
|
|
|
681
|
$builder->line('#include ') |
|
938
|
|
|
|
|
|
|
->line('#include ') |
|
939
|
|
|
|
|
|
|
->raw(<<'C'); |
|
940
|
|
|
|
|
|
|
#ifdef _WIN32 |
|
941
|
|
|
|
|
|
|
# define WIN32_LEAN_AND_MEAN |
|
942
|
|
|
|
|
|
|
# include |
|
943
|
|
|
|
|
|
|
# include |
|
944
|
|
|
|
|
|
|
# include |
|
945
|
|
|
|
|
|
|
# define close(fd) closesocket(fd) |
|
946
|
|
|
|
|
|
|
/* Windows has no O_NONBLOCK / fcntl(F_SETFL, O_NONBLOCK); use |
|
947
|
|
|
|
|
|
|
* ioctlsocket(FIONBIO) instead. Wrap both fcntl invocations the |
|
948
|
|
|
|
|
|
|
* codegen emits so they vanish on Windows. */ |
|
949
|
|
|
|
|
|
|
# define hs_set_nonblocking(fd) do { u_long _m = 1; ioctlsocket((fd), FIONBIO, &_m); } while(0) |
|
950
|
|
|
|
|
|
|
/* sockets don't raise SIGPIPE on Windows. */ |
|
951
|
|
|
|
|
|
|
# ifndef MSG_NOSIGNAL |
|
952
|
|
|
|
|
|
|
# define MSG_NOSIGNAL 0 |
|
953
|
|
|
|
|
|
|
# endif |
|
954
|
|
|
|
|
|
|
#else |
|
955
|
|
|
|
|
|
|
# include |
|
956
|
|
|
|
|
|
|
# include |
|
957
|
|
|
|
|
|
|
# include |
|
958
|
|
|
|
|
|
|
# include |
|
959
|
|
|
|
|
|
|
# include |
|
960
|
|
|
|
|
|
|
# include |
|
961
|
|
|
|
|
|
|
# define hs_set_nonblocking(fd) do { int _f = fcntl((fd), F_GETFL, 0); fcntl((fd), F_SETFL, _f | O_NONBLOCK); } while(0) |
|
962
|
|
|
|
|
|
|
#endif |
|
963
|
|
|
|
|
|
|
C |
|
964
|
|
|
|
|
|
|
|
|
965
|
|
|
|
|
|
|
# Backend-specific includes |
|
966
|
20
|
|
|
|
|
556
|
$builder->line($backend->includes); |
|
967
|
|
|
|
|
|
|
|
|
968
|
|
|
|
|
|
|
# Add signal and time headers for graceful shutdown |
|
969
|
20
|
|
|
|
|
316
|
$builder->line('#include ') |
|
970
|
|
|
|
|
|
|
->line('#include '); |
|
971
|
|
|
|
|
|
|
|
|
972
|
|
|
|
|
|
|
# Compression support - include zlib if compression is enabled |
|
973
|
20
|
50
|
|
|
|
207
|
if ($self->{_compression_enabled}) { |
|
974
|
0
|
|
|
|
|
0
|
$builder->line('#include ') |
|
975
|
|
|
|
|
|
|
->line('#define HYPERSONIC_COMPRESSION 1'); |
|
976
|
|
|
|
|
|
|
} |
|
977
|
|
|
|
|
|
|
|
|
978
|
|
|
|
|
|
|
# TLS support - include OpenSSL headers if TLS is enabled |
|
979
|
20
|
50
|
|
|
|
203
|
if ($self->{tls}) { |
|
980
|
0
|
|
|
|
|
0
|
$builder->raw(Hypersonic::TLS::gen_includes()) |
|
981
|
|
|
|
|
|
|
->line('#define HYPERSONIC_TLS 1'); |
|
982
|
|
|
|
|
|
|
} |
|
983
|
|
|
|
|
|
|
|
|
984
|
|
|
|
|
|
|
# HTTP/2 support - include nghttp2 if enabled |
|
985
|
20
|
50
|
|
|
|
168
|
if ($self->{http2}) { |
|
986
|
0
|
|
|
|
|
0
|
require Hypersonic::Protocol::HTTP2; |
|
987
|
0
|
|
|
|
|
0
|
Hypersonic::Protocol::HTTP2->gen_includes($builder); |
|
988
|
|
|
|
|
|
|
} |
|
989
|
|
|
|
|
|
|
|
|
990
|
|
|
|
|
|
|
# Security hardening configuration |
|
991
|
20
|
|
|
|
|
137
|
my $max_connections = $self->{max_connections}; |
|
992
|
20
|
|
|
|
|
92
|
my $max_request_size = $self->{max_request_size}; |
|
993
|
20
|
|
|
|
|
94
|
my $keepalive_timeout = $self->{keepalive_timeout}; |
|
994
|
20
|
|
|
|
|
77
|
my $recv_timeout = $self->{recv_timeout}; |
|
995
|
20
|
|
|
|
|
95
|
my $drain_timeout = $self->{drain_timeout}; |
|
996
|
|
|
|
|
|
|
|
|
997
|
|
|
|
|
|
|
# Backend-specific defines |
|
998
|
20
|
|
|
|
|
265
|
$builder->blank |
|
999
|
|
|
|
|
|
|
->line($backend->defines) |
|
1000
|
|
|
|
|
|
|
->line("#define RECV_BUF_SIZE $max_request_size") |
|
1001
|
|
|
|
|
|
|
->line("#define MAX_CONNECTIONS $max_connections"); |
|
1002
|
|
|
|
|
|
|
|
|
1003
|
|
|
|
|
|
|
# Enable security headers macro if configured |
|
1004
|
20
|
100
|
66
|
|
|
407
|
if ($self->{enable_security_headers} && $has_dynamic) { |
|
1005
|
4
|
|
|
|
|
36
|
$builder->line('#define HYPERSONIC_SECURITY_HEADERS 1'); |
|
1006
|
|
|
|
|
|
|
} |
|
1007
|
|
|
|
|
|
|
|
|
1008
|
|
|
|
|
|
|
$builder |
|
1009
|
20
|
|
|
|
|
546
|
->line("#define KEEPALIVE_TIMEOUT $keepalive_timeout") |
|
1010
|
|
|
|
|
|
|
->line("#define RECV_TIMEOUT $recv_timeout") |
|
1011
|
|
|
|
|
|
|
->line("#define DRAIN_TIMEOUT $drain_timeout") |
|
1012
|
|
|
|
|
|
|
->blank; |
|
1013
|
|
|
|
|
|
|
|
|
1014
|
|
|
|
|
|
|
# TLS-aware I/O macros - compile-time decision, zero runtime overhead |
|
1015
|
20
|
|
|
|
|
627
|
$builder->comment('TLS-aware I/O wrappers - compile-time branching') |
|
1016
|
|
|
|
|
|
|
->line('#ifdef HYPERSONIC_TLS') |
|
1017
|
|
|
|
|
|
|
->line('#define HYPERSONIC_SEND(fd, buf, len) do { \\') |
|
1018
|
|
|
|
|
|
|
->line(' TLSConnection* _tc = get_tls_connection(fd); \\') |
|
1019
|
|
|
|
|
|
|
->line(' if (_tc) tls_send(_tc, buf, len); \\') |
|
1020
|
|
|
|
|
|
|
->line(' else send(fd, buf, len, 0); \\') |
|
1021
|
|
|
|
|
|
|
->line('} while(0)') |
|
1022
|
|
|
|
|
|
|
->line('#define HYPERSONIC_CLOSE(fd) tls_close(fd)') |
|
1023
|
|
|
|
|
|
|
->line('#else') |
|
1024
|
|
|
|
|
|
|
->line('#define HYPERSONIC_SEND(fd, buf, len) send(fd, buf, len, 0)') |
|
1025
|
|
|
|
|
|
|
->line('#define HYPERSONIC_CLOSE(fd) close(fd)') |
|
1026
|
|
|
|
|
|
|
->line('#endif') |
|
1027
|
|
|
|
|
|
|
->blank; |
|
1028
|
|
|
|
|
|
|
|
|
1029
|
|
|
|
|
|
|
# User-defined C helpers (early, so they're available to all routes) |
|
1030
|
20
|
50
|
|
|
|
156
|
if (my $helpers = $self->{c_helpers}) { |
|
1031
|
0
|
|
|
|
|
0
|
$builder->comment('User-defined C helpers'); |
|
1032
|
0
|
0
|
|
|
|
0
|
if (ref $helpers eq 'CODE') { |
|
1033
|
0
|
|
|
|
|
0
|
my $helper_builder = XS::JIT::Builder->new; |
|
1034
|
0
|
|
|
|
|
0
|
$helpers->($helper_builder); |
|
1035
|
0
|
|
|
|
|
0
|
$builder->raw($helper_builder->code); |
|
1036
|
|
|
|
|
|
|
} else { |
|
1037
|
|
|
|
|
|
|
# Raw C string |
|
1038
|
0
|
|
|
|
|
0
|
$builder->raw($helpers); |
|
1039
|
|
|
|
|
|
|
} |
|
1040
|
0
|
|
|
|
|
0
|
$builder->blank; |
|
1041
|
|
|
|
|
|
|
} |
|
1042
|
|
|
|
|
|
|
|
|
1043
|
|
|
|
|
|
|
# Graceful shutdown support |
|
1044
|
20
|
|
|
|
|
482
|
$builder->comment('Graceful shutdown support') |
|
1045
|
|
|
|
|
|
|
->line('static volatile sig_atomic_t g_shutdown = 0;') |
|
1046
|
|
|
|
|
|
|
->line('static volatile int g_active_connections = 0;') |
|
1047
|
|
|
|
|
|
|
->blank |
|
1048
|
|
|
|
|
|
|
->line('static void handle_shutdown_signal(int sig) {') |
|
1049
|
|
|
|
|
|
|
->line(' (void)sig;') |
|
1050
|
|
|
|
|
|
|
->line(' g_shutdown = 1;') |
|
1051
|
|
|
|
|
|
|
->line('}') |
|
1052
|
|
|
|
|
|
|
->blank; |
|
1053
|
|
|
|
|
|
|
|
|
1054
|
|
|
|
|
|
|
# Compression support - JIT compiled zlib functions |
|
1055
|
20
|
50
|
|
|
|
149
|
if ($self->{_compression_enabled}) { |
|
1056
|
0
|
|
|
|
|
0
|
my $config = $self->{_compression_config}; |
|
1057
|
0
|
|
0
|
|
|
0
|
my $min_size = $config->{min_size} // 1024; |
|
1058
|
0
|
|
0
|
|
|
0
|
my $level = $config->{level} // 6; |
|
1059
|
|
|
|
|
|
|
|
|
1060
|
0
|
|
|
|
|
0
|
$builder->comment('Gzip compression support - JIT compiled') |
|
1061
|
|
|
|
|
|
|
->line('static __thread unsigned char gzip_out_buf[131072];') |
|
1062
|
|
|
|
|
|
|
->blank |
|
1063
|
|
|
|
|
|
|
->line('static int accepts_gzip(const char* accept_encoding, size_t len) {') |
|
1064
|
|
|
|
|
|
|
->line(' if (!accept_encoding || len == 0) return 0;') |
|
1065
|
|
|
|
|
|
|
->line(' const char* p = accept_encoding;') |
|
1066
|
|
|
|
|
|
|
->line(' const char* end = accept_encoding + len;') |
|
1067
|
|
|
|
|
|
|
->line(' while (p < end - 3) {') |
|
1068
|
|
|
|
|
|
|
->line(' if (p[0] == \'g\' && p[1] == \'z\' && p[2] == \'i\' && p[3] == \'p\') return 1;') |
|
1069
|
|
|
|
|
|
|
->line(' p++;') |
|
1070
|
|
|
|
|
|
|
->line(' }') |
|
1071
|
|
|
|
|
|
|
->line(' return 0;') |
|
1072
|
|
|
|
|
|
|
->line('}') |
|
1073
|
|
|
|
|
|
|
->blank |
|
1074
|
|
|
|
|
|
|
->line('static size_t gzip_compress(const char* input, size_t input_len, unsigned char** output) {') |
|
1075
|
|
|
|
|
|
|
->line(' size_t max_out;') |
|
1076
|
|
|
|
|
|
|
->line(' z_stream strm;') |
|
1077
|
|
|
|
|
|
|
->line(' int ret;') |
|
1078
|
|
|
|
|
|
|
->line(' size_t compressed_len;') |
|
1079
|
|
|
|
|
|
|
->line(" if (input_len < $min_size) return 0;") |
|
1080
|
|
|
|
|
|
|
->line(' max_out = compressBound(input_len) + 18;') |
|
1081
|
|
|
|
|
|
|
->line(' if (max_out > sizeof(gzip_out_buf)) return 0;') |
|
1082
|
|
|
|
|
|
|
->line(' memset(&strm, 0, sizeof(strm));') |
|
1083
|
|
|
|
|
|
|
->line(" if (deflateInit2(&strm, $level, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY) != Z_OK) return 0;") |
|
1084
|
|
|
|
|
|
|
->line(' strm.next_in = (Bytef*)input;') |
|
1085
|
|
|
|
|
|
|
->line(' strm.avail_in = input_len;') |
|
1086
|
|
|
|
|
|
|
->line(' strm.next_out = gzip_out_buf;') |
|
1087
|
|
|
|
|
|
|
->line(' strm.avail_out = sizeof(gzip_out_buf);') |
|
1088
|
|
|
|
|
|
|
->line(' ret = deflate(&strm, Z_FINISH);') |
|
1089
|
|
|
|
|
|
|
->line(' compressed_len = strm.total_out;') |
|
1090
|
|
|
|
|
|
|
->line(' deflateEnd(&strm);') |
|
1091
|
|
|
|
|
|
|
->line(' if (ret != Z_STREAM_END || compressed_len >= input_len) return 0;') |
|
1092
|
|
|
|
|
|
|
->line(' *output = gzip_out_buf;') |
|
1093
|
|
|
|
|
|
|
->line(' return compressed_len;') |
|
1094
|
|
|
|
|
|
|
->line('}') |
|
1095
|
|
|
|
|
|
|
->blank; |
|
1096
|
|
|
|
|
|
|
} |
|
1097
|
|
|
|
|
|
|
|
|
1098
|
|
|
|
|
|
|
# Connection tracking for keep-alive timeout - O(1) using fd as index |
|
1099
|
20
|
|
|
|
|
1153
|
$builder->comment('Connection tracking - O(1) using fd as direct index') |
|
1100
|
|
|
|
|
|
|
->line('#define MAX_FD 65536') |
|
1101
|
|
|
|
|
|
|
->line('static time_t g_conn_time[MAX_FD];') |
|
1102
|
|
|
|
|
|
|
->line('static time_t g_current_time = 0;') |
|
1103
|
|
|
|
|
|
|
->blank |
|
1104
|
|
|
|
|
|
|
->line("static $inline void track_connection(int fd, time_t now) {") |
|
1105
|
|
|
|
|
|
|
->line(' if (fd >= 0 && fd < MAX_FD) {') |
|
1106
|
|
|
|
|
|
|
->line(' g_conn_time[fd] = now;') |
|
1107
|
|
|
|
|
|
|
->line(' g_active_connections++;') |
|
1108
|
|
|
|
|
|
|
->line(' }') |
|
1109
|
|
|
|
|
|
|
->line('}') |
|
1110
|
|
|
|
|
|
|
->blank |
|
1111
|
|
|
|
|
|
|
->line("static $inline void update_connection(int fd, time_t now) {") |
|
1112
|
|
|
|
|
|
|
->line(' if (fd >= 0 && fd < MAX_FD) {') |
|
1113
|
|
|
|
|
|
|
->line(' g_conn_time[fd] = now;') |
|
1114
|
|
|
|
|
|
|
->line(' }') |
|
1115
|
|
|
|
|
|
|
->line('}') |
|
1116
|
|
|
|
|
|
|
->blank |
|
1117
|
|
|
|
|
|
|
->line("static $inline void remove_connection(int fd) {") |
|
1118
|
|
|
|
|
|
|
->line(' if (fd >= 0 && fd < MAX_FD && g_conn_time[fd] > 0) {') |
|
1119
|
|
|
|
|
|
|
->line(' g_conn_time[fd] = 0;') |
|
1120
|
|
|
|
|
|
|
->line(' g_active_connections--;') |
|
1121
|
|
|
|
|
|
|
->line(' }') |
|
1122
|
|
|
|
|
|
|
->line('}') |
|
1123
|
|
|
|
|
|
|
->blank; |
|
1124
|
|
|
|
|
|
|
|
|
1125
|
|
|
|
|
|
|
# TLS code generation - SSL context, accept, read/write wrappers |
|
1126
|
20
|
50
|
|
|
|
108
|
if ($self->{tls}) { |
|
1127
|
|
|
|
|
|
|
$builder->comment('TLS/HTTPS support via OpenSSL') |
|
1128
|
0
|
|
|
|
|
0
|
->raw(Hypersonic::TLS::gen_ssl_ctx_init(http2 => $self->{http2})) |
|
1129
|
|
|
|
|
|
|
->blank |
|
1130
|
|
|
|
|
|
|
->raw(Hypersonic::TLS::gen_ssl_accept()) |
|
1131
|
|
|
|
|
|
|
->blank |
|
1132
|
|
|
|
|
|
|
->raw(Hypersonic::TLS::gen_ssl_io()) |
|
1133
|
|
|
|
|
|
|
->blank |
|
1134
|
|
|
|
|
|
|
->raw(Hypersonic::TLS::gen_ssl_close()) |
|
1135
|
|
|
|
|
|
|
->blank; |
|
1136
|
|
|
|
|
|
|
} |
|
1137
|
|
|
|
|
|
|
|
|
1138
|
|
|
|
|
|
|
# HTTP/2 code generation - nghttp2 callbacks, session init, dispatchers |
|
1139
|
20
|
50
|
|
|
|
88
|
if ($self->{http2}) { |
|
1140
|
0
|
|
|
|
|
0
|
require Hypersonic::Protocol::HTTP2; |
|
1141
|
0
|
|
|
|
|
0
|
$builder->comment('HTTP/2 support via nghttp2'); |
|
1142
|
0
|
|
|
|
|
0
|
Hypersonic::Protocol::HTTP2->gen_connection_struct($builder); |
|
1143
|
0
|
|
|
|
|
0
|
Hypersonic::Protocol::HTTP2->gen_connection_preface_check($builder); |
|
1144
|
0
|
|
|
|
|
0
|
Hypersonic::Protocol::HTTP2->gen_response_sender($builder); |
|
1145
|
0
|
|
|
|
|
0
|
Hypersonic::Protocol::HTTP2->gen_404_response($builder); |
|
1146
|
0
|
|
|
|
|
0
|
Hypersonic::Protocol::HTTP2->gen_callbacks($builder); |
|
1147
|
0
|
|
|
|
|
0
|
Hypersonic::Protocol::HTTP2->gen_session_init($builder); |
|
1148
|
0
|
|
|
|
|
0
|
Hypersonic::Protocol::HTTP2->gen_dispatcher($builder); |
|
1149
|
0
|
|
|
|
|
0
|
Hypersonic::Protocol::HTTP2->gen_input_processor($builder); |
|
1150
|
0
|
|
|
|
|
0
|
$builder->blank; |
|
1151
|
|
|
|
|
|
|
} |
|
1152
|
|
|
|
|
|
|
|
|
1153
|
|
|
|
|
|
|
# Streaming support - JIT: only generate when streaming handlers detected |
|
1154
|
20
|
|
|
|
|
145
|
my $analysis = $self->{route_analysis}; |
|
1155
|
20
|
100
|
|
|
|
99
|
if ($analysis->{needs_streaming}) { |
|
1156
|
2
|
|
|
|
|
47
|
require Hypersonic::Stream; |
|
1157
|
|
|
|
|
|
|
Hypersonic::Stream->generate_c_code($builder, { |
|
1158
|
|
|
|
|
|
|
max_streams => $self->{max_connections}, |
|
1159
|
2
|
|
|
|
|
55
|
}); |
|
1160
|
|
|
|
|
|
|
|
|
1161
|
|
|
|
|
|
|
# SSE support - compile SSE methods when streaming is enabled |
|
1162
|
2
|
|
|
|
|
2144
|
require Hypersonic::SSE; |
|
1163
|
|
|
|
|
|
|
Hypersonic::SSE->generate_c_code($builder, { |
|
1164
|
|
|
|
|
|
|
max_sse_instances => $self->{max_connections}, |
|
1165
|
2
|
|
|
|
|
21
|
}); |
|
1166
|
|
|
|
|
|
|
} |
|
1167
|
|
|
|
|
|
|
|
|
1168
|
|
|
|
|
|
|
# WebSocket support - JIT: only generate when WebSocket routes exist |
|
1169
|
20
|
50
|
|
|
|
118
|
if ($analysis->{needs_websocket}) { |
|
1170
|
0
|
|
|
|
|
0
|
require Hypersonic::WebSocket; |
|
1171
|
0
|
|
|
|
|
0
|
require Hypersonic::Protocol::WebSocket; |
|
1172
|
0
|
|
|
|
|
0
|
require Hypersonic::Protocol::WebSocket::Frame; |
|
1173
|
|
|
|
|
|
|
|
|
1174
|
|
|
|
|
|
|
# Generate WebSocket frame encoding functions |
|
1175
|
|
|
|
|
|
|
Hypersonic::Protocol::WebSocket::Frame->generate_c_code($builder, { |
|
1176
|
|
|
|
|
|
|
max_connections => $self->{max_connections}, |
|
1177
|
0
|
|
|
|
|
0
|
}); |
|
1178
|
|
|
|
|
|
|
|
|
1179
|
|
|
|
|
|
|
# Generate WebSocket connection management |
|
1180
|
|
|
|
|
|
|
Hypersonic::WebSocket->generate_c_code($builder, { |
|
1181
|
|
|
|
|
|
|
max_websockets => $self->{max_connections}, |
|
1182
|
0
|
|
|
|
|
0
|
}); |
|
1183
|
|
|
|
|
|
|
} |
|
1184
|
|
|
|
|
|
|
|
|
1185
|
|
|
|
|
|
|
# WebSocket Handler - JIT: connection registry, only when websocket routes exist |
|
1186
|
20
|
50
|
|
|
|
132
|
if ($analysis->{needs_websocket_handler}) { |
|
1187
|
0
|
|
|
|
|
0
|
require Hypersonic::WebSocket::Handler; |
|
1188
|
|
|
|
|
|
|
Hypersonic::WebSocket::Handler->generate_c_code($builder, { |
|
1189
|
|
|
|
|
|
|
max_connections => $self->{max_connections}, |
|
1190
|
0
|
|
|
|
|
0
|
}); |
|
1191
|
|
|
|
|
|
|
} |
|
1192
|
|
|
|
|
|
|
|
|
1193
|
|
|
|
|
|
|
# WebSocket Rooms - JIT: broadcast groups, only when explicitly enabled |
|
1194
|
20
|
50
|
|
|
|
90
|
if ($analysis->{needs_websocket_rooms}) { |
|
1195
|
0
|
|
|
|
|
0
|
require Hypersonic::WebSocket::Room; |
|
1196
|
|
|
|
|
|
|
Hypersonic::WebSocket::Room->generate_c_code($builder, { |
|
1197
|
|
|
|
|
|
|
max_rooms => $self->{max_rooms}, |
|
1198
|
|
|
|
|
|
|
max_clients_per_room => $self->{max_clients_per_room}, |
|
1199
|
0
|
|
|
|
|
0
|
}); |
|
1200
|
|
|
|
|
|
|
} |
|
1201
|
|
|
|
|
|
|
|
|
1202
|
|
|
|
|
|
|
# Future/Pool - JIT: async thread pool for blocking operations |
|
1203
|
20
|
50
|
|
|
|
100
|
if ($analysis->{needs_async_pool}) { |
|
1204
|
0
|
|
|
|
|
0
|
require Hypersonic::Future; |
|
1205
|
0
|
|
|
|
|
0
|
require Hypersonic::Future::Pool; |
|
1206
|
0
|
|
0
|
|
|
0
|
my $async_config = $self->{_async_config} // {}; |
|
1207
|
|
|
|
|
|
|
Hypersonic::Future->generate_c_code($builder, { |
|
1208
|
0
|
|
0
|
|
|
0
|
max_futures => $async_config->{max_futures} // 65536, |
|
1209
|
|
|
|
|
|
|
}); |
|
1210
|
|
|
|
|
|
|
Hypersonic::Future::Pool->generate_c_code($builder, { |
|
1211
|
|
|
|
|
|
|
workers => $async_config->{workers} // 8, |
|
1212
|
0
|
|
0
|
|
|
0
|
queue_size => $async_config->{queue_size} // 4096, |
|
|
|
|
0
|
|
|
|
|
|
1213
|
|
|
|
|
|
|
}); |
|
1214
|
|
|
|
|
|
|
} |
|
1215
|
|
|
|
|
|
|
|
|
1216
|
|
|
|
|
|
|
# need_xs_builder routes - call handlers with fresh builder |
|
1217
|
20
|
50
|
|
|
|
120
|
if ($analysis->{needs_xs_builder}) { |
|
1218
|
0
|
|
|
|
|
0
|
my @xs_builder_routes; |
|
1219
|
0
|
|
|
|
|
0
|
for my $i (0 .. $#{$self->{routes}}) { |
|
|
0
|
|
|
|
|
0
|
|
|
1220
|
0
|
|
|
|
|
0
|
my $route = $self->{routes}[$i]; |
|
1221
|
0
|
0
|
|
|
|
0
|
next unless $route->{need_xs_builder}; |
|
1222
|
|
|
|
|
|
|
|
|
1223
|
|
|
|
|
|
|
# Create fresh builder for this handler |
|
1224
|
0
|
|
|
|
|
0
|
my $ext_builder = XS::JIT::Builder->new; |
|
1225
|
|
|
|
|
|
|
|
|
1226
|
|
|
|
|
|
|
# Call handler with clean builder |
|
1227
|
0
|
|
|
|
|
0
|
my $result = $route->{handler}->($ext_builder); |
|
1228
|
|
|
|
|
|
|
|
|
1229
|
|
|
|
|
|
|
die "need_xs_builder handler for $route->{method} $route->{path} must return hashref with xs_function" |
|
1230
|
0
|
0
|
0
|
|
|
0
|
unless ref($result) eq 'HASH' && $result->{xs_function}; |
|
1231
|
|
|
|
|
|
|
|
|
1232
|
|
|
|
|
|
|
# Store result |
|
1233
|
0
|
|
|
|
|
0
|
$route->{_xs_result} = $result; |
|
1234
|
0
|
|
|
|
|
0
|
push @xs_builder_routes, { |
|
1235
|
|
|
|
|
|
|
route_idx => $i, |
|
1236
|
|
|
|
|
|
|
route => $route, |
|
1237
|
|
|
|
|
|
|
result => $result, |
|
1238
|
|
|
|
|
|
|
code => $ext_builder->code, |
|
1239
|
|
|
|
|
|
|
}; |
|
1240
|
|
|
|
|
|
|
} |
|
1241
|
|
|
|
|
|
|
|
|
1242
|
|
|
|
|
|
|
# Emit generated code |
|
1243
|
0
|
0
|
|
|
|
0
|
if (@xs_builder_routes) { |
|
1244
|
0
|
|
|
|
|
0
|
$builder->comment('User XS builder routes'); |
|
1245
|
0
|
|
|
|
|
0
|
for my $xsr (@xs_builder_routes) { |
|
1246
|
|
|
|
|
|
|
$builder->comment("Route: $xsr->{route}{method} $xsr->{route}{path}") |
|
1247
|
|
|
|
|
|
|
->raw($xsr->{code}) |
|
1248
|
0
|
|
|
|
|
0
|
->blank; |
|
1249
|
|
|
|
|
|
|
} |
|
1250
|
|
|
|
|
|
|
} |
|
1251
|
|
|
|
|
|
|
|
|
1252
|
|
|
|
|
|
|
# Store for function merging later |
|
1253
|
0
|
|
|
|
|
0
|
$self->{_xs_builder_routes} = \@xs_builder_routes; |
|
1254
|
|
|
|
|
|
|
} |
|
1255
|
|
|
|
|
|
|
|
|
1256
|
|
|
|
|
|
|
# JIT: WebSocket handler storage (independent of dynamic routes) |
|
1257
|
20
|
50
|
|
|
|
100
|
if ($analysis->{needs_websocket}) { |
|
1258
|
0
|
|
|
|
|
0
|
$builder->comment('WebSocket handler storage') |
|
1259
|
|
|
|
|
|
|
->line('static SV* g_websocket_handlers = NULL;') |
|
1260
|
|
|
|
|
|
|
->blank; |
|
1261
|
|
|
|
|
|
|
} |
|
1262
|
|
|
|
|
|
|
|
|
1263
|
|
|
|
|
|
|
# Global storage for dynamic handler dispatch (only if needed) |
|
1264
|
20
|
100
|
|
|
|
73
|
if ($has_dynamic) { |
|
1265
|
4
|
|
|
|
|
36
|
$builder->comment('Storage for dynamic handler callbacks') |
|
1266
|
|
|
|
|
|
|
->line('static SV* g_handler_array = NULL;') |
|
1267
|
|
|
|
|
|
|
->line('static SV* g_server_obj = NULL;'); |
|
1268
|
|
|
|
|
|
|
|
|
1269
|
|
|
|
|
|
|
# JIT: Only generate middleware storage if middleware is present |
|
1270
|
4
|
100
|
|
|
|
14
|
if ($analysis->{has_any_middleware}) { |
|
1271
|
1
|
|
|
|
|
10
|
$builder->line('static SV* g_before_middleware = NULL;') |
|
1272
|
|
|
|
|
|
|
->line('static SV* g_after_middleware = NULL;') |
|
1273
|
|
|
|
|
|
|
->line('static SV* g_route_before_middleware = NULL;') |
|
1274
|
|
|
|
|
|
|
->line('static SV* g_route_after_middleware = NULL;'); |
|
1275
|
|
|
|
|
|
|
} |
|
1276
|
4
|
|
|
|
|
11
|
$builder->blank; |
|
1277
|
|
|
|
|
|
|
|
|
1278
|
|
|
|
|
|
|
# Generate param info table for named path parameters |
|
1279
|
|
|
|
|
|
|
# Structure: { param_name, segment_position } per handler |
|
1280
|
4
|
|
|
|
|
38
|
$builder->comment('Path parameter info per dynamic handler') |
|
1281
|
|
|
|
|
|
|
->line('typedef struct { const char* name; int position; } ParamInfo;') |
|
1282
|
|
|
|
|
|
|
->line('typedef struct { int count; ParamInfo params[8]; } RouteParamInfo;') |
|
1283
|
|
|
|
|
|
|
->blank; |
|
1284
|
|
|
|
|
|
|
|
|
1285
|
4
|
|
50
|
|
|
6
|
my @route_params = @{$self->{route_param_info} // []}; |
|
|
4
|
|
|
|
|
56
|
|
|
1286
|
4
|
|
|
|
|
10
|
my $handler_count = scalar @route_params; |
|
1287
|
|
|
|
|
|
|
|
|
1288
|
4
|
|
|
|
|
83
|
$builder->line("static RouteParamInfo g_route_params[$handler_count] = {"); |
|
1289
|
4
|
|
|
|
|
36
|
for my $i (0 .. $#route_params) { |
|
1290
|
11
|
|
50
|
|
|
31
|
my $params = $route_params[$i] // []; |
|
1291
|
11
|
|
|
|
|
17
|
my $count = scalar @$params; |
|
1292
|
11
|
|
|
|
|
15
|
my @param_strs; |
|
1293
|
11
|
|
|
|
|
19
|
for my $p (@$params) { |
|
1294
|
0
|
|
|
|
|
0
|
push @param_strs, qq({ "$p->{name}", $p->{position} }); |
|
1295
|
|
|
|
|
|
|
} |
|
1296
|
|
|
|
|
|
|
# Pad to 8 elements with {NULL, 0} |
|
1297
|
11
|
|
|
|
|
30
|
my $padding = 8 - scalar(@param_strs); |
|
1298
|
11
|
|
|
|
|
27
|
for (1 .. $padding) { |
|
1299
|
88
|
|
|
|
|
162
|
push @param_strs, '{NULL, 0}'; |
|
1300
|
|
|
|
|
|
|
} |
|
1301
|
11
|
|
|
|
|
51
|
my $params_str = join(', ', @param_strs); |
|
1302
|
11
|
|
|
|
|
60
|
$builder->line(" { $count, { $params_str } },"); |
|
1303
|
|
|
|
|
|
|
} |
|
1304
|
4
|
|
|
|
|
20
|
$builder->line('};') |
|
1305
|
|
|
|
|
|
|
->blank; |
|
1306
|
|
|
|
|
|
|
|
|
1307
|
|
|
|
|
|
|
# JIT: Streaming handler flags array (only if streaming is enabled) |
|
1308
|
4
|
50
|
66
|
|
|
37
|
if ($analysis->{needs_streaming} && $self->{_streaming_flags}) { |
|
1309
|
2
|
|
|
|
|
4
|
my @flags = @{$self->{_streaming_flags}}; |
|
|
2
|
|
|
|
|
9
|
|
|
1310
|
2
|
|
|
|
|
6
|
my $flags_str = join(', ', @flags); |
|
1311
|
2
|
|
|
|
|
14
|
$builder->comment('Streaming handler flags - 1 = streaming, 0 = normal') |
|
1312
|
|
|
|
|
|
|
->line("static int g_streaming_handlers[$handler_count] = { $flags_str };") |
|
1313
|
|
|
|
|
|
|
->blank; |
|
1314
|
|
|
|
|
|
|
} |
|
1315
|
|
|
|
|
|
|
} |
|
1316
|
|
|
|
|
|
|
|
|
1317
|
|
|
|
|
|
|
# JIT: WebSocket route paths array (only if WebSocket routes exist) |
|
1318
|
20
|
0
|
33
|
|
|
149
|
if ($analysis->{needs_websocket} && $self->{_websocket_paths}) { |
|
1319
|
0
|
|
|
|
|
0
|
my @paths = @{$self->{_websocket_paths}}; |
|
|
0
|
|
|
|
|
0
|
|
|
1320
|
0
|
|
|
|
|
0
|
my $ws_count = scalar @paths; |
|
1321
|
0
|
|
|
|
|
0
|
$builder->comment('WebSocket route paths'); |
|
1322
|
0
|
|
|
|
|
0
|
for my $i (0 .. $#paths) { |
|
1323
|
0
|
|
|
|
|
0
|
my $escaped = _escape_c_string($paths[$i]); |
|
1324
|
0
|
|
|
|
|
0
|
$builder->line(qq{static const char WS_PATH_$i\[] = "$escaped";}); |
|
1325
|
|
|
|
|
|
|
} |
|
1326
|
0
|
|
|
|
|
0
|
$builder->line("static const char* g_ws_paths[$ws_count] = {"); |
|
1327
|
0
|
|
|
|
|
0
|
for my $i (0 .. $#paths) { |
|
1328
|
0
|
0
|
|
|
|
0
|
my $comma = ($i < $#paths) ? ',' : ''; |
|
1329
|
0
|
|
|
|
|
0
|
$builder->line(" WS_PATH_$i$comma"); |
|
1330
|
|
|
|
|
|
|
} |
|
1331
|
0
|
|
|
|
|
0
|
$builder->line('};') |
|
1332
|
|
|
|
|
|
|
->line("static const int g_ws_path_count = $ws_count;") |
|
1333
|
|
|
|
|
|
|
->blank; |
|
1334
|
|
|
|
|
|
|
} |
|
1335
|
|
|
|
|
|
|
|
|
1336
|
|
|
|
|
|
|
# Emit FULL pre-computed HTTP responses (headers + body) |
|
1337
|
20
|
|
|
|
|
417
|
for my $i (0 .. $#$full_responses) { |
|
1338
|
35
|
|
|
|
|
164
|
my $resp = $full_responses->[$i]; |
|
1339
|
35
|
|
|
|
|
286
|
my $escaped = _escape_c_string($resp); |
|
1340
|
35
|
|
|
|
|
91
|
my $len = length($resp); |
|
1341
|
35
|
|
|
|
|
438
|
$builder->line("static const char RESP_$i\[] = \"$escaped\";") |
|
1342
|
|
|
|
|
|
|
->line("static const int RESP_${i}_LEN = $len;"); |
|
1343
|
|
|
|
|
|
|
} |
|
1344
|
20
|
|
|
|
|
76
|
$builder->blank; |
|
1345
|
|
|
|
|
|
|
|
|
1346
|
|
|
|
|
|
|
# 404 response via Protocol module |
|
1347
|
|
|
|
|
|
|
my $security_hdrs_404 = $self->{enable_security_headers} |
|
1348
|
20
|
50
|
|
|
|
294
|
? $self->_get_security_headers_string() |
|
1349
|
|
|
|
|
|
|
: ''; |
|
1350
|
20
|
|
|
|
|
490
|
my $resp_404 = $PROTOCOL->build_404_response( |
|
1351
|
|
|
|
|
|
|
security_headers => $security_hdrs_404, |
|
1352
|
|
|
|
|
|
|
); |
|
1353
|
|
|
|
|
|
|
|
|
1354
|
20
|
|
|
|
|
125
|
my $escaped_404 = _escape_c_string($resp_404); |
|
1355
|
20
|
|
|
|
|
378
|
$builder->line("static const char RESP_404[] = \"$escaped_404\";") |
|
1356
|
|
|
|
|
|
|
->line("static const int RESP_404_LEN = " . length($resp_404) . ";") |
|
1357
|
|
|
|
|
|
|
->blank; |
|
1358
|
|
|
|
|
|
|
|
|
1359
|
|
|
|
|
|
|
# Security headers constant for dynamic responses |
|
1360
|
20
|
100
|
66
|
|
|
323
|
if ($self->{enable_security_headers} && $has_dynamic) { |
|
1361
|
4
|
|
|
|
|
31
|
$builder->raw($self->_gen_security_headers_c_constant()) |
|
1362
|
|
|
|
|
|
|
->blank; |
|
1363
|
|
|
|
|
|
|
} |
|
1364
|
|
|
|
|
|
|
|
|
1365
|
|
|
|
|
|
|
# Generate dynamic handler caller if needed |
|
1366
|
20
|
100
|
|
|
|
68
|
if ($has_dynamic) { |
|
1367
|
4
|
|
|
|
|
63
|
$builder->raw($self->_gen_dynamic_handler_caller()) |
|
1368
|
|
|
|
|
|
|
->blank; |
|
1369
|
|
|
|
|
|
|
} |
|
1370
|
|
|
|
|
|
|
|
|
1371
|
|
|
|
|
|
|
# Generate XS builder route dispatcher if needed |
|
1372
|
20
|
0
|
33
|
|
|
95
|
if ($analysis->{needs_xs_builder} && $self->{_xs_builder_routes} && @{$self->{_xs_builder_routes}}) { |
|
|
0
|
|
0
|
|
|
0
|
|
|
1373
|
0
|
|
|
|
|
0
|
$builder->raw($self->_gen_xs_builder_dispatcher()) |
|
1374
|
|
|
|
|
|
|
->blank; |
|
1375
|
|
|
|
|
|
|
} |
|
1376
|
|
|
|
|
|
|
|
|
1377
|
|
|
|
|
|
|
# Generate WebSocket handler caller if needed |
|
1378
|
20
|
50
|
|
|
|
107
|
if ($analysis->{needs_websocket}) { |
|
1379
|
0
|
|
|
|
|
0
|
$builder->raw($self->_gen_websocket_handler_caller()) |
|
1380
|
|
|
|
|
|
|
->blank; |
|
1381
|
0
|
|
|
|
|
0
|
$builder->raw($self->_gen_websocket_data_processor()) |
|
1382
|
|
|
|
|
|
|
->blank; |
|
1383
|
|
|
|
|
|
|
} |
|
1384
|
|
|
|
|
|
|
|
|
1385
|
|
|
|
|
|
|
# Group routes by method for dispatch generation |
|
1386
|
20
|
|
|
|
|
54
|
my %methods; |
|
1387
|
20
|
|
|
|
|
41
|
for my $route (@{$self->{routes}}) { |
|
|
20
|
|
|
|
|
111
|
|
|
1388
|
46
|
|
|
|
|
62
|
push @{$methods{$route->{method}}}, $route; |
|
|
46
|
|
|
|
|
218
|
|
|
1389
|
|
|
|
|
|
|
} |
|
1390
|
|
|
|
|
|
|
|
|
1391
|
|
|
|
|
|
|
# Generate inline C dispatch function |
|
1392
|
20
|
|
|
|
|
417
|
$builder->comment('Inline dispatch - returns response pointer and length') |
|
1393
|
|
|
|
|
|
|
->comment('Returns: 0=static response, 1=dynamic route, 2=XS builder route, -1=404') |
|
1394
|
|
|
|
|
|
|
->comment('For dynamic/XS routes: returns handler_idx in *handler_idx_out') |
|
1395
|
|
|
|
|
|
|
->line("static $inline int dispatch_request(const char* method, int method_len, const char* path, int path_len, const char** resp_out, int* resp_len_out, int* handler_idx_out) {") |
|
1396
|
|
|
|
|
|
|
->line(' *handler_idx_out = -1;'); |
|
1397
|
|
|
|
|
|
|
|
|
1398
|
20
|
|
|
|
|
88
|
for my $method (sort keys %methods) { |
|
1399
|
27
|
|
|
|
|
170
|
my $mlen = length($method); |
|
1400
|
27
|
|
|
|
|
317
|
$builder->if("method_len == $mlen && memcmp(method, \"$method\", $mlen) == 0"); |
|
1401
|
|
|
|
|
|
|
|
|
1402
|
27
|
|
|
|
|
47
|
for my $r (@{$methods{$method}}) { |
|
|
27
|
|
|
|
|
70
|
|
|
1403
|
46
|
|
|
|
|
216
|
my $path = $r->{path}; |
|
1404
|
46
|
|
|
|
|
91
|
my $plen = length($path); |
|
1405
|
|
|
|
|
|
|
|
|
1406
|
46
|
100
|
|
|
|
138
|
if (!$r->{dynamic}) { |
|
1407
|
|
|
|
|
|
|
# Static route - return pre-computed response |
|
1408
|
35
|
|
|
|
|
92
|
my $escaped_path = _escape_c_string($path); |
|
1409
|
35
|
|
|
|
|
105
|
my $idx = $r->{response_idx}; |
|
1410
|
|
|
|
|
|
|
|
|
1411
|
35
|
|
|
|
|
481
|
$builder->if("path_len == $plen && memcmp(path, \"$escaped_path\", $plen) == 0") |
|
1412
|
|
|
|
|
|
|
->line("*resp_out = RESP_$idx;") |
|
1413
|
|
|
|
|
|
|
->line("*resp_len_out = RESP_${idx}_LEN;") |
|
1414
|
|
|
|
|
|
|
->line("return 0;") |
|
1415
|
|
|
|
|
|
|
->endif; |
|
1416
|
|
|
|
|
|
|
} else { |
|
1417
|
|
|
|
|
|
|
# Dynamic route - check for path params or exact match |
|
1418
|
11
|
|
|
|
|
28
|
my $handler_idx = $r->{handler_idx}; |
|
1419
|
|
|
|
|
|
|
|
|
1420
|
|
|
|
|
|
|
# Check if this is a need_xs_builder route - dispatch to XS directly |
|
1421
|
11
|
50
|
33
|
|
|
52
|
if ($r->{need_xs_builder} && $r->{_xs_result}) { |
|
|
|
50
|
|
|
|
|
|
|
1422
|
0
|
|
|
|
|
0
|
my $xs_func = $r->{_xs_result}{xs_function}; |
|
1423
|
0
|
|
|
|
|
0
|
my $escaped_path = _escape_c_string($path); |
|
1424
|
|
|
|
|
|
|
|
|
1425
|
0
|
0
|
|
|
|
0
|
if ($path =~ /:(\w+)/) { |
|
1426
|
|
|
|
|
|
|
# Path has parameters |
|
1427
|
0
|
|
|
|
|
0
|
my ($prefix) = $path =~ m{^([^:]+)}; |
|
1428
|
0
|
|
|
|
|
0
|
my $prefix_len = length($prefix); |
|
1429
|
0
|
|
|
|
|
0
|
my $escaped_prefix = _escape_c_string($prefix); |
|
1430
|
|
|
|
|
|
|
|
|
1431
|
0
|
|
|
|
|
0
|
$builder->if("path_len >= $prefix_len && memcmp(path, \"$escaped_prefix\", $prefix_len) == 0") |
|
1432
|
|
|
|
|
|
|
->comment("XS builder route - call $xs_func directly") |
|
1433
|
|
|
|
|
|
|
->line("*handler_idx_out = $handler_idx;") |
|
1434
|
|
|
|
|
|
|
->line("return 2;") # Special return code for XS builder routes |
|
1435
|
|
|
|
|
|
|
->endif; |
|
1436
|
|
|
|
|
|
|
} else { |
|
1437
|
|
|
|
|
|
|
# Exact match |
|
1438
|
0
|
|
|
|
|
0
|
$builder->if("path_len == $plen && memcmp(path, \"$escaped_path\", $plen) == 0") |
|
1439
|
|
|
|
|
|
|
->comment("XS builder route - call $xs_func directly") |
|
1440
|
|
|
|
|
|
|
->line("*handler_idx_out = $handler_idx;") |
|
1441
|
|
|
|
|
|
|
->line("return 2;") # Special return code for XS builder routes |
|
1442
|
|
|
|
|
|
|
->endif; |
|
1443
|
|
|
|
|
|
|
} |
|
1444
|
|
|
|
|
|
|
} elsif ($path =~ /:(\w+)/) { |
|
1445
|
|
|
|
|
|
|
# Path has parameters - generate pattern matching |
|
1446
|
0
|
|
|
|
|
0
|
my ($prefix) = $path =~ m{^([^:]+)}; |
|
1447
|
0
|
|
|
|
|
0
|
my $prefix_len = length($prefix); |
|
1448
|
0
|
|
|
|
|
0
|
my $escaped_prefix = _escape_c_string($prefix); |
|
1449
|
|
|
|
|
|
|
|
|
1450
|
0
|
|
|
|
|
0
|
$builder->if("path_len >= $prefix_len && memcmp(path, \"$escaped_prefix\", $prefix_len) == 0") |
|
1451
|
|
|
|
|
|
|
->line("*resp_out = NULL;") |
|
1452
|
|
|
|
|
|
|
->line("*handler_idx_out = $handler_idx;") |
|
1453
|
|
|
|
|
|
|
->line("return 1;") |
|
1454
|
|
|
|
|
|
|
->endif; |
|
1455
|
|
|
|
|
|
|
} else { |
|
1456
|
|
|
|
|
|
|
# Exact match dynamic route |
|
1457
|
11
|
|
|
|
|
22
|
my $escaped_path = _escape_c_string($path); |
|
1458
|
11
|
|
|
|
|
109
|
$builder->if("path_len == $plen && memcmp(path, \"$escaped_path\", $plen) == 0") |
|
1459
|
|
|
|
|
|
|
->line("*resp_out = NULL;") |
|
1460
|
|
|
|
|
|
|
->line("*handler_idx_out = $handler_idx;") |
|
1461
|
|
|
|
|
|
|
->line("return 1;") |
|
1462
|
|
|
|
|
|
|
->endif; |
|
1463
|
|
|
|
|
|
|
} |
|
1464
|
|
|
|
|
|
|
} |
|
1465
|
|
|
|
|
|
|
} |
|
1466
|
|
|
|
|
|
|
|
|
1467
|
27
|
|
|
|
|
94
|
$builder->endif; |
|
1468
|
|
|
|
|
|
|
} |
|
1469
|
|
|
|
|
|
|
|
|
1470
|
20
|
|
|
|
|
186
|
$builder->line('*resp_out = RESP_404;') |
|
1471
|
|
|
|
|
|
|
->line('*resp_len_out = RESP_404_LEN;') |
|
1472
|
|
|
|
|
|
|
->line('return -1;') |
|
1473
|
|
|
|
|
|
|
->line('}') |
|
1474
|
|
|
|
|
|
|
->blank; |
|
1475
|
|
|
|
|
|
|
|
|
1476
|
|
|
|
|
|
|
# Generate the pure C event loop using backend module |
|
1477
|
20
|
|
|
|
|
226
|
$self->_gen_event_loop($builder, $backend); |
|
1478
|
20
|
|
|
|
|
52
|
$builder->blank; |
|
1479
|
|
|
|
|
|
|
|
|
1480
|
|
|
|
|
|
|
# Keep the Perl-callable dispatch for testing/compatibility |
|
1481
|
20
|
|
|
|
|
1151
|
$builder->comment('Perl-callable dispatch wrapper') |
|
1482
|
|
|
|
|
|
|
->xs_function('hypersonic_dispatch') |
|
1483
|
|
|
|
|
|
|
->xs_preamble |
|
1484
|
|
|
|
|
|
|
->line('if (items != 1) croak_xs_usage(cv, "req_ref");') |
|
1485
|
|
|
|
|
|
|
->declare_sv('req_ref', 'ST(0)') |
|
1486
|
|
|
|
|
|
|
->if('!SvROK(req_ref) || SvTYPE(SvRV(req_ref)) != SVt_PVAV') |
|
1487
|
|
|
|
|
|
|
->line('ST(0) = &PL_sv_undef;') |
|
1488
|
|
|
|
|
|
|
->line('XSRETURN(1);') |
|
1489
|
|
|
|
|
|
|
->endif |
|
1490
|
|
|
|
|
|
|
->declare_av('req', '(AV*)SvRV(req_ref)') |
|
1491
|
|
|
|
|
|
|
->line('SV** ary = AvARRAY(req);') |
|
1492
|
|
|
|
|
|
|
->line('STRLEN method_len, path_len;') |
|
1493
|
|
|
|
|
|
|
->line('const char* method = SvPV(ary[0], method_len);') |
|
1494
|
|
|
|
|
|
|
->line('const char* path = SvPV(ary[1], path_len);') |
|
1495
|
|
|
|
|
|
|
->line('const char* resp;') |
|
1496
|
|
|
|
|
|
|
->line('int resp_len;') |
|
1497
|
|
|
|
|
|
|
->line('int handler_idx;') |
|
1498
|
|
|
|
|
|
|
->line('int rc = dispatch_request(method, (int)method_len, path, (int)path_len, &resp, &resp_len, &handler_idx);') |
|
1499
|
|
|
|
|
|
|
->if('rc == 0') |
|
1500
|
|
|
|
|
|
|
->comment('Static route') |
|
1501
|
|
|
|
|
|
|
->line('ST(0) = sv_2mortal(newSVpvn(resp, resp_len));') |
|
1502
|
|
|
|
|
|
|
->elsif('rc == 1') |
|
1503
|
|
|
|
|
|
|
->comment('Dynamic route - for testing, just return undef') |
|
1504
|
|
|
|
|
|
|
->line('ST(0) = &PL_sv_undef;') |
|
1505
|
|
|
|
|
|
|
->else |
|
1506
|
|
|
|
|
|
|
->line('ST(0) = &PL_sv_undef;') |
|
1507
|
|
|
|
|
|
|
->endif |
|
1508
|
|
|
|
|
|
|
->xs_return('1') |
|
1509
|
|
|
|
|
|
|
->xs_end; |
|
1510
|
|
|
|
|
|
|
|
|
1511
|
20
|
|
|
|
|
900
|
return $builder->code; |
|
1512
|
|
|
|
|
|
|
} |
|
1513
|
|
|
|
|
|
|
|
|
1514
|
|
|
|
|
|
|
# Generate optimized method parser - delegates to Protocol module |
|
1515
|
|
|
|
|
|
|
sub _gen_method_parser { |
|
1516
|
20
|
|
|
20
|
|
55
|
my ($self, $builder) = @_; |
|
1517
|
20
|
|
|
|
|
59
|
my $analysis = $self->{route_analysis}; |
|
1518
|
|
|
|
|
|
|
|
|
1519
|
|
|
|
|
|
|
# Delegate to Protocol module for HTTP/1.1 parsing |
|
1520
|
|
|
|
|
|
|
# This separation allows future HTTP/2 support without changing core logic |
|
1521
|
20
|
|
|
|
|
134
|
return $PROTOCOL->gen_method_parser($builder, $analysis); |
|
1522
|
|
|
|
|
|
|
} |
|
1523
|
|
|
|
|
|
|
|
|
1524
|
|
|
|
|
|
|
# Generate WebSocket frame handling for established connections |
|
1525
|
|
|
|
|
|
|
sub _gen_websocket_frame_handler { |
|
1526
|
0
|
|
|
0
|
|
0
|
my ($self, $builder) = @_; |
|
1527
|
|
|
|
|
|
|
|
|
1528
|
0
|
|
|
|
|
0
|
$builder->comment('Check if this is an established WebSocket connection') |
|
1529
|
|
|
|
|
|
|
->if('fd >= 0 && fd < WS_MAX && ws_registry[fd].state == WS_STATE_OPEN') |
|
1530
|
|
|
|
|
|
|
->comment('This is a WebSocket connection - process frames') |
|
1531
|
|
|
|
|
|
|
->line('process_websocket_data(aTHX_ fd, recv_buf, len);') |
|
1532
|
|
|
|
|
|
|
->line('continue;') |
|
1533
|
|
|
|
|
|
|
->endif |
|
1534
|
|
|
|
|
|
|
->blank; |
|
1535
|
|
|
|
|
|
|
} |
|
1536
|
|
|
|
|
|
|
|
|
1537
|
|
|
|
|
|
|
# Generate WebSocket upgrade detection and dispatch |
|
1538
|
|
|
|
|
|
|
sub _gen_websocket_dispatch { |
|
1539
|
0
|
|
|
0
|
|
0
|
my ($self, $builder) = @_; |
|
1540
|
|
|
|
|
|
|
|
|
1541
|
0
|
|
|
|
|
0
|
$builder->comment('WebSocket upgrade detection') |
|
1542
|
|
|
|
|
|
|
->comment('Check for Upgrade: websocket header') |
|
1543
|
|
|
|
|
|
|
->line('int is_websocket = 0;') |
|
1544
|
|
|
|
|
|
|
->line('int ws_handler_idx = -1;') |
|
1545
|
|
|
|
|
|
|
->line('const char* ws_key = NULL;') |
|
1546
|
|
|
|
|
|
|
->line('int ws_key_len = 0;') |
|
1547
|
|
|
|
|
|
|
->blank |
|
1548
|
|
|
|
|
|
|
->comment('Look for Upgrade header in raw request') |
|
1549
|
|
|
|
|
|
|
->line('const char* upgrade_pos = strstr(recv_buf, "Upgrade:");') |
|
1550
|
|
|
|
|
|
|
->if('!upgrade_pos') |
|
1551
|
|
|
|
|
|
|
->line('upgrade_pos = strstr(recv_buf, "upgrade:");') |
|
1552
|
|
|
|
|
|
|
->endif |
|
1553
|
|
|
|
|
|
|
->if('upgrade_pos') |
|
1554
|
|
|
|
|
|
|
->line('const char* val_start = upgrade_pos + 8;') |
|
1555
|
|
|
|
|
|
|
->line('while (*val_start == \' \') val_start++;') |
|
1556
|
|
|
|
|
|
|
->comment('Check for websocket (case-insensitive)') |
|
1557
|
|
|
|
|
|
|
->if('strncasecmp(val_start, "websocket", 9) == 0') |
|
1558
|
|
|
|
|
|
|
->line('is_websocket = 1;') |
|
1559
|
|
|
|
|
|
|
->endif |
|
1560
|
|
|
|
|
|
|
->endif |
|
1561
|
|
|
|
|
|
|
->blank |
|
1562
|
|
|
|
|
|
|
->if('is_websocket') |
|
1563
|
|
|
|
|
|
|
->comment('Extract Sec-WebSocket-Key') |
|
1564
|
|
|
|
|
|
|
->line('const char* key_pos = strstr(recv_buf, "Sec-WebSocket-Key:");') |
|
1565
|
|
|
|
|
|
|
->if('!key_pos') |
|
1566
|
|
|
|
|
|
|
->line('key_pos = strstr(recv_buf, "sec-websocket-key:");') |
|
1567
|
|
|
|
|
|
|
->endif |
|
1568
|
|
|
|
|
|
|
->if('key_pos') |
|
1569
|
|
|
|
|
|
|
->line('const char* key_start = key_pos + 18;') |
|
1570
|
|
|
|
|
|
|
->line('while (*key_start == \' \') key_start++;') |
|
1571
|
|
|
|
|
|
|
->line('const char* key_end = key_start;') |
|
1572
|
|
|
|
|
|
|
->line('while (*key_end && *key_end != \'\\r\' && *key_end != \'\\n\') key_end++;') |
|
1573
|
|
|
|
|
|
|
->line('ws_key = key_start;') |
|
1574
|
|
|
|
|
|
|
->line('ws_key_len = key_end - key_start;') |
|
1575
|
|
|
|
|
|
|
->endif |
|
1576
|
|
|
|
|
|
|
->blank |
|
1577
|
|
|
|
|
|
|
->comment('Match path to WebSocket routes') |
|
1578
|
|
|
|
|
|
|
->line('int clean_path_len = path_len;') |
|
1579
|
|
|
|
|
|
|
->line('const char* qmark = memchr(path, \'?\', path_len);') |
|
1580
|
|
|
|
|
|
|
->if('qmark') |
|
1581
|
|
|
|
|
|
|
->line('clean_path_len = qmark - path;') |
|
1582
|
|
|
|
|
|
|
->endif |
|
1583
|
|
|
|
|
|
|
->for('int i = 0', 'i < g_ws_path_count', 'i++') |
|
1584
|
|
|
|
|
|
|
->line('int ws_path_len = strlen(g_ws_paths[i]);') |
|
1585
|
|
|
|
|
|
|
->if('clean_path_len == ws_path_len && memcmp(path, g_ws_paths[i], ws_path_len) == 0') |
|
1586
|
|
|
|
|
|
|
->line('ws_handler_idx = i;') |
|
1587
|
|
|
|
|
|
|
->line('break;') |
|
1588
|
|
|
|
|
|
|
->endif |
|
1589
|
|
|
|
|
|
|
->endfor |
|
1590
|
|
|
|
|
|
|
->blank |
|
1591
|
|
|
|
|
|
|
->if('ws_handler_idx >= 0 && ws_key') |
|
1592
|
|
|
|
|
|
|
->comment('WebSocket upgrade - call Perl handler') |
|
1593
|
|
|
|
|
|
|
->line('call_websocket_handler(aTHX_ ws_handler_idx, fd, path, path_len, ws_key, ws_key_len, recv_buf, len);') |
|
1594
|
|
|
|
|
|
|
->line('continue;') # Skip normal dispatch, connection stays open |
|
1595
|
|
|
|
|
|
|
->endif |
|
1596
|
|
|
|
|
|
|
->endif |
|
1597
|
|
|
|
|
|
|
->blank; |
|
1598
|
|
|
|
|
|
|
} |
|
1599
|
|
|
|
|
|
|
|
|
1600
|
|
|
|
|
|
|
# Unified event loop generator - uses backend module for platform-specific code |
|
1601
|
|
|
|
|
|
|
sub _gen_event_loop { |
|
1602
|
20
|
|
|
20
|
|
90
|
my ($self, $builder, $backend) = @_; |
|
1603
|
20
|
|
|
|
|
46
|
my $has_dynamic = grep { $_->{dynamic} } @{$self->{routes}}; |
|
|
46
|
|
|
|
|
122
|
|
|
|
20
|
|
|
|
|
73
|
|
|
1604
|
20
|
|
|
|
|
47
|
my $analysis = $self->{route_analysis}; |
|
1605
|
20
|
|
50
|
|
|
89
|
my $has_body_access = $analysis->{has_body_access} // 0; |
|
1606
|
20
|
|
|
|
|
129
|
my $backend_name = $backend->name; |
|
1607
|
20
|
|
|
|
|
102
|
my $event_struct = $backend->event_struct; |
|
1608
|
|
|
|
|
|
|
|
|
1609
|
20
|
|
|
|
|
545
|
$builder->comment("Pure C event loop using $backend_name backend - WITH SECURITY HARDENING") |
|
1610
|
|
|
|
|
|
|
->xs_function('hypersonic_run_event_loop') |
|
1611
|
|
|
|
|
|
|
->xs_preamble |
|
1612
|
|
|
|
|
|
|
->check_items(2, 2, 'listen_fd, server_obj') |
|
1613
|
|
|
|
|
|
|
->blank |
|
1614
|
|
|
|
|
|
|
->declare('int', 'listen_fd', '(int)SvIV(ST(0))') |
|
1615
|
|
|
|
|
|
|
->declare_sv('server_obj', 'ST(1)') |
|
1616
|
|
|
|
|
|
|
->blank; |
|
1617
|
|
|
|
|
|
|
|
|
1618
|
|
|
|
|
|
|
# Handler storage (only if dynamic routes) |
|
1619
|
20
|
100
|
|
|
|
81
|
if ($has_dynamic) { |
|
1620
|
4
|
|
|
|
|
78
|
$builder->comment('Store server object for dynamic handler access') |
|
1621
|
|
|
|
|
|
|
->if('SvROK(server_obj)') |
|
1622
|
|
|
|
|
|
|
->declare_hv('self', '(HV*)SvRV(server_obj)') |
|
1623
|
|
|
|
|
|
|
->line('SV** handlers_ref = hv_fetch(self, "dynamic_handlers", 16, 0);') |
|
1624
|
|
|
|
|
|
|
->if('handlers_ref && SvROK(*handlers_ref)') |
|
1625
|
|
|
|
|
|
|
->line('g_handler_array = *handlers_ref;') |
|
1626
|
|
|
|
|
|
|
->line('SvREFCNT_inc(g_handler_array);') |
|
1627
|
|
|
|
|
|
|
->endif |
|
1628
|
|
|
|
|
|
|
->line('g_server_obj = server_obj;') |
|
1629
|
|
|
|
|
|
|
->line('SvREFCNT_inc(g_server_obj);'); |
|
1630
|
|
|
|
|
|
|
|
|
1631
|
|
|
|
|
|
|
# JIT: Only fetch middleware if any middleware is present |
|
1632
|
4
|
100
|
|
|
|
14
|
if ($analysis->{has_any_middleware}) { |
|
1633
|
1
|
|
|
|
|
3
|
$builder->blank |
|
1634
|
|
|
|
|
|
|
->comment('JIT: Middleware storage (Perl coderefs only - builders are inline C)'); |
|
1635
|
|
|
|
|
|
|
|
|
1636
|
|
|
|
|
|
|
# Only fetch Perl middleware if we have any |
|
1637
|
1
|
50
|
|
|
|
2
|
if ($analysis->{has_global_before}) { |
|
1638
|
1
|
|
|
|
|
7
|
$builder->line('SV** before_ref = hv_fetch(self, "_perl_before_mw", 15, 0);') |
|
1639
|
|
|
|
|
|
|
->if('before_ref && SvROK(*before_ref)') |
|
1640
|
|
|
|
|
|
|
->line('g_before_middleware = *before_ref;') |
|
1641
|
|
|
|
|
|
|
->line('SvREFCNT_inc(g_before_middleware);') |
|
1642
|
|
|
|
|
|
|
->endif; |
|
1643
|
|
|
|
|
|
|
} |
|
1644
|
1
|
50
|
|
|
|
2
|
if ($analysis->{has_global_after}) { |
|
1645
|
1
|
|
|
|
|
7
|
$builder->line('SV** after_ref = hv_fetch(self, "_perl_after_mw", 14, 0);') |
|
1646
|
|
|
|
|
|
|
->if('after_ref && SvROK(*after_ref)') |
|
1647
|
|
|
|
|
|
|
->line('g_after_middleware = *after_ref;') |
|
1648
|
|
|
|
|
|
|
->line('SvREFCNT_inc(g_after_middleware);') |
|
1649
|
|
|
|
|
|
|
->endif; |
|
1650
|
|
|
|
|
|
|
} |
|
1651
|
|
|
|
|
|
|
|
|
1652
|
|
|
|
|
|
|
# Per-route middleware arrays |
|
1653
|
1
|
50
|
|
|
|
3
|
if ($analysis->{has_route_middleware}) { |
|
1654
|
0
|
|
|
|
|
0
|
$builder->line('SV** route_before_ref = hv_fetch(self, "_route_before_mw", 16, 0);') |
|
1655
|
|
|
|
|
|
|
->if('route_before_ref && SvROK(*route_before_ref)') |
|
1656
|
|
|
|
|
|
|
->line('g_route_before_middleware = *route_before_ref;') |
|
1657
|
|
|
|
|
|
|
->line('SvREFCNT_inc(g_route_before_middleware);') |
|
1658
|
|
|
|
|
|
|
->endif |
|
1659
|
|
|
|
|
|
|
->line('SV** route_after_ref = hv_fetch(self, "_route_after_mw", 15, 0);') |
|
1660
|
|
|
|
|
|
|
->if('route_after_ref && SvROK(*route_after_ref)') |
|
1661
|
|
|
|
|
|
|
->line('g_route_after_middleware = *route_after_ref;') |
|
1662
|
|
|
|
|
|
|
->line('SvREFCNT_inc(g_route_after_middleware);') |
|
1663
|
|
|
|
|
|
|
->endif; |
|
1664
|
|
|
|
|
|
|
} |
|
1665
|
|
|
|
|
|
|
} |
|
1666
|
|
|
|
|
|
|
|
|
1667
|
4
|
|
|
|
|
12
|
$builder->endif; |
|
1668
|
|
|
|
|
|
|
} else { |
|
1669
|
16
|
|
|
|
|
188
|
$builder->line('(void)server_obj; /* Not needed for static-only routes */'); |
|
1670
|
|
|
|
|
|
|
} |
|
1671
|
20
|
|
|
|
|
85
|
$builder->blank; |
|
1672
|
|
|
|
|
|
|
|
|
1673
|
|
|
|
|
|
|
# WebSocket handler storage (only if WebSocket routes) |
|
1674
|
20
|
50
|
|
|
|
67
|
if ($analysis->{needs_websocket}) { |
|
1675
|
0
|
|
|
|
|
0
|
$builder->comment('Store WebSocket handlers for dispatch') |
|
1676
|
|
|
|
|
|
|
->if('SvROK(server_obj)') |
|
1677
|
|
|
|
|
|
|
->declare_hv('ws_self', '(HV*)SvRV(server_obj)') |
|
1678
|
|
|
|
|
|
|
->line('SV** ws_handlers_ref = hv_fetch(ws_self, "_websocket_handlers", 19, 0);') |
|
1679
|
|
|
|
|
|
|
->if('ws_handlers_ref && SvROK(*ws_handlers_ref)') |
|
1680
|
|
|
|
|
|
|
->line('g_websocket_handlers = *ws_handlers_ref;') |
|
1681
|
|
|
|
|
|
|
->line('SvREFCNT_inc(g_websocket_handlers);') |
|
1682
|
|
|
|
|
|
|
->endif |
|
1683
|
|
|
|
|
|
|
->endif |
|
1684
|
|
|
|
|
|
|
->blank; |
|
1685
|
|
|
|
|
|
|
} |
|
1686
|
|
|
|
|
|
|
|
|
1687
|
|
|
|
|
|
|
# Signal handlers and initialization |
|
1688
|
20
|
|
|
|
|
298
|
$builder->comment('Setup graceful shutdown signal handlers') |
|
1689
|
|
|
|
|
|
|
->line('signal(SIGTERM, handle_shutdown_signal);') |
|
1690
|
|
|
|
|
|
|
->line('signal(SIGINT, handle_shutdown_signal);') |
|
1691
|
|
|
|
|
|
|
->blank |
|
1692
|
|
|
|
|
|
|
->comment('Initialize connection tracking') |
|
1693
|
|
|
|
|
|
|
->line('memset(g_conn_time, 0, sizeof(g_conn_time));') |
|
1694
|
|
|
|
|
|
|
->line('g_active_connections = 0;') |
|
1695
|
|
|
|
|
|
|
->blank; |
|
1696
|
|
|
|
|
|
|
|
|
1697
|
|
|
|
|
|
|
# TLS initialization |
|
1698
|
20
|
50
|
|
|
|
80
|
if ($self->{tls}) { |
|
1699
|
0
|
|
|
|
|
0
|
my $cert_file = _escape_c_string($self->{cert_file}); |
|
1700
|
0
|
|
|
|
|
0
|
my $key_file = _escape_c_string($self->{key_file}); |
|
1701
|
0
|
|
|
|
|
0
|
$builder->comment('Initialize TLS/HTTPS') |
|
1702
|
|
|
|
|
|
|
->line('#ifdef HYPERSONIC_TLS') |
|
1703
|
|
|
|
|
|
|
->line("if (init_ssl_ctx(\"$cert_file\", \"$key_file\") != 0) {") |
|
1704
|
|
|
|
|
|
|
->line(' croak("Failed to initialize TLS context - check cert/key files");') |
|
1705
|
|
|
|
|
|
|
->line('}') |
|
1706
|
|
|
|
|
|
|
->line('memset(g_tls_connections, 0, sizeof(g_tls_connections));') |
|
1707
|
|
|
|
|
|
|
->line('#endif') |
|
1708
|
|
|
|
|
|
|
->blank; |
|
1709
|
|
|
|
|
|
|
} |
|
1710
|
|
|
|
|
|
|
|
|
1711
|
20
|
|
|
|
|
210
|
$builder->comment('Thread-local receive buffer - each worker gets its own') |
|
1712
|
|
|
|
|
|
|
->line('static __thread char recv_buf[RECV_BUF_SIZE];') |
|
1713
|
|
|
|
|
|
|
->blank; |
|
1714
|
|
|
|
|
|
|
|
|
1715
|
|
|
|
|
|
|
# Backend-specific: Create event loop and add listen socket |
|
1716
|
20
|
|
|
|
|
204
|
$backend->gen_create($builder, 'listen_fd'); |
|
1717
|
|
|
|
|
|
|
|
|
1718
|
|
|
|
|
|
|
# Async Pool: Initialize thread pool and add notify fd to event loop |
|
1719
|
20
|
50
|
|
|
|
105
|
if ($analysis->{needs_async_pool}) { |
|
1720
|
0
|
|
|
|
|
0
|
$builder->blank |
|
1721
|
|
|
|
|
|
|
->comment('Initialize async thread pool') |
|
1722
|
|
|
|
|
|
|
->line('pool_init();') |
|
1723
|
|
|
|
|
|
|
->line('int pool_notify_fd = pool_get_notify_fd();'); |
|
1724
|
0
|
|
|
|
|
0
|
$backend->gen_add_pool_notify($builder, 'ev_fd', 'pool_notify_fd'); |
|
1725
|
|
|
|
|
|
|
} |
|
1726
|
|
|
|
|
|
|
|
|
1727
|
|
|
|
|
|
|
# Declare event structure variable based on backend |
|
1728
|
20
|
50
|
|
|
|
108
|
if ($backend_name eq 'kqueue') { |
|
|
|
50
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
1729
|
|
|
|
|
|
|
# kqueue's gen_create already declares 'ev', but we need events array |
|
1730
|
0
|
|
|
|
|
0
|
$builder->line('struct kevent events[MAX_EVENTS];'); |
|
1731
|
|
|
|
|
|
|
} elsif ($backend_name eq 'epoll') { |
|
1732
|
|
|
|
|
|
|
# epoll's gen_create already declares 'ev' |
|
1733
|
20
|
|
|
|
|
92
|
$builder->line('struct epoll_event events[MAX_EVENTS];'); |
|
1734
|
|
|
|
|
|
|
} elsif ($backend_name eq 'io_uring') { |
|
1735
|
|
|
|
|
|
|
# io_uring uses completion queue entries |
|
1736
|
0
|
|
|
|
|
0
|
$builder->line('struct io_uring_cqe** events = NULL;'); |
|
1737
|
|
|
|
|
|
|
} else { |
|
1738
|
|
|
|
|
|
|
# poll/select manage their own fd arrays internally |
|
1739
|
|
|
|
|
|
|
# Declare a dummy events pointer to satisfy gen_wait signature |
|
1740
|
0
|
|
|
|
|
0
|
$builder->line('void* events = NULL;'); |
|
1741
|
|
|
|
|
|
|
} |
|
1742
|
|
|
|
|
|
|
|
|
1743
|
20
|
|
|
|
|
117
|
$builder->line('time_t last_cleanup = time(NULL);') |
|
1744
|
|
|
|
|
|
|
->line('int accepting = 1; /* Flag to control accepting new connections */') |
|
1745
|
|
|
|
|
|
|
->blank; |
|
1746
|
|
|
|
|
|
|
|
|
1747
|
|
|
|
|
|
|
# Main event loop |
|
1748
|
20
|
|
|
|
|
187
|
$builder->while('!g_shutdown || g_active_connections > 0') |
|
1749
|
|
|
|
|
|
|
->comment('Use timeout for keep-alive cleanup and shutdown check'); |
|
1750
|
|
|
|
|
|
|
|
|
1751
|
|
|
|
|
|
|
# Backend-specific: Wait for events |
|
1752
|
20
|
|
|
|
|
189
|
$backend->gen_wait($builder, 'ev_fd', 'events', 'n', '1000'); |
|
1753
|
|
|
|
|
|
|
|
|
1754
|
20
|
|
|
|
|
143
|
$builder->blank |
|
1755
|
|
|
|
|
|
|
->comment('Check for graceful shutdown - stop accepting new connections') |
|
1756
|
|
|
|
|
|
|
->if('g_shutdown && accepting'); |
|
1757
|
|
|
|
|
|
|
|
|
1758
|
|
|
|
|
|
|
# Backend-specific: Remove listen socket from event loop |
|
1759
|
20
|
|
|
|
|
107
|
$backend->gen_del($builder, 'ev_fd', 'listen_fd'); |
|
1760
|
20
|
|
|
|
|
273
|
$builder->line('accepting = 0;') |
|
1761
|
|
|
|
|
|
|
->endif |
|
1762
|
|
|
|
|
|
|
->blank |
|
1763
|
|
|
|
|
|
|
->comment('Get time once per event batch') |
|
1764
|
|
|
|
|
|
|
->line('time_t now = time(NULL);') |
|
1765
|
|
|
|
|
|
|
->line('g_current_time = now;') |
|
1766
|
|
|
|
|
|
|
->blank; |
|
1767
|
|
|
|
|
|
|
|
|
1768
|
|
|
|
|
|
|
# Keep-alive cleanup |
|
1769
|
20
|
|
|
|
|
374
|
$builder->comment('Periodic keep-alive timeout cleanup') |
|
1770
|
|
|
|
|
|
|
->if('now - last_cleanup >= 5') |
|
1771
|
|
|
|
|
|
|
->declare('int', 'cleanup_i', '0') |
|
1772
|
|
|
|
|
|
|
->for('cleanup_i = 0', 'cleanup_i < MAX_FD', 'cleanup_i++') |
|
1773
|
|
|
|
|
|
|
->if('g_conn_time[cleanup_i] > 0') |
|
1774
|
|
|
|
|
|
|
->if('now - g_conn_time[cleanup_i] > KEEPALIVE_TIMEOUT') |
|
1775
|
|
|
|
|
|
|
->comment('Close idle connection') |
|
1776
|
|
|
|
|
|
|
->line('int idle_fd = cleanup_i;'); |
|
1777
|
|
|
|
|
|
|
|
|
1778
|
|
|
|
|
|
|
# Backend-specific: Remove idle connection |
|
1779
|
20
|
|
|
|
|
84
|
$backend->gen_del($builder, 'ev_fd', 'idle_fd'); |
|
1780
|
20
|
|
|
|
|
283
|
$builder->line('HYPERSONIC_CLOSE(idle_fd);') |
|
1781
|
|
|
|
|
|
|
->line('remove_connection(idle_fd);') |
|
1782
|
|
|
|
|
|
|
->endif |
|
1783
|
|
|
|
|
|
|
->endif |
|
1784
|
|
|
|
|
|
|
->endfor |
|
1785
|
|
|
|
|
|
|
->line('last_cleanup = now;') |
|
1786
|
|
|
|
|
|
|
->endif |
|
1787
|
|
|
|
|
|
|
->blank; |
|
1788
|
|
|
|
|
|
|
|
|
1789
|
|
|
|
|
|
|
# Event processing loop |
|
1790
|
20
|
|
|
|
|
181
|
$builder->declare('int', 'i', '0') |
|
1791
|
|
|
|
|
|
|
->for('i = 0', 'i < n', 'i++'); |
|
1792
|
|
|
|
|
|
|
|
|
1793
|
|
|
|
|
|
|
# Backend-specific: Get fd from event |
|
1794
|
20
|
|
|
|
|
152
|
$backend->gen_get_fd($builder, 'events', 'i', 'fd'); |
|
1795
|
|
|
|
|
|
|
|
|
1796
|
20
|
|
|
|
|
235
|
$builder->blank |
|
1797
|
|
|
|
|
|
|
->if('fd == listen_fd && accepting'); |
|
1798
|
|
|
|
|
|
|
|
|
1799
|
|
|
|
|
|
|
# Accept loop |
|
1800
|
20
|
|
|
|
|
1669
|
$builder->comment('Accept new connections with limit check') |
|
1801
|
|
|
|
|
|
|
->while('1') |
|
1802
|
|
|
|
|
|
|
->comment('Check connection limit before accepting') |
|
1803
|
|
|
|
|
|
|
->if('g_active_connections >= MAX_CONNECTIONS') |
|
1804
|
|
|
|
|
|
|
->line('break; /* At capacity, stop accepting */') |
|
1805
|
|
|
|
|
|
|
->endif |
|
1806
|
|
|
|
|
|
|
->blank |
|
1807
|
|
|
|
|
|
|
->line('struct sockaddr_in client_addr;') |
|
1808
|
|
|
|
|
|
|
->line('socklen_t client_len = sizeof(client_addr);') |
|
1809
|
|
|
|
|
|
|
->line('int client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);') |
|
1810
|
|
|
|
|
|
|
->if('client_fd < 0') |
|
1811
|
|
|
|
|
|
|
->line('break;') |
|
1812
|
|
|
|
|
|
|
->endif |
|
1813
|
|
|
|
|
|
|
->blank |
|
1814
|
|
|
|
|
|
|
->comment('Set non-blocking (hs_set_nonblocking handles Win/POSIX divergence)') |
|
1815
|
|
|
|
|
|
|
->line('hs_set_nonblocking(client_fd);') |
|
1816
|
|
|
|
|
|
|
->blank |
|
1817
|
|
|
|
|
|
|
->comment('Disable Nagle') |
|
1818
|
|
|
|
|
|
|
->line('int one = 1;') |
|
1819
|
|
|
|
|
|
|
->line('setsockopt(client_fd, IPPROTO_TCP, TCP_NODELAY, (const char*)&one, sizeof(one));') |
|
1820
|
|
|
|
|
|
|
->blank |
|
1821
|
|
|
|
|
|
|
->comment('Set receive timeout for security') |
|
1822
|
|
|
|
|
|
|
->line('struct timeval tv;') |
|
1823
|
|
|
|
|
|
|
->line('tv.tv_sec = RECV_TIMEOUT;') |
|
1824
|
|
|
|
|
|
|
->line('tv.tv_usec = 0;') |
|
1825
|
|
|
|
|
|
|
->line('setsockopt(client_fd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv));') |
|
1826
|
|
|
|
|
|
|
->blank |
|
1827
|
|
|
|
|
|
|
->comment('Track connection for keep-alive timeout') |
|
1828
|
|
|
|
|
|
|
->line('track_connection(client_fd, now);') |
|
1829
|
|
|
|
|
|
|
->blank |
|
1830
|
|
|
|
|
|
|
->comment('TLS handshake if enabled') |
|
1831
|
|
|
|
|
|
|
->line('#ifdef HYPERSONIC_TLS') |
|
1832
|
|
|
|
|
|
|
->line('if (tls_accept(client_fd) < 0) {') |
|
1833
|
|
|
|
|
|
|
->line(' close(client_fd);') |
|
1834
|
|
|
|
|
|
|
->line(' remove_connection(client_fd);') |
|
1835
|
|
|
|
|
|
|
->line(' continue;') |
|
1836
|
|
|
|
|
|
|
->line('}') |
|
1837
|
|
|
|
|
|
|
->line('#endif') |
|
1838
|
|
|
|
|
|
|
->blank |
|
1839
|
|
|
|
|
|
|
->comment('Add to event loop'); |
|
1840
|
|
|
|
|
|
|
|
|
1841
|
|
|
|
|
|
|
# Backend-specific: Add client to event loop |
|
1842
|
20
|
|
|
|
|
193
|
$backend->gen_add($builder, 'ev_fd', 'client_fd'); |
|
1843
|
20
|
|
|
|
|
88
|
$builder->endwhile; |
|
1844
|
|
|
|
|
|
|
|
|
1845
|
|
|
|
|
|
|
# Async Pool: Handle pool notify fd - process completed futures |
|
1846
|
20
|
50
|
|
|
|
83
|
if ($analysis->{needs_async_pool}) { |
|
1847
|
0
|
|
|
|
|
0
|
$builder->elsif('fd == pool_notify_fd') |
|
1848
|
|
|
|
|
|
|
->comment('Thread pool notification - process completed async operations') |
|
1849
|
|
|
|
|
|
|
->line('pool_process_ready();'); |
|
1850
|
|
|
|
|
|
|
} |
|
1851
|
|
|
|
|
|
|
|
|
1852
|
|
|
|
|
|
|
# Handle client request |
|
1853
|
20
|
|
|
|
|
429
|
$builder->elsif('fd != listen_fd') |
|
1854
|
|
|
|
|
|
|
->comment('Handle client request') |
|
1855
|
|
|
|
|
|
|
->line('#ifdef HYPERSONIC_TLS') |
|
1856
|
|
|
|
|
|
|
->line('TLSConnection* tls_conn = get_tls_connection(fd);') |
|
1857
|
|
|
|
|
|
|
->line('ssize_t len = tls_conn ? tls_recv(tls_conn, recv_buf, RECV_BUF_SIZE - 1) : -1;') |
|
1858
|
|
|
|
|
|
|
->line('#else') |
|
1859
|
|
|
|
|
|
|
->line('ssize_t len = recv(fd, recv_buf, RECV_BUF_SIZE - 1, 0);') |
|
1860
|
|
|
|
|
|
|
->line('#endif') |
|
1861
|
|
|
|
|
|
|
->blank |
|
1862
|
|
|
|
|
|
|
->if('len <= 0') |
|
1863
|
|
|
|
|
|
|
->comment('Connection closed or error'); |
|
1864
|
|
|
|
|
|
|
|
|
1865
|
|
|
|
|
|
|
# Backend-specific: Remove from event loop |
|
1866
|
20
|
|
|
|
|
136
|
$backend->gen_del($builder, 'ev_fd', 'fd'); |
|
1867
|
20
|
|
|
|
|
189
|
$builder->line('#ifdef HYPERSONIC_TLS') |
|
1868
|
|
|
|
|
|
|
->line('tls_close(fd);') |
|
1869
|
|
|
|
|
|
|
->line('#else') |
|
1870
|
|
|
|
|
|
|
->line('close(fd);') |
|
1871
|
|
|
|
|
|
|
->line('#endif'); |
|
1872
|
|
|
|
|
|
|
|
|
1873
|
|
|
|
|
|
|
# Reset WebSocket state if WebSocket routes exist |
|
1874
|
20
|
50
|
|
|
|
85
|
if ($analysis->{needs_websocket}) { |
|
1875
|
0
|
|
|
|
|
0
|
$builder->line('ws_reset(fd);'); |
|
1876
|
|
|
|
|
|
|
} |
|
1877
|
|
|
|
|
|
|
|
|
1878
|
20
|
|
|
|
|
590
|
$builder->line('remove_connection(fd);') |
|
1879
|
|
|
|
|
|
|
->line('continue;') |
|
1880
|
|
|
|
|
|
|
->endif |
|
1881
|
|
|
|
|
|
|
->blank |
|
1882
|
|
|
|
|
|
|
->comment('Update connection activity for keep-alive timeout') |
|
1883
|
|
|
|
|
|
|
->line('update_connection(fd, now);') |
|
1884
|
|
|
|
|
|
|
->blank |
|
1885
|
|
|
|
|
|
|
->line('recv_buf[len] = \'\\0\';') |
|
1886
|
|
|
|
|
|
|
->blank; |
|
1887
|
|
|
|
|
|
|
|
|
1888
|
|
|
|
|
|
|
# WebSocket frame handling - JIT: only generate if WebSocket routes exist |
|
1889
|
20
|
50
|
|
|
|
175
|
if ($analysis->{needs_websocket}) { |
|
1890
|
0
|
|
|
|
|
0
|
$self->_gen_websocket_frame_handler($builder); |
|
1891
|
|
|
|
|
|
|
} |
|
1892
|
|
|
|
|
|
|
|
|
1893
|
|
|
|
|
|
|
# Method parser - delegates to Protocol module |
|
1894
|
20
|
|
|
|
|
275
|
$self->_gen_method_parser($builder); |
|
1895
|
20
|
|
|
|
|
155
|
$builder->blank; |
|
1896
|
|
|
|
|
|
|
|
|
1897
|
|
|
|
|
|
|
# Path parsing - delegates to Protocol module |
|
1898
|
20
|
|
|
|
|
279
|
$PROTOCOL->gen_path_parser($builder); |
|
1899
|
20
|
|
|
|
|
75
|
$builder->blank; |
|
1900
|
|
|
|
|
|
|
|
|
1901
|
|
|
|
|
|
|
# WebSocket upgrade detection - JIT: only generate if WebSocket routes exist |
|
1902
|
20
|
50
|
|
|
|
71
|
if ($analysis->{needs_websocket}) { |
|
1903
|
0
|
|
|
|
|
0
|
$self->_gen_websocket_dispatch($builder); |
|
1904
|
|
|
|
|
|
|
} |
|
1905
|
|
|
|
|
|
|
|
|
1906
|
|
|
|
|
|
|
# Dispatch |
|
1907
|
20
|
|
|
|
|
197
|
$builder->comment('Dispatch request') |
|
1908
|
|
|
|
|
|
|
->line('const char* resp;') |
|
1909
|
|
|
|
|
|
|
->line('int resp_len;') |
|
1910
|
|
|
|
|
|
|
->line('int handler_idx;') |
|
1911
|
|
|
|
|
|
|
->line('int dispatch_result = dispatch_request(method, method_len, path, path_len, &resp, &resp_len, &handler_idx);') |
|
1912
|
|
|
|
|
|
|
->blank; |
|
1913
|
|
|
|
|
|
|
|
|
1914
|
|
|
|
|
|
|
# Dynamic dispatch |
|
1915
|
20
|
100
|
|
|
|
171
|
if ($has_dynamic) { |
|
1916
|
|
|
|
|
|
|
# Check for XS builder routes first (dispatch_result == 2) |
|
1917
|
4
|
50
|
33
|
|
|
21
|
if ($analysis->{needs_xs_builder} && $self->{_xs_builder_routes} && @{$self->{_xs_builder_routes}}) { |
|
|
0
|
|
0
|
|
|
0
|
|
|
1918
|
0
|
|
|
|
|
0
|
$builder->if('dispatch_result == 2') |
|
1919
|
|
|
|
|
|
|
->comment('XS builder route - call generated XS function directly'); |
|
1920
|
|
|
|
|
|
|
|
|
1921
|
|
|
|
|
|
|
# Body parsing for XS builder routes (they may need body access) |
|
1922
|
0
|
|
|
|
|
0
|
$PROTOCOL->gen_body_parser($builder, has_body_access => $has_body_access); |
|
1923
|
|
|
|
|
|
|
|
|
1924
|
0
|
|
|
|
|
0
|
$builder->blank |
|
1925
|
|
|
|
|
|
|
->line('char* xs_resp;') |
|
1926
|
|
|
|
|
|
|
->line('int xs_resp_len;') |
|
1927
|
|
|
|
|
|
|
->line('call_xs_builder_handler(aTHX_ handler_idx, fd, method, method_len, path, path_len, body, body_len, &xs_resp, &xs_resp_len);') |
|
1928
|
|
|
|
|
|
|
->line('HYPERSONIC_SEND(fd, xs_resp, xs_resp_len);') |
|
1929
|
|
|
|
|
|
|
->elsif('dispatch_result == 1'); |
|
1930
|
|
|
|
|
|
|
} else { |
|
1931
|
4
|
|
|
|
|
23
|
$builder->if('dispatch_result == 1'); |
|
1932
|
|
|
|
|
|
|
} |
|
1933
|
|
|
|
|
|
|
|
|
1934
|
4
|
|
|
|
|
24
|
$builder->comment('Dynamic route - call Perl handler'); |
|
1935
|
|
|
|
|
|
|
|
|
1936
|
|
|
|
|
|
|
# Body parsing - delegates to Protocol module |
|
1937
|
4
|
|
|
|
|
26
|
$PROTOCOL->gen_body_parser($builder, has_body_access => $has_body_access); |
|
1938
|
|
|
|
|
|
|
|
|
1939
|
4
|
|
|
|
|
130
|
$builder->blank |
|
1940
|
|
|
|
|
|
|
->line('char* dyn_resp;') |
|
1941
|
|
|
|
|
|
|
->line('int dyn_resp_len;') |
|
1942
|
|
|
|
|
|
|
->line('call_dynamic_handler(aTHX_ handler_idx, fd, method, method_len, path, full_path_len, body, body_len, recv_buf, len, &dyn_resp, &dyn_resp_len);') |
|
1943
|
|
|
|
|
|
|
->comment('dyn_resp_len == -1 means streaming handler (response already sent)') |
|
1944
|
|
|
|
|
|
|
->line('if (dyn_resp_len >= 0) {') |
|
1945
|
|
|
|
|
|
|
->line(' HYPERSONIC_SEND(fd, dyn_resp, dyn_resp_len);') |
|
1946
|
|
|
|
|
|
|
->line('}') |
|
1947
|
|
|
|
|
|
|
->else |
|
1948
|
|
|
|
|
|
|
->line('HYPERSONIC_SEND(fd, resp, resp_len);') |
|
1949
|
|
|
|
|
|
|
->endif; |
|
1950
|
|
|
|
|
|
|
} else { |
|
1951
|
16
|
|
|
|
|
140
|
$builder->line('HYPERSONIC_SEND(fd, resp, resp_len);'); |
|
1952
|
|
|
|
|
|
|
} |
|
1953
|
20
|
|
|
|
|
64
|
$builder->blank; |
|
1954
|
|
|
|
|
|
|
|
|
1955
|
|
|
|
|
|
|
# Keep-alive check - delegates to Protocol module |
|
1956
|
20
|
|
|
|
|
153
|
$PROTOCOL->gen_keepalive_check($builder); |
|
1957
|
20
|
|
|
|
|
113
|
$builder->blank |
|
1958
|
|
|
|
|
|
|
->if('!keep_alive'); |
|
1959
|
|
|
|
|
|
|
|
|
1960
|
|
|
|
|
|
|
# Backend-specific: Remove from event loop on close |
|
1961
|
20
|
|
|
|
|
186
|
$backend->gen_del($builder, 'ev_fd', 'fd'); |
|
1962
|
20
|
|
|
|
|
100
|
$builder->line('HYPERSONIC_CLOSE(fd);'); |
|
1963
|
|
|
|
|
|
|
|
|
1964
|
|
|
|
|
|
|
# Reset WebSocket state if WebSocket routes exist |
|
1965
|
20
|
50
|
|
|
|
64
|
if ($analysis->{needs_websocket}) { |
|
1966
|
0
|
|
|
|
|
0
|
$builder->line('ws_reset(fd);'); |
|
1967
|
|
|
|
|
|
|
} |
|
1968
|
|
|
|
|
|
|
|
|
1969
|
20
|
|
|
|
|
158
|
$builder->line('remove_connection(fd);') |
|
1970
|
|
|
|
|
|
|
->endif; |
|
1971
|
|
|
|
|
|
|
|
|
1972
|
|
|
|
|
|
|
# Close event processing |
|
1973
|
20
|
|
|
|
|
171
|
$builder->endif # fd == listen_fd |
|
1974
|
|
|
|
|
|
|
->endfor # for i |
|
1975
|
|
|
|
|
|
|
->endwhile # main loop |
|
1976
|
|
|
|
|
|
|
->blank; |
|
1977
|
|
|
|
|
|
|
|
|
1978
|
|
|
|
|
|
|
# Async Pool: Shutdown thread pool on server exit |
|
1979
|
20
|
50
|
|
|
|
71
|
if ($analysis->{needs_async_pool}) { |
|
1980
|
0
|
|
|
|
|
0
|
$builder->comment('Shutdown async thread pool') |
|
1981
|
|
|
|
|
|
|
->line('pool_shutdown();'); |
|
1982
|
|
|
|
|
|
|
} |
|
1983
|
|
|
|
|
|
|
|
|
1984
|
20
|
|
|
|
|
243
|
$builder->line('close(ev_fd);') |
|
1985
|
|
|
|
|
|
|
->xs_return('0') |
|
1986
|
|
|
|
|
|
|
->xs_end; |
|
1987
|
|
|
|
|
|
|
|
|
1988
|
20
|
|
|
|
|
44
|
return $builder; |
|
1989
|
|
|
|
|
|
|
} |
|
1990
|
|
|
|
|
|
|
|
|
1991
|
|
|
|
|
|
|
sub _gen_xs_builder_dispatcher { |
|
1992
|
0
|
|
|
0
|
|
0
|
my ($self) = @_; |
|
1993
|
|
|
|
|
|
|
|
|
1994
|
0
|
|
|
|
|
0
|
my $builder = XS::JIT::Builder->new; |
|
1995
|
0
|
|
0
|
|
|
0
|
my $xs_routes = $self->{_xs_builder_routes} || []; |
|
1996
|
|
|
|
|
|
|
|
|
1997
|
0
|
0
|
|
|
|
0
|
return '' unless @$xs_routes; |
|
1998
|
|
|
|
|
|
|
|
|
1999
|
0
|
|
|
|
|
0
|
$builder->comment('XS Builder route dispatcher') |
|
2000
|
|
|
|
|
|
|
->comment('Dispatches to user-defined XS functions based on handler_idx') |
|
2001
|
|
|
|
|
|
|
->line('static void call_xs_builder_handler(pTHX_ int handler_idx, int fd,') |
|
2002
|
|
|
|
|
|
|
->line(' const char* method, int method_len,') |
|
2003
|
|
|
|
|
|
|
->line(' const char* path, int path_len,') |
|
2004
|
|
|
|
|
|
|
->line(' const char* body, int body_len,') |
|
2005
|
|
|
|
|
|
|
->line(' char** resp_out, int* resp_len_out) {') |
|
2006
|
|
|
|
|
|
|
->line(' switch (handler_idx) {'); |
|
2007
|
|
|
|
|
|
|
|
|
2008
|
0
|
|
|
|
|
0
|
for my $entry (@$xs_routes) { |
|
2009
|
0
|
|
|
|
|
0
|
my $handler_idx = $entry->{route}{handler_idx}; |
|
2010
|
0
|
|
|
|
|
0
|
my $xs_func = $entry->{result}{xs_function}; |
|
2011
|
|
|
|
|
|
|
|
|
2012
|
0
|
|
|
|
|
0
|
$builder->line(" case $handler_idx:") |
|
2013
|
|
|
|
|
|
|
->line(" $xs_func(aTHX_ fd, method, method_len, path, path_len, body, body_len, resp_out, resp_len_out);") |
|
2014
|
|
|
|
|
|
|
->line(" break;"); |
|
2015
|
|
|
|
|
|
|
} |
|
2016
|
|
|
|
|
|
|
|
|
2017
|
0
|
|
|
|
|
0
|
$builder->line(' default:') |
|
2018
|
|
|
|
|
|
|
->line(' *resp_out = (char*)RESP_404;') |
|
2019
|
|
|
|
|
|
|
->line(' *resp_len_out = RESP_404_LEN;') |
|
2020
|
|
|
|
|
|
|
->line(' break;') |
|
2021
|
|
|
|
|
|
|
->line(' }') |
|
2022
|
|
|
|
|
|
|
->line('}') |
|
2023
|
|
|
|
|
|
|
->blank; |
|
2024
|
|
|
|
|
|
|
|
|
2025
|
0
|
|
|
|
|
0
|
return $builder->code; |
|
2026
|
|
|
|
|
|
|
} |
|
2027
|
|
|
|
|
|
|
|
|
2028
|
|
|
|
|
|
|
sub _gen_dynamic_handler_caller { |
|
2029
|
4
|
|
|
4
|
|
38
|
my ($self) = @_; |
|
2030
|
4
|
|
|
|
|
12
|
my $analysis = $self->{route_analysis}; |
|
2031
|
4
|
|
|
|
|
88
|
my $builder = XS::JIT::Builder->new; |
|
2032
|
|
|
|
|
|
|
|
|
2033
|
|
|
|
|
|
|
# ============================================================ |
|
2034
|
|
|
|
|
|
|
# JIT PHILOSOPHY: Only generate code for features actually used |
|
2035
|
|
|
|
|
|
|
# ============================================================ |
|
2036
|
|
|
|
|
|
|
|
|
2037
|
|
|
|
|
|
|
# Status code helper - always needed for dynamic routes |
|
2038
|
4
|
|
|
|
|
190
|
$builder->comment('Status code to text mapping') |
|
2039
|
|
|
|
|
|
|
->line('static const char* get_status_text(int code) {') |
|
2040
|
|
|
|
|
|
|
->line(' switch(code) {') |
|
2041
|
|
|
|
|
|
|
->line(' case 200: return "OK";') |
|
2042
|
|
|
|
|
|
|
->line(' case 201: return "Created";') |
|
2043
|
|
|
|
|
|
|
->line(' case 202: return "Accepted";') |
|
2044
|
|
|
|
|
|
|
->line(' case 204: return "No Content";') |
|
2045
|
|
|
|
|
|
|
->line(' case 301: return "Moved Permanently";') |
|
2046
|
|
|
|
|
|
|
->line(' case 302: return "Found";') |
|
2047
|
|
|
|
|
|
|
->line(' case 303: return "See Other";') |
|
2048
|
|
|
|
|
|
|
->line(' case 304: return "Not Modified";') |
|
2049
|
|
|
|
|
|
|
->line(' case 400: return "Bad Request";') |
|
2050
|
|
|
|
|
|
|
->line(' case 401: return "Unauthorized";') |
|
2051
|
|
|
|
|
|
|
->line(' case 403: return "Forbidden";') |
|
2052
|
|
|
|
|
|
|
->line(' case 404: return "Not Found";') |
|
2053
|
|
|
|
|
|
|
->line(' case 405: return "Method Not Allowed";') |
|
2054
|
|
|
|
|
|
|
->line(' case 408: return "Request Timeout";') |
|
2055
|
|
|
|
|
|
|
->line(' case 409: return "Conflict";') |
|
2056
|
|
|
|
|
|
|
->line(' case 413: return "Payload Too Large";') |
|
2057
|
|
|
|
|
|
|
->line(' case 422: return "Unprocessable Entity";') |
|
2058
|
|
|
|
|
|
|
->line(' case 429: return "Too Many Requests";') |
|
2059
|
|
|
|
|
|
|
->line(' case 500: return "Internal Server Error";') |
|
2060
|
|
|
|
|
|
|
->line(' case 502: return "Bad Gateway";') |
|
2061
|
|
|
|
|
|
|
->line(' case 503: return "Service Unavailable";') |
|
2062
|
|
|
|
|
|
|
->line(' default: return "Unknown";') |
|
2063
|
|
|
|
|
|
|
->line(' }') |
|
2064
|
|
|
|
|
|
|
->line('}') |
|
2065
|
|
|
|
|
|
|
->blank; |
|
2066
|
|
|
|
|
|
|
|
|
2067
|
|
|
|
|
|
|
# Path segment parser - always needed for path params |
|
2068
|
4
|
|
|
|
|
144
|
$builder->comment('Parse path segments into array - stops at ? for query string') |
|
2069
|
|
|
|
|
|
|
->line('static int parse_path_segments(const char* path, int path_len,') |
|
2070
|
|
|
|
|
|
|
->line(' const char** segments, int* seg_lens, int max_segs) {') |
|
2071
|
|
|
|
|
|
|
->line(' int count = 0;') |
|
2072
|
|
|
|
|
|
|
->line(' const char* start;') |
|
2073
|
|
|
|
|
|
|
->line(' const char* end;') |
|
2074
|
|
|
|
|
|
|
->line(' const char* query;') |
|
2075
|
|
|
|
|
|
|
->line(' const char* seg_end;') |
|
2076
|
|
|
|
|
|
|
->line(' start = path;') |
|
2077
|
|
|
|
|
|
|
->line(' end = path + path_len;') |
|
2078
|
|
|
|
|
|
|
->line(' query = memchr(path, \'?\', path_len);') |
|
2079
|
|
|
|
|
|
|
->line(' if (query) end = query;') |
|
2080
|
|
|
|
|
|
|
->line(' while (start < end && count < max_segs) {') |
|
2081
|
|
|
|
|
|
|
->line(' if (*start == \'/\') start++;') |
|
2082
|
|
|
|
|
|
|
->line(' if (start >= end) break;') |
|
2083
|
|
|
|
|
|
|
->line(' seg_end = start;') |
|
2084
|
|
|
|
|
|
|
->line(' while (seg_end < end && *seg_end != \'/\') seg_end++;') |
|
2085
|
|
|
|
|
|
|
->line(' segments[count] = start;') |
|
2086
|
|
|
|
|
|
|
->line(' seg_lens[count] = seg_end - start;') |
|
2087
|
|
|
|
|
|
|
->line(' count++;') |
|
2088
|
|
|
|
|
|
|
->line(' start = seg_end;') |
|
2089
|
|
|
|
|
|
|
->line(' }') |
|
2090
|
|
|
|
|
|
|
->line(' return count;') |
|
2091
|
|
|
|
|
|
|
->line('}') |
|
2092
|
|
|
|
|
|
|
->blank; |
|
2093
|
|
|
|
|
|
|
|
|
2094
|
|
|
|
|
|
|
# URL decoder - only if query/form parsing needed |
|
2095
|
4
|
50
|
33
|
|
|
65
|
if ($analysis->{needs_query} || $analysis->{needs_form}) { |
|
2096
|
0
|
|
|
|
|
0
|
$builder->comment('JIT: URL decode helper (query/form parsing enabled)') |
|
2097
|
|
|
|
|
|
|
->line('static int url_decode(char* str, int len) {') |
|
2098
|
|
|
|
|
|
|
->line(' char* dst = str;') |
|
2099
|
|
|
|
|
|
|
->line(' const char* src = str;') |
|
2100
|
|
|
|
|
|
|
->line(' const char* end = str + len;') |
|
2101
|
|
|
|
|
|
|
->line(' while (src < end) {') |
|
2102
|
|
|
|
|
|
|
->line(' if (*src == \'%\' && src + 2 < end) {') |
|
2103
|
|
|
|
|
|
|
->line(' int hi = src[1];') |
|
2104
|
|
|
|
|
|
|
->line(' int lo = src[2];') |
|
2105
|
|
|
|
|
|
|
->line(' hi = (hi >= \'0\' && hi <= \'9\') ? hi - \'0\' :') |
|
2106
|
|
|
|
|
|
|
->line(' (hi >= \'A\' && hi <= \'F\') ? hi - \'A\' + 10 :') |
|
2107
|
|
|
|
|
|
|
->line(' (hi >= \'a\' && hi <= \'f\') ? hi - \'a\' + 10 : -1;') |
|
2108
|
|
|
|
|
|
|
->line(' lo = (lo >= \'0\' && lo <= \'9\') ? lo - \'0\' :') |
|
2109
|
|
|
|
|
|
|
->line(' (lo >= \'A\' && lo <= \'F\') ? lo - \'A\' + 10 :') |
|
2110
|
|
|
|
|
|
|
->line(' (lo >= \'a\' && lo <= \'f\') ? lo - \'a\' + 10 : -1;') |
|
2111
|
|
|
|
|
|
|
->line(' if (hi >= 0 && lo >= 0) {') |
|
2112
|
|
|
|
|
|
|
->line(' *dst++ = (char)((hi << 4) | lo);') |
|
2113
|
|
|
|
|
|
|
->line(' src += 3;') |
|
2114
|
|
|
|
|
|
|
->line(' continue;') |
|
2115
|
|
|
|
|
|
|
->line(' }') |
|
2116
|
|
|
|
|
|
|
->line(' } else if (*src == \'+\') {') |
|
2117
|
|
|
|
|
|
|
->line(' *dst++ = \' \';') |
|
2118
|
|
|
|
|
|
|
->line(' src++;') |
|
2119
|
|
|
|
|
|
|
->line(' continue;') |
|
2120
|
|
|
|
|
|
|
->line(' }') |
|
2121
|
|
|
|
|
|
|
->line(' *dst++ = *src++;') |
|
2122
|
|
|
|
|
|
|
->line(' }') |
|
2123
|
|
|
|
|
|
|
->line(' return dst - str;') |
|
2124
|
|
|
|
|
|
|
->line('}') |
|
2125
|
|
|
|
|
|
|
->blank; |
|
2126
|
|
|
|
|
|
|
|
|
2127
|
0
|
|
|
|
|
0
|
$builder->comment('JIT: Query string parser (query/form parsing enabled)') |
|
2128
|
|
|
|
|
|
|
->line('static void parse_query_string(pTHX_ const char* query, int query_len, HV* hv) {') |
|
2129
|
|
|
|
|
|
|
->line(' const char* start = query;') |
|
2130
|
|
|
|
|
|
|
->line(' const char* end = query + query_len;') |
|
2131
|
|
|
|
|
|
|
->line(' while (start < end) {') |
|
2132
|
|
|
|
|
|
|
->line(' const char* eq = memchr(start, \'=\', end - start);') |
|
2133
|
|
|
|
|
|
|
->line(' const char* amp = memchr(start, \'&\', end - start);') |
|
2134
|
|
|
|
|
|
|
->line(' if (!amp) amp = end;') |
|
2135
|
|
|
|
|
|
|
->line(' if (eq && eq < amp) {') |
|
2136
|
|
|
|
|
|
|
->line(' int key_len = eq - start;') |
|
2137
|
|
|
|
|
|
|
->line(' const char* val = eq + 1;') |
|
2138
|
|
|
|
|
|
|
->line(' int val_len = amp - val;') |
|
2139
|
|
|
|
|
|
|
->line(' char val_buf[1024];') |
|
2140
|
|
|
|
|
|
|
->line(' if (val_len < (int)sizeof(val_buf)) {') |
|
2141
|
|
|
|
|
|
|
->line(' memcpy(val_buf, val, val_len);') |
|
2142
|
|
|
|
|
|
|
->line(' val_len = url_decode(val_buf, val_len);') |
|
2143
|
|
|
|
|
|
|
->line(' hv_store(hv, start, key_len, newSVpvn(val_buf, val_len), 0);') |
|
2144
|
|
|
|
|
|
|
->line(' }') |
|
2145
|
|
|
|
|
|
|
->line(' } else if (amp > start) {') |
|
2146
|
|
|
|
|
|
|
->line(' hv_store(hv, start, amp - start, newSVpvn("", 0), 0);') |
|
2147
|
|
|
|
|
|
|
->line(' }') |
|
2148
|
|
|
|
|
|
|
->line(' start = amp + 1;') |
|
2149
|
|
|
|
|
|
|
->line(' }') |
|
2150
|
|
|
|
|
|
|
->line('}') |
|
2151
|
|
|
|
|
|
|
->blank; |
|
2152
|
|
|
|
|
|
|
} |
|
2153
|
|
|
|
|
|
|
|
|
2154
|
|
|
|
|
|
|
# Header parser - only if headers/cookies/json/form needed |
|
2155
|
4
|
50
|
66
|
|
|
67
|
if ($analysis->{needs_headers} || $analysis->{needs_cookies} || |
|
|
|
|
66
|
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
2156
|
|
|
|
|
|
|
$analysis->{needs_json} || $analysis->{needs_form}) { |
|
2157
|
1
|
|
|
|
|
35
|
$builder->comment('JIT: Header parser (headers/cookies/json/form enabled)') |
|
2158
|
|
|
|
|
|
|
->line('static void parse_headers(pTHX_ const char* raw, int raw_len, HV* hv) {') |
|
2159
|
|
|
|
|
|
|
->line(' const char* line = memchr(raw, \'\\n\', raw_len);') |
|
2160
|
|
|
|
|
|
|
->line(' if (!line) return;') |
|
2161
|
|
|
|
|
|
|
->line(' line++;') |
|
2162
|
|
|
|
|
|
|
->line(' const char* end = raw + raw_len;') |
|
2163
|
|
|
|
|
|
|
->line(' while (line < end) {') |
|
2164
|
|
|
|
|
|
|
->line(' const char* line_end = memchr(line, \'\\r\', end - line);') |
|
2165
|
|
|
|
|
|
|
->line(' if (!line_end) line_end = memchr(line, \'\\n\', end - line);') |
|
2166
|
|
|
|
|
|
|
->line(' if (!line_end || line_end == line) break;') |
|
2167
|
|
|
|
|
|
|
->line(' const char* colon = memchr(line, \':\', line_end - line);') |
|
2168
|
|
|
|
|
|
|
->line(' if (colon) {') |
|
2169
|
|
|
|
|
|
|
->line(' int name_len = colon - line;') |
|
2170
|
|
|
|
|
|
|
->line(' const char* value = colon + 1;') |
|
2171
|
|
|
|
|
|
|
->line(' while (value < line_end && (*value == \' \' || *value == \'\\t\')) value++;') |
|
2172
|
|
|
|
|
|
|
->line(' int value_len = line_end - value;') |
|
2173
|
|
|
|
|
|
|
->line(' char name_buf[128];') |
|
2174
|
|
|
|
|
|
|
->line(' if (name_len < (int)sizeof(name_buf)) {') |
|
2175
|
|
|
|
|
|
|
->line(' int i;') |
|
2176
|
|
|
|
|
|
|
->line(' for (i = 0; i < name_len; i++) {') |
|
2177
|
|
|
|
|
|
|
->line(' char c = line[i];') |
|
2178
|
|
|
|
|
|
|
->line(' name_buf[i] = (c >= \'A\' && c <= \'Z\') ? c + 32 : (c == \'-\') ? \'_\' : c;') |
|
2179
|
|
|
|
|
|
|
->line(' }') |
|
2180
|
|
|
|
|
|
|
->line(' hv_store(hv, name_buf, name_len, newSVpvn(value, value_len), 0);') |
|
2181
|
|
|
|
|
|
|
->line(' }') |
|
2182
|
|
|
|
|
|
|
->line(' }') |
|
2183
|
|
|
|
|
|
|
->line(' line = line_end;') |
|
2184
|
|
|
|
|
|
|
->line(' if (*line == \'\\r\') line++;') |
|
2185
|
|
|
|
|
|
|
->line(' if (line < end && *line == \'\\n\') line++;') |
|
2186
|
|
|
|
|
|
|
->line(' }') |
|
2187
|
|
|
|
|
|
|
->line('}') |
|
2188
|
|
|
|
|
|
|
->blank; |
|
2189
|
|
|
|
|
|
|
} |
|
2190
|
|
|
|
|
|
|
|
|
2191
|
|
|
|
|
|
|
# JIT: Generate C helpers from builder middleware (static functions at file scope) |
|
2192
|
4
|
50
|
33
|
|
|
49
|
if ($analysis->{has_builder_before} || $analysis->{has_builder_after}) { |
|
2193
|
0
|
|
|
|
|
0
|
my %seen_classes; |
|
2194
|
0
|
|
|
|
|
0
|
$builder->comment('JIT: Builder middleware C helpers (zero Perl in hot path)'); |
|
2195
|
0
|
|
|
|
|
0
|
for my $mw (@{$analysis->{builder_before}}, @{$analysis->{builder_after}}) { |
|
|
0
|
|
|
|
|
0
|
|
|
|
0
|
|
|
|
|
0
|
|
|
2196
|
0
|
|
|
|
|
0
|
my $class = ref($mw); |
|
2197
|
0
|
0
|
|
|
|
0
|
next if $seen_classes{$class}++; # Deduplicate by class |
|
2198
|
0
|
0
|
|
|
|
0
|
if ($mw->can('build_helpers')) { |
|
2199
|
0
|
|
|
|
|
0
|
$mw->build_helpers($builder); |
|
2200
|
0
|
|
|
|
|
0
|
$builder->blank; |
|
2201
|
|
|
|
|
|
|
} |
|
2202
|
|
|
|
|
|
|
} |
|
2203
|
|
|
|
|
|
|
} |
|
2204
|
|
|
|
|
|
|
|
|
2205
|
|
|
|
|
|
|
# JIT: Generate middleware helper function BEFORE call_dynamic_handler |
|
2206
|
4
|
100
|
|
|
|
14
|
if ($analysis->{has_any_middleware}) { |
|
2207
|
1
|
|
|
|
|
42
|
$builder->comment('JIT: Middleware helper - call array of handlers, short-circuit on defined return') |
|
2208
|
|
|
|
|
|
|
->line('static SV* call_middleware_chain(pTHX_ AV* handlers, SV* req_ref) {') |
|
2209
|
|
|
|
|
|
|
->line(' dSP;') |
|
2210
|
|
|
|
|
|
|
->line(' SSize_t len = av_len(handlers) + 1;') |
|
2211
|
|
|
|
|
|
|
->line(' SSize_t i;') |
|
2212
|
|
|
|
|
|
|
->line(' for (i = 0; i < len; i++) {') |
|
2213
|
|
|
|
|
|
|
->line(' SV** handler_sv = av_fetch(handlers, i, 0);') |
|
2214
|
|
|
|
|
|
|
->line(' if (!handler_sv || !SvROK(*handler_sv)) continue;') |
|
2215
|
|
|
|
|
|
|
->line(' PUSHMARK(SP);') |
|
2216
|
|
|
|
|
|
|
->line(' XPUSHs(req_ref);') |
|
2217
|
|
|
|
|
|
|
->line(' PUTBACK;') |
|
2218
|
|
|
|
|
|
|
->line(' int count = call_sv(*handler_sv, G_SCALAR | G_EVAL);') |
|
2219
|
|
|
|
|
|
|
->line(' SPAGAIN;') |
|
2220
|
|
|
|
|
|
|
->line(' if (SvTRUE(ERRSV)) {') |
|
2221
|
|
|
|
|
|
|
->line(' POPs;') |
|
2222
|
|
|
|
|
|
|
->line(' continue;') |
|
2223
|
|
|
|
|
|
|
->line(' }') |
|
2224
|
|
|
|
|
|
|
->line(' if (count == 1) {') |
|
2225
|
|
|
|
|
|
|
->line(' SV* result = POPs;') |
|
2226
|
|
|
|
|
|
|
->line(' PUTBACK;') |
|
2227
|
|
|
|
|
|
|
->line(' if (SvOK(result)) {') |
|
2228
|
|
|
|
|
|
|
->line(' return SvREFCNT_inc(result);') |
|
2229
|
|
|
|
|
|
|
->line(' }') |
|
2230
|
|
|
|
|
|
|
->line(' }') |
|
2231
|
|
|
|
|
|
|
->line(' PUTBACK;') |
|
2232
|
|
|
|
|
|
|
->line(' }') |
|
2233
|
|
|
|
|
|
|
->line(' return NULL;') |
|
2234
|
|
|
|
|
|
|
->line('}') |
|
2235
|
|
|
|
|
|
|
->blank; |
|
2236
|
|
|
|
|
|
|
} |
|
2237
|
|
|
|
|
|
|
|
|
2238
|
|
|
|
|
|
|
# Main handler function |
|
2239
|
4
|
|
|
|
|
315
|
$builder->comment('Call dynamic Perl handler and format HTTP response') |
|
2240
|
|
|
|
|
|
|
->line('static void call_dynamic_handler(pTHX_ int handler_idx, int client_fd,') |
|
2241
|
|
|
|
|
|
|
->line(' const char* method, int method_len,') |
|
2242
|
|
|
|
|
|
|
->line(' const char* path, int path_len,') |
|
2243
|
|
|
|
|
|
|
->line(' const char* body, int body_len,') |
|
2244
|
|
|
|
|
|
|
->line(' const char* raw_request, int raw_request_len,') |
|
2245
|
|
|
|
|
|
|
->line(' char** resp_out, int* resp_len_out) {') |
|
2246
|
|
|
|
|
|
|
->line(' dSP;') |
|
2247
|
|
|
|
|
|
|
->line(' int count;') |
|
2248
|
|
|
|
|
|
|
->line(' SV* result;') |
|
2249
|
|
|
|
|
|
|
->line(' STRLEN len;') |
|
2250
|
|
|
|
|
|
|
->line(' const char* body_str;') |
|
2251
|
|
|
|
|
|
|
->line(' int status = 200;') |
|
2252
|
|
|
|
|
|
|
->line(' const char* content_type = "text/plain";') |
|
2253
|
|
|
|
|
|
|
->line(' AV* handlers;') |
|
2254
|
|
|
|
|
|
|
->line(' SV** handler_sv;') |
|
2255
|
|
|
|
|
|
|
->line(' const char* query_start;') |
|
2256
|
|
|
|
|
|
|
->line(' int clean_path_len;') |
|
2257
|
|
|
|
|
|
|
->line(' AV* req;') |
|
2258
|
|
|
|
|
|
|
->line(' const char* segments[16];') |
|
2259
|
|
|
|
|
|
|
->line(' int seg_lens[16];') |
|
2260
|
|
|
|
|
|
|
->line(' int seg_count;') |
|
2261
|
|
|
|
|
|
|
->line(' AV* seg_av;') |
|
2262
|
|
|
|
|
|
|
->line(' int i;') |
|
2263
|
|
|
|
|
|
|
->line(' HV* params_hv;') |
|
2264
|
|
|
|
|
|
|
->line(' RouteParamInfo* param_info;') |
|
2265
|
|
|
|
|
|
|
->line(' SV* req_ref;') |
|
2266
|
|
|
|
|
|
|
->line(' SV* mw_result = NULL;') |
|
2267
|
|
|
|
|
|
|
->line(' int short_circuit = 0;') |
|
2268
|
|
|
|
|
|
|
->line(' HV* headers_hv = NULL;') |
|
2269
|
|
|
|
|
|
|
->line(' int is_streaming = 0;') |
|
2270
|
|
|
|
|
|
|
->line(' SV* stream_sv = NULL;') |
|
2271
|
|
|
|
|
|
|
->blank |
|
2272
|
|
|
|
|
|
|
->line(' if (!g_handler_array) {') |
|
2273
|
|
|
|
|
|
|
->line(' *resp_out = (char*)RESP_404;') |
|
2274
|
|
|
|
|
|
|
->line(' *resp_len_out = RESP_404_LEN;') |
|
2275
|
|
|
|
|
|
|
->line(' return;') |
|
2276
|
|
|
|
|
|
|
->line(' }') |
|
2277
|
|
|
|
|
|
|
->blank |
|
2278
|
|
|
|
|
|
|
->line(' handlers = (AV*)SvRV(g_handler_array);') |
|
2279
|
|
|
|
|
|
|
->line(' handler_sv = av_fetch(handlers, handler_idx, 0);') |
|
2280
|
|
|
|
|
|
|
->line(' if (!handler_sv || !SvROK(*handler_sv)) {') |
|
2281
|
|
|
|
|
|
|
->line(' *resp_out = (char*)RESP_404;') |
|
2282
|
|
|
|
|
|
|
->line(' *resp_len_out = RESP_404_LEN;') |
|
2283
|
|
|
|
|
|
|
->line(' return;') |
|
2284
|
|
|
|
|
|
|
->line(' }') |
|
2285
|
|
|
|
|
|
|
->blank; |
|
2286
|
|
|
|
|
|
|
|
|
2287
|
|
|
|
|
|
|
# Query string separation |
|
2288
|
4
|
|
|
|
|
93
|
$builder->comment('Separate path from query string') |
|
2289
|
|
|
|
|
|
|
->line(' query_start = memchr(path, \'?\', path_len);') |
|
2290
|
|
|
|
|
|
|
->line(' clean_path_len = query_start ? (query_start - path) : path_len;') |
|
2291
|
|
|
|
|
|
|
->blank |
|
2292
|
|
|
|
|
|
|
->comment('Build array-based request object (JIT slots)') |
|
2293
|
|
|
|
|
|
|
->comment('Slot layout: METHOD=0, PATH=1, BODY=2, PARAMS=3, QUERY=4, QUERY_STRING=5,') |
|
2294
|
|
|
|
|
|
|
->comment(' HEADERS=6, COOKIES=7, JSON=8, FORM=9, SEGMENTS=10, ID=11') |
|
2295
|
|
|
|
|
|
|
->line(' req = newAV();') |
|
2296
|
|
|
|
|
|
|
->line(' av_extend(req, 11);') # Pre-allocate 12 slots (0-11) |
|
2297
|
|
|
|
|
|
|
->line(' av_store(req, 0, newSVpvn(method, method_len));') # SLOT_METHOD |
|
2298
|
|
|
|
|
|
|
->line(' av_store(req, 1, newSVpvn(path, clean_path_len));') # SLOT_PATH |
|
2299
|
|
|
|
|
|
|
->line(' av_store(req, 2, newSVpvn(body, body_len));') # SLOT_BODY |
|
2300
|
|
|
|
|
|
|
->blank; |
|
2301
|
|
|
|
|
|
|
|
|
2302
|
|
|
|
|
|
|
# Path segments and params - always needed |
|
2303
|
4
|
|
|
|
|
162
|
$builder->comment('Parse path segments and named params') |
|
2304
|
|
|
|
|
|
|
->line(' seg_count = parse_path_segments(path, path_len, segments, seg_lens, 16);') |
|
2305
|
|
|
|
|
|
|
->line(' seg_av = newAV();') |
|
2306
|
|
|
|
|
|
|
->line(' for (i = 0; i < seg_count; i++) {') |
|
2307
|
|
|
|
|
|
|
->line(' av_push(seg_av, newSVpvn(segments[i], seg_lens[i]));') |
|
2308
|
|
|
|
|
|
|
->line(' }') |
|
2309
|
|
|
|
|
|
|
->line(' av_store(req, 10, newRV_noinc((SV*)seg_av));') # SLOT_SEGMENTS |
|
2310
|
|
|
|
|
|
|
->blank |
|
2311
|
|
|
|
|
|
|
->comment('Build named params from route_param_info table') |
|
2312
|
|
|
|
|
|
|
->line(' params_hv = newHV();') |
|
2313
|
|
|
|
|
|
|
->line(' param_info = &g_route_params[handler_idx];') |
|
2314
|
|
|
|
|
|
|
->line(' for (i = 0; i < param_info->count && i < seg_count; i++) {') |
|
2315
|
|
|
|
|
|
|
->line(' int pos = param_info->params[i].position;') |
|
2316
|
|
|
|
|
|
|
->line(' if (pos < seg_count) {') |
|
2317
|
|
|
|
|
|
|
->line(' hv_store(params_hv, param_info->params[i].name,') |
|
2318
|
|
|
|
|
|
|
->line(' strlen(param_info->params[i].name),') |
|
2319
|
|
|
|
|
|
|
->line(' newSVpvn(segments[pos], seg_lens[pos]), 0);') |
|
2320
|
|
|
|
|
|
|
->line(' }') |
|
2321
|
|
|
|
|
|
|
->line(' }') |
|
2322
|
|
|
|
|
|
|
->line(' av_store(req, 3, newRV_noinc((SV*)params_hv));') # SLOT_PARAMS |
|
2323
|
|
|
|
|
|
|
->line(' if (seg_count > 0) {') |
|
2324
|
|
|
|
|
|
|
->line(' av_store(req, 11, newSVpvn(segments[seg_count-1], seg_lens[seg_count-1]));') # SLOT_ID |
|
2325
|
|
|
|
|
|
|
->line(' } else {') |
|
2326
|
|
|
|
|
|
|
->line(' av_store(req, 11, newSVpvn("", 0));') # SLOT_ID (empty) |
|
2327
|
|
|
|
|
|
|
->line(' }') |
|
2328
|
|
|
|
|
|
|
->blank; |
|
2329
|
|
|
|
|
|
|
|
|
2330
|
|
|
|
|
|
|
# Query string parsing - JIT conditional |
|
2331
|
4
|
50
|
|
|
|
31
|
if ($analysis->{needs_query}) { |
|
2332
|
0
|
|
|
|
|
0
|
$builder->comment('JIT: Parse query string (parse_query enabled)') |
|
2333
|
|
|
|
|
|
|
->line(' if (query_start) {') |
|
2334
|
|
|
|
|
|
|
->line(' HV* query_hv = newHV();') |
|
2335
|
|
|
|
|
|
|
->line(' int query_len = path_len - (query_start - path) - 1;') |
|
2336
|
|
|
|
|
|
|
->line(' parse_query_string(aTHX_ query_start + 1, query_len, query_hv);') |
|
2337
|
|
|
|
|
|
|
->line(' av_store(req, 4, newRV_noinc((SV*)query_hv));') # SLOT_QUERY |
|
2338
|
|
|
|
|
|
|
->line(' av_store(req, 5, newSVpvn(query_start + 1, query_len));') # SLOT_QUERY_STRING |
|
2339
|
|
|
|
|
|
|
->line(' } else {') |
|
2340
|
|
|
|
|
|
|
->line(' av_store(req, 4, newRV_noinc((SV*)newHV()));') # SLOT_QUERY |
|
2341
|
|
|
|
|
|
|
->line(' av_store(req, 5, newSVpvn("", 0));') # SLOT_QUERY_STRING |
|
2342
|
|
|
|
|
|
|
->line(' }') |
|
2343
|
|
|
|
|
|
|
->blank; |
|
2344
|
|
|
|
|
|
|
} else { |
|
2345
|
4
|
|
|
|
|
26
|
$builder->comment('JIT: Query parsing SKIPPED (no routes use parse_query)') |
|
2346
|
|
|
|
|
|
|
->line(' av_store(req, 4, newRV_noinc((SV*)newHV()));') # SLOT_QUERY |
|
2347
|
|
|
|
|
|
|
->line(' av_store(req, 5, newSVpvn("", 0));') # SLOT_QUERY_STRING |
|
2348
|
|
|
|
|
|
|
->blank; |
|
2349
|
|
|
|
|
|
|
} |
|
2350
|
|
|
|
|
|
|
|
|
2351
|
|
|
|
|
|
|
# Header parsing - JIT conditional |
|
2352
|
|
|
|
|
|
|
my $needs_header_parse = $analysis->{needs_headers} || $analysis->{needs_cookies} || |
|
2353
|
4
|
|
33
|
|
|
143
|
$analysis->{needs_json} || $analysis->{needs_form}; |
|
2354
|
|
|
|
|
|
|
|
|
2355
|
4
|
100
|
|
|
|
16
|
if ($needs_header_parse) { |
|
2356
|
1
|
|
|
|
|
7
|
$builder->comment('JIT: Parse headers (headers/cookies/json/form enabled)') |
|
2357
|
|
|
|
|
|
|
->line(' headers_hv = newHV();') |
|
2358
|
|
|
|
|
|
|
->line(' parse_headers(aTHX_ raw_request, raw_request_len, headers_hv);') |
|
2359
|
|
|
|
|
|
|
->line(' av_store(req, 6, newRV_noinc((SV*)headers_hv));') # SLOT_HEADERS |
|
2360
|
|
|
|
|
|
|
->blank; |
|
2361
|
|
|
|
|
|
|
|
|
2362
|
|
|
|
|
|
|
# Cookie parsing |
|
2363
|
1
|
50
|
|
|
|
2
|
if ($analysis->{needs_cookies}) { |
|
2364
|
1
|
|
|
|
|
34
|
$builder->comment('JIT: Parse cookies (parse_cookies enabled)') |
|
2365
|
|
|
|
|
|
|
->line(' SV** cookie_sv = hv_fetch(headers_hv, "cookie", 6, 0);') |
|
2366
|
|
|
|
|
|
|
->line(' if (cookie_sv && SvOK(*cookie_sv)) {') |
|
2367
|
|
|
|
|
|
|
->line(' HV* cookies_hv = newHV();') |
|
2368
|
|
|
|
|
|
|
->line(' STRLEN cookie_len;') |
|
2369
|
|
|
|
|
|
|
->line(' const char* cookie_str = SvPV(*cookie_sv, cookie_len);') |
|
2370
|
|
|
|
|
|
|
->line(' const char* start = cookie_str;') |
|
2371
|
|
|
|
|
|
|
->line(' const char* end = cookie_str + cookie_len;') |
|
2372
|
|
|
|
|
|
|
->line(' while (start < end) {') |
|
2373
|
|
|
|
|
|
|
->line(' while (start < end && (*start == \' \' || *start == \';\')) start++;') |
|
2374
|
|
|
|
|
|
|
->line(' if (start >= end) break;') |
|
2375
|
|
|
|
|
|
|
->line(' const char* eq = memchr(start, \'=\', end - start);') |
|
2376
|
|
|
|
|
|
|
->line(' const char* semi = memchr(start, \';\', end - start);') |
|
2377
|
|
|
|
|
|
|
->line(' if (!semi) semi = end;') |
|
2378
|
|
|
|
|
|
|
->line(' if (eq && eq < semi) {') |
|
2379
|
|
|
|
|
|
|
->line(' int name_len = eq - start;') |
|
2380
|
|
|
|
|
|
|
->line(' const char* val = eq + 1;') |
|
2381
|
|
|
|
|
|
|
->line(' int val_len = semi - val;') |
|
2382
|
|
|
|
|
|
|
->line(' hv_store(cookies_hv, start, name_len, newSVpvn(val, val_len), 0);') |
|
2383
|
|
|
|
|
|
|
->line(' }') |
|
2384
|
|
|
|
|
|
|
->line(' start = semi + 1;') |
|
2385
|
|
|
|
|
|
|
->line(' }') |
|
2386
|
|
|
|
|
|
|
->line(' av_store(req, 7, newRV_noinc((SV*)cookies_hv));') # SLOT_COOKIES |
|
2387
|
|
|
|
|
|
|
->line(' } else {') |
|
2388
|
|
|
|
|
|
|
->line(' av_store(req, 7, newRV_noinc((SV*)newHV()));') # SLOT_COOKIES |
|
2389
|
|
|
|
|
|
|
->line(' }') |
|
2390
|
|
|
|
|
|
|
->blank; |
|
2391
|
|
|
|
|
|
|
} else { |
|
2392
|
0
|
|
|
|
|
0
|
$builder->comment('JIT: Cookie parsing SKIPPED') |
|
2393
|
|
|
|
|
|
|
->line(' av_store(req, 7, newRV_noinc((SV*)newHV()));') # SLOT_COOKIES |
|
2394
|
|
|
|
|
|
|
->blank; |
|
2395
|
|
|
|
|
|
|
} |
|
2396
|
|
|
|
|
|
|
|
|
2397
|
|
|
|
|
|
|
# Form/JSON parsing |
|
2398
|
1
|
50
|
33
|
|
|
8
|
if ($analysis->{needs_form} || $analysis->{needs_json}) { |
|
2399
|
1
|
|
|
|
|
9
|
$builder->comment('JIT: Form/JSON body parsing') |
|
2400
|
|
|
|
|
|
|
->line(' SV** ct_sv = hv_fetch(headers_hv, "content_type", 12, 0);') |
|
2401
|
|
|
|
|
|
|
->line(' if (ct_sv && SvOK(*ct_sv) && body_len > 0) {') |
|
2402
|
|
|
|
|
|
|
->line(' STRLEN ct_len;') |
|
2403
|
|
|
|
|
|
|
->line(' const char* ct_str = SvPV(*ct_sv, ct_len);'); |
|
2404
|
|
|
|
|
|
|
|
|
2405
|
1
|
50
|
|
|
|
3
|
if ($analysis->{needs_form}) { |
|
2406
|
0
|
|
|
|
|
0
|
$builder->line(' if (ct_len >= 33 && memcmp(ct_str, "application/x-www-form-urlencoded", 33) == 0) {') |
|
2407
|
|
|
|
|
|
|
->line(' HV* form_hv = newHV();') |
|
2408
|
|
|
|
|
|
|
->line(' parse_query_string(aTHX_ body, body_len, form_hv);') |
|
2409
|
|
|
|
|
|
|
->line(' av_store(req, 9, newRV_noinc((SV*)form_hv));') # SLOT_FORM |
|
2410
|
|
|
|
|
|
|
->line(' } else {') |
|
2411
|
|
|
|
|
|
|
->line(' av_store(req, 9, newRV_noinc((SV*)newHV()));') # SLOT_FORM |
|
2412
|
|
|
|
|
|
|
->line(' }'); |
|
2413
|
|
|
|
|
|
|
} else { |
|
2414
|
1
|
|
|
|
|
4
|
$builder->line(' av_store(req, 9, newRV_noinc((SV*)newHV()));'); # SLOT_FORM |
|
2415
|
|
|
|
|
|
|
} |
|
2416
|
|
|
|
|
|
|
|
|
2417
|
1
|
50
|
|
|
|
4
|
if ($analysis->{needs_json}) { |
|
2418
|
1
|
|
|
|
|
28
|
$builder->line(' if (ct_len >= 16 && memcmp(ct_str, "application/json", 16) == 0) {') |
|
2419
|
|
|
|
|
|
|
->line(' dSP;') |
|
2420
|
|
|
|
|
|
|
->line(' PUSHMARK(SP);') |
|
2421
|
|
|
|
|
|
|
->line(' XPUSHs(sv_2mortal(newSVpvn(body, body_len)));') |
|
2422
|
|
|
|
|
|
|
->line(' PUTBACK;') |
|
2423
|
|
|
|
|
|
|
->line(' int json_count = call_pv("Hypersonic::_decode_json", G_SCALAR | G_EVAL);') |
|
2424
|
|
|
|
|
|
|
->line(' SPAGAIN;') |
|
2425
|
|
|
|
|
|
|
->line(' if (json_count == 1 && !SvTRUE(ERRSV)) {') |
|
2426
|
|
|
|
|
|
|
->line(' SV* json_sv = POPs;') |
|
2427
|
|
|
|
|
|
|
->line(' av_store(req, 8, SvREFCNT_inc(json_sv));') # SLOT_JSON |
|
2428
|
|
|
|
|
|
|
->line(' } else {') |
|
2429
|
|
|
|
|
|
|
->line(' if (SvTRUE(ERRSV)) POPs;') |
|
2430
|
|
|
|
|
|
|
->line(' av_store(req, 8, &PL_sv_undef);') # SLOT_JSON |
|
2431
|
|
|
|
|
|
|
->line(' }') |
|
2432
|
|
|
|
|
|
|
->line(' PUTBACK;') |
|
2433
|
|
|
|
|
|
|
->line(' } else {') |
|
2434
|
|
|
|
|
|
|
->line(' av_store(req, 8, &PL_sv_undef);') # SLOT_JSON |
|
2435
|
|
|
|
|
|
|
->line(' }'); |
|
2436
|
|
|
|
|
|
|
} else { |
|
2437
|
0
|
|
|
|
|
0
|
$builder->line(' av_store(req, 8, &PL_sv_undef);'); # SLOT_JSON |
|
2438
|
|
|
|
|
|
|
} |
|
2439
|
|
|
|
|
|
|
|
|
2440
|
1
|
|
|
|
|
6
|
$builder->line(' } else {') |
|
2441
|
|
|
|
|
|
|
->line(' av_store(req, 9, newRV_noinc((SV*)newHV()));') # SLOT_FORM |
|
2442
|
|
|
|
|
|
|
->line(' av_store(req, 8, &PL_sv_undef);') # SLOT_JSON |
|
2443
|
|
|
|
|
|
|
->line(' }') |
|
2444
|
|
|
|
|
|
|
->blank; |
|
2445
|
|
|
|
|
|
|
} else { |
|
2446
|
0
|
|
|
|
|
0
|
$builder->comment('JIT: Form/JSON parsing SKIPPED') |
|
2447
|
|
|
|
|
|
|
->line(' av_store(req, 9, newRV_noinc((SV*)newHV()));') # SLOT_FORM |
|
2448
|
|
|
|
|
|
|
->line(' av_store(req, 8, &PL_sv_undef);') # SLOT_JSON |
|
2449
|
|
|
|
|
|
|
->blank; |
|
2450
|
|
|
|
|
|
|
} |
|
2451
|
|
|
|
|
|
|
} else { |
|
2452
|
3
|
|
|
|
|
62
|
$builder->comment('JIT: All header-based parsing SKIPPED (no routes use these features)') |
|
2453
|
|
|
|
|
|
|
->line(' av_store(req, 6, newRV_noinc((SV*)newHV()));') # SLOT_HEADERS |
|
2454
|
|
|
|
|
|
|
->line(' av_store(req, 7, newRV_noinc((SV*)newHV()));') # SLOT_COOKIES |
|
2455
|
|
|
|
|
|
|
->line(' av_store(req, 8, &PL_sv_undef);') # SLOT_JSON |
|
2456
|
|
|
|
|
|
|
->line(' av_store(req, 9, newRV_noinc((SV*)newHV()));') # SLOT_FORM |
|
2457
|
|
|
|
|
|
|
->blank; |
|
2458
|
|
|
|
|
|
|
} |
|
2459
|
|
|
|
|
|
|
|
|
2460
|
|
|
|
|
|
|
# Call handler and build response |
|
2461
|
4
|
|
|
|
|
32
|
$builder->comment('Bless array into Hypersonic::Request and call Perl handler') |
|
2462
|
|
|
|
|
|
|
->line(' req_ref = newRV_noinc((SV*)req);') |
|
2463
|
|
|
|
|
|
|
->line(' sv_bless(req_ref, gv_stashpv("Hypersonic::Request", GV_ADD));') |
|
2464
|
|
|
|
|
|
|
->line(' ENTER;') |
|
2465
|
|
|
|
|
|
|
->line(' SAVETMPS;'); |
|
2466
|
|
|
|
|
|
|
|
|
2467
|
|
|
|
|
|
|
# JIT: Add middleware short-circuit variable only if middleware present |
|
2468
|
|
|
|
|
|
|
# (mw_result and short_circuit already declared at function top) |
|
2469
|
|
|
|
|
|
|
|
|
2470
|
|
|
|
|
|
|
# JIT: Builder-based before middleware (inline C - no Perl calls) |
|
2471
|
4
|
50
|
|
|
|
16
|
if ($analysis->{has_builder_before}) { |
|
2472
|
|
|
|
|
|
|
my $ctx = { |
|
2473
|
|
|
|
|
|
|
req_var => 'req', |
|
2474
|
|
|
|
|
|
|
req_ref_var => 'req_ref', |
|
2475
|
|
|
|
|
|
|
slots => $analysis->{middleware_slots}, |
|
2476
|
0
|
|
|
|
|
0
|
}; |
|
2477
|
0
|
|
|
|
|
0
|
$builder->blank |
|
2478
|
|
|
|
|
|
|
->comment('JIT: Builder before middleware (inline C - zero Perl overhead)'); |
|
2479
|
0
|
|
|
|
|
0
|
for my $mw (@{$analysis->{builder_before}}) { |
|
|
0
|
|
|
|
|
0
|
|
|
2480
|
0
|
0
|
|
|
|
0
|
if ($mw->can('build_before')) { |
|
2481
|
0
|
|
|
|
|
0
|
$mw->build_before($builder, $ctx); |
|
2482
|
|
|
|
|
|
|
} |
|
2483
|
|
|
|
|
|
|
} |
|
2484
|
|
|
|
|
|
|
} |
|
2485
|
|
|
|
|
|
|
|
|
2486
|
|
|
|
|
|
|
# JIT: Call global before middleware (Perl coderefs via call_sv) |
|
2487
|
4
|
100
|
|
|
|
13
|
if ($analysis->{has_global_before}) { |
|
2488
|
1
|
|
|
|
|
24
|
$builder->blank |
|
2489
|
|
|
|
|
|
|
->comment('JIT: Call global before middleware (Perl)') |
|
2490
|
|
|
|
|
|
|
->line(' if (g_before_middleware && SvROK(g_before_middleware)) {') |
|
2491
|
|
|
|
|
|
|
->line(' AV* before_arr = (AV*)SvRV(g_before_middleware);') |
|
2492
|
|
|
|
|
|
|
->line(' mw_result = call_middleware_chain(aTHX_ before_arr, req_ref);') |
|
2493
|
|
|
|
|
|
|
->line(' if (mw_result) {') |
|
2494
|
|
|
|
|
|
|
->line(' result = mw_result;') |
|
2495
|
|
|
|
|
|
|
->line(' short_circuit = 1;') |
|
2496
|
|
|
|
|
|
|
->line(' }') |
|
2497
|
|
|
|
|
|
|
->line(' }'); |
|
2498
|
|
|
|
|
|
|
} |
|
2499
|
|
|
|
|
|
|
|
|
2500
|
|
|
|
|
|
|
# JIT: Call per-route before middleware |
|
2501
|
4
|
50
|
|
|
|
19
|
if ($analysis->{has_route_middleware}) { |
|
2502
|
0
|
|
|
|
|
0
|
$builder->blank |
|
2503
|
|
|
|
|
|
|
->comment('JIT: Call per-route before middleware') |
|
2504
|
|
|
|
|
|
|
->line(' if (!short_circuit && g_route_before_middleware && SvROK(g_route_before_middleware)) {') |
|
2505
|
|
|
|
|
|
|
->line(' AV* route_before = (AV*)SvRV(g_route_before_middleware);') |
|
2506
|
|
|
|
|
|
|
->line(' SV** handler_arr_ref = av_fetch(route_before, handler_idx, 0);') |
|
2507
|
|
|
|
|
|
|
->line(' if (handler_arr_ref && SvROK(*handler_arr_ref)) {') |
|
2508
|
|
|
|
|
|
|
->line(' AV* handler_arr = (AV*)SvRV(*handler_arr_ref);') |
|
2509
|
|
|
|
|
|
|
->line(' if (av_len(handler_arr) >= 0) {') |
|
2510
|
|
|
|
|
|
|
->line(' mw_result = call_middleware_chain(aTHX_ handler_arr, req_ref);') |
|
2511
|
|
|
|
|
|
|
->line(' if (mw_result) {') |
|
2512
|
|
|
|
|
|
|
->line(' result = mw_result;') |
|
2513
|
|
|
|
|
|
|
->line(' short_circuit = 1;') |
|
2514
|
|
|
|
|
|
|
->line(' }') |
|
2515
|
|
|
|
|
|
|
->line(' }') |
|
2516
|
|
|
|
|
|
|
->line(' }') |
|
2517
|
|
|
|
|
|
|
->line(' }'); |
|
2518
|
|
|
|
|
|
|
} |
|
2519
|
|
|
|
|
|
|
|
|
2520
|
|
|
|
|
|
|
# JIT: Streaming handler support |
|
2521
|
4
|
100
|
|
|
|
19
|
if ($analysis->{needs_streaming}) { |
|
2522
|
2
|
|
|
|
|
47
|
$builder->blank |
|
2523
|
|
|
|
|
|
|
->comment('JIT: Check if this is a streaming handler') |
|
2524
|
|
|
|
|
|
|
->line(' is_streaming = g_streaming_handlers[handler_idx];') |
|
2525
|
|
|
|
|
|
|
->if('is_streaming') |
|
2526
|
|
|
|
|
|
|
->comment('Create Hypersonic::Stream object for streaming handler') |
|
2527
|
|
|
|
|
|
|
->line('dSP;') |
|
2528
|
|
|
|
|
|
|
->line('PUSHMARK(SP);') |
|
2529
|
|
|
|
|
|
|
->line('XPUSHs(sv_2mortal(newSVpv("Hypersonic::Stream", 0)));') |
|
2530
|
|
|
|
|
|
|
->line('XPUSHs(sv_2mortal(newSVpv("fd", 0)));') |
|
2531
|
|
|
|
|
|
|
->line('XPUSHs(sv_2mortal(newSViv(client_fd)));') |
|
2532
|
|
|
|
|
|
|
->line('PUTBACK;') |
|
2533
|
|
|
|
|
|
|
->line('int stream_count = call_method("new", G_SCALAR);') |
|
2534
|
|
|
|
|
|
|
->line('SPAGAIN;') |
|
2535
|
|
|
|
|
|
|
->if('stream_count > 0') |
|
2536
|
|
|
|
|
|
|
->line('stream_sv = POPs;') |
|
2537
|
|
|
|
|
|
|
->line('SvREFCNT_inc(stream_sv);') |
|
2538
|
|
|
|
|
|
|
->endif |
|
2539
|
|
|
|
|
|
|
->line('PUTBACK;') |
|
2540
|
|
|
|
|
|
|
->endif; |
|
2541
|
|
|
|
|
|
|
} |
|
2542
|
|
|
|
|
|
|
|
|
2543
|
|
|
|
|
|
|
# Call the main handler (conditionally if middleware present) |
|
2544
|
4
|
100
|
|
|
|
82
|
if ($analysis->{has_any_middleware}) { |
|
2545
|
1
|
|
|
|
|
7
|
$builder->blank |
|
2546
|
|
|
|
|
|
|
->comment('Call main handler (unless middleware short-circuited)') |
|
2547
|
|
|
|
|
|
|
->line(' if (!short_circuit) {') |
|
2548
|
|
|
|
|
|
|
->line(' PUSHMARK(SP);') |
|
2549
|
|
|
|
|
|
|
->line(' XPUSHs(req_ref);'); |
|
2550
|
|
|
|
|
|
|
|
|
2551
|
|
|
|
|
|
|
# For streaming handlers with middleware |
|
2552
|
1
|
50
|
|
|
|
2
|
if ($analysis->{needs_streaming}) { |
|
2553
|
0
|
|
|
|
|
0
|
$builder->line(' if (is_streaming && stream_sv) XPUSHs(stream_sv);'); |
|
2554
|
|
|
|
|
|
|
} |
|
2555
|
|
|
|
|
|
|
|
|
2556
|
1
|
|
|
|
|
7
|
$builder->line(' PUTBACK;') |
|
2557
|
|
|
|
|
|
|
->line(' count = call_sv(*handler_sv, G_SCALAR | G_EVAL);') |
|
2558
|
|
|
|
|
|
|
->line(' SPAGAIN;') |
|
2559
|
|
|
|
|
|
|
->line(' if (count == 1) result = POPs;') |
|
2560
|
|
|
|
|
|
|
->line(' PUTBACK;') |
|
2561
|
|
|
|
|
|
|
->line(' }'); |
|
2562
|
|
|
|
|
|
|
} else { |
|
2563
|
3
|
|
|
|
|
20
|
$builder->line(' PUSHMARK(SP);') |
|
2564
|
|
|
|
|
|
|
->line(' XPUSHs(sv_2mortal(req_ref));'); |
|
2565
|
|
|
|
|
|
|
|
|
2566
|
|
|
|
|
|
|
# For streaming handlers without middleware |
|
2567
|
3
|
100
|
|
|
|
9
|
if ($analysis->{needs_streaming}) { |
|
2568
|
2
|
|
|
|
|
5
|
$builder->line(' if (is_streaming && stream_sv) XPUSHs(stream_sv);'); |
|
2569
|
|
|
|
|
|
|
} |
|
2570
|
|
|
|
|
|
|
|
|
2571
|
3
|
|
|
|
|
78
|
$builder->line(' PUTBACK;') |
|
2572
|
|
|
|
|
|
|
->line(' count = call_sv(*handler_sv, G_SCALAR | G_EVAL);') |
|
2573
|
|
|
|
|
|
|
->line(' SPAGAIN;'); |
|
2574
|
|
|
|
|
|
|
} |
|
2575
|
|
|
|
|
|
|
|
|
2576
|
|
|
|
|
|
|
# JIT: Call per-route after middleware |
|
2577
|
4
|
50
|
|
|
|
17
|
if ($analysis->{has_route_middleware}) { |
|
2578
|
0
|
|
|
|
|
0
|
$builder->blank |
|
2579
|
|
|
|
|
|
|
->comment('JIT: Call per-route after middleware') |
|
2580
|
|
|
|
|
|
|
->line(' if (g_route_after_middleware && SvROK(g_route_after_middleware)) {') |
|
2581
|
|
|
|
|
|
|
->line(' AV* route_after = (AV*)SvRV(g_route_after_middleware);') |
|
2582
|
|
|
|
|
|
|
->line(' SV** handler_arr_ref = av_fetch(route_after, handler_idx, 0);') |
|
2583
|
|
|
|
|
|
|
->line(' if (handler_arr_ref && SvROK(*handler_arr_ref)) {') |
|
2584
|
|
|
|
|
|
|
->line(' AV* handler_arr = (AV*)SvRV(*handler_arr_ref);') |
|
2585
|
|
|
|
|
|
|
->line(' if (av_len(handler_arr) >= 0) {') |
|
2586
|
|
|
|
|
|
|
->line(' SV* after_result = call_middleware_chain(aTHX_ handler_arr, req_ref);') |
|
2587
|
|
|
|
|
|
|
->line(' if (after_result) {') |
|
2588
|
|
|
|
|
|
|
->line(' result = after_result;') |
|
2589
|
|
|
|
|
|
|
->line(' }') |
|
2590
|
|
|
|
|
|
|
->line(' }') |
|
2591
|
|
|
|
|
|
|
->line(' }') |
|
2592
|
|
|
|
|
|
|
->line(' }'); |
|
2593
|
|
|
|
|
|
|
} |
|
2594
|
|
|
|
|
|
|
|
|
2595
|
|
|
|
|
|
|
# JIT: Call global after middleware (Perl coderefs via call_sv) |
|
2596
|
4
|
100
|
|
|
|
14
|
if ($analysis->{has_global_after}) { |
|
2597
|
1
|
|
|
|
|
9
|
$builder->blank |
|
2598
|
|
|
|
|
|
|
->comment('JIT: Call global after middleware (Perl)') |
|
2599
|
|
|
|
|
|
|
->line(' if (g_after_middleware && SvROK(g_after_middleware)) {') |
|
2600
|
|
|
|
|
|
|
->line(' AV* after_arr = (AV*)SvRV(g_after_middleware);') |
|
2601
|
|
|
|
|
|
|
->line(' SV* after_result = call_middleware_chain(aTHX_ after_arr, req_ref);') |
|
2602
|
|
|
|
|
|
|
->line(' if (after_result) {') |
|
2603
|
|
|
|
|
|
|
->line(' result = after_result;') |
|
2604
|
|
|
|
|
|
|
->line(' }') |
|
2605
|
|
|
|
|
|
|
->line(' }'); |
|
2606
|
|
|
|
|
|
|
} |
|
2607
|
|
|
|
|
|
|
|
|
2608
|
|
|
|
|
|
|
# JIT: Builder-based after middleware (inline C - no Perl calls) |
|
2609
|
4
|
50
|
|
|
|
43
|
if ($analysis->{has_builder_after}) { |
|
2610
|
|
|
|
|
|
|
my $ctx = { |
|
2611
|
|
|
|
|
|
|
req_var => 'req', |
|
2612
|
|
|
|
|
|
|
req_ref_var => 'req_ref', |
|
2613
|
|
|
|
|
|
|
res_var => 'result', |
|
2614
|
|
|
|
|
|
|
slots => $analysis->{middleware_slots}, |
|
2615
|
0
|
|
|
|
|
0
|
}; |
|
2616
|
0
|
|
|
|
|
0
|
$builder->blank |
|
2617
|
|
|
|
|
|
|
->comment('JIT: Builder after middleware (inline C - zero Perl overhead)'); |
|
2618
|
0
|
|
|
|
|
0
|
for my $mw (@{$analysis->{builder_after}}) { |
|
|
0
|
|
|
|
|
0
|
|
|
2619
|
0
|
0
|
|
|
|
0
|
if ($mw->can('build_after')) { |
|
2620
|
0
|
|
|
|
|
0
|
$mw->build_after($builder, $ctx); |
|
2621
|
|
|
|
|
|
|
} |
|
2622
|
|
|
|
|
|
|
} |
|
2623
|
|
|
|
|
|
|
} |
|
2624
|
|
|
|
|
|
|
|
|
2625
|
|
|
|
|
|
|
# JIT: Streaming handlers - early return (response already sent via Stream) |
|
2626
|
4
|
100
|
|
|
|
17
|
if ($analysis->{needs_streaming}) { |
|
2627
|
2
|
|
|
|
|
99
|
$builder->blank |
|
2628
|
|
|
|
|
|
|
->comment('JIT: Streaming handlers return early - response sent via Stream object') |
|
2629
|
|
|
|
|
|
|
->if('is_streaming') |
|
2630
|
|
|
|
|
|
|
->line('if (stream_sv) SvREFCNT_dec(stream_sv);') |
|
2631
|
|
|
|
|
|
|
->line('FREETMPS;') |
|
2632
|
|
|
|
|
|
|
->line('LEAVE;') |
|
2633
|
|
|
|
|
|
|
->line('*resp_out = NULL;') |
|
2634
|
|
|
|
|
|
|
->line('*resp_len_out = -1;') |
|
2635
|
|
|
|
|
|
|
->comment('Signal streaming response - caller should not send') |
|
2636
|
|
|
|
|
|
|
->line('return;') |
|
2637
|
|
|
|
|
|
|
->endif; |
|
2638
|
|
|
|
|
|
|
} |
|
2639
|
|
|
|
|
|
|
|
|
2640
|
|
|
|
|
|
|
$builder->blank |
|
2641
|
4
|
|
|
|
|
93
|
->line(' if (SvTRUE(ERRSV)) {') |
|
2642
|
|
|
|
|
|
|
->line(' static char error_resp[512];') |
|
2643
|
|
|
|
|
|
|
->line(' int err_len = snprintf(error_resp, sizeof(error_resp),') |
|
2644
|
|
|
|
|
|
|
->line(' "HTTP/1.1 500 Internal Server Error\\r\\n"') |
|
2645
|
|
|
|
|
|
|
->line(' "Content-Type: text/plain\\r\\n"') |
|
2646
|
|
|
|
|
|
|
->line(' "Content-Length: 21\\r\\n"') |
|
2647
|
|
|
|
|
|
|
->line(' "Connection: close\\r\\n\\r\\n"') |
|
2648
|
|
|
|
|
|
|
->line(' "Internal Server Error");') |
|
2649
|
|
|
|
|
|
|
->line(' *resp_out = error_resp;') |
|
2650
|
|
|
|
|
|
|
->line(' *resp_len_out = err_len;'); |
|
2651
|
|
|
|
|
|
|
|
|
2652
|
|
|
|
|
|
|
# Middleware version already has result set, no-middleware version needs POPs |
|
2653
|
4
|
100
|
|
|
|
15
|
if ($analysis->{has_any_middleware}) { |
|
2654
|
1
|
|
|
|
|
6
|
$builder->line(' } else if (SvOK(result)) {') |
|
2655
|
|
|
|
|
|
|
->line(' HV* custom_headers = NULL;'); |
|
2656
|
|
|
|
|
|
|
} else { |
|
2657
|
3
|
|
|
|
|
35
|
$builder->line(' POPs;') |
|
2658
|
|
|
|
|
|
|
->line(' } else if (count == 1) {') |
|
2659
|
|
|
|
|
|
|
->line(' result = POPs;') |
|
2660
|
|
|
|
|
|
|
->line(' if (SvOK(result)) {') |
|
2661
|
|
|
|
|
|
|
->line(' HV* custom_headers = NULL;'); |
|
2662
|
|
|
|
|
|
|
} |
|
2663
|
|
|
|
|
|
|
|
|
2664
|
4
|
|
|
|
|
326
|
$builder |
|
2665
|
|
|
|
|
|
|
->comment(' Handle arrayref [status, headers, body]') |
|
2666
|
|
|
|
|
|
|
->line(' if (SvROK(result) && SvTYPE(SvRV(result)) == SVt_PVAV) {') |
|
2667
|
|
|
|
|
|
|
->line(' AV* arr = (AV*)SvRV(result);') |
|
2668
|
|
|
|
|
|
|
->line(' SV** status_sv = av_fetch(arr, 0, 0);') |
|
2669
|
|
|
|
|
|
|
->line(' SV** headers_sv = av_fetch(arr, 1, 0);') |
|
2670
|
|
|
|
|
|
|
->line(' SV** body_sv = av_fetch(arr, 2, 0);') |
|
2671
|
|
|
|
|
|
|
->line(' if (status_sv) status = (int)SvIV(*status_sv);') |
|
2672
|
|
|
|
|
|
|
->line(' if (body_sv) body_str = SvPV(*body_sv, len);') |
|
2673
|
|
|
|
|
|
|
->line(' else { body_str = ""; len = 0; }') |
|
2674
|
|
|
|
|
|
|
->line(' if (headers_sv && SvROK(*headers_sv) && SvTYPE(SvRV(*headers_sv)) == SVt_PVHV) {') |
|
2675
|
|
|
|
|
|
|
->line(' custom_headers = (HV*)SvRV(*headers_sv);') |
|
2676
|
|
|
|
|
|
|
->line(' SV** ct_sv = hv_fetch(custom_headers, "Content-Type", 12, 0);') |
|
2677
|
|
|
|
|
|
|
->line(' if (ct_sv && SvOK(*ct_sv)) {') |
|
2678
|
|
|
|
|
|
|
->line(' STRLEN ct_len;') |
|
2679
|
|
|
|
|
|
|
->line(' content_type = SvPV(*ct_sv, ct_len);') |
|
2680
|
|
|
|
|
|
|
->line(' }') |
|
2681
|
|
|
|
|
|
|
->line(' }') |
|
2682
|
|
|
|
|
|
|
->line(' }') |
|
2683
|
|
|
|
|
|
|
->comment(' Handle hashref {status, headers, body}') |
|
2684
|
|
|
|
|
|
|
->line(' else if (SvROK(result) && SvTYPE(SvRV(result)) == SVt_PVHV) {') |
|
2685
|
|
|
|
|
|
|
->line(' HV* hash = (HV*)SvRV(result);') |
|
2686
|
|
|
|
|
|
|
->line(' SV** status_sv = hv_fetch(hash, "status", 6, 0);') |
|
2687
|
|
|
|
|
|
|
->line(' SV** headers_sv = hv_fetch(hash, "headers", 7, 0);') |
|
2688
|
|
|
|
|
|
|
->line(' SV** body_sv = hv_fetch(hash, "body", 4, 0);') |
|
2689
|
|
|
|
|
|
|
->line(' if (status_sv) status = (int)SvIV(*status_sv);') |
|
2690
|
|
|
|
|
|
|
->line(' if (body_sv) body_str = SvPV(*body_sv, len);') |
|
2691
|
|
|
|
|
|
|
->line(' else { body_str = ""; len = 0; }') |
|
2692
|
|
|
|
|
|
|
->line(' if (headers_sv && SvROK(*headers_sv) && SvTYPE(SvRV(*headers_sv)) == SVt_PVHV) {') |
|
2693
|
|
|
|
|
|
|
->line(' custom_headers = (HV*)SvRV(*headers_sv);') |
|
2694
|
|
|
|
|
|
|
->line(' SV** ct_sv = hv_fetch(custom_headers, "Content-Type", 12, 0);') |
|
2695
|
|
|
|
|
|
|
->line(' if (ct_sv && SvOK(*ct_sv)) {') |
|
2696
|
|
|
|
|
|
|
->line(' STRLEN ct_len;') |
|
2697
|
|
|
|
|
|
|
->line(' content_type = SvPV(*ct_sv, ct_len);') |
|
2698
|
|
|
|
|
|
|
->line(' }') |
|
2699
|
|
|
|
|
|
|
->line(' }') |
|
2700
|
|
|
|
|
|
|
->line(' }') |
|
2701
|
|
|
|
|
|
|
->comment(' Plain string response') |
|
2702
|
|
|
|
|
|
|
->line(' else {') |
|
2703
|
|
|
|
|
|
|
->line(' body_str = SvPV(result, len);') |
|
2704
|
|
|
|
|
|
|
->line(' }') |
|
2705
|
|
|
|
|
|
|
->blank |
|
2706
|
|
|
|
|
|
|
->comment(' Auto-detect JSON content type') |
|
2707
|
|
|
|
|
|
|
->line(' if (strcmp(content_type, "text/plain") == 0 && len > 0 &&') |
|
2708
|
|
|
|
|
|
|
->line(' (body_str[0] == \'{\' || body_str[0] == \'[\')) {') |
|
2709
|
|
|
|
|
|
|
->line(' content_type = "application/json";') |
|
2710
|
|
|
|
|
|
|
->line(' }') |
|
2711
|
|
|
|
|
|
|
->blank; |
|
2712
|
|
|
|
|
|
|
|
|
2713
|
|
|
|
|
|
|
# JIT: Add compression logic only if compression is enabled |
|
2714
|
4
|
50
|
|
|
|
17
|
if ($self->{_compression_enabled}) { |
|
2715
|
0
|
|
|
|
|
0
|
my $config = $self->{_compression_config}; |
|
2716
|
0
|
|
0
|
|
|
0
|
my $min_size = $config->{min_size} // 1024; |
|
2717
|
|
|
|
|
|
|
|
|
2718
|
0
|
|
|
|
|
0
|
$builder |
|
2719
|
|
|
|
|
|
|
->comment(' Gzip compression - check Accept-Encoding') |
|
2720
|
|
|
|
|
|
|
->line('#ifdef HYPERSONIC_COMPRESSION') |
|
2721
|
|
|
|
|
|
|
->line(' int use_gzip = 0;') |
|
2722
|
|
|
|
|
|
|
->line(' unsigned char* compressed_body = NULL;') |
|
2723
|
|
|
|
|
|
|
->line(' size_t compressed_len = 0;') |
|
2724
|
|
|
|
|
|
|
->blank |
|
2725
|
|
|
|
|
|
|
->comment(' Get Accept-Encoding from request (from SLOT_HEADERS)') |
|
2726
|
|
|
|
|
|
|
->line(' SV** req_arr = AvARRAY(req);') |
|
2727
|
|
|
|
|
|
|
->line(' HV* hdrs = NULL;') |
|
2728
|
|
|
|
|
|
|
->line(' if (req_arr[6] && SvROK(req_arr[6])) {') |
|
2729
|
|
|
|
|
|
|
->line(' hdrs = (HV*)SvRV(req_arr[6]);') |
|
2730
|
|
|
|
|
|
|
->line(' }') |
|
2731
|
|
|
|
|
|
|
->line(' if (hdrs && len >= ' . $min_size . ') {') |
|
2732
|
|
|
|
|
|
|
->line(' SV** ae = hv_fetch(hdrs, "accept_encoding", 15, 0);') |
|
2733
|
|
|
|
|
|
|
->line(' if (ae && SvOK(*ae)) {') |
|
2734
|
|
|
|
|
|
|
->line(' STRLEN ae_len;') |
|
2735
|
|
|
|
|
|
|
->line(' const char* ae_str = SvPV(*ae, ae_len);') |
|
2736
|
|
|
|
|
|
|
->line(' if (accepts_gzip(ae_str, ae_len)) {') |
|
2737
|
|
|
|
|
|
|
->line(' compressed_len = gzip_compress(body_str, len, &compressed_body);') |
|
2738
|
|
|
|
|
|
|
->line(' if (compressed_len > 0) {') |
|
2739
|
|
|
|
|
|
|
->line(' use_gzip = 1;') |
|
2740
|
|
|
|
|
|
|
->line(' body_str = (const char*)compressed_body;') |
|
2741
|
|
|
|
|
|
|
->line(' len = compressed_len;') |
|
2742
|
|
|
|
|
|
|
->line(' }') |
|
2743
|
|
|
|
|
|
|
->line(' }') |
|
2744
|
|
|
|
|
|
|
->line(' }') |
|
2745
|
|
|
|
|
|
|
->line(' }') |
|
2746
|
|
|
|
|
|
|
->line('#endif') |
|
2747
|
|
|
|
|
|
|
->blank; |
|
2748
|
|
|
|
|
|
|
} |
|
2749
|
|
|
|
|
|
|
|
|
2750
|
|
|
|
|
|
|
$builder |
|
2751
|
4
|
|
|
|
|
456
|
->comment(' Build response with custom headers support') |
|
2752
|
|
|
|
|
|
|
->line(' static __thread char resp_buf[65536];') |
|
2753
|
|
|
|
|
|
|
->line(' int hdr_len;') |
|
2754
|
|
|
|
|
|
|
->line('#ifdef HYPERSONIC_SECURITY_HEADERS') |
|
2755
|
|
|
|
|
|
|
->line(' hdr_len = snprintf(resp_buf, 2048,') |
|
2756
|
|
|
|
|
|
|
->line(' "HTTP/1.1 %d %s\\r\\n"') |
|
2757
|
|
|
|
|
|
|
->line(' "Content-Type: %s\\r\\n"') |
|
2758
|
|
|
|
|
|
|
->line(' "Content-Length: %zu\\r\\n"') |
|
2759
|
|
|
|
|
|
|
->line(' "Connection: keep-alive\\r\\n"') |
|
2760
|
|
|
|
|
|
|
->line(' "%s",') |
|
2761
|
|
|
|
|
|
|
->line(' status, get_status_text(status), content_type, len, SECURITY_HEADERS);') |
|
2762
|
|
|
|
|
|
|
->line('#else') |
|
2763
|
|
|
|
|
|
|
->line(' hdr_len = snprintf(resp_buf, 512,') |
|
2764
|
|
|
|
|
|
|
->line(' "HTTP/1.1 %d %s\\r\\n"') |
|
2765
|
|
|
|
|
|
|
->line(' "Content-Type: %s\\r\\n"') |
|
2766
|
|
|
|
|
|
|
->line(' "Content-Length: %zu\\r\\n"') |
|
2767
|
|
|
|
|
|
|
->line(' "Connection: keep-alive\\r\\n",') |
|
2768
|
|
|
|
|
|
|
->line(' status, get_status_text(status), content_type, len);') |
|
2769
|
|
|
|
|
|
|
->line('#endif') |
|
2770
|
|
|
|
|
|
|
->blank |
|
2771
|
|
|
|
|
|
|
->comment(' Add custom headers from response (Location, Set-Cookie, etc.)') |
|
2772
|
|
|
|
|
|
|
->line(' if (custom_headers) {') |
|
2773
|
|
|
|
|
|
|
->line(' HE* entry;') |
|
2774
|
|
|
|
|
|
|
->line(' hv_iterinit(custom_headers);') |
|
2775
|
|
|
|
|
|
|
->line(' while ((entry = hv_iternext(custom_headers))) {') |
|
2776
|
|
|
|
|
|
|
->line(' I32 klen;') |
|
2777
|
|
|
|
|
|
|
->line(' const char* key = hv_iterkey(entry, &klen);') |
|
2778
|
|
|
|
|
|
|
->comment(' Skip Content-Type/Content-Length (already added)') |
|
2779
|
|
|
|
|
|
|
->line(' if (klen == 12 && memcmp(key, "Content-Type", 12) == 0) continue;') |
|
2780
|
|
|
|
|
|
|
->line(' if (klen == 14 && memcmp(key, "Content-Length", 14) == 0) continue;') |
|
2781
|
|
|
|
|
|
|
->line(' SV* val = hv_iterval(custom_headers, entry);') |
|
2782
|
|
|
|
|
|
|
->comment(' Handle Set-Cookie array (multiple cookies)') |
|
2783
|
|
|
|
|
|
|
->line(' if (SvROK(val) && SvTYPE(SvRV(val)) == SVt_PVAV) {') |
|
2784
|
|
|
|
|
|
|
->line(' AV* arr = (AV*)SvRV(val);') |
|
2785
|
|
|
|
|
|
|
->line(' SSize_t arr_len = av_len(arr) + 1;') |
|
2786
|
|
|
|
|
|
|
->line(' SSize_t j;') |
|
2787
|
|
|
|
|
|
|
->line(' for (j = 0; j < arr_len; j++) {') |
|
2788
|
|
|
|
|
|
|
->line(' SV** item = av_fetch(arr, j, 0);') |
|
2789
|
|
|
|
|
|
|
->line(' if (item && SvOK(*item)) {') |
|
2790
|
|
|
|
|
|
|
->line(' STRLEN vlen;') |
|
2791
|
|
|
|
|
|
|
->line(' const char* vstr = SvPV(*item, vlen);') |
|
2792
|
|
|
|
|
|
|
->line(' hdr_len += snprintf(resp_buf + hdr_len, sizeof(resp_buf) - hdr_len,') |
|
2793
|
|
|
|
|
|
|
->line(' "%.*s: %.*s\\r\\n", (int)klen, key, (int)vlen, vstr);') |
|
2794
|
|
|
|
|
|
|
->line(' }') |
|
2795
|
|
|
|
|
|
|
->line(' }') |
|
2796
|
|
|
|
|
|
|
->line(' } else if (SvOK(val)) {') |
|
2797
|
|
|
|
|
|
|
->line(' STRLEN vlen;') |
|
2798
|
|
|
|
|
|
|
->line(' const char* vstr = SvPV(val, vlen);') |
|
2799
|
|
|
|
|
|
|
->line(' hdr_len += snprintf(resp_buf + hdr_len, sizeof(resp_buf) - hdr_len,') |
|
2800
|
|
|
|
|
|
|
->line(' "%.*s: %.*s\\r\\n", (int)klen, key, (int)vlen, vstr);') |
|
2801
|
|
|
|
|
|
|
->line(' }') |
|
2802
|
|
|
|
|
|
|
->line(' }') |
|
2803
|
|
|
|
|
|
|
->line(' }') |
|
2804
|
|
|
|
|
|
|
->blank |
|
2805
|
|
|
|
|
|
|
->comment(' Add Content-Encoding header if gzip was used') |
|
2806
|
|
|
|
|
|
|
->line('#ifdef HYPERSONIC_COMPRESSION') |
|
2807
|
|
|
|
|
|
|
->line(' if (use_gzip) {') |
|
2808
|
|
|
|
|
|
|
->line(' hdr_len += snprintf(resp_buf + hdr_len, sizeof(resp_buf) - hdr_len,') |
|
2809
|
|
|
|
|
|
|
->line(' "Content-Encoding: gzip\\r\\n");') |
|
2810
|
|
|
|
|
|
|
->line(' }') |
|
2811
|
|
|
|
|
|
|
->line('#endif') |
|
2812
|
|
|
|
|
|
|
->blank |
|
2813
|
|
|
|
|
|
|
->comment(' End headers') |
|
2814
|
|
|
|
|
|
|
->line(' memcpy(resp_buf + hdr_len, "\\r\\n", 2);') |
|
2815
|
|
|
|
|
|
|
->line(' hdr_len += 2;') |
|
2816
|
|
|
|
|
|
|
->blank |
|
2817
|
|
|
|
|
|
|
->line(' if (hdr_len + len < sizeof(resp_buf)) {') |
|
2818
|
|
|
|
|
|
|
->line(' memcpy(resp_buf + hdr_len, body_str, len);') |
|
2819
|
|
|
|
|
|
|
->line(' *resp_out = resp_buf;') |
|
2820
|
|
|
|
|
|
|
->line(' *resp_len_out = hdr_len + (int)len;') |
|
2821
|
|
|
|
|
|
|
->line(' } else {') |
|
2822
|
|
|
|
|
|
|
->line(' *resp_out = (char*)RESP_404;') |
|
2823
|
|
|
|
|
|
|
->line(' *resp_len_out = RESP_404_LEN;') |
|
2824
|
|
|
|
|
|
|
->line(' }'); |
|
2825
|
|
|
|
|
|
|
|
|
2826
|
|
|
|
|
|
|
# Different closing braces based on middleware presence |
|
2827
|
4
|
100
|
|
|
|
24
|
if ($analysis->{has_any_middleware}) { |
|
2828
|
|
|
|
|
|
|
# Middleware: } else if (SvOK(result)) { ... } else { 404 } |
|
2829
|
1
|
|
|
|
|
4
|
$builder->line(' } else {') |
|
2830
|
|
|
|
|
|
|
->line(' *resp_out = (char*)RESP_404;') |
|
2831
|
|
|
|
|
|
|
->line(' *resp_len_out = RESP_404_LEN;') |
|
2832
|
|
|
|
|
|
|
->line(' }'); |
|
2833
|
|
|
|
|
|
|
} else { |
|
2834
|
|
|
|
|
|
|
# No middleware: } else if (count == 1) { result = POPs; if (SvOK(result)) { ... } } else { 404 } |
|
2835
|
3
|
|
|
|
|
34
|
$builder->line(' } else {') |
|
2836
|
|
|
|
|
|
|
->line(' *resp_out = (char*)RESP_404;') |
|
2837
|
|
|
|
|
|
|
->line(' *resp_len_out = RESP_404_LEN;') |
|
2838
|
|
|
|
|
|
|
->line(' }') |
|
2839
|
|
|
|
|
|
|
->line(' } else {') |
|
2840
|
|
|
|
|
|
|
->line(' *resp_out = (char*)RESP_404;') |
|
2841
|
|
|
|
|
|
|
->line(' *resp_len_out = RESP_404_LEN;') |
|
2842
|
|
|
|
|
|
|
->line(' }'); |
|
2843
|
|
|
|
|
|
|
} |
|
2844
|
|
|
|
|
|
|
|
|
2845
|
4
|
|
|
|
|
42
|
$builder->blank |
|
2846
|
|
|
|
|
|
|
->line(' PUTBACK;') |
|
2847
|
|
|
|
|
|
|
->line(' FREETMPS;') |
|
2848
|
|
|
|
|
|
|
->line(' LEAVE;') |
|
2849
|
|
|
|
|
|
|
->line('}'); |
|
2850
|
|
|
|
|
|
|
|
|
2851
|
4
|
|
|
|
|
299
|
return $builder->code; |
|
2852
|
|
|
|
|
|
|
} |
|
2853
|
|
|
|
|
|
|
|
|
2854
|
|
|
|
|
|
|
# JSON decoder helper - called from C via call_pv |
|
2855
|
|
|
|
|
|
|
sub _decode_json { |
|
2856
|
0
|
|
|
0
|
|
0
|
my ($json_str) = @_; |
|
2857
|
0
|
|
|
|
|
0
|
require Cpanel::JSON::XS; |
|
2858
|
0
|
|
|
|
|
0
|
return Cpanel::JSON::XS::decode_json($json_str); |
|
2859
|
|
|
|
|
|
|
} |
|
2860
|
|
|
|
|
|
|
|
|
2861
|
|
|
|
|
|
|
# Generate WebSocket handler caller function |
|
2862
|
|
|
|
|
|
|
sub _gen_websocket_handler_caller { |
|
2863
|
0
|
|
|
0
|
|
0
|
my ($self) = @_; |
|
2864
|
|
|
|
|
|
|
|
|
2865
|
0
|
|
|
|
|
0
|
my $builder = XS::JIT::Builder->new; |
|
2866
|
|
|
|
|
|
|
|
|
2867
|
0
|
|
|
|
|
0
|
$builder->comment('WebSocket handler caller - performs handshake and calls Perl handler') |
|
2868
|
|
|
|
|
|
|
->line('static void call_websocket_handler(pTHX_ int handler_idx, int fd,') |
|
2869
|
|
|
|
|
|
|
->line(' const char* path, int path_len,') |
|
2870
|
|
|
|
|
|
|
->line(' const char* ws_key, int ws_key_len,') |
|
2871
|
|
|
|
|
|
|
->line(' const char* raw_request, int raw_request_len) {') |
|
2872
|
|
|
|
|
|
|
->line(' dSP;') |
|
2873
|
|
|
|
|
|
|
->blank |
|
2874
|
|
|
|
|
|
|
->if('!g_websocket_handlers || !SvROK(g_websocket_handlers)') |
|
2875
|
|
|
|
|
|
|
->line('return;') |
|
2876
|
|
|
|
|
|
|
->endif |
|
2877
|
|
|
|
|
|
|
->blank |
|
2878
|
|
|
|
|
|
|
->line('AV* handlers = (AV*)SvRV(g_websocket_handlers);') |
|
2879
|
|
|
|
|
|
|
->line('SV** handler_sv = av_fetch(handlers, handler_idx, 0);') |
|
2880
|
|
|
|
|
|
|
->if('!handler_sv || !SvROK(*handler_sv)') |
|
2881
|
|
|
|
|
|
|
->line('return;') |
|
2882
|
|
|
|
|
|
|
->endif |
|
2883
|
|
|
|
|
|
|
->blank |
|
2884
|
|
|
|
|
|
|
->comment('Generate WebSocket accept key') |
|
2885
|
|
|
|
|
|
|
->line('ENTER;') |
|
2886
|
|
|
|
|
|
|
->line('SAVETMPS;') |
|
2887
|
|
|
|
|
|
|
->blank |
|
2888
|
|
|
|
|
|
|
->comment('Build handshake response via Protocol::WebSocket') |
|
2889
|
|
|
|
|
|
|
->line('PUSHMARK(SP);') |
|
2890
|
|
|
|
|
|
|
->line('XPUSHs(sv_2mortal(newSVpvs("Hypersonic::Protocol::WebSocket")));') |
|
2891
|
|
|
|
|
|
|
->line('XPUSHs(sv_2mortal(newSVpvs("key")));') |
|
2892
|
|
|
|
|
|
|
->line('XPUSHs(sv_2mortal(newSVpvn(ws_key, ws_key_len)));') |
|
2893
|
|
|
|
|
|
|
->line('PUTBACK;') |
|
2894
|
|
|
|
|
|
|
->line('int count = call_method("build_response", G_SCALAR);') |
|
2895
|
|
|
|
|
|
|
->line('SPAGAIN;') |
|
2896
|
|
|
|
|
|
|
->blank |
|
2897
|
|
|
|
|
|
|
->if('count != 1') |
|
2898
|
|
|
|
|
|
|
->line('FREETMPS;') |
|
2899
|
|
|
|
|
|
|
->line('LEAVE;') |
|
2900
|
|
|
|
|
|
|
->line('return;') |
|
2901
|
|
|
|
|
|
|
->endif |
|
2902
|
|
|
|
|
|
|
->blank |
|
2903
|
|
|
|
|
|
|
->line('SV* response_sv = POPs;') |
|
2904
|
|
|
|
|
|
|
->line('STRLEN resp_len;') |
|
2905
|
|
|
|
|
|
|
->line('const char* response = SvPV(response_sv, resp_len);') |
|
2906
|
|
|
|
|
|
|
->blank |
|
2907
|
|
|
|
|
|
|
->comment('Send WebSocket handshake response') |
|
2908
|
|
|
|
|
|
|
->line('send(fd, response, resp_len, 0);') |
|
2909
|
|
|
|
|
|
|
->blank |
|
2910
|
|
|
|
|
|
|
->comment('Create Stream object for WebSocket') |
|
2911
|
|
|
|
|
|
|
->line('PUSHMARK(SP);') |
|
2912
|
|
|
|
|
|
|
->line('XPUSHs(sv_2mortal(newSVpvs("Hypersonic::Stream")));') |
|
2913
|
|
|
|
|
|
|
->line('XPUSHs(sv_2mortal(newSVpvs("fd")));') |
|
2914
|
|
|
|
|
|
|
->line('XPUSHs(sv_2mortal(newSViv(fd)));') |
|
2915
|
|
|
|
|
|
|
->line('PUTBACK;') |
|
2916
|
|
|
|
|
|
|
->line('count = call_method("new", G_SCALAR);') |
|
2917
|
|
|
|
|
|
|
->line('SPAGAIN;') |
|
2918
|
|
|
|
|
|
|
->blank |
|
2919
|
|
|
|
|
|
|
->if('count != 1') |
|
2920
|
|
|
|
|
|
|
->line('FREETMPS;') |
|
2921
|
|
|
|
|
|
|
->line('LEAVE;') |
|
2922
|
|
|
|
|
|
|
->line('return;') |
|
2923
|
|
|
|
|
|
|
->endif |
|
2924
|
|
|
|
|
|
|
->blank |
|
2925
|
|
|
|
|
|
|
->line('SV* stream_sv = POPs;') |
|
2926
|
|
|
|
|
|
|
->line('SvREFCNT_inc(stream_sv);') |
|
2927
|
|
|
|
|
|
|
->blank |
|
2928
|
|
|
|
|
|
|
->comment('Create WebSocket object') |
|
2929
|
|
|
|
|
|
|
->line('PUSHMARK(SP);') |
|
2930
|
|
|
|
|
|
|
->line('XPUSHs(sv_2mortal(newSVpvs("Hypersonic::WebSocket")));') |
|
2931
|
|
|
|
|
|
|
->line('XPUSHs(stream_sv);') |
|
2932
|
|
|
|
|
|
|
->line('XPUSHs(sv_2mortal(newSVpvs("fd")));') |
|
2933
|
|
|
|
|
|
|
->line('XPUSHs(sv_2mortal(newSViv(fd)));') |
|
2934
|
|
|
|
|
|
|
->line('PUTBACK;') |
|
2935
|
|
|
|
|
|
|
->line('count = call_method("new", G_SCALAR);') |
|
2936
|
|
|
|
|
|
|
->line('SPAGAIN;') |
|
2937
|
|
|
|
|
|
|
->blank |
|
2938
|
|
|
|
|
|
|
->if('count != 1') |
|
2939
|
|
|
|
|
|
|
->line('SvREFCNT_dec(stream_sv);') |
|
2940
|
|
|
|
|
|
|
->line('FREETMPS;') |
|
2941
|
|
|
|
|
|
|
->line('LEAVE;') |
|
2942
|
|
|
|
|
|
|
->line('return;') |
|
2943
|
|
|
|
|
|
|
->endif |
|
2944
|
|
|
|
|
|
|
->blank |
|
2945
|
|
|
|
|
|
|
->line('SV* ws_sv = POPs;') |
|
2946
|
|
|
|
|
|
|
->line('SvREFCNT_inc(ws_sv);') |
|
2947
|
|
|
|
|
|
|
->blank |
|
2948
|
|
|
|
|
|
|
->comment('Set WebSocket state to OPEN and store object in registry') |
|
2949
|
|
|
|
|
|
|
->if('fd >= 0 && fd < WS_MAX') |
|
2950
|
|
|
|
|
|
|
->line('ws_registry[fd].state = WS_STATE_OPEN;') |
|
2951
|
|
|
|
|
|
|
->line('ws_registry[fd].ws_object = ws_sv;') |
|
2952
|
|
|
|
|
|
|
->line('SvREFCNT_inc(ws_sv);') |
|
2953
|
|
|
|
|
|
|
->endif |
|
2954
|
|
|
|
|
|
|
->blank |
|
2955
|
|
|
|
|
|
|
->comment('Register with Handler for global broadcast support') |
|
2956
|
|
|
|
|
|
|
->line('PUSHMARK(SP);') |
|
2957
|
|
|
|
|
|
|
->line('XPUSHs(sv_2mortal(newSVpvs("Hypersonic::WebSocket::Handler")));') |
|
2958
|
|
|
|
|
|
|
->line('XPUSHs(sv_2mortal(newSViv(fd)));') |
|
2959
|
|
|
|
|
|
|
->line('XPUSHs(ws_sv);') |
|
2960
|
|
|
|
|
|
|
->line('PUTBACK;') |
|
2961
|
|
|
|
|
|
|
->line('call_method("new", G_DISCARD);') |
|
2962
|
|
|
|
|
|
|
->blank |
|
2963
|
|
|
|
|
|
|
->comment('Call the Perl WebSocket handler with the WebSocket object') |
|
2964
|
|
|
|
|
|
|
->line('PUSHMARK(SP);') |
|
2965
|
|
|
|
|
|
|
->line('XPUSHs(ws_sv);') |
|
2966
|
|
|
|
|
|
|
->line('PUTBACK;') |
|
2967
|
|
|
|
|
|
|
->line('call_sv(*handler_sv, G_DISCARD | G_EVAL);') |
|
2968
|
|
|
|
|
|
|
->blank |
|
2969
|
|
|
|
|
|
|
->if('SvTRUE(ERRSV)') |
|
2970
|
|
|
|
|
|
|
->line('warn("WebSocket handler error: %s", SvPV_nolen(ERRSV));') |
|
2971
|
|
|
|
|
|
|
->endif |
|
2972
|
|
|
|
|
|
|
->blank |
|
2973
|
|
|
|
|
|
|
->comment('Emit open event') |
|
2974
|
|
|
|
|
|
|
->line('PUSHMARK(SP);') |
|
2975
|
|
|
|
|
|
|
->line('XPUSHs(ws_sv);') |
|
2976
|
|
|
|
|
|
|
->line('XPUSHs(sv_2mortal(newSVpvs("open")));') |
|
2977
|
|
|
|
|
|
|
->line('PUTBACK;') |
|
2978
|
|
|
|
|
|
|
->line('call_method("emit", G_DISCARD | G_EVAL);') |
|
2979
|
|
|
|
|
|
|
->blank |
|
2980
|
|
|
|
|
|
|
->line('SvREFCNT_dec(stream_sv);') |
|
2981
|
|
|
|
|
|
|
->line('SvREFCNT_dec(ws_sv);') |
|
2982
|
|
|
|
|
|
|
->line('FREETMPS;') |
|
2983
|
|
|
|
|
|
|
->line('LEAVE;') |
|
2984
|
|
|
|
|
|
|
->line('}') |
|
2985
|
|
|
|
|
|
|
->blank; |
|
2986
|
|
|
|
|
|
|
|
|
2987
|
0
|
|
|
|
|
0
|
return $builder->code; |
|
2988
|
|
|
|
|
|
|
} |
|
2989
|
|
|
|
|
|
|
|
|
2990
|
|
|
|
|
|
|
# Generate WebSocket data processor for established connections |
|
2991
|
|
|
|
|
|
|
sub _gen_websocket_data_processor { |
|
2992
|
0
|
|
|
0
|
|
0
|
my ($self) = @_; |
|
2993
|
|
|
|
|
|
|
|
|
2994
|
0
|
|
|
|
|
0
|
my $builder = XS::JIT::Builder->new; |
|
2995
|
|
|
|
|
|
|
|
|
2996
|
0
|
|
|
|
|
0
|
$builder->comment('Process incoming WebSocket data on established connection') |
|
2997
|
|
|
|
|
|
|
->line('static void process_websocket_data(pTHX_ int fd, const char* data, ssize_t len) {') |
|
2998
|
|
|
|
|
|
|
->line(' dSP;') |
|
2999
|
|
|
|
|
|
|
->blank |
|
3000
|
|
|
|
|
|
|
->comment('Get WebSocket object from registry') |
|
3001
|
|
|
|
|
|
|
->if('fd < 0 || fd >= WS_MAX || !ws_registry[fd].ws_object') |
|
3002
|
|
|
|
|
|
|
->line('return;') |
|
3003
|
|
|
|
|
|
|
->endif |
|
3004
|
|
|
|
|
|
|
->blank |
|
3005
|
|
|
|
|
|
|
->line('SV* ws_sv = ws_registry[fd].ws_object;') |
|
3006
|
|
|
|
|
|
|
->blank |
|
3007
|
|
|
|
|
|
|
->comment('Parse WebSocket frame') |
|
3008
|
|
|
|
|
|
|
->line('WSFrame frame;') |
|
3009
|
|
|
|
|
|
|
->line('int result = ws_decode_frame((const uint8_t*)data, len, &frame);') |
|
3010
|
|
|
|
|
|
|
->blank |
|
3011
|
|
|
|
|
|
|
->if('result < 0') |
|
3012
|
|
|
|
|
|
|
->comment('Incomplete frame or error - wait for more data') |
|
3013
|
|
|
|
|
|
|
->line('return;') |
|
3014
|
|
|
|
|
|
|
->endif |
|
3015
|
|
|
|
|
|
|
->blank |
|
3016
|
|
|
|
|
|
|
->comment('Handle different opcodes') |
|
3017
|
|
|
|
|
|
|
->if('frame.opcode == WS_OP_TEXT || frame.opcode == WS_OP_BINARY') |
|
3018
|
|
|
|
|
|
|
->comment('Data frame - emit message event') |
|
3019
|
|
|
|
|
|
|
->line('ENTER;') |
|
3020
|
|
|
|
|
|
|
->line('SAVETMPS;') |
|
3021
|
|
|
|
|
|
|
->line('PUSHMARK(SP);') |
|
3022
|
|
|
|
|
|
|
->line('XPUSHs(ws_sv);') |
|
3023
|
|
|
|
|
|
|
->line('XPUSHs(sv_2mortal(newSVpvs("message")));') |
|
3024
|
|
|
|
|
|
|
->line('XPUSHs(sv_2mortal(newSVpvn((const char*)frame.payload, frame.payload_length)));') |
|
3025
|
|
|
|
|
|
|
->line('PUTBACK;') |
|
3026
|
|
|
|
|
|
|
->line('call_method("emit", G_DISCARD | G_EVAL);') |
|
3027
|
|
|
|
|
|
|
->line('FREETMPS;') |
|
3028
|
|
|
|
|
|
|
->line('LEAVE;') |
|
3029
|
|
|
|
|
|
|
->elsif('frame.opcode == WS_OP_CLOSE') |
|
3030
|
|
|
|
|
|
|
->comment('Close frame - emit close event and respond') |
|
3031
|
|
|
|
|
|
|
->line('int close_code = 1000;') |
|
3032
|
|
|
|
|
|
|
->if('frame.payload_length >= 2') |
|
3033
|
|
|
|
|
|
|
->line('close_code = (frame.payload[0] << 8) | frame.payload[1];') |
|
3034
|
|
|
|
|
|
|
->endif |
|
3035
|
|
|
|
|
|
|
->blank |
|
3036
|
|
|
|
|
|
|
->comment('Send close response') |
|
3037
|
|
|
|
|
|
|
->line('uint8_t close_frame[4];') |
|
3038
|
|
|
|
|
|
|
->line('close_frame[0] = 0x88;') # FIN + Close opcode |
|
3039
|
|
|
|
|
|
|
->line('close_frame[1] = 2;') # Length 2 (just the code) |
|
3040
|
|
|
|
|
|
|
->line('close_frame[2] = (close_code >> 8) & 0xFF;') |
|
3041
|
|
|
|
|
|
|
->line('close_frame[3] = close_code & 0xFF;') |
|
3042
|
|
|
|
|
|
|
->line('send(fd, close_frame, 4, 0);') |
|
3043
|
|
|
|
|
|
|
->blank |
|
3044
|
|
|
|
|
|
|
->comment('Emit close event') |
|
3045
|
|
|
|
|
|
|
->line('ENTER;') |
|
3046
|
|
|
|
|
|
|
->line('SAVETMPS;') |
|
3047
|
|
|
|
|
|
|
->line('PUSHMARK(SP);') |
|
3048
|
|
|
|
|
|
|
->line('XPUSHs(ws_sv);') |
|
3049
|
|
|
|
|
|
|
->line('XPUSHs(sv_2mortal(newSVpvs("close")));') |
|
3050
|
|
|
|
|
|
|
->line('XPUSHs(sv_2mortal(newSViv(close_code)));') |
|
3051
|
|
|
|
|
|
|
->line('PUTBACK;') |
|
3052
|
|
|
|
|
|
|
->line('call_method("emit", G_DISCARD | G_EVAL);') |
|
3053
|
|
|
|
|
|
|
->line('FREETMPS;') |
|
3054
|
|
|
|
|
|
|
->line('LEAVE;') |
|
3055
|
|
|
|
|
|
|
->blank |
|
3056
|
|
|
|
|
|
|
->comment('Unregister from Handler') |
|
3057
|
|
|
|
|
|
|
->line('ENTER;') |
|
3058
|
|
|
|
|
|
|
->line('SAVETMPS;') |
|
3059
|
|
|
|
|
|
|
->line('PUSHMARK(SP);') |
|
3060
|
|
|
|
|
|
|
->line('XPUSHs(sv_2mortal(newSVpvs("Hypersonic::WebSocket::Handler")));') |
|
3061
|
|
|
|
|
|
|
->line('XPUSHs(sv_2mortal(newSViv(fd)));') |
|
3062
|
|
|
|
|
|
|
->line('PUTBACK;') |
|
3063
|
|
|
|
|
|
|
->line('call_method("close", G_DISCARD);') |
|
3064
|
|
|
|
|
|
|
->line('FREETMPS;') |
|
3065
|
|
|
|
|
|
|
->line('LEAVE;') |
|
3066
|
|
|
|
|
|
|
->blank |
|
3067
|
|
|
|
|
|
|
->comment('Mark connection as closed') |
|
3068
|
|
|
|
|
|
|
->line('ws_registry[fd].state = WS_STATE_CLOSED;') |
|
3069
|
|
|
|
|
|
|
->elsif('frame.opcode == WS_OP_PING') |
|
3070
|
|
|
|
|
|
|
->comment('Ping - auto pong') |
|
3071
|
|
|
|
|
|
|
->line('uint8_t pong_frame[256];') |
|
3072
|
|
|
|
|
|
|
->line('size_t pong_len = ws_encode_pong(pong_frame, sizeof(pong_frame), frame.payload, frame.payload_length);') |
|
3073
|
|
|
|
|
|
|
->if('pong_len > 0') |
|
3074
|
|
|
|
|
|
|
->line('send(fd, pong_frame, pong_len, 0);') |
|
3075
|
|
|
|
|
|
|
->endif |
|
3076
|
|
|
|
|
|
|
->endif |
|
3077
|
|
|
|
|
|
|
->line('}') |
|
3078
|
|
|
|
|
|
|
->blank; |
|
3079
|
|
|
|
|
|
|
|
|
3080
|
0
|
|
|
|
|
0
|
return $builder->code; |
|
3081
|
|
|
|
|
|
|
} |
|
3082
|
|
|
|
|
|
|
|
|
3083
|
|
|
|
|
|
|
sub _escape_c_string { |
|
3084
|
105
|
|
|
105
|
|
215
|
my ($str) = @_; |
|
3085
|
105
|
|
|
|
|
423
|
$str =~ s/\\/\\\\/g; |
|
3086
|
105
|
|
|
|
|
266
|
$str =~ s/"/\\"/g; |
|
3087
|
105
|
|
|
|
|
620
|
$str =~ s/\n/\\n/g; |
|
3088
|
105
|
|
|
|
|
519
|
$str =~ s/\r/\\r/g; |
|
3089
|
105
|
|
|
|
|
191
|
$str =~ s/\t/\\t/g; |
|
3090
|
105
|
|
|
|
|
280
|
return $str; |
|
3091
|
|
|
|
|
|
|
} |
|
3092
|
|
|
|
|
|
|
|
|
3093
|
|
|
|
|
|
|
# Deparse a handler coderef to analyze what request features it uses |
|
3094
|
|
|
|
|
|
|
# Returns deparsed code as string, or undef on failure |
|
3095
|
|
|
|
|
|
|
sub _deparse_handler { |
|
3096
|
11
|
|
|
11
|
|
27
|
my ($coderef) = @_; |
|
3097
|
11
|
50
|
|
|
|
59
|
return undef unless ref($coderef) eq 'CODE'; |
|
3098
|
|
|
|
|
|
|
|
|
3099
|
11
|
|
|
|
|
21
|
my $code = eval { |
|
3100
|
11
|
100
|
|
|
|
92
|
require B::Deparse unless $DEPARSER; |
|
3101
|
11
|
|
66
|
|
|
728
|
$DEPARSER //= B::Deparse->new('-q'); # -q = don't quote simple strings |
|
3102
|
11
|
|
|
|
|
20686
|
$DEPARSER->coderef2text($coderef); |
|
3103
|
|
|
|
|
|
|
}; |
|
3104
|
11
|
50
|
|
|
|
118
|
return $@ ? undef : $code; |
|
3105
|
|
|
|
|
|
|
} |
|
3106
|
|
|
|
|
|
|
|
|
3107
|
|
|
|
|
|
|
# Find common prefix of multiple paths |
|
3108
|
|
|
|
|
|
|
sub _find_common_prefix { |
|
3109
|
11
|
|
|
11
|
|
56
|
my @paths = @_; |
|
3110
|
11
|
50
|
|
|
|
40
|
return '' unless @paths; |
|
3111
|
11
|
50
|
|
|
|
31
|
return $paths[0] if @paths == 1; |
|
3112
|
|
|
|
|
|
|
|
|
3113
|
11
|
|
|
|
|
32
|
my $prefix = $paths[0]; |
|
3114
|
11
|
|
|
|
|
101
|
for my $path (@paths[1..$#paths]) { |
|
3115
|
26
|
|
|
|
|
86
|
while (index($path, $prefix) != 0) { |
|
3116
|
56
|
|
|
|
|
120
|
$prefix = substr($prefix, 0, -1); |
|
3117
|
56
|
50
|
|
|
|
134
|
return '' if $prefix eq ''; |
|
3118
|
|
|
|
|
|
|
} |
|
3119
|
|
|
|
|
|
|
} |
|
3120
|
|
|
|
|
|
|
# Don't include trailing non-slash char as prefix |
|
3121
|
|
|
|
|
|
|
# e.g., /api/hello and /api/health -> /api/ not /api/he |
|
3122
|
11
|
100
|
66
|
|
|
265
|
if ($prefix !~ m{/$} && $prefix =~ m{^(.*/)[^/]+$}) { |
|
3123
|
3
|
|
|
|
|
95
|
$prefix = $1; |
|
3124
|
|
|
|
|
|
|
} |
|
3125
|
11
|
|
|
|
|
41
|
return $prefix; |
|
3126
|
|
|
|
|
|
|
} |
|
3127
|
|
|
|
|
|
|
|
|
3128
|
|
|
|
|
|
|
# HTTP status code to text mapping |
|
3129
|
|
|
|
|
|
|
my %STATUS_TEXT = ( |
|
3130
|
|
|
|
|
|
|
200 => 'OK', |
|
3131
|
|
|
|
|
|
|
201 => 'Created', |
|
3132
|
|
|
|
|
|
|
202 => 'Accepted', |
|
3133
|
|
|
|
|
|
|
204 => 'No Content', |
|
3134
|
|
|
|
|
|
|
301 => 'Moved Permanently', |
|
3135
|
|
|
|
|
|
|
302 => 'Found', |
|
3136
|
|
|
|
|
|
|
303 => 'See Other', |
|
3137
|
|
|
|
|
|
|
304 => 'Not Modified', |
|
3138
|
|
|
|
|
|
|
307 => 'Temporary Redirect', |
|
3139
|
|
|
|
|
|
|
308 => 'Permanent Redirect', |
|
3140
|
|
|
|
|
|
|
400 => 'Bad Request', |
|
3141
|
|
|
|
|
|
|
401 => 'Unauthorized', |
|
3142
|
|
|
|
|
|
|
403 => 'Forbidden', |
|
3143
|
|
|
|
|
|
|
404 => 'Not Found', |
|
3144
|
|
|
|
|
|
|
405 => 'Method Not Allowed', |
|
3145
|
|
|
|
|
|
|
408 => 'Request Timeout', |
|
3146
|
|
|
|
|
|
|
409 => 'Conflict', |
|
3147
|
|
|
|
|
|
|
410 => 'Gone', |
|
3148
|
|
|
|
|
|
|
413 => 'Payload Too Large', |
|
3149
|
|
|
|
|
|
|
415 => 'Unsupported Media Type', |
|
3150
|
|
|
|
|
|
|
422 => 'Unprocessable Entity', |
|
3151
|
|
|
|
|
|
|
429 => 'Too Many Requests', |
|
3152
|
|
|
|
|
|
|
500 => 'Internal Server Error', |
|
3153
|
|
|
|
|
|
|
501 => 'Not Implemented', |
|
3154
|
|
|
|
|
|
|
502 => 'Bad Gateway', |
|
3155
|
|
|
|
|
|
|
503 => 'Service Unavailable', |
|
3156
|
|
|
|
|
|
|
504 => 'Gateway Timeout', |
|
3157
|
|
|
|
|
|
|
); |
|
3158
|
|
|
|
|
|
|
|
|
3159
|
|
|
|
|
|
|
sub _status_text { |
|
3160
|
0
|
|
|
0
|
|
0
|
my ($code) = @_; |
|
3161
|
0
|
|
0
|
|
|
0
|
return $STATUS_TEXT{$code} // 'Unknown'; |
|
3162
|
|
|
|
|
|
|
} |
|
3163
|
|
|
|
|
|
|
|
|
3164
|
|
|
|
|
|
|
# ============================================================ |
|
3165
|
|
|
|
|
|
|
# STATIC FILE SERVING - JIT compiled for maximum performance |
|
3166
|
|
|
|
|
|
|
# Files are read at compile time and baked into C string constants |
|
3167
|
|
|
|
|
|
|
# ============================================================ |
|
3168
|
|
|
|
|
|
|
|
|
3169
|
|
|
|
|
|
|
sub _compile_static_files { |
|
3170
|
0
|
|
|
0
|
|
0
|
my ($self, $static_dirs) = @_; |
|
3171
|
|
|
|
|
|
|
|
|
3172
|
0
|
|
|
|
|
0
|
require File::Find; |
|
3173
|
0
|
|
|
|
|
0
|
require Digest::MD5; |
|
3174
|
|
|
|
|
|
|
|
|
3175
|
0
|
|
|
|
|
0
|
my @static_files; |
|
3176
|
|
|
|
|
|
|
|
|
3177
|
0
|
|
|
|
|
0
|
for my $config (@$static_dirs) { |
|
3178
|
0
|
|
|
|
|
0
|
my $prefix = $config->{prefix}; |
|
3179
|
0
|
|
|
|
|
0
|
my $dir = $config->{directory}; |
|
3180
|
0
|
|
|
|
|
0
|
my $max_age = $config->{max_age}; |
|
3181
|
0
|
|
|
|
|
0
|
my $gen_etag = $config->{etag}; |
|
3182
|
|
|
|
|
|
|
|
|
3183
|
|
|
|
|
|
|
# Recursively find all files |
|
3184
|
|
|
|
|
|
|
File::Find::find({ |
|
3185
|
|
|
|
|
|
|
no_chdir => 1, |
|
3186
|
|
|
|
|
|
|
wanted => sub { |
|
3187
|
0
|
0
|
|
0
|
|
0
|
return unless -f $_; |
|
3188
|
0
|
|
|
|
|
0
|
my $file_path = $_; |
|
3189
|
0
|
|
|
|
|
0
|
my $rel_path = $file_path; |
|
3190
|
0
|
|
|
|
|
0
|
$rel_path =~ s{^\Q$dir\E/?}{}; |
|
3191
|
|
|
|
|
|
|
|
|
3192
|
|
|
|
|
|
|
# URL path for this file |
|
3193
|
0
|
|
|
|
|
0
|
my $url_path = "$prefix/$rel_path"; |
|
3194
|
0
|
|
|
|
|
0
|
$url_path =~ s{//+}{/}g; |
|
3195
|
|
|
|
|
|
|
|
|
3196
|
|
|
|
|
|
|
# Read file content |
|
3197
|
0
|
0
|
|
|
|
0
|
open my $fh, '<:raw', $file_path or return; |
|
3198
|
0
|
|
|
|
|
0
|
local $/; |
|
3199
|
0
|
|
|
|
|
0
|
my $content = <$fh>; |
|
3200
|
0
|
|
|
|
|
0
|
close $fh; |
|
3201
|
|
|
|
|
|
|
|
|
3202
|
|
|
|
|
|
|
# Get MIME type |
|
3203
|
0
|
|
|
|
|
0
|
my $mime = _get_mime_type($file_path); |
|
3204
|
|
|
|
|
|
|
|
|
3205
|
|
|
|
|
|
|
# Generate ETag (MD5 of content) |
|
3206
|
0
|
|
|
|
|
0
|
my $etag = ''; |
|
3207
|
0
|
0
|
|
|
|
0
|
if ($gen_etag) { |
|
3208
|
0
|
|
|
|
|
0
|
$etag = Digest::MD5::md5_hex($content); |
|
3209
|
|
|
|
|
|
|
} |
|
3210
|
|
|
|
|
|
|
|
|
3211
|
|
|
|
|
|
|
# Store file info |
|
3212
|
0
|
|
|
|
|
0
|
push @static_files, { |
|
3213
|
|
|
|
|
|
|
url_path => $url_path, |
|
3214
|
|
|
|
|
|
|
content => $content, |
|
3215
|
|
|
|
|
|
|
mime => $mime, |
|
3216
|
|
|
|
|
|
|
etag => $etag, |
|
3217
|
|
|
|
|
|
|
max_age => $max_age, |
|
3218
|
|
|
|
|
|
|
length => length($content), |
|
3219
|
|
|
|
|
|
|
}; |
|
3220
|
|
|
|
|
|
|
}, |
|
3221
|
0
|
|
|
|
|
0
|
}, $dir); |
|
3222
|
|
|
|
|
|
|
} |
|
3223
|
|
|
|
|
|
|
|
|
3224
|
|
|
|
|
|
|
# Store for code generation |
|
3225
|
0
|
|
|
|
|
0
|
$self->{_static_files} = \@static_files; |
|
3226
|
|
|
|
|
|
|
|
|
3227
|
|
|
|
|
|
|
# Create static routes - these are essentially pre-computed responses |
|
3228
|
0
|
|
|
|
|
0
|
for my $file (@static_files) { |
|
3229
|
0
|
|
|
|
|
0
|
my $url_path = $file->{url_path}; |
|
3230
|
0
|
|
|
|
|
0
|
my $content = $file->{content}; |
|
3231
|
0
|
|
|
|
|
0
|
my $mime = $file->{mime}; |
|
3232
|
0
|
|
|
|
|
0
|
my $etag = $file->{etag}; |
|
3233
|
0
|
|
|
|
|
0
|
my $max_age = $file->{max_age}; |
|
3234
|
0
|
|
|
|
|
0
|
my $len = $file->{length}; |
|
3235
|
|
|
|
|
|
|
|
|
3236
|
|
|
|
|
|
|
# Build complete HTTP response at compile time |
|
3237
|
0
|
|
|
|
|
0
|
my $response = "HTTP/1.1 200 OK\r\n" |
|
3238
|
|
|
|
|
|
|
. "Content-Type: $mime\r\n" |
|
3239
|
|
|
|
|
|
|
. "Content-Length: $len\r\n" |
|
3240
|
|
|
|
|
|
|
. "Connection: keep-alive\r\n"; |
|
3241
|
0
|
0
|
|
|
|
0
|
$response .= "Cache-Control: public, max-age=$max_age\r\n" if $max_age; |
|
3242
|
0
|
0
|
|
|
|
0
|
$response .= "ETag: \"$etag\"\r\n" if $etag; |
|
3243
|
|
|
|
|
|
|
|
|
3244
|
|
|
|
|
|
|
# Add security headers |
|
3245
|
0
|
0
|
|
|
|
0
|
if ($self->{enable_security_headers}) { |
|
3246
|
0
|
|
|
|
|
0
|
$response .= $self->_get_security_headers_string(); |
|
3247
|
|
|
|
|
|
|
} |
|
3248
|
|
|
|
|
|
|
|
|
3249
|
0
|
|
|
|
|
0
|
$response .= "\r\n" . $content; |
|
3250
|
|
|
|
|
|
|
|
|
3251
|
|
|
|
|
|
|
# Store as static route |
|
3252
|
0
|
|
|
|
|
0
|
push @{$self->{routes}}, { |
|
3253
|
|
|
|
|
|
|
method => 'GET', |
|
3254
|
|
|
|
|
|
|
path => $url_path, |
|
3255
|
0
|
|
|
0
|
|
0
|
handler => sub { $content }, # Dummy handler for static |
|
3256
|
0
|
|
|
|
|
0
|
dynamic => 0, |
|
3257
|
|
|
|
|
|
|
params => [], |
|
3258
|
|
|
|
|
|
|
segments => [split('/', $url_path)], |
|
3259
|
|
|
|
|
|
|
features => {}, |
|
3260
|
|
|
|
|
|
|
before => [], |
|
3261
|
|
|
|
|
|
|
after => [], |
|
3262
|
|
|
|
|
|
|
# Mark as static file with pre-built response |
|
3263
|
|
|
|
|
|
|
_static_response => $response, |
|
3264
|
|
|
|
|
|
|
_static_file => 1, |
|
3265
|
|
|
|
|
|
|
}; |
|
3266
|
|
|
|
|
|
|
} |
|
3267
|
|
|
|
|
|
|
} |
|
3268
|
|
|
|
|
|
|
|
|
3269
|
|
|
|
|
|
|
# Generate security headers string for HTTP responses |
|
3270
|
|
|
|
|
|
|
# Pre-computed at compile time - zero runtime overhead |
|
3271
|
|
|
|
|
|
|
sub _get_security_headers_string { |
|
3272
|
59
|
|
|
59
|
|
228
|
my ($self) = @_; |
|
3273
|
59
|
|
|
|
|
324
|
my $headers = ''; |
|
3274
|
|
|
|
|
|
|
|
|
3275
|
59
|
|
|
|
|
113
|
for my $name (sort keys %{$self->{security_headers}}) { |
|
|
59
|
|
|
|
|
954
|
|
|
3276
|
413
|
|
|
|
|
933
|
my $value = $self->{security_headers}{$name}; |
|
3277
|
413
|
100
|
66
|
|
|
1227
|
next unless defined $value && length($value); |
|
3278
|
236
|
|
|
|
|
480
|
$headers .= "$name: $value\r\n"; |
|
3279
|
|
|
|
|
|
|
} |
|
3280
|
|
|
|
|
|
|
|
|
3281
|
59
|
|
|
|
|
199
|
return $headers; |
|
3282
|
|
|
|
|
|
|
} |
|
3283
|
|
|
|
|
|
|
|
|
3284
|
|
|
|
|
|
|
# Generate security headers as C string constant for dynamic routes |
|
3285
|
|
|
|
|
|
|
sub _gen_security_headers_c_constant { |
|
3286
|
4
|
|
|
4
|
|
11
|
my ($self) = @_; |
|
3287
|
4
|
50
|
|
|
|
16
|
return '' unless $self->{enable_security_headers}; |
|
3288
|
|
|
|
|
|
|
|
|
3289
|
4
|
|
|
|
|
12
|
my $headers = $self->_get_security_headers_string(); |
|
3290
|
4
|
50
|
|
|
|
25
|
return '' unless length($headers); |
|
3291
|
|
|
|
|
|
|
|
|
3292
|
4
|
|
|
|
|
10
|
my $escaped = _escape_c_string($headers); |
|
3293
|
4
|
|
|
|
|
50
|
return "static const char SECURITY_HEADERS[] = \"$escaped\";\n" |
|
3294
|
|
|
|
|
|
|
. "static const int SECURITY_HEADERS_LEN = " . length($headers) . ";\n"; |
|
3295
|
|
|
|
|
|
|
} |
|
3296
|
|
|
|
|
|
|
|
|
3297
|
|
|
|
|
|
|
sub dispatch { |
|
3298
|
31
|
|
|
31
|
1
|
1022322
|
my ($self, $req) = @_; |
|
3299
|
31
|
100
|
|
|
|
103
|
die "Must call compile() first" unless $self->{compiled}; |
|
3300
|
30
|
|
|
|
|
212
|
return $self->{dispatch_fn}->($req); |
|
3301
|
|
|
|
|
|
|
} |
|
3302
|
|
|
|
|
|
|
|
|
3303
|
|
|
|
|
|
|
sub run { |
|
3304
|
7
|
|
|
7
|
1
|
211
|
my ($self, %opts) = @_; |
|
3305
|
|
|
|
|
|
|
|
|
3306
|
7
|
100
|
|
|
|
50
|
die "Must call compile() before run()" unless $self->{compiled}; |
|
3307
|
|
|
|
|
|
|
|
|
3308
|
6
|
|
33
|
|
|
162
|
my $host = $opts{host} // $self->{host}; |
|
3309
|
6
|
|
33
|
|
|
32
|
my $port = $opts{port} // $self->{port}; |
|
3310
|
6
|
|
50
|
|
|
22
|
my $workers = $opts{workers} // 1; |
|
3311
|
|
|
|
|
|
|
|
|
3312
|
|
|
|
|
|
|
# Windows lacks fork(); the multi-worker model assumes SO_REUSEPORT |
|
3313
|
|
|
|
|
|
|
# + fork()-per-worker so the kernel can balance connections. With |
|
3314
|
|
|
|
|
|
|
# neither primitive available the only safe behavior is single |
|
3315
|
|
|
|
|
|
|
# process. Warn loudly so users know they're not getting the |
|
3316
|
|
|
|
|
|
|
# workers they asked for. |
|
3317
|
6
|
50
|
33
|
|
|
50
|
if ($^O eq 'MSWin32' && $workers > 1) { |
|
3318
|
0
|
|
|
|
|
0
|
warn "Hypersonic: Windows lacks fork(); forcing workers=1.\n"; |
|
3319
|
0
|
|
|
|
|
0
|
$workers = 1; |
|
3320
|
|
|
|
|
|
|
} |
|
3321
|
|
|
|
|
|
|
|
|
3322
|
|
|
|
|
|
|
# Protocol mode indication |
|
3323
|
6
|
50
|
|
|
|
115
|
my $mode = $self->{http2} ? "HTTP/2" : ($self->{tls} ? "HTTPS/TLS" : "HTTP"); |
|
|
|
50
|
|
|
|
|
|
|
3324
|
6
|
|
|
|
|
300
|
print "Hypersonic listening on $host:$port ($mode, pure C event loop, $workers workers)\n"; |
|
3325
|
|
|
|
|
|
|
|
|
3326
|
|
|
|
|
|
|
# Fork workers if requested |
|
3327
|
|
|
|
|
|
|
# Each worker creates its OWN listening socket with SO_REUSEPORT |
|
3328
|
|
|
|
|
|
|
# This avoids thundering herd and lets kernel distribute connections |
|
3329
|
6
|
50
|
|
|
|
33
|
if ($workers > 1) { |
|
3330
|
0
|
|
|
|
|
0
|
for my $i (1 .. $workers - 1) { |
|
3331
|
0
|
|
|
|
|
0
|
my $pid = fork(); |
|
3332
|
0
|
0
|
|
|
|
0
|
if (!defined $pid) { |
|
3333
|
0
|
|
|
|
|
0
|
die "Fork failed: $!"; |
|
3334
|
|
|
|
|
|
|
} |
|
3335
|
0
|
0
|
|
|
|
0
|
if ($pid == 0) { |
|
3336
|
|
|
|
|
|
|
# Child - create own listen socket and run event loop |
|
3337
|
0
|
|
|
|
|
0
|
my $listen_fd = Hypersonic::Socket::create_listen_socket($port); |
|
3338
|
0
|
0
|
|
|
|
0
|
die "Worker $i: Failed to create listen socket" if $listen_fd < 0; |
|
3339
|
0
|
|
|
|
|
0
|
$self->{run_loop_fn}->($listen_fd, $self); |
|
3340
|
0
|
|
|
|
|
0
|
exit(0); |
|
3341
|
|
|
|
|
|
|
} |
|
3342
|
|
|
|
|
|
|
} |
|
3343
|
|
|
|
|
|
|
} |
|
3344
|
|
|
|
|
|
|
|
|
3345
|
|
|
|
|
|
|
# Parent (or single worker) - create listen socket and run event loop |
|
3346
|
6
|
|
|
|
|
468
|
my $listen_fd = Hypersonic::Socket::create_listen_socket($port); |
|
3347
|
6
|
50
|
|
|
|
36
|
die "Failed to create listen socket on port $port" if $listen_fd < 0; |
|
3348
|
6
|
|
|
|
|
4109715
|
$self->{run_loop_fn}->($listen_fd, $self); |
|
3349
|
|
|
|
|
|
|
} |
|
3350
|
|
|
|
|
|
|
|
|
3351
|
|
|
|
|
|
|
|
|
3352
|
|
|
|
|
|
|
1; |
|
3353
|
|
|
|
|
|
|
|
|
3354
|
|
|
|
|
|
|
__END__ |