| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
package Hypersonic::Response; |
|
2
|
|
|
|
|
|
|
|
|
3
|
3
|
|
|
3
|
|
4751
|
use strict; |
|
|
3
|
|
|
|
|
9
|
|
|
|
3
|
|
|
|
|
130
|
|
|
4
|
3
|
|
|
3
|
|
17
|
use warnings; |
|
|
3
|
|
|
|
|
6
|
|
|
|
3
|
|
|
|
|
211
|
|
|
5
|
3
|
|
|
3
|
|
59
|
use 5.010; |
|
|
3
|
|
|
|
|
11
|
|
|
6
|
|
|
|
|
|
|
|
|
7
|
|
|
|
|
|
|
our $VERSION = '0.15'; |
|
8
|
|
|
|
|
|
|
|
|
9
|
|
|
|
|
|
|
# JIT-compiled Response object using array-based slots for maximum speed |
|
10
|
|
|
|
|
|
|
# Generates XS accessors at compile time via XS::JIT::Builder |
|
11
|
|
|
|
|
|
|
|
|
12
|
3
|
|
|
3
|
|
17
|
use XS::JIT; |
|
|
3
|
|
|
|
|
10
|
|
|
|
3
|
|
|
|
|
81
|
|
|
13
|
3
|
|
|
3
|
|
14
|
use XS::JIT::Builder; |
|
|
3
|
|
|
|
|
45
|
|
|
|
3
|
|
|
|
|
291
|
|
|
14
|
|
|
|
|
|
|
|
|
15
|
|
|
|
|
|
|
# Response slots (array indices) |
|
16
|
|
|
|
|
|
|
use constant { |
|
17
|
3
|
|
|
|
|
459
|
SLOT_STATUS => 0, |
|
18
|
|
|
|
|
|
|
SLOT_HEADERS => 1, # HV* of headers |
|
19
|
|
|
|
|
|
|
SLOT_BODY => 2, |
|
20
|
|
|
|
|
|
|
SLOT_COOKIES => 3, # AV* of cookie strings |
|
21
|
|
|
|
|
|
|
SLOT_COUNT => 4, |
|
22
|
3
|
|
|
3
|
|
20
|
}; |
|
|
3
|
|
|
|
|
7
|
|
|
23
|
|
|
|
|
|
|
|
|
24
|
|
|
|
|
|
|
# Export slot constants for direct access |
|
25
|
3
|
|
|
3
|
|
23
|
use Exporter 'import'; |
|
|
3
|
|
|
|
|
8
|
|
|
|
3
|
|
|
|
|
14414
|
|
|
26
|
|
|
|
|
|
|
our @EXPORT_OK = qw( |
|
27
|
|
|
|
|
|
|
SLOT_STATUS SLOT_HEADERS SLOT_BODY SLOT_COOKIES SLOT_COUNT |
|
28
|
|
|
|
|
|
|
res |
|
29
|
|
|
|
|
|
|
); |
|
30
|
|
|
|
|
|
|
|
|
31
|
|
|
|
|
|
|
# Shortcut constructor for use in handlers |
|
32
|
2
|
|
|
2
|
1
|
5079
|
sub res { __PACKAGE__->new(@_) } |
|
33
|
|
|
|
|
|
|
|
|
34
|
|
|
|
|
|
|
my $COMPILED = 0; |
|
35
|
|
|
|
|
|
|
my $MODULE_ID = 0; |
|
36
|
|
|
|
|
|
|
|
|
37
|
|
|
|
|
|
|
# Unified compile interface |
|
38
|
|
|
|
|
|
|
sub compile { |
|
39
|
0
|
|
|
0
|
0
|
0
|
my ($class, %opts) = @_; |
|
40
|
0
|
|
|
|
|
0
|
return $class->compile_accessors(%opts); |
|
41
|
|
|
|
|
|
|
} |
|
42
|
|
|
|
|
|
|
|
|
43
|
|
|
|
|
|
|
# Generate and compile XS accessors |
|
44
|
|
|
|
|
|
|
sub compile_accessors { |
|
45
|
2
|
|
|
2
|
0
|
8
|
my ($class, %opts) = @_; |
|
46
|
|
|
|
|
|
|
|
|
47
|
2
|
50
|
|
|
|
5
|
return 1 if $COMPILED; |
|
48
|
|
|
|
|
|
|
|
|
49
|
2
|
|
50
|
|
|
16
|
my $cache_dir = $opts{cache_dir} // '_hypersonic_cache/response'; |
|
50
|
2
|
|
|
|
|
5
|
my $module_name = 'Hypersonic::Response::Accessors_' . $MODULE_ID++; |
|
51
|
|
|
|
|
|
|
|
|
52
|
2
|
|
|
|
|
27
|
my $builder = XS::JIT::Builder->new; |
|
53
|
|
|
|
|
|
|
|
|
54
|
|
|
|
|
|
|
# Generate read-only accessors for simple slots |
|
55
|
2
|
|
|
|
|
29
|
$builder->op_ro_accessor('jit_get_status', SLOT_STATUS); |
|
56
|
2
|
|
|
|
|
15
|
$builder->op_ro_accessor('jit_get_body', SLOT_BODY); |
|
57
|
2
|
|
|
|
|
24
|
$builder->op_ro_accessor('jit_get_headers_hv', SLOT_HEADERS); |
|
58
|
2
|
|
|
|
|
12
|
$builder->op_ro_accessor('jit_get_cookies_av', SLOT_COOKIES); |
|
59
|
|
|
|
|
|
|
|
|
60
|
|
|
|
|
|
|
# Generate status setter (returns $self for chaining) |
|
61
|
2
|
|
|
|
|
63
|
$builder->xs_function('jit_set_status') |
|
62
|
|
|
|
|
|
|
->xs_preamble |
|
63
|
|
|
|
|
|
|
->line('SV** ary = AvARRAY((AV*)SvRV(ST(0)));') |
|
64
|
|
|
|
|
|
|
->if('items > 1') |
|
65
|
|
|
|
|
|
|
->line('SvREFCNT_dec(ary[' . SLOT_STATUS . ']);') |
|
66
|
|
|
|
|
|
|
->line('ary[' . SLOT_STATUS . '] = newSVsv(ST(1));') |
|
67
|
|
|
|
|
|
|
->endif |
|
68
|
|
|
|
|
|
|
->line('ST(0) = ST(0);') |
|
69
|
|
|
|
|
|
|
->xs_return('1') |
|
70
|
|
|
|
|
|
|
->xs_end; |
|
71
|
|
|
|
|
|
|
|
|
72
|
|
|
|
|
|
|
# Generate body setter (returns $self for chaining) |
|
73
|
2
|
|
|
|
|
44
|
$builder->xs_function('jit_set_body') |
|
74
|
|
|
|
|
|
|
->xs_preamble |
|
75
|
|
|
|
|
|
|
->line('SV** ary = AvARRAY((AV*)SvRV(ST(0)));') |
|
76
|
|
|
|
|
|
|
->if('items > 1') |
|
77
|
|
|
|
|
|
|
->line('SvREFCNT_dec(ary[' . SLOT_BODY . ']);') |
|
78
|
|
|
|
|
|
|
->line('ary[' . SLOT_BODY . '] = newSVsv(ST(1));') |
|
79
|
|
|
|
|
|
|
->endif |
|
80
|
|
|
|
|
|
|
->line('ST(0) = ST(0);') |
|
81
|
|
|
|
|
|
|
->xs_return('1') |
|
82
|
|
|
|
|
|
|
->xs_end; |
|
83
|
|
|
|
|
|
|
|
|
84
|
|
|
|
|
|
|
# Generate single header setter (returns $self for chaining) |
|
85
|
2
|
|
|
|
|
47
|
$builder->xs_function('jit_set_header') |
|
86
|
|
|
|
|
|
|
->xs_preamble |
|
87
|
|
|
|
|
|
|
->line('SV** ary = AvARRAY((AV*)SvRV(ST(0)));') |
|
88
|
|
|
|
|
|
|
->line('HV* headers = (HV*)SvRV(ary[' . SLOT_HEADERS . ']);') |
|
89
|
|
|
|
|
|
|
->if('items > 2') |
|
90
|
|
|
|
|
|
|
->line('STRLEN klen;') |
|
91
|
|
|
|
|
|
|
->line('const char* key = SvPV(ST(1), klen);') |
|
92
|
|
|
|
|
|
|
->line('SV* val = newSVsv(ST(2));') |
|
93
|
|
|
|
|
|
|
->line('hv_store(headers, key, klen, val, 0);') |
|
94
|
|
|
|
|
|
|
->endif |
|
95
|
|
|
|
|
|
|
->line('ST(0) = ST(0);') |
|
96
|
|
|
|
|
|
|
->xs_return('1') |
|
97
|
|
|
|
|
|
|
->xs_end; |
|
98
|
|
|
|
|
|
|
|
|
99
|
|
|
|
|
|
|
# Generate header getter |
|
100
|
2
|
|
|
|
|
110
|
$builder->xs_function('jit_get_header') |
|
101
|
|
|
|
|
|
|
->xs_preamble |
|
102
|
|
|
|
|
|
|
->line('SV** ary = AvARRAY((AV*)SvRV(ST(0)));') |
|
103
|
|
|
|
|
|
|
->line('HV* headers = (HV*)SvRV(ary[' . SLOT_HEADERS . ']);') |
|
104
|
|
|
|
|
|
|
->if('items > 1') |
|
105
|
|
|
|
|
|
|
->line('STRLEN klen;') |
|
106
|
|
|
|
|
|
|
->line('const char* key = SvPV(ST(1), klen);') |
|
107
|
|
|
|
|
|
|
->line('SV** val = hv_fetch(headers, key, klen, 0);') |
|
108
|
|
|
|
|
|
|
->line('ST(0) = val && *val ? *val : &PL_sv_undef;') |
|
109
|
|
|
|
|
|
|
->else |
|
110
|
|
|
|
|
|
|
->line('ST(0) = newRV_inc((SV*)headers);') |
|
111
|
|
|
|
|
|
|
->endif |
|
112
|
|
|
|
|
|
|
->xs_return('1') |
|
113
|
|
|
|
|
|
|
->xs_end; |
|
114
|
|
|
|
|
|
|
|
|
115
|
|
|
|
|
|
|
# Generate add cookie (pushes to cookies array, returns $self) |
|
116
|
2
|
|
|
|
|
34
|
$builder->xs_function('jit_add_cookie') |
|
117
|
|
|
|
|
|
|
->xs_preamble |
|
118
|
|
|
|
|
|
|
->line('SV** ary = AvARRAY((AV*)SvRV(ST(0)));') |
|
119
|
|
|
|
|
|
|
->line('AV* cookies = (AV*)SvRV(ary[' . SLOT_COOKIES . ']);') |
|
120
|
|
|
|
|
|
|
->if('items > 1') |
|
121
|
|
|
|
|
|
|
->line('av_push(cookies, newSVsv(ST(1)));') |
|
122
|
|
|
|
|
|
|
->endif |
|
123
|
|
|
|
|
|
|
->line('ST(0) = ST(0);') |
|
124
|
|
|
|
|
|
|
->xs_return('1') |
|
125
|
|
|
|
|
|
|
->xs_end; |
|
126
|
|
|
|
|
|
|
|
|
127
|
|
|
|
|
|
|
# Generate cookies count |
|
128
|
2
|
|
|
|
|
26
|
$builder->xs_function('jit_cookies_count') |
|
129
|
|
|
|
|
|
|
->xs_preamble |
|
130
|
|
|
|
|
|
|
->line('SV** ary = AvARRAY((AV*)SvRV(ST(0)));') |
|
131
|
|
|
|
|
|
|
->line('AV* cookies = (AV*)SvRV(ary[' . SLOT_COOKIES . ']);') |
|
132
|
|
|
|
|
|
|
->line('ST(0) = sv_2mortal(newSViv(av_len(cookies) + 1));') |
|
133
|
|
|
|
|
|
|
->xs_return('1') |
|
134
|
|
|
|
|
|
|
->xs_end; |
|
135
|
|
|
|
|
|
|
|
|
136
|
|
|
|
|
|
|
# Generate cookie at index |
|
137
|
2
|
|
|
|
|
30
|
$builder->xs_function('jit_cookie_at') |
|
138
|
|
|
|
|
|
|
->xs_preamble |
|
139
|
|
|
|
|
|
|
->line('SV** ary = AvARRAY((AV*)SvRV(ST(0)));') |
|
140
|
|
|
|
|
|
|
->line('AV* cookies = (AV*)SvRV(ary[' . SLOT_COOKIES . ']);') |
|
141
|
|
|
|
|
|
|
->if('items > 1') |
|
142
|
|
|
|
|
|
|
->line('IV idx = SvIV(ST(1));') |
|
143
|
|
|
|
|
|
|
->line('SV** val = av_fetch(cookies, idx, 0);') |
|
144
|
|
|
|
|
|
|
->line('ST(0) = val && *val ? *val : &PL_sv_undef;') |
|
145
|
|
|
|
|
|
|
->else |
|
146
|
|
|
|
|
|
|
->line('ST(0) = &PL_sv_undef;') |
|
147
|
|
|
|
|
|
|
->endif |
|
148
|
|
|
|
|
|
|
->xs_return('1') |
|
149
|
|
|
|
|
|
|
->xs_end; |
|
150
|
|
|
|
|
|
|
|
|
151
|
|
|
|
|
|
|
# ============================================================ |
|
152
|
|
|
|
|
|
|
# JIT-compiled fluent API methods (return $self for chaining) |
|
153
|
|
|
|
|
|
|
# ============================================================ |
|
154
|
|
|
|
|
|
|
|
|
155
|
|
|
|
|
|
|
# JIT text() - sets Content-Type: text/plain + body |
|
156
|
2
|
|
|
|
|
56
|
$builder->xs_function('jit_text') |
|
157
|
|
|
|
|
|
|
->xs_preamble |
|
158
|
|
|
|
|
|
|
->line('SV** ary = AvARRAY((AV*)SvRV(ST(0)));') |
|
159
|
|
|
|
|
|
|
->line('HV* headers = (HV*)SvRV(ary[' . SLOT_HEADERS . ']);') |
|
160
|
|
|
|
|
|
|
->if('items > 1') |
|
161
|
|
|
|
|
|
|
->line('hv_store(headers, "Content-Type", 12, newSVpv("text/plain", 10), 0);') |
|
162
|
|
|
|
|
|
|
->line('SvREFCNT_dec(ary[' . SLOT_BODY . ']);') |
|
163
|
|
|
|
|
|
|
->line('ary[' . SLOT_BODY . '] = newSVsv(ST(1));') |
|
164
|
|
|
|
|
|
|
->endif |
|
165
|
|
|
|
|
|
|
->line('ST(0) = ST(0);') |
|
166
|
|
|
|
|
|
|
->xs_return('1') |
|
167
|
|
|
|
|
|
|
->xs_end; |
|
168
|
|
|
|
|
|
|
|
|
169
|
|
|
|
|
|
|
# JIT html() - sets Content-Type: text/html + body |
|
170
|
2
|
|
|
|
|
27
|
$builder->xs_function('jit_html') |
|
171
|
|
|
|
|
|
|
->xs_preamble |
|
172
|
|
|
|
|
|
|
->line('SV** ary = AvARRAY((AV*)SvRV(ST(0)));') |
|
173
|
|
|
|
|
|
|
->line('HV* headers = (HV*)SvRV(ary[' . SLOT_HEADERS . ']);') |
|
174
|
|
|
|
|
|
|
->if('items > 1') |
|
175
|
|
|
|
|
|
|
->line('hv_store(headers, "Content-Type", 12, newSVpv("text/html", 9), 0);') |
|
176
|
|
|
|
|
|
|
->line('SvREFCNT_dec(ary[' . SLOT_BODY . ']);') |
|
177
|
|
|
|
|
|
|
->line('ary[' . SLOT_BODY . '] = newSVsv(ST(1));') |
|
178
|
|
|
|
|
|
|
->endif |
|
179
|
|
|
|
|
|
|
->line('ST(0) = ST(0);') |
|
180
|
|
|
|
|
|
|
->xs_return('1') |
|
181
|
|
|
|
|
|
|
->xs_end; |
|
182
|
|
|
|
|
|
|
|
|
183
|
|
|
|
|
|
|
# JIT xml() - sets Content-Type: application/xml + body |
|
184
|
2
|
|
|
|
|
45
|
$builder->xs_function('jit_xml') |
|
185
|
|
|
|
|
|
|
->xs_preamble |
|
186
|
|
|
|
|
|
|
->line('SV** ary = AvARRAY((AV*)SvRV(ST(0)));') |
|
187
|
|
|
|
|
|
|
->line('HV* headers = (HV*)SvRV(ary[' . SLOT_HEADERS . ']);') |
|
188
|
|
|
|
|
|
|
->if('items > 1') |
|
189
|
|
|
|
|
|
|
->line('hv_store(headers, "Content-Type", 12, newSVpv("application/xml", 15), 0);') |
|
190
|
|
|
|
|
|
|
->line('SvREFCNT_dec(ary[' . SLOT_BODY . ']);') |
|
191
|
|
|
|
|
|
|
->line('ary[' . SLOT_BODY . '] = newSVsv(ST(1));') |
|
192
|
|
|
|
|
|
|
->endif |
|
193
|
|
|
|
|
|
|
->line('ST(0) = ST(0);') |
|
194
|
|
|
|
|
|
|
->xs_return('1') |
|
195
|
|
|
|
|
|
|
->xs_end; |
|
196
|
|
|
|
|
|
|
|
|
197
|
|
|
|
|
|
|
# JIT content_type() - sets Content-Type header |
|
198
|
2
|
|
|
|
|
33
|
$builder->xs_function('jit_content_type') |
|
199
|
|
|
|
|
|
|
->xs_preamble |
|
200
|
|
|
|
|
|
|
->line('SV** ary = AvARRAY((AV*)SvRV(ST(0)));') |
|
201
|
|
|
|
|
|
|
->line('HV* headers = (HV*)SvRV(ary[' . SLOT_HEADERS . ']);') |
|
202
|
|
|
|
|
|
|
->if('items > 1') |
|
203
|
|
|
|
|
|
|
->line('hv_store(headers, "Content-Type", 12, newSVsv(ST(1)), 0);') |
|
204
|
|
|
|
|
|
|
->endif |
|
205
|
|
|
|
|
|
|
->line('ST(0) = ST(0);') |
|
206
|
|
|
|
|
|
|
->xs_return('1') |
|
207
|
|
|
|
|
|
|
->xs_end; |
|
208
|
|
|
|
|
|
|
|
|
209
|
|
|
|
|
|
|
# JIT redirect() - sets status + Location header |
|
210
|
2
|
|
|
|
|
47
|
$builder->xs_function('jit_redirect') |
|
211
|
|
|
|
|
|
|
->xs_preamble |
|
212
|
|
|
|
|
|
|
->line('SV** ary = AvARRAY((AV*)SvRV(ST(0)));') |
|
213
|
|
|
|
|
|
|
->line('HV* headers = (HV*)SvRV(ary[' . SLOT_HEADERS . ']);') |
|
214
|
|
|
|
|
|
|
->if('items > 1') |
|
215
|
|
|
|
|
|
|
->line('hv_store(headers, "Location", 8, newSVsv(ST(1)), 0);') |
|
216
|
|
|
|
|
|
|
->line('SvREFCNT_dec(ary[' . SLOT_STATUS . ']);') |
|
217
|
|
|
|
|
|
|
# Check items > 2 AND ST(2) is defined (not undef) |
|
218
|
|
|
|
|
|
|
->if('items > 2 && SvOK(ST(2))') |
|
219
|
|
|
|
|
|
|
->line('ary[' . SLOT_STATUS . '] = newSVsv(ST(2));') |
|
220
|
|
|
|
|
|
|
->else |
|
221
|
|
|
|
|
|
|
->line('ary[' . SLOT_STATUS . '] = newSViv(302);') |
|
222
|
|
|
|
|
|
|
->endif |
|
223
|
|
|
|
|
|
|
->endif |
|
224
|
|
|
|
|
|
|
->line('ST(0) = ST(0);') |
|
225
|
|
|
|
|
|
|
->xs_return('1') |
|
226
|
|
|
|
|
|
|
->xs_end; |
|
227
|
|
|
|
|
|
|
|
|
228
|
|
|
|
|
|
|
# ============================================================ |
|
229
|
|
|
|
|
|
|
# JIT-compiled to_http() - direct HTTP response string in C |
|
230
|
|
|
|
|
|
|
# This is the highest-impact optimization for dynamic routes |
|
231
|
|
|
|
|
|
|
# ============================================================ |
|
232
|
2
|
|
|
|
|
266
|
$builder->xs_function('jit_to_http') |
|
233
|
|
|
|
|
|
|
->xs_preamble |
|
234
|
|
|
|
|
|
|
->line('SV** ary = AvARRAY((AV*)SvRV(ST(0)));') |
|
235
|
|
|
|
|
|
|
->line('IV status = SvIV(ary[' . SLOT_STATUS . ']);') |
|
236
|
|
|
|
|
|
|
->line('HV* headers = (HV*)SvRV(ary[' . SLOT_HEADERS . ']);') |
|
237
|
|
|
|
|
|
|
->line('AV* cookies = (AV*)SvRV(ary[' . SLOT_COOKIES . ']);') |
|
238
|
|
|
|
|
|
|
->line('STRLEN body_len;') |
|
239
|
|
|
|
|
|
|
->line('const char* body = SvPV(ary[' . SLOT_BODY . '], body_len);') |
|
240
|
|
|
|
|
|
|
->line('') |
|
241
|
|
|
|
|
|
|
->line('/* Status line lookup table */') |
|
242
|
|
|
|
|
|
|
->line('const char* status_text;') |
|
243
|
|
|
|
|
|
|
->line('switch(status) {') |
|
244
|
|
|
|
|
|
|
->line(' case 200: status_text = "OK"; break;') |
|
245
|
|
|
|
|
|
|
->line(' case 201: status_text = "Created"; break;') |
|
246
|
|
|
|
|
|
|
->line(' case 204: status_text = "No Content"; break;') |
|
247
|
|
|
|
|
|
|
->line(' case 301: status_text = "Moved Permanently"; break;') |
|
248
|
|
|
|
|
|
|
->line(' case 302: status_text = "Found"; break;') |
|
249
|
|
|
|
|
|
|
->line(' case 304: status_text = "Not Modified"; break;') |
|
250
|
|
|
|
|
|
|
->line(' case 307: status_text = "Temporary Redirect"; break;') |
|
251
|
|
|
|
|
|
|
->line(' case 308: status_text = "Permanent Redirect"; break;') |
|
252
|
|
|
|
|
|
|
->line(' case 400: status_text = "Bad Request"; break;') |
|
253
|
|
|
|
|
|
|
->line(' case 401: status_text = "Unauthorized"; break;') |
|
254
|
|
|
|
|
|
|
->line(' case 403: status_text = "Forbidden"; break;') |
|
255
|
|
|
|
|
|
|
->line(' case 404: status_text = "Not Found"; break;') |
|
256
|
|
|
|
|
|
|
->line(' case 409: status_text = "Conflict"; break;') |
|
257
|
|
|
|
|
|
|
->line(' case 422: status_text = "Unprocessable Entity"; break;') |
|
258
|
|
|
|
|
|
|
->line(' case 429: status_text = "Too Many Requests"; break;') |
|
259
|
|
|
|
|
|
|
->line(' case 500: status_text = "Internal Server Error"; break;') |
|
260
|
|
|
|
|
|
|
->line(' case 502: status_text = "Bad Gateway"; break;') |
|
261
|
|
|
|
|
|
|
->line(' case 503: status_text = "Service Unavailable"; break;') |
|
262
|
|
|
|
|
|
|
->line(' case 504: status_text = "Gateway Timeout"; break;') |
|
263
|
|
|
|
|
|
|
->line(' default: status_text = "Unknown"; break;') |
|
264
|
|
|
|
|
|
|
->line('}') |
|
265
|
|
|
|
|
|
|
->line('') |
|
266
|
|
|
|
|
|
|
->line('/* Calculate total size needed */') |
|
267
|
|
|
|
|
|
|
->line('SSize_t total_size = 64; /* Status line overhead */') |
|
268
|
|
|
|
|
|
|
->line('total_size += body_len;') |
|
269
|
|
|
|
|
|
|
->line('total_size += 32; /* Content-Length header */') |
|
270
|
|
|
|
|
|
|
->line('') |
|
271
|
|
|
|
|
|
|
->line('/* Iterate headers to calculate size */') |
|
272
|
|
|
|
|
|
|
->line('HE* entry;') |
|
273
|
|
|
|
|
|
|
->line('hv_iterinit(headers);') |
|
274
|
|
|
|
|
|
|
->line('while ((entry = hv_iternext(headers))) {') |
|
275
|
|
|
|
|
|
|
->line(' I32 klen;') |
|
276
|
|
|
|
|
|
|
->line(' const char* key = hv_iterkey(entry, &klen);') |
|
277
|
|
|
|
|
|
|
->line(' SV* val = hv_iterval(headers, entry);') |
|
278
|
|
|
|
|
|
|
->line(' STRLEN vlen;') |
|
279
|
|
|
|
|
|
|
->line(' SvPV(val, vlen);') |
|
280
|
|
|
|
|
|
|
->line(' total_size += klen + vlen + 4; /* key: value\\r\\n */') |
|
281
|
|
|
|
|
|
|
->line('}') |
|
282
|
|
|
|
|
|
|
->line('') |
|
283
|
|
|
|
|
|
|
->line('/* Add cookies size */') |
|
284
|
|
|
|
|
|
|
->line('SSize_t ncookies = av_len(cookies) + 1;') |
|
285
|
|
|
|
|
|
|
->line('for (SSize_t i = 0; i < ncookies; i++) {') |
|
286
|
|
|
|
|
|
|
->line(' SV** cv = av_fetch(cookies, i, 0);') |
|
287
|
|
|
|
|
|
|
->line(' if (cv && *cv) {') |
|
288
|
|
|
|
|
|
|
->line(' STRLEN clen;') |
|
289
|
|
|
|
|
|
|
->line(' SvPV(*cv, clen);') |
|
290
|
|
|
|
|
|
|
->line(' total_size += 14 + clen + 2; /* Set-Cookie: ... \\r\\n */') |
|
291
|
|
|
|
|
|
|
->line(' }') |
|
292
|
|
|
|
|
|
|
->line('}') |
|
293
|
|
|
|
|
|
|
->line('') |
|
294
|
|
|
|
|
|
|
->line('/* Build response string */') |
|
295
|
|
|
|
|
|
|
->line('SV* result = newSV(total_size + 4);') |
|
296
|
|
|
|
|
|
|
->line('SvPOK_on(result);') |
|
297
|
|
|
|
|
|
|
->line('char* p = SvPVX(result);') |
|
298
|
|
|
|
|
|
|
->line('') |
|
299
|
|
|
|
|
|
|
->line('/* Status line */') |
|
300
|
|
|
|
|
|
|
->line('p += sprintf(p, "HTTP/1.1 %d %s\\r\\n", (int)status, status_text);') |
|
301
|
|
|
|
|
|
|
->line('') |
|
302
|
|
|
|
|
|
|
->line('/* Headers */') |
|
303
|
|
|
|
|
|
|
->line('hv_iterinit(headers);') |
|
304
|
|
|
|
|
|
|
->line('while ((entry = hv_iternext(headers))) {') |
|
305
|
|
|
|
|
|
|
->line(' I32 klen;') |
|
306
|
|
|
|
|
|
|
->line(' const char* key = hv_iterkey(entry, &klen);') |
|
307
|
|
|
|
|
|
|
->line(' SV* val = hv_iterval(headers, entry);') |
|
308
|
|
|
|
|
|
|
->line(' STRLEN vlen;') |
|
309
|
|
|
|
|
|
|
->line(' const char* vp = SvPV(val, vlen);') |
|
310
|
|
|
|
|
|
|
->line(' memcpy(p, key, klen); p += klen;') |
|
311
|
|
|
|
|
|
|
->line(' *p++ = \':\'; *p++ = \' \';') |
|
312
|
|
|
|
|
|
|
->line(' memcpy(p, vp, vlen); p += vlen;') |
|
313
|
|
|
|
|
|
|
->line(' *p++ = \'\\r\'; *p++ = \'\\n\';') |
|
314
|
|
|
|
|
|
|
->line('}') |
|
315
|
|
|
|
|
|
|
->line('') |
|
316
|
|
|
|
|
|
|
->line('/* Cookies */') |
|
317
|
|
|
|
|
|
|
->line('for (SSize_t i = 0; i < ncookies; i++) {') |
|
318
|
|
|
|
|
|
|
->line(' SV** cv = av_fetch(cookies, i, 0);') |
|
319
|
|
|
|
|
|
|
->line(' if (cv && *cv) {') |
|
320
|
|
|
|
|
|
|
->line(' STRLEN clen;') |
|
321
|
|
|
|
|
|
|
->line(' const char* cookie = SvPV(*cv, clen);') |
|
322
|
|
|
|
|
|
|
->line(' memcpy(p, "Set-Cookie: ", 12); p += 12;') |
|
323
|
|
|
|
|
|
|
->line(' memcpy(p, cookie, clen); p += clen;') |
|
324
|
|
|
|
|
|
|
->line(' *p++ = \'\\r\'; *p++ = \'\\n\';') |
|
325
|
|
|
|
|
|
|
->line(' }') |
|
326
|
|
|
|
|
|
|
->line('}') |
|
327
|
|
|
|
|
|
|
->line('') |
|
328
|
|
|
|
|
|
|
->line('/* Content-Length and blank line */') |
|
329
|
|
|
|
|
|
|
->line('p += sprintf(p, "Content-Length: %lu\\r\\n\\r\\n", (unsigned long)body_len);') |
|
330
|
|
|
|
|
|
|
->line('') |
|
331
|
|
|
|
|
|
|
->line('/* Body */') |
|
332
|
|
|
|
|
|
|
->line('memcpy(p, body, body_len);') |
|
333
|
|
|
|
|
|
|
->line('p += body_len;') |
|
334
|
|
|
|
|
|
|
->line('') |
|
335
|
|
|
|
|
|
|
->line('SvCUR_set(result, p - SvPVX(result));') |
|
336
|
|
|
|
|
|
|
->line('ST(0) = sv_2mortal(result);') |
|
337
|
|
|
|
|
|
|
->xs_return('1') |
|
338
|
|
|
|
|
|
|
->xs_end; |
|
339
|
|
|
|
|
|
|
|
|
340
|
|
|
|
|
|
|
# ============================================================ |
|
341
|
|
|
|
|
|
|
# JIT-compiled HTTP date formatting (for Last-Modified, etc.) |
|
342
|
|
|
|
|
|
|
# ============================================================ |
|
343
|
2
|
|
|
|
|
10
|
$builder->line('#include ') |
|
344
|
|
|
|
|
|
|
->blank; |
|
345
|
|
|
|
|
|
|
|
|
346
|
2
|
|
|
|
|
45
|
$builder->xs_function('jit_http_date') |
|
347
|
|
|
|
|
|
|
->xs_preamble |
|
348
|
|
|
|
|
|
|
->line('time_t t = items > 0 && SvOK(ST(0)) ? (time_t)SvIV(ST(0)) : time(NULL);') |
|
349
|
|
|
|
|
|
|
->line('struct tm* gm = gmtime(&t);') |
|
350
|
|
|
|
|
|
|
->blank |
|
351
|
|
|
|
|
|
|
->line('static const char* days[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};') |
|
352
|
|
|
|
|
|
|
->line('static const char* months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};') |
|
353
|
|
|
|
|
|
|
->blank |
|
354
|
|
|
|
|
|
|
->line('char buf[32];') |
|
355
|
|
|
|
|
|
|
->line('int len = sprintf(buf, "%s, %02d %s %04d %02d:%02d:%02d GMT",') |
|
356
|
|
|
|
|
|
|
->line(' days[gm->tm_wday], gm->tm_mday, months[gm->tm_mon],') |
|
357
|
|
|
|
|
|
|
->line(' gm->tm_year + 1900, gm->tm_hour, gm->tm_min, gm->tm_sec);') |
|
358
|
|
|
|
|
|
|
->blank |
|
359
|
|
|
|
|
|
|
->line('ST(0) = sv_2mortal(newSVpv(buf, len));') |
|
360
|
|
|
|
|
|
|
->xs_return('1') |
|
361
|
|
|
|
|
|
|
->xs_end; |
|
362
|
|
|
|
|
|
|
|
|
363
|
|
|
|
|
|
|
# Compile via XS::JIT |
|
364
|
2
|
|
|
|
|
1314124
|
XS::JIT->compile( |
|
365
|
|
|
|
|
|
|
code => $builder->code, |
|
366
|
|
|
|
|
|
|
name => $module_name, |
|
367
|
|
|
|
|
|
|
cache_dir => $cache_dir, |
|
368
|
|
|
|
|
|
|
functions => { |
|
369
|
|
|
|
|
|
|
# Status |
|
370
|
|
|
|
|
|
|
'Hypersonic::Response::_jit_get_status' => { source => 'jit_get_status', is_xs_native => 1 }, |
|
371
|
|
|
|
|
|
|
'Hypersonic::Response::_jit_set_status' => { source => 'jit_set_status', is_xs_native => 1 }, |
|
372
|
|
|
|
|
|
|
|
|
373
|
|
|
|
|
|
|
# Body |
|
374
|
|
|
|
|
|
|
'Hypersonic::Response::_jit_get_body' => { source => 'jit_get_body', is_xs_native => 1 }, |
|
375
|
|
|
|
|
|
|
'Hypersonic::Response::_jit_set_body' => { source => 'jit_set_body', is_xs_native => 1 }, |
|
376
|
|
|
|
|
|
|
|
|
377
|
|
|
|
|
|
|
# Headers |
|
378
|
|
|
|
|
|
|
'Hypersonic::Response::_jit_get_headers_hv' => { source => 'jit_get_headers_hv', is_xs_native => 1 }, |
|
379
|
|
|
|
|
|
|
'Hypersonic::Response::_jit_set_header' => { source => 'jit_set_header', is_xs_native => 1 }, |
|
380
|
|
|
|
|
|
|
'Hypersonic::Response::_jit_get_header' => { source => 'jit_get_header', is_xs_native => 1 }, |
|
381
|
|
|
|
|
|
|
|
|
382
|
|
|
|
|
|
|
# Cookies |
|
383
|
|
|
|
|
|
|
'Hypersonic::Response::_jit_get_cookies_av' => { source => 'jit_get_cookies_av', is_xs_native => 1 }, |
|
384
|
|
|
|
|
|
|
'Hypersonic::Response::_jit_add_cookie' => { source => 'jit_add_cookie', is_xs_native => 1 }, |
|
385
|
|
|
|
|
|
|
'Hypersonic::Response::_jit_cookies_count' => { source => 'jit_cookies_count', is_xs_native => 1 }, |
|
386
|
|
|
|
|
|
|
'Hypersonic::Response::_jit_cookie_at' => { source => 'jit_cookie_at', is_xs_native => 1 }, |
|
387
|
|
|
|
|
|
|
|
|
388
|
|
|
|
|
|
|
# Fluent API methods (JIT-compiled) |
|
389
|
|
|
|
|
|
|
'Hypersonic::Response::_jit_text' => { source => 'jit_text', is_xs_native => 1 }, |
|
390
|
|
|
|
|
|
|
'Hypersonic::Response::_jit_html' => { source => 'jit_html', is_xs_native => 1 }, |
|
391
|
|
|
|
|
|
|
'Hypersonic::Response::_jit_xml' => { source => 'jit_xml', is_xs_native => 1 }, |
|
392
|
|
|
|
|
|
|
'Hypersonic::Response::_jit_content_type' => { source => 'jit_content_type', is_xs_native => 1 }, |
|
393
|
|
|
|
|
|
|
'Hypersonic::Response::_jit_redirect' => { source => 'jit_redirect', is_xs_native => 1 }, |
|
394
|
|
|
|
|
|
|
|
|
395
|
|
|
|
|
|
|
# Direct HTTP response generation |
|
396
|
|
|
|
|
|
|
'Hypersonic::Response::_jit_to_http' => { source => 'jit_to_http', is_xs_native => 1 }, |
|
397
|
|
|
|
|
|
|
|
|
398
|
|
|
|
|
|
|
# Utility functions |
|
399
|
|
|
|
|
|
|
'Hypersonic::Response::_jit_http_date' => { source => 'jit_http_date', is_xs_native => 1 }, |
|
400
|
|
|
|
|
|
|
}, |
|
401
|
|
|
|
|
|
|
); |
|
402
|
|
|
|
|
|
|
|
|
403
|
2
|
|
|
|
|
66
|
$COMPILED = 1; |
|
404
|
2
|
|
|
|
|
30
|
return 1; |
|
405
|
|
|
|
|
|
|
} |
|
406
|
|
|
|
|
|
|
|
|
407
|
|
|
|
|
|
|
# Constructor - creates array-based response object |
|
408
|
|
|
|
|
|
|
sub new { |
|
409
|
85
|
|
|
85
|
1
|
327629
|
my ($class, %opts) = @_; |
|
410
|
|
|
|
|
|
|
|
|
411
|
|
|
|
|
|
|
# Compile accessors if not already done |
|
412
|
85
|
100
|
|
|
|
224
|
$class->compile_accessors(cache_dir => $opts{cache_dir}) unless $COMPILED; |
|
413
|
|
|
|
|
|
|
|
|
414
|
85
|
|
|
|
|
158
|
my $self = bless [], $class; |
|
415
|
|
|
|
|
|
|
|
|
416
|
|
|
|
|
|
|
# Initialize slots |
|
417
|
85
|
|
100
|
|
|
396
|
$self->[SLOT_STATUS] = $opts{status} // 200; |
|
418
|
85
|
|
100
|
|
|
311
|
$self->[SLOT_HEADERS] = $opts{headers} // {}; |
|
419
|
85
|
|
100
|
|
|
215
|
$self->[SLOT_BODY] = $opts{body} // ''; |
|
420
|
85
|
|
|
|
|
110
|
$self->[SLOT_COOKIES] = []; |
|
421
|
|
|
|
|
|
|
|
|
422
|
85
|
|
|
|
|
303
|
return $self; |
|
423
|
|
|
|
|
|
|
} |
|
424
|
|
|
|
|
|
|
|
|
425
|
|
|
|
|
|
|
# Fluent API methods - all return $self for chaining |
|
426
|
|
|
|
|
|
|
# Core methods use JIT-compiled XS for maximum speed when available |
|
427
|
|
|
|
|
|
|
|
|
428
|
|
|
|
|
|
|
# Set HTTP status code (JIT-compiled when available) |
|
429
|
|
|
|
|
|
|
sub status { |
|
430
|
29
|
|
|
29
|
1
|
1552
|
my ($self, $code) = @_; |
|
431
|
29
|
50
|
33
|
|
|
122
|
if ($COMPILED && defined $code) { |
|
432
|
29
|
|
|
|
|
146
|
return $self->_jit_set_status($code); |
|
433
|
|
|
|
|
|
|
} |
|
434
|
0
|
0
|
|
|
|
0
|
$self->[SLOT_STATUS] = $code if defined $code; |
|
435
|
0
|
|
|
|
|
0
|
return $self; |
|
436
|
|
|
|
|
|
|
} |
|
437
|
|
|
|
|
|
|
|
|
438
|
|
|
|
|
|
|
# Set a response header (JIT-compiled when available) |
|
439
|
|
|
|
|
|
|
sub header { |
|
440
|
9
|
|
|
9
|
1
|
1086
|
my ($self, $name, $value) = @_; |
|
441
|
9
|
50
|
|
|
|
18
|
if ($COMPILED) { |
|
442
|
9
|
|
|
|
|
44
|
return $self->_jit_set_header($name, $value); |
|
443
|
|
|
|
|
|
|
} |
|
444
|
0
|
|
|
|
|
0
|
$self->[SLOT_HEADERS]{$name} = $value; |
|
445
|
0
|
|
|
|
|
0
|
return $self; |
|
446
|
|
|
|
|
|
|
} |
|
447
|
|
|
|
|
|
|
|
|
448
|
|
|
|
|
|
|
# Set multiple headers at once |
|
449
|
|
|
|
|
|
|
sub headers { |
|
450
|
2
|
|
|
2
|
1
|
1945
|
my ($self, %headers) = @_; |
|
451
|
2
|
50
|
|
|
|
5
|
if ($COMPILED) { |
|
452
|
2
|
|
|
|
|
15
|
$self->_jit_set_header($_, $headers{$_}) for keys %headers; |
|
453
|
|
|
|
|
|
|
} else { |
|
454
|
0
|
|
|
|
|
0
|
$self->[SLOT_HEADERS]{$_} = $headers{$_} for keys %headers; |
|
455
|
|
|
|
|
|
|
} |
|
456
|
2
|
|
|
|
|
4
|
return $self; |
|
457
|
|
|
|
|
|
|
} |
|
458
|
|
|
|
|
|
|
|
|
459
|
|
|
|
|
|
|
# Set response body (JIT-compiled when available) |
|
460
|
|
|
|
|
|
|
sub body { |
|
461
|
21
|
|
|
21
|
1
|
1223
|
my ($self, $content) = @_; |
|
462
|
21
|
50
|
|
|
|
32
|
if ($COMPILED) { |
|
463
|
21
|
|
|
|
|
64
|
return $self->_jit_set_body($content); |
|
464
|
|
|
|
|
|
|
} |
|
465
|
0
|
|
|
|
|
0
|
$self->[SLOT_BODY] = $content; |
|
466
|
0
|
|
|
|
|
0
|
return $self; |
|
467
|
|
|
|
|
|
|
} |
|
468
|
|
|
|
|
|
|
|
|
469
|
|
|
|
|
|
|
# Set JSON response (auto-sets Content-Type and encodes data) |
|
470
|
|
|
|
|
|
|
sub json { |
|
471
|
11
|
|
|
11
|
1
|
21
|
my ($self, $data) = @_; |
|
472
|
11
|
|
|
|
|
1380
|
require Cpanel::JSON::XS; |
|
473
|
11
|
|
|
|
|
4330
|
$self->[SLOT_HEADERS]{'Content-Type'} = 'application/json'; |
|
474
|
11
|
|
|
|
|
88
|
$self->[SLOT_BODY] = Cpanel::JSON::XS::encode_json($data); |
|
475
|
11
|
|
|
|
|
29
|
return $self; |
|
476
|
|
|
|
|
|
|
} |
|
477
|
|
|
|
|
|
|
|
|
478
|
|
|
|
|
|
|
# Set plain text response (JIT-compiled when available) |
|
479
|
|
|
|
|
|
|
sub text { |
|
480
|
7
|
|
|
7
|
1
|
22
|
my ($self, $content) = @_; |
|
481
|
7
|
50
|
|
|
|
30
|
if ($COMPILED) { |
|
482
|
7
|
|
|
|
|
188
|
return $self->_jit_text($content); |
|
483
|
|
|
|
|
|
|
} |
|
484
|
0
|
|
|
|
|
0
|
$self->[SLOT_HEADERS]{'Content-Type'} = 'text/plain'; |
|
485
|
0
|
|
|
|
|
0
|
$self->[SLOT_BODY] = $content; |
|
486
|
0
|
|
|
|
|
0
|
return $self; |
|
487
|
|
|
|
|
|
|
} |
|
488
|
|
|
|
|
|
|
|
|
489
|
|
|
|
|
|
|
# Set HTML response (JIT-compiled when available) |
|
490
|
|
|
|
|
|
|
sub html { |
|
491
|
1
|
|
|
1
|
1
|
9
|
my ($self, $content) = @_; |
|
492
|
1
|
50
|
|
|
|
15
|
if ($COMPILED) { |
|
493
|
1
|
|
|
|
|
11
|
return $self->_jit_html($content); |
|
494
|
|
|
|
|
|
|
} |
|
495
|
0
|
|
|
|
|
0
|
$self->[SLOT_HEADERS]{'Content-Type'} = 'text/html'; |
|
496
|
0
|
|
|
|
|
0
|
$self->[SLOT_BODY] = $content; |
|
497
|
0
|
|
|
|
|
0
|
return $self; |
|
498
|
|
|
|
|
|
|
} |
|
499
|
|
|
|
|
|
|
|
|
500
|
|
|
|
|
|
|
# Set XML response (JIT-compiled when available) |
|
501
|
|
|
|
|
|
|
sub xml { |
|
502
|
1
|
|
|
1
|
1
|
5
|
my ($self, $content) = @_; |
|
503
|
1
|
50
|
|
|
|
3
|
if ($COMPILED) { |
|
504
|
1
|
|
|
|
|
43
|
return $self->_jit_xml($content); |
|
505
|
|
|
|
|
|
|
} |
|
506
|
0
|
|
|
|
|
0
|
$self->[SLOT_HEADERS]{'Content-Type'} = 'application/xml'; |
|
507
|
0
|
|
|
|
|
0
|
$self->[SLOT_BODY] = $content; |
|
508
|
0
|
|
|
|
|
0
|
return $self; |
|
509
|
|
|
|
|
|
|
} |
|
510
|
|
|
|
|
|
|
|
|
511
|
|
|
|
|
|
|
# Set redirect response (JIT-compiled when available) |
|
512
|
|
|
|
|
|
|
sub redirect { |
|
513
|
3
|
|
|
3
|
1
|
14
|
my ($self, $url, $code) = @_; |
|
514
|
3
|
50
|
|
|
|
6
|
if ($COMPILED) { |
|
515
|
3
|
|
|
|
|
16
|
return $self->_jit_redirect($url, $code); |
|
516
|
|
|
|
|
|
|
} |
|
517
|
0
|
|
0
|
|
|
0
|
$self->[SLOT_STATUS] = $code // 302; |
|
518
|
0
|
|
|
|
|
0
|
$self->[SLOT_HEADERS]{'Location'} = $url; |
|
519
|
0
|
|
|
|
|
0
|
return $self; |
|
520
|
|
|
|
|
|
|
} |
|
521
|
|
|
|
|
|
|
|
|
522
|
|
|
|
|
|
|
# Set a cookie |
|
523
|
|
|
|
|
|
|
sub cookie { |
|
524
|
18
|
|
|
18
|
1
|
1826
|
my ($self, $name, $value, %opts) = @_; |
|
525
|
|
|
|
|
|
|
|
|
526
|
18
|
|
|
|
|
36
|
my $cookie = "$name=$value"; |
|
527
|
18
|
100
|
|
|
|
48
|
$cookie .= "; Path=$opts{path}" if defined $opts{path}; |
|
528
|
18
|
100
|
|
|
|
41
|
$cookie .= "; Domain=$opts{domain}" if defined $opts{domain}; |
|
529
|
18
|
100
|
|
|
|
48
|
$cookie .= "; Max-Age=$opts{max_age}" if defined $opts{max_age}; |
|
530
|
18
|
100
|
|
|
|
41
|
$cookie .= "; Expires=$opts{expires}" if defined $opts{expires}; |
|
531
|
18
|
100
|
|
|
|
41
|
$cookie .= "; HttpOnly" if $opts{httponly}; |
|
532
|
18
|
100
|
|
|
|
107
|
$cookie .= "; Secure" if $opts{secure}; |
|
533
|
18
|
100
|
|
|
|
44
|
$cookie .= "; SameSite=$opts{samesite}" if defined $opts{samesite}; |
|
534
|
|
|
|
|
|
|
|
|
535
|
18
|
|
|
|
|
24
|
push @{$self->[SLOT_COOKIES]}, $cookie; |
|
|
18
|
|
|
|
|
44
|
|
|
536
|
18
|
|
|
|
|
46
|
return $self; |
|
537
|
|
|
|
|
|
|
} |
|
538
|
|
|
|
|
|
|
|
|
539
|
|
|
|
|
|
|
# Clear a cookie (set to expire immediately) |
|
540
|
|
|
|
|
|
|
sub clear_cookie { |
|
541
|
2
|
|
|
2
|
1
|
9
|
my ($self, $name, %opts) = @_; |
|
542
|
2
|
|
|
|
|
8
|
return $self->cookie($name, '', max_age => 0, %opts); |
|
543
|
|
|
|
|
|
|
} |
|
544
|
|
|
|
|
|
|
|
|
545
|
|
|
|
|
|
|
# Set Content-Type header directly (JIT-compiled when available) |
|
546
|
|
|
|
|
|
|
sub content_type { |
|
547
|
4
|
|
|
4
|
1
|
35
|
my ($self, $type) = @_; |
|
548
|
4
|
50
|
|
|
|
68
|
if ($COMPILED) { |
|
549
|
4
|
|
|
|
|
27
|
return $self->_jit_content_type($type); |
|
550
|
|
|
|
|
|
|
} |
|
551
|
0
|
|
|
|
|
0
|
$self->[SLOT_HEADERS]{'Content-Type'} = $type; |
|
552
|
0
|
|
|
|
|
0
|
return $self; |
|
553
|
|
|
|
|
|
|
} |
|
554
|
|
|
|
|
|
|
|
|
555
|
|
|
|
|
|
|
# Set Cache-Control header |
|
556
|
|
|
|
|
|
|
sub cache { |
|
557
|
1
|
|
|
1
|
1
|
6
|
my ($self, $directive) = @_; |
|
558
|
1
|
|
|
|
|
4
|
$self->[SLOT_HEADERS]{'Cache-Control'} = $directive; |
|
559
|
1
|
|
|
|
|
2
|
return $self; |
|
560
|
|
|
|
|
|
|
} |
|
561
|
|
|
|
|
|
|
|
|
562
|
|
|
|
|
|
|
# Disable caching |
|
563
|
|
|
|
|
|
|
sub no_cache { |
|
564
|
1
|
|
|
1
|
1
|
9
|
my ($self) = @_; |
|
565
|
1
|
|
|
|
|
3
|
$self->[SLOT_HEADERS]{'Cache-Control'} = 'no-store, no-cache, must-revalidate'; |
|
566
|
1
|
|
|
|
|
2
|
$self->[SLOT_HEADERS]{'Pragma'} = 'no-cache'; |
|
567
|
1
|
|
|
|
|
3
|
return $self; |
|
568
|
|
|
|
|
|
|
} |
|
569
|
|
|
|
|
|
|
|
|
570
|
|
|
|
|
|
|
# Set ETag header |
|
571
|
|
|
|
|
|
|
sub etag { |
|
572
|
1
|
|
|
1
|
1
|
7
|
my ($self, $value) = @_; |
|
573
|
1
|
|
|
|
|
5
|
$self->[SLOT_HEADERS]{'ETag'} = qq("$value"); |
|
574
|
1
|
|
|
|
|
2
|
return $self; |
|
575
|
|
|
|
|
|
|
} |
|
576
|
|
|
|
|
|
|
|
|
577
|
|
|
|
|
|
|
# Set Last-Modified header |
|
578
|
|
|
|
|
|
|
sub last_modified { |
|
579
|
1
|
|
|
1
|
1
|
7
|
my ($self, $time) = @_; |
|
580
|
1
|
|
|
|
|
6
|
$self->[SLOT_HEADERS]{'Last-Modified'} = _http_date($time); |
|
581
|
1
|
|
|
|
|
4
|
return $self; |
|
582
|
|
|
|
|
|
|
} |
|
583
|
|
|
|
|
|
|
|
|
584
|
|
|
|
|
|
|
# Set Content-Disposition for file downloads |
|
585
|
|
|
|
|
|
|
sub attachment { |
|
586
|
1
|
|
|
1
|
1
|
7
|
my ($self, $filename) = @_; |
|
587
|
1
|
50
|
|
|
|
3
|
if (defined $filename) { |
|
588
|
1
|
|
|
|
|
5
|
$self->[SLOT_HEADERS]{'Content-Disposition'} = qq(attachment; filename="$filename"); |
|
589
|
|
|
|
|
|
|
} else { |
|
590
|
0
|
|
|
|
|
0
|
$self->[SLOT_HEADERS]{'Content-Disposition'} = 'attachment'; |
|
591
|
|
|
|
|
|
|
} |
|
592
|
1
|
|
|
|
|
2
|
return $self; |
|
593
|
|
|
|
|
|
|
} |
|
594
|
|
|
|
|
|
|
|
|
595
|
|
|
|
|
|
|
# Convenience: 201 Created with optional Location header |
|
596
|
|
|
|
|
|
|
sub created { |
|
597
|
1
|
|
|
1
|
1
|
7
|
my ($self, $location) = @_; |
|
598
|
1
|
|
|
|
|
3
|
$self->[SLOT_STATUS] = 201; |
|
599
|
1
|
50
|
|
|
|
5
|
$self->[SLOT_HEADERS]{'Location'} = $location if defined $location; |
|
600
|
1
|
|
|
|
|
2
|
return $self; |
|
601
|
|
|
|
|
|
|
} |
|
602
|
|
|
|
|
|
|
|
|
603
|
|
|
|
|
|
|
# Convenience: 204 No Content |
|
604
|
|
|
|
|
|
|
sub no_content { |
|
605
|
1
|
|
|
1
|
1
|
7
|
my ($self) = @_; |
|
606
|
1
|
|
|
|
|
2
|
$self->[SLOT_STATUS] = 204; |
|
607
|
1
|
|
|
|
|
2
|
$self->[SLOT_BODY] = ''; |
|
608
|
1
|
|
|
|
|
2
|
return $self; |
|
609
|
|
|
|
|
|
|
} |
|
610
|
|
|
|
|
|
|
|
|
611
|
|
|
|
|
|
|
# Convenience: 400 Bad Request |
|
612
|
|
|
|
|
|
|
sub bad_request { |
|
613
|
3
|
|
|
3
|
1
|
16
|
my ($self, $message) = @_; |
|
614
|
3
|
|
|
|
|
6
|
$self->[SLOT_STATUS] = 400; |
|
615
|
3
|
|
|
|
|
7
|
$self->[SLOT_HEADERS]{'Content-Type'} = 'application/json'; |
|
616
|
3
|
|
50
|
|
|
13
|
$self->[SLOT_BODY] = _json_error($message // 'Bad Request'); |
|
617
|
3
|
|
|
|
|
5
|
return $self; |
|
618
|
|
|
|
|
|
|
} |
|
619
|
|
|
|
|
|
|
|
|
620
|
|
|
|
|
|
|
# Convenience: 401 Unauthorized |
|
621
|
|
|
|
|
|
|
sub unauthorized { |
|
622
|
2
|
|
|
2
|
1
|
11
|
my ($self, $message) = @_; |
|
623
|
2
|
|
|
|
|
5
|
$self->[SLOT_STATUS] = 401; |
|
624
|
2
|
|
|
|
|
5
|
$self->[SLOT_HEADERS]{'Content-Type'} = 'application/json'; |
|
625
|
2
|
|
100
|
|
|
10
|
$self->[SLOT_BODY] = _json_error($message // 'Unauthorized'); |
|
626
|
2
|
|
|
|
|
5
|
return $self; |
|
627
|
|
|
|
|
|
|
} |
|
628
|
|
|
|
|
|
|
|
|
629
|
|
|
|
|
|
|
# Convenience: 403 Forbidden |
|
630
|
|
|
|
|
|
|
sub forbidden { |
|
631
|
2
|
|
|
2
|
1
|
15
|
my ($self, $message) = @_; |
|
632
|
2
|
|
|
|
|
6
|
$self->[SLOT_STATUS] = 403; |
|
633
|
2
|
|
|
|
|
6
|
$self->[SLOT_HEADERS]{'Content-Type'} = 'application/json'; |
|
634
|
2
|
|
100
|
|
|
13
|
$self->[SLOT_BODY] = _json_error($message // 'Forbidden'); |
|
635
|
2
|
|
|
|
|
5
|
return $self; |
|
636
|
|
|
|
|
|
|
} |
|
637
|
|
|
|
|
|
|
|
|
638
|
|
|
|
|
|
|
# Convenience: 404 Not Found |
|
639
|
|
|
|
|
|
|
sub not_found { |
|
640
|
2
|
|
|
2
|
1
|
12
|
my ($self, $message) = @_; |
|
641
|
2
|
|
|
|
|
5
|
$self->[SLOT_STATUS] = 404; |
|
642
|
2
|
|
|
|
|
7
|
$self->[SLOT_HEADERS]{'Content-Type'} = 'application/json'; |
|
643
|
2
|
|
100
|
|
|
11
|
$self->[SLOT_BODY] = _json_error($message // 'Not Found'); |
|
644
|
2
|
|
|
|
|
5
|
return $self; |
|
645
|
|
|
|
|
|
|
} |
|
646
|
|
|
|
|
|
|
|
|
647
|
|
|
|
|
|
|
# Convenience: 409 Conflict |
|
648
|
|
|
|
|
|
|
sub conflict { |
|
649
|
1
|
|
|
1
|
1
|
7
|
my ($self, $message) = @_; |
|
650
|
1
|
|
|
|
|
3
|
$self->[SLOT_STATUS] = 409; |
|
651
|
1
|
|
|
|
|
3
|
$self->[SLOT_HEADERS]{'Content-Type'} = 'application/json'; |
|
652
|
1
|
|
50
|
|
|
5
|
$self->[SLOT_BODY] = _json_error($message // 'Conflict'); |
|
653
|
1
|
|
|
|
|
2
|
return $self; |
|
654
|
|
|
|
|
|
|
} |
|
655
|
|
|
|
|
|
|
|
|
656
|
|
|
|
|
|
|
# Convenience: 422 Unprocessable Entity |
|
657
|
|
|
|
|
|
|
sub unprocessable { |
|
658
|
1
|
|
|
1
|
1
|
6
|
my ($self, $message) = @_; |
|
659
|
1
|
|
|
|
|
1
|
$self->[SLOT_STATUS] = 422; |
|
660
|
1
|
|
|
|
|
3
|
$self->[SLOT_HEADERS]{'Content-Type'} = 'application/json'; |
|
661
|
1
|
|
50
|
|
|
4
|
$self->[SLOT_BODY] = _json_error($message // 'Unprocessable Entity'); |
|
662
|
1
|
|
|
|
|
2
|
return $self; |
|
663
|
|
|
|
|
|
|
} |
|
664
|
|
|
|
|
|
|
|
|
665
|
|
|
|
|
|
|
# Convenience: 429 Too Many Requests |
|
666
|
|
|
|
|
|
|
sub too_many_requests { |
|
667
|
1
|
|
|
1
|
1
|
5
|
my ($self, $retry_after) = @_; |
|
668
|
1
|
|
|
|
|
1
|
$self->[SLOT_STATUS] = 429; |
|
669
|
1
|
|
|
|
|
4
|
$self->[SLOT_HEADERS]{'Content-Type'} = 'application/json'; |
|
670
|
1
|
50
|
|
|
|
3
|
$self->[SLOT_HEADERS]{'Retry-After'} = $retry_after if defined $retry_after; |
|
671
|
1
|
|
|
|
|
3
|
$self->[SLOT_BODY] = _json_error('Too Many Requests'); |
|
672
|
1
|
|
|
|
|
3
|
return $self; |
|
673
|
|
|
|
|
|
|
} |
|
674
|
|
|
|
|
|
|
|
|
675
|
|
|
|
|
|
|
# Convenience: 500 Internal Server Error |
|
676
|
|
|
|
|
|
|
sub server_error { |
|
677
|
2
|
|
|
2
|
1
|
12
|
my ($self, $message) = @_; |
|
678
|
2
|
|
|
|
|
5
|
$self->[SLOT_STATUS] = 500; |
|
679
|
2
|
|
|
|
|
4
|
$self->[SLOT_HEADERS]{'Content-Type'} = 'application/json'; |
|
680
|
2
|
|
100
|
|
|
11
|
$self->[SLOT_BODY] = _json_error($message // 'Internal Server Error'); |
|
681
|
2
|
|
|
|
|
5
|
return $self; |
|
682
|
|
|
|
|
|
|
} |
|
683
|
|
|
|
|
|
|
|
|
684
|
|
|
|
|
|
|
# Convenience: 503 Service Unavailable |
|
685
|
|
|
|
|
|
|
sub unavailable { |
|
686
|
1
|
|
|
1
|
1
|
5
|
my ($self, $retry_after) = @_; |
|
687
|
1
|
|
|
|
|
2
|
$self->[SLOT_STATUS] = 503; |
|
688
|
1
|
|
|
|
|
3
|
$self->[SLOT_HEADERS]{'Content-Type'} = 'application/json'; |
|
689
|
1
|
50
|
|
|
|
3
|
$self->[SLOT_HEADERS]{'Retry-After'} = $retry_after if defined $retry_after; |
|
690
|
1
|
|
|
|
|
2
|
$self->[SLOT_BODY] = _json_error('Service Unavailable'); |
|
691
|
1
|
|
|
|
|
2
|
return $self; |
|
692
|
|
|
|
|
|
|
} |
|
693
|
|
|
|
|
|
|
|
|
694
|
|
|
|
|
|
|
# Convert to hashref for Hypersonic handler return |
|
695
|
|
|
|
|
|
|
sub finalize { |
|
696
|
6
|
|
|
6
|
1
|
4475
|
my ($self) = @_; |
|
697
|
|
|
|
|
|
|
|
|
698
|
6
|
|
|
|
|
10
|
my $headers = { %{$self->[SLOT_HEADERS]} }; |
|
|
6
|
|
|
|
|
15
|
|
|
699
|
|
|
|
|
|
|
|
|
700
|
|
|
|
|
|
|
# Add Set-Cookie headers |
|
701
|
6
|
100
|
|
|
|
7
|
if (@{$self->[SLOT_COOKIES]}) { |
|
|
6
|
|
|
|
|
17
|
|
|
702
|
|
|
|
|
|
|
# Store cookies as arrayref for multiple Set-Cookie headers |
|
703
|
1
|
|
|
|
|
4
|
$headers->{'Set-Cookie'} = $self->[SLOT_COOKIES]; |
|
704
|
|
|
|
|
|
|
} |
|
705
|
|
|
|
|
|
|
|
|
706
|
|
|
|
|
|
|
return { |
|
707
|
6
|
|
|
|
|
22
|
status => $self->[SLOT_STATUS], |
|
708
|
|
|
|
|
|
|
headers => $headers, |
|
709
|
|
|
|
|
|
|
body => $self->[SLOT_BODY], |
|
710
|
|
|
|
|
|
|
}; |
|
711
|
|
|
|
|
|
|
} |
|
712
|
|
|
|
|
|
|
|
|
713
|
|
|
|
|
|
|
# Direct HTTP response string generation (JIT-compiled when available) |
|
714
|
|
|
|
|
|
|
# This bypasses the intermediate hashref for maximum performance |
|
715
|
|
|
|
|
|
|
# Returns a complete HTTP response string ready to send |
|
716
|
|
|
|
|
|
|
sub to_http { |
|
717
|
29
|
|
|
29
|
0
|
91
|
my ($self) = @_; |
|
718
|
29
|
50
|
|
|
|
57
|
if ($COMPILED) { |
|
719
|
29
|
|
|
|
|
215
|
return $self->_jit_to_http(); |
|
720
|
|
|
|
|
|
|
} |
|
721
|
|
|
|
|
|
|
# Fallback to Perl implementation |
|
722
|
0
|
|
|
|
|
0
|
return $self->_perl_to_http(); |
|
723
|
|
|
|
|
|
|
} |
|
724
|
|
|
|
|
|
|
|
|
725
|
|
|
|
|
|
|
# Pure Perl fallback for to_http |
|
726
|
|
|
|
|
|
|
sub _perl_to_http { |
|
727
|
0
|
|
|
0
|
|
0
|
my ($self) = @_; |
|
728
|
|
|
|
|
|
|
|
|
729
|
0
|
|
|
|
|
0
|
my $status = $self->[SLOT_STATUS]; |
|
730
|
0
|
|
|
|
|
0
|
my %status_text = ( |
|
731
|
|
|
|
|
|
|
200 => 'OK', 201 => 'Created', 204 => 'No Content', |
|
732
|
|
|
|
|
|
|
301 => 'Moved Permanently', 302 => 'Found', 304 => 'Not Modified', |
|
733
|
|
|
|
|
|
|
307 => 'Temporary Redirect', 308 => 'Permanent Redirect', |
|
734
|
|
|
|
|
|
|
400 => 'Bad Request', 401 => 'Unauthorized', 403 => 'Forbidden', |
|
735
|
|
|
|
|
|
|
404 => 'Not Found', 409 => 'Conflict', 422 => 'Unprocessable Entity', |
|
736
|
|
|
|
|
|
|
429 => 'Too Many Requests', 500 => 'Internal Server Error', |
|
737
|
|
|
|
|
|
|
502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Timeout', |
|
738
|
|
|
|
|
|
|
); |
|
739
|
|
|
|
|
|
|
|
|
740
|
0
|
|
0
|
|
|
0
|
my $resp = "HTTP/1.1 $status " . ($status_text{$status} // 'Unknown') . "\r\n"; |
|
741
|
|
|
|
|
|
|
|
|
742
|
|
|
|
|
|
|
# Headers |
|
743
|
0
|
|
|
|
|
0
|
my $headers = $self->[SLOT_HEADERS]; |
|
744
|
0
|
|
|
|
|
0
|
for my $key (keys %$headers) { |
|
745
|
0
|
|
|
|
|
0
|
$resp .= "$key: $headers->{$key}\r\n"; |
|
746
|
|
|
|
|
|
|
} |
|
747
|
|
|
|
|
|
|
|
|
748
|
|
|
|
|
|
|
# Cookies |
|
749
|
0
|
|
|
|
|
0
|
for my $cookie (@{$self->[SLOT_COOKIES]}) { |
|
|
0
|
|
|
|
|
0
|
|
|
750
|
0
|
|
|
|
|
0
|
$resp .= "Set-Cookie: $cookie\r\n"; |
|
751
|
|
|
|
|
|
|
} |
|
752
|
|
|
|
|
|
|
|
|
753
|
|
|
|
|
|
|
# Content-Length and body |
|
754
|
0
|
|
|
|
|
0
|
my $body = $self->[SLOT_BODY]; |
|
755
|
0
|
|
|
|
|
0
|
$resp .= "Content-Length: " . length($body) . "\r\n\r\n"; |
|
756
|
0
|
|
|
|
|
0
|
$resp .= $body; |
|
757
|
|
|
|
|
|
|
|
|
758
|
0
|
|
|
|
|
0
|
return $resp; |
|
759
|
|
|
|
|
|
|
} |
|
760
|
|
|
|
|
|
|
|
|
761
|
|
|
|
|
|
|
# Allow using Response object directly as return value (auto-finalize) |
|
762
|
|
|
|
|
|
|
sub TO_JSON { |
|
763
|
1
|
|
|
1
|
0
|
6
|
my ($self) = @_; |
|
764
|
1
|
|
|
|
|
4
|
return $self->finalize; |
|
765
|
|
|
|
|
|
|
} |
|
766
|
|
|
|
|
|
|
|
|
767
|
|
|
|
|
|
|
# Helper: format HTTP date |
|
768
|
|
|
|
|
|
|
sub _http_date { |
|
769
|
1
|
|
|
1
|
|
4
|
my ($time) = @_; |
|
770
|
|
|
|
|
|
|
|
|
771
|
|
|
|
|
|
|
# Use JIT-compiled version if available |
|
772
|
1
|
50
|
33
|
|
|
7
|
if ($COMPILED && defined &_jit_http_date) { |
|
773
|
1
|
|
|
|
|
73
|
return _jit_http_date($time); |
|
774
|
|
|
|
|
|
|
} |
|
775
|
|
|
|
|
|
|
|
|
776
|
|
|
|
|
|
|
# Perl fallback |
|
777
|
0
|
|
0
|
|
|
0
|
$time //= time(); |
|
778
|
0
|
|
|
|
|
0
|
my @t = gmtime($time); |
|
779
|
0
|
|
|
|
|
0
|
my @days = qw(Sun Mon Tue Wed Thu Fri Sat); |
|
780
|
0
|
|
|
|
|
0
|
my @months = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec); |
|
781
|
0
|
|
|
|
|
0
|
return sprintf("%s, %02d %s %04d %02d:%02d:%02d GMT", |
|
782
|
|
|
|
|
|
|
$days[$t[6]], $t[3], $months[$t[4]], $t[5]+1900, $t[2], $t[1], $t[0]); |
|
783
|
|
|
|
|
|
|
} |
|
784
|
|
|
|
|
|
|
|
|
785
|
|
|
|
|
|
|
# Helper: format JSON error |
|
786
|
|
|
|
|
|
|
sub _json_error { |
|
787
|
15
|
|
|
15
|
|
28
|
my ($message) = @_; |
|
788
|
15
|
|
|
|
|
38
|
$message =~ s/"/\\"/g; # Escape quotes |
|
789
|
15
|
|
|
|
|
38
|
return qq({"error":"$message"}); |
|
790
|
|
|
|
|
|
|
} |
|
791
|
|
|
|
|
|
|
|
|
792
|
|
|
|
|
|
|
1; |
|
793
|
|
|
|
|
|
|
|
|
794
|
|
|
|
|
|
|
__END__ |