File Coverage

feersum_psgi.c.inc
Criterion Covered Total %
statement 584 714 81.7
branch 400 676 59.1
condition n/a
subroutine n/a
pod n/a
total 984 1390 70.7


line stmt bran cond sub pod time code
1              
2             // Extract first IP from X-Forwarded-For header (leftmost = original client)
3             static SV*
4 18           extract_forwarded_addr(pTHX_ struct feer_req *r)
5             {
6             size_t val_len;
7 18           const char *val = find_header_value(r, "x-forwarded-for", 15, &val_len);
8 18 100         if (!val || val_len == 0) return NULL;
    50          
9              
10             // Skip leading whitespace
11 15 50         while (val_len > 0 && (*val == ' ' || *val == '\t')) { val++; val_len--; }
    50          
    50          
12 15 50         if (val_len == 0) return NULL;
13              
14             // Find end of first IP (comma or space or end)
15 15           size_t ip_len = 0;
16 187 100         while (ip_len < val_len && val[ip_len] != ',' && val[ip_len] != ' ') ip_len++;
    100          
    100          
17              
18 15 50         if (ip_len == 0 || ip_len > 45) return NULL; // max IPv6 length is 45 chars
    50          
19              
20             // Copy to null-terminated buffer for inet_pton validation
21             char ip_buf[46];
22 15           memcpy(ip_buf, val, ip_len);
23 15           ip_buf[ip_len] = '\0';
24              
25             // Validate as IPv4 or IPv6 address using inet_pton
26             struct in_addr addr4;
27             struct in6_addr addr6;
28 15 100         if (inet_pton(AF_INET, ip_buf, &addr4) == 1) {
29 9           return newSVpvn(val, ip_len); // valid IPv4
30             }
31 6 100         if (inet_pton(AF_INET6, ip_buf, &addr6) == 1) {
32 2           return newSVpvn(val, ip_len); // valid IPv6
33             }
34              
35             // Not a valid IP address - return NULL (caller will use original REMOTE_ADDR)
36             trace("X-Forwarded-For contains invalid IP: %s\n", ip_buf);
37 4           return NULL;
38             }
39              
40             static SV*
41 17           extract_forwarded_proto(pTHX_ struct feer_req *r)
42             {
43             size_t val_len;
44 17           const char *val = find_header_value(r, "x-forwarded-proto", 17, &val_len);
45 17 100         if (!val || val_len == 0) return NULL;
    50          
46              
47             // Skip whitespace
48 7 50         while (val_len > 0 && (*val == ' ' || *val == '\t')) { val++; val_len--; }
    50          
    50          
49              
50             // Check for exact https/http (reject "httpx", "https2", etc.)
51 7 50         if (val_len >= 5 && str_case_eq_fixed("https", val, 5) &&
    50          
52 7 100         (val_len == 5 || val[5] == ' ' || val[5] == '\t' || val[5] == ','))
    50          
    50          
    50          
53 7           return newSVpvs("https");
54 0 0         if (val_len >= 4 && str_case_eq_fixed("http", val, 4) &&
    0          
55 0 0         (val_len == 4 || val[4] == ' ' || val[4] == '\t' || val[4] == ','))
    0          
    0          
    0          
56 0           return newSVpvs("http");
57              
58 0           return NULL;
59             }
60              
61             /* Determine the URL scheme for a connection.
62             * Returns a new SV ("https" or forwarded proto), or NULL for default "http". */
63             static SV *
64 303           feer_determine_url_scheme(pTHX_ struct feer_conn *c)
65             {
66             #ifdef FEERSUM_HAS_H2
67             /* H2 requires TLS (ALPN), so scheme is always https.
68             * The :scheme pseudo-header is validated but not propagated. */
69             if (c->is_h2_stream) return newSVpvs("https");
70             #endif
71             #ifdef FEERSUM_HAS_TLS
72 303 100         if (c->tls) return newSVpvs("https");
73             #endif
74 264 100         if (c->proxy_ssl) return newSVpvs("https");
75 262 100         if (c->proxy_proto_version > 0 && c->proxy_dst_port == 443)
    100          
76 5           return newSVpvs("https");
77 257 100         if (c->cached_use_reverse_proxy && c->req) {
    50          
78 17           SV *fwd = extract_forwarded_proto(aTHX_ c->req);
79 17 100         if (fwd) return fwd;
80             }
81 250           return NULL;
82             }
83              
84             // Initialize PSGI env constants (called once at startup)
85             static void
86 56           feersum_init_psgi_env_constants(pTHX)
87             {
88 56 50         if (psgi_env_version) return; // already initialized
89              
90             // Only share truly immutable values that middleware will never modify
91 56           psgi_env_version = newRV((SV*)psgi_ver);
92 56           psgi_env_errors = newRV((SV*)PL_stderrgv);
93             }
94              
95             // Build PSGI env hash directly (optimized - no template clone)
96             static HV*
97 292           feersum_build_psgi_env(pTHX)
98             {
99 292           HV *e = newHV();
100             // Pre-size hash: ~13 constants + ~10 per-request + ~15 headers = ~38
101 292           hv_ksplit(e, 48);
102              
103             // Truly immutable constants - safe to share via refcount
104 292           hv_stores(e, "psgi.version", SvREFCNT_inc_simple_NN(psgi_env_version));
105 292           hv_stores(e, "psgi.errors", SvREFCNT_inc_simple_NN(psgi_env_errors));
106              
107             // Boolean constants - PL_sv_yes/no are immortal and safe to share
108 292           hv_stores(e, "psgi.run_once", SvREFCNT_inc_simple_NN(&PL_sv_no));
109 292           hv_stores(e, "psgi.nonblocking", SvREFCNT_inc_simple_NN(&PL_sv_yes));
110 292           hv_stores(e, "psgi.multithread", SvREFCNT_inc_simple_NN(&PL_sv_no));
111 292           hv_stores(e, "psgi.streaming", SvREFCNT_inc_simple_NN(&PL_sv_yes));
112 292           hv_stores(e, "psgix.input.buffered", SvREFCNT_inc_simple_NN(&PL_sv_yes));
113 292           hv_stores(e, "psgix.output.buffered", SvREFCNT_inc_simple_NN(&PL_sv_yes));
114 292           hv_stores(e, "psgix.body.scalar_refs", SvREFCNT_inc_simple_NN(&PL_sv_yes));
115 292           hv_stores(e, "psgix.output.guard", SvREFCNT_inc_simple_NN(&PL_sv_yes));
116              
117 292           hv_stores(e, "SCRIPT_NAME", newSVpvs(""));
118              
119 292           return e;
120             }
121              
122             static HV*
123 292           feersum_env(pTHX_ struct feer_conn *c)
124             {
125             HV *e;
126             int i,j;
127 292           struct feer_req *r = c->req;
128              
129             // Initialize constants on first call
130 292 100         if (unlikely(!psgi_env_version))
131 56           feersum_init_psgi_env_constants(aTHX);
132              
133             // Build env hash directly instead of cloning template (2x faster)
134 292           e = feersum_build_psgi_env(aTHX);
135              
136             trace("generating header (fd %d) %.*s\n",
137             c->fd, (int)r->uri_len, r->uri);
138              
139             // SERVER_NAME and SERVER_PORT - copy because these SVs are SvREADONLY
140             // and middleware may modify in-place (e.g., Plack::Middleware::ReverseProxy).
141             {
142 292           struct feer_listen *conn_lsnr = c->listener;
143 292 50         hv_stores(e, "SERVER_NAME",
144             conn_lsnr->server_name ? newSVsv(conn_lsnr->server_name) : newSVpvs(""));
145 292 50         hv_stores(e, "SERVER_PORT",
146             conn_lsnr->server_port ? newSVsv(conn_lsnr->server_port) : newSVpvs("0"));
147             }
148 292           hv_stores(e, "REQUEST_URI", feersum_env_uri(aTHX_ r));
149             #ifdef FEERSUM_HAS_H2
150             hv_stores(e, "REQUEST_METHOD", feersum_env_method_h2(aTHX_ c, r));
151             if (unlikely(c->is_h2_stream))
152             hv_stores(e, "SERVER_PROTOCOL", newSVpvs("HTTP/2"));
153             else
154             #else
155 292           hv_stores(e, "REQUEST_METHOD", feersum_env_method(aTHX_ r));
156             #endif
157 292           hv_stores(e, "SERVER_PROTOCOL", SvREFCNT_inc_simple_NN(feersum_env_protocol(aTHX_ r)));
158              
159 292           feersum_set_conn_remote_info(aTHX_ c);
160              
161             // Reverse proxy mode: trust X-Forwarded-For for REMOTE_ADDR
162 292 100         if (c->cached_use_reverse_proxy) {
163 13           SV *fwd_addr = extract_forwarded_addr(aTHX_ r);
164 13 100         hv_stores(e, "REMOTE_ADDR", fwd_addr ? fwd_addr : SvREFCNT_inc_simple_NN(c->remote_addr));
165             } else {
166 279           hv_stores(e, "REMOTE_ADDR", SvREFCNT_inc_simple_NN(c->remote_addr));
167             }
168 292           hv_stores(e, "REMOTE_PORT", SvREFCNT_inc_simple_NN(c->remote_port));
169              
170             {
171 292           SV *scheme = feer_determine_url_scheme(aTHX_ c);
172 292 100         hv_stores(e, "psgi.url_scheme", scheme ? scheme : newSVpvs("http"));
173             }
174              
175 292           hv_stores(e, "CONTENT_LENGTH", newSViv(c->expected_cl));
176              
177             // Always provide psgi.input (for both PSGI and native handlers)
178             // For requests without body, it will be an empty stream (returns 0 on read)
179 292           hv_stores(e, "psgi.input", new_feer_conn_handle(aTHX_ c, 0));
180              
181 292 50         hv_stores(e, "psgi.multiprocess",
    50          
182             SvREFCNT_inc_simple_NN(c->server->multiprocess ? &PL_sv_yes : &PL_sv_no));
183              
184 292 100         if (c->cached_request_cb_is_psgi && c->server->psgix_io) {
    50          
185 88           SV *fake_fh = newSViv(c->fd); // fd value for psgix.io magic backing SV
186 88           SV *selfref = sv_2mortal(feer_conn_2sv(c));
187 88           sv_magicext(fake_fh, selfref, PERL_MAGIC_ext, &psgix_io_vtbl, NULL, 0);
188 88           hv_stores(e, "psgix.io", fake_fh);
189             }
190              
191 292 50         if (c->trailers) {
192 0           hv_stores(e, "psgix.h2.trailers", newRV_inc((SV*)c->trailers));
193             }
194              
195 292 100         if (c->proxy_tlvs) {
196 1           hv_stores(e, "psgix.proxy_tlvs", SvREFCNT_inc_simple_NN(c->proxy_tlvs));
197             }
198              
199 292 100         if (likely(!r->path)) feersum_set_path_and_query(aTHX_ r);
200 292           hv_stores(e, "PATH_INFO", SvREFCNT_inc_simple_NN(r->path));
201 292           hv_stores(e, "QUERY_STRING", SvREFCNT_inc_simple_NN(r->query));
202              
203 292           SV *cur_val = NULL; // tracks current header value for multi-value header merging
204 292           char *kbuf = header_key_buf; // use static buffer (pre-initialized with "HTTP_")
205              
206 1130 100         for (i=0; inum_headers; i++) {
207 838           struct phr_header *hdr = &(r->headers[i]);
208             // Note: obs-fold (hdr->name == NULL) is rejected at parse time per RFC 7230
209 884           if (unlikely(hdr->name_len == 14) &&
210 46           str_case_eq_fixed("content-length", hdr->name, 14))
211             {
212             // content length shouldn't show up as HTTP_CONTENT_LENGTH but
213             // as CONTENT_LENGTH in the env-hash.
214 46           continue;
215             }
216 802           else if (unlikely(hdr->name_len == 12) &&
217 10           str_case_eq_fixed("content-type", hdr->name, 12))
218             {
219 7           hv_stores(e, "CONTENT_TYPE", newSVpvn(hdr->value, hdr->value_len));
220 7           continue;
221             }
222              
223             // Skip headers with names too long for our buffer (defensive - should be
224             // rejected at parse time with 431, but guard against edge cases)
225 785 50         if (unlikely(hdr->name_len > MAX_HEADER_NAME_LEN)) {
226             trace("skipping oversized header name (len=%zu) on fd %d\n",
227             hdr->name_len, c->fd);
228 0           continue;
229             }
230              
231 785           size_t klen = 5+hdr->name_len;
232 785           char *key = kbuf + 5;
233 7429 100         for (j=0; jname_len; j++) {
234             // Use combined lookup table (uppercase + dash-to-underscore)
235 6644           *key++ = ascii_upper_dash[(unsigned char)hdr->name[j]];
236             }
237              
238 785           SV **fetched = hv_fetch(e, kbuf, klen, 1);
239             trace("adding header to env (fd %d) %.*s: %.*s\n",
240             c->fd, (int)klen, kbuf, (int)hdr->value_len, hdr->value);
241              
242             // hv_fetch with lval=1 should always succeed, but check for OOM safety
243 785 50         if (unlikely(fetched == NULL)) {
244             trace("hv_fetch returned NULL (OOM?) on fd %d\n", c->fd);
245 0           continue;
246             }
247 785           cur_val = *fetched; // track for multi-value header merging
248 785 100         if (unlikely(SvPOK(cur_val))) {
249             trace("... is multivalue\n");
250             // RFC 9113 §8.2.3: cookie uses "; ", others use ", "
251 2 50         const char *sep = (hdr->name_len == 6
252 0 0         && str_case_eq_fixed("cookie", hdr->name, 6)) ? "; " : ", ";
253 2           sv_catpvn(cur_val, sep, 2);
254 2           sv_catpvn(cur_val, hdr->value, hdr->value_len);
255             }
256             else {
257             // change from undef to a real value
258 783           sv_setpvn(cur_val, hdr->value, hdr->value_len);
259             }
260             }
261              
262             #ifdef FEERSUM_HAS_H2
263             /* Map :authority pseudo-header to HTTP_HOST for H2 streams (RFC 9113 §8.3.1).
264             * Only set if no regular Host header was already present. */
265             if (unlikely(c->is_h2_stream)) {
266             struct feer_h2_stream *stream = (struct feer_h2_stream *)c->read_ev_timer.data;
267             if (stream && stream->h2_authority && !hv_exists(e, "HTTP_HOST", 9)) {
268             STRLEN alen;
269             const char *aval = SvPV(stream->h2_authority, alen);
270             hv_stores(e, "HTTP_HOST", newSVpvn(aval, alen));
271             }
272             /* Extended CONNECT (RFC 8441): REQUEST_METHOD already set to GET
273             * above; add remaining H1-equivalent headers so existing PSGI
274             * WebSocket middleware works transparently.
275             * Matches HAProxy/nghttpx H2↔H1 upgrade translation. */
276             if (stream && stream->is_tunnel && stream->h2_protocol) {
277             STRLEN plen;
278             const char *pval = SvPV(stream->h2_protocol, plen);
279             hv_stores(e, "HTTP_UPGRADE", newSVpvn(pval, plen));
280             hv_stores(e, "HTTP_CONNECTION", newSVpvs("Upgrade"));
281             hv_stores(e, "psgix.h2.protocol", newSVpvn(pval, plen));
282             hv_stores(e, "psgix.h2.extended_connect", newSViv(1));
283             }
284             }
285             #endif
286              
287 292           return e;
288             }
289              
290             #define COPY_NORM_HEADER(_str) \
291             for (i = 0; i < r->num_headers; i++) {\
292             struct phr_header *hdr = &(r->headers[i]);\
293             /* Invariant: obs-fold and oversized names already rejected at parse time */\
294             if (unlikely(hdr->name_len > MAX_HEADER_NAME_LEN)) continue; /* defense-in-depth */\
295             char *k = kbuf;\
296             for (j = 0; j < hdr->name_len; j++) { char n = hdr->name[j]; *k++ = _str; }\
297             SV** val = hv_fetch(e, kbuf, hdr->name_len, 1);\
298             if (unlikely(!val)) continue; /* OOM safety */\
299             if (unlikely(SvPOK(*val))) {\
300             const char *sep = (hdr->name_len == 6\
301             && str_case_eq_fixed("cookie", hdr->name, 6)) ? "; " : ", ";\
302             sv_catpvn(*val, sep, 2);\
303             sv_catpvn(*val, hdr->value, hdr->value_len);\
304             } else {\
305             sv_setpvn(*val, hdr->value, hdr->value_len);\
306             }\
307             }\
308             break;
309              
310             // Static buffer for feersum_env_headers (reuses header_key_buf area after HTTP_ prefix)
311             static HV*
312 7           feersum_env_headers(pTHX_ struct feer_req *r, int norm)
313             {
314             size_t i; size_t j; HV* e;
315 7           e = newHV();
316             // Pre-allocate hash buckets based on expected header count to avoid rehashing
317 7 50         if (r->num_headers > 0)
318 7           hv_ksplit(e, r->num_headers);
319 7           char *kbuf = header_key_buf + 5; // reuse static buffer, skip the "HTTP_" prefix area
320 7           switch (norm) {
321 1           case HEADER_NORM_SKIP:
322 26 50         COPY_NORM_HEADER(n)
    100          
    50          
    50          
    0          
    0          
    100          
323 2           case HEADER_NORM_LOCASE:
324 71 50         COPY_NORM_HEADER(ascii_lower[(unsigned char)n])
    100          
    50          
    100          
    50          
    100          
    100          
325 1           case HEADER_NORM_UPCASE:
326 26 50         COPY_NORM_HEADER(ascii_upper[(unsigned char)n])
    100          
    50          
    50          
    0          
    0          
    100          
327 2           case HEADER_NORM_LOCASE_DASH:
328 82 50         COPY_NORM_HEADER(ascii_lower_dash[(unsigned char)n])
    100          
    50          
    50          
    0          
    0          
    100          
329 1           case HEADER_NORM_UPCASE_DASH:
330 26 50         COPY_NORM_HEADER(ascii_upper_dash[(unsigned char)n])
    100          
    50          
    50          
    0          
    0          
    100          
331 0           default:
332 0           break;
333             }
334 7           return e;
335             }
336              
337             INLINE_UNLESS_DEBUG static SV*
338 10           feersum_env_header(pTHX_ struct feer_req *r, SV *name)
339             {
340             size_t i;
341 10           SV *result = NULL;
342             STRLEN nlen;
343 10           const char *np = SvPV(name, nlen);
344             /* RFC 9113 §8.2.3: cookie headers joined with "; ", others with ", " */
345 10 100         const bool is_cookie = (nlen == 6 && str_case_eq_fixed("cookie", np, 6));
    100          
346 52 100         for (i = 0; i < r->num_headers; i++) {
347 42           struct phr_header *hdr = &(r->headers[i]);
348 42 100         if (unlikely(hdr->name_len == nlen
    100          
349             && str_case_eq_both(np, hdr->name, nlen))) {
350 12 100         if (likely(!result)) {
351 10           result = newSVpvn(hdr->value, hdr->value_len);
352             } else {
353 2 100         sv_catpvn(result, is_cookie ? "; " : ", ", 2);
354 2           sv_catpvn(result, hdr->value, hdr->value_len);
355             }
356             }
357             }
358 10 50         return result ? result : &PL_sv_undef;
359             }
360              
361             INLINE_UNLESS_DEBUG static ssize_t
362 18           feersum_env_content_length(pTHX_ struct feer_conn *c)
363             {
364 18           return c->expected_cl;
365             }
366              
367             static SV*
368 13           feersum_env_io(pTHX_ struct feer_conn *c)
369             {
370 13           dSP;
371              
372             // Prevent double-call and misuse after response started
373 13 100         if (unlikely(c->io_taken))
374 1           croak("io() already called on this connection");
375 12 50         if (unlikely(c->responding != RESPOND_NOT_STARTED))
376 0           croak("io() cannot be called after send_response/start_response");
377              
378             trace("feersum_env_io for fd=%d\n", c->fd);
379              
380             #ifdef FEERSUM_HAS_H2
381             /* H2 tunnel: auto-accept, create socketpair, expose sv[1] as IO handle */
382             if (c->is_h2_stream) {
383             struct feer_h2_stream *stream = (struct feer_h2_stream *)c->read_ev_timer.data;
384             if (!stream || !stream->is_tunnel)
385             croak("io() is not supported on regular HTTP/2 streams");
386             h2_tunnel_auto_accept(aTHX_ c, stream);
387             /* Re-fetch stream: auto_accept → session_send may have freed it */
388             stream = (struct feer_h2_stream *)c->read_ev_timer.data;
389             if (!stream)
390             croak("io() tunnel: stream freed during auto-accept");
391             feer_h2_setup_tunnel(aTHX_ stream);
392             if (!stream->tunnel_established)
393             croak("Failed to create tunnel socketpair");
394              
395             SV *sv = newSViv(stream->tunnel_sv1);
396              
397             ENTER;
398             SAVETMPS;
399             PUSHMARK(SP);
400             XPUSHs(sv);
401             mXPUSHs(newSViv(stream->tunnel_sv1));
402             PUTBACK;
403              
404             call_pv("Feersum::Connection::_raw", G_VOID|G_DISCARD|G_EVAL);
405             SPAGAIN;
406              
407             if (unlikely(SvTRUE(ERRSV))) {
408             FREETMPS;
409             LEAVE;
410             SvREFCNT_dec(sv);
411             croak("Failed to create tunnel IO handle: %-p", ERRSV);
412             }
413              
414             if (unlikely(!SvROK(sv))) {
415             /* _raw failed: new_from_fd returned undef.
416             * Leave tunnel_sv1 intact so feer_h2_stream_free closes it. */
417             FREETMPS;
418             LEAVE;
419             SvREFCNT_dec(sv);
420             croak("Failed to create tunnel IO handle");
421             }
422              
423             SV *io_glob = SvRV(sv);
424             GvSV(io_glob) = newRV_inc(c->self);
425              
426             /* sv[1] now owned by the IO handle */
427             stream->tunnel_sv1 = -1;
428             c->io_taken = 1;
429              
430             FREETMPS;
431             LEAVE;
432             return sv;
433             }
434             #endif
435              
436             #ifdef FEERSUM_HAS_TLS
437             /* TLS tunnel: create socketpair relay for bidirectional I/O over TLS */
438 12 100         if (c->tls) {
439 9           feer_tls_setup_tunnel(c);
440 9 50         if (!c->tls_tunnel)
441 0           croak("Failed to create TLS tunnel socketpair");
442              
443 9           SV *sv = newSViv(c->tls_tunnel_sv1);
444              
445 9           ENTER;
446 9           SAVETMPS;
447 9 50         PUSHMARK(SP);
448 9 50         XPUSHs(sv);
449 9 50         mXPUSHs(newSViv(c->tls_tunnel_sv1));
450 9           PUTBACK;
451              
452 9           call_pv("Feersum::Connection::_raw", G_VOID|G_DISCARD|G_EVAL);
453 9           SPAGAIN;
454              
455 9 50         if (unlikely(SvTRUE(ERRSV))) {
    50          
456 0 0         FREETMPS;
457 0           LEAVE;
458 0           SvREFCNT_dec(sv);
459 0 0         croak("Failed to create TLS tunnel IO handle: %-p", ERRSV);
460             }
461              
462 9 50         if (unlikely(!SvROK(sv))) {
463             /* _raw failed: new_from_fd returned undef.
464             * Leave tls_tunnel_sv1 intact so feer_tls_free_conn closes it. */
465 0 0         FREETMPS;
466 0           LEAVE;
467 0           SvREFCNT_dec(sv);
468 0           croak("Failed to create TLS tunnel IO handle");
469             }
470              
471 9           SV *io_glob = SvRV(sv);
472 9           GvSV(io_glob) = newRV_inc(c->self);
473              
474             /* sv[1] now owned by the IO handle */
475 9           c->tls_tunnel_sv1 = -1;
476 9           c->io_taken = 1;
477 9           stop_read_timer(c);
478 9           stop_write_timer(c);
479              
480             /* Keep read watcher active — TLS reads relay to tunnel */
481              
482 9 50         FREETMPS;
483 9           LEAVE;
484 9           return sv;
485             }
486             #endif
487              
488             // Create a scalar to hold the IO handle
489 3           SV *sv = newSViv(c->fd);
490              
491 3           ENTER;
492 3           SAVETMPS;
493              
494 3 50         PUSHMARK(SP);
495 3 50         XPUSHs(sv);
496 3 50         mXPUSHs(newSViv(c->fd));
497 3           PUTBACK;
498              
499             // Call Feersum::Connection::_raw to create IO::Socket::INET
500 3           call_pv("Feersum::Connection::_raw", G_VOID|G_DISCARD|G_EVAL);
501 3           SPAGAIN;
502              
503 3 50         if (unlikely(SvTRUE(ERRSV))) {
    50          
504 0 0         FREETMPS;
505 0           LEAVE;
506 0           SvREFCNT_dec(sv);
507 0 0         croak("Failed to create IO handle: %-p", ERRSV);
508             }
509              
510             // Verify _raw created a valid reference
511 3 50         if (unlikely(!SvROK(sv))) {
512 0 0         FREETMPS;
513 0           LEAVE;
514 0           SvREFCNT_dec(sv);
515 0           croak("Failed to create IO handle: new_from_fd returned undef");
516             }
517              
518             // Store back-reference to connection in the glob's scalar slot
519 3           SV *io_glob = SvRV(sv);
520 3           GvSV(io_glob) = newRV_inc(c->self);
521              
522             // Push any remaining rbuf data into the socket buffer
523 3 50         if (likely(c->rbuf && SvOK(c->rbuf) && SvCUR(c->rbuf))) {
    50          
    50          
    100          
524             STRLEN rbuf_len;
525 1           const char *rbuf_ptr = SvPV(c->rbuf, rbuf_len);
526 1           IO *io = GvIOp(io_glob);
527 1 50         if (io) {
528 1           SSize_t pushed = PerlIO_unread(IoIFP(io), (const void *)rbuf_ptr, rbuf_len);
529 1 50         if (likely(pushed == (SSize_t)rbuf_len)) {
530 1           SvCUR_set(c->rbuf, 0);
531 1           *SvPVX(c->rbuf) = '\0';
532 0 0         } else if (pushed > 0) {
533 0           sv_chop(c->rbuf, rbuf_ptr + pushed);
534 0           trouble("PerlIO_unread partial in io(): %zd of %"Sz_uf" bytes fd=%d\n",
535             pushed, (Sz)rbuf_len, c->fd);
536             } else {
537 0           trouble("PerlIO_unread failed in io() fd=%d\n", c->fd);
538             }
539             }
540             }
541              
542             // Stop Feersum's watchers - user now owns the socket
543 3           stop_read_watcher(c);
544 3           stop_read_timer(c);
545 3           stop_write_timer(c);
546             // don't stop write watcher in case there's outstanding data
547              
548             // Mark that io() was called
549 3           c->io_taken = 1;
550              
551 3 50         FREETMPS;
552 3           LEAVE;
553              
554 3           return sv;
555             }
556              
557             static SSize_t
558 4           feersum_return_from_io(pTHX_ struct feer_conn *c, SV *io_sv, const char *func_name)
559             {
560             #ifdef FEERSUM_HAS_H2
561             if (unlikely(c->is_h2_stream))
562             croak("%s: not supported on HTTP/2 streams", func_name);
563             #endif
564             #ifdef FEERSUM_HAS_TLS
565 4 100         if (c->tls_tunnel)
566 1           croak("%s: not supported on TLS tunnel connections "
567             "(io() over TLS uses a socketpair relay that cannot be returned)",
568             func_name);
569             #endif
570              
571 3 50         if (!SvROK(io_sv) || !isGV_with_GP(SvRV(io_sv)))
    50          
    50          
    0          
572 0           croak("%s requires a filehandle", func_name);
573              
574 3           GV *gv = (GV *)SvRV(io_sv);
575 3 50         IO *io = GvIO(gv);
    50          
    0          
    50          
576 3 50         if (!io || !IoIFP(io))
    50          
577 0           croak("%s: invalid filehandle", func_name);
578              
579 3           PerlIO *fp = IoIFP(io);
580              
581             // Check if there's buffered data to pull back
582 3           SSize_t cnt = PerlIO_get_cnt(fp);
583 3 50         if (cnt > 0) {
584             // Get pointer to buffered data
585             // Note: ptr remains valid until next PerlIO operation on fp.
586             // sv_catpvn doesn't touch fp, so this is safe.
587 0           STDCHAR *ptr = PerlIO_get_ptr(fp);
588 0 0         if (ptr) {
589             // Ensure we have an rbuf
590 0 0         if (!c->rbuf)
591 0           c->rbuf = newSV(READ_BUFSZ);
592              
593             // Append buffered data to feersum's rbuf
594 0           sv_catpvn(c->rbuf, (const char *)ptr, cnt);
595              
596             // Mark buffer as consumed (must happen before any other PerlIO ops)
597 0           PerlIO_set_ptrcnt(fp, ptr + cnt, 0);
598              
599             trace("pulled %zd bytes back to feersum fd=%d\n", (size_t)cnt, c->fd);
600             }
601             }
602              
603             /* Reset connection state for next request (like keepalive reset) */
604 3           change_responding_state(c, RESPOND_NOT_STARTED);
605 3           change_receiving_state(c, RECEIVE_HEADERS);
606 3           c->expected_cl = 0;
607 3           c->received_cl = 0;
608 3           c->io_taken = 0;
609 3           free_request(c);
610              
611 3 50         if (c->rbuf && cnt <= 0)
    50          
612 3           SvCUR_set(c->rbuf, 0);
613              
614 3           start_read_watcher(c);
615 3           restart_read_timer(c);
616 3           restart_header_timer(c);
617              
618 3           return cnt > 0 ? cnt : 0;
619             }
620              
621             static void
622 513           feersum_start_response (pTHX_ struct feer_conn *c, SV *message, AV *headers,
623             int streaming)
624             {
625             const char *ptr;
626             I32 i;
627              
628             trace("start_response fd=%d streaming=%d\n", c->fd, streaming);
629              
630 513 50         if (unlikely(!SvOK(message) || !(SvIOK(message) || SvPOK(message)))) {
    100          
    50          
    50          
631 0           croak("Must define an HTTP status code or message");
632             }
633              
634 513           I32 avl = av_len(headers);
635 513 50         if (unlikely((avl+1) % 2 == 1)) {
636 0           croak("expected even-length array, got %d", avl+1);
637             }
638              
639             #ifdef FEERSUM_HAS_H2
640             if (unlikely(c->is_h2_stream)) {
641             feersum_h2_start_response(aTHX_ c, message, headers, streaming);
642             return;
643             }
644             #endif
645              
646 513 100         if (unlikely(c->responding != RESPOND_NOT_STARTED))
647 4           croak("already responding?!");
648 509 100         change_responding_state(c, streaming ? RESPOND_STREAMING : RESPOND_NORMAL);
649              
650             // int or 3 chars? use a stock message
651 509           UV code = 0;
652 509 100         if (SvIOK(message))
653 431           code = SvIV(message);
654             else {
655 78           STRLEN mlen = SvCUR(message);
656 78           const int numtype = grok_number(SvPVX_const(message), mlen > 3 ? 3 : mlen, &code);
657 78 50         if (unlikely(numtype != IS_NUMBER_IN_UV))
658 0           code = 0;
659             }
660             trace2("starting response fd=%d code=%"UVuf"\n",c->fd,code);
661              
662 509 50         if (unlikely(!code))
663 0           croak("first parameter is not a number or doesn't start with digits");
664              
665             if (FEERSUM_RESP_START_ENABLED()) {
666             FEERSUM_RESP_START(c->fd, (int)code);
667             }
668              
669             // for PSGI it's always just an IV so optimize for that
670 509 100         if (likely(!SvPOK(message) || SvCUR(message) == 3)) {
    100          
671             // Use cached status SVs for common codes to avoid newSVpvf overhead
672 432           switch (code) {
673 428           case 200: message = status_200; break;
674 0           case 201: message = status_201; break;
675 3           case 204: message = status_204; break;
676 0           case 301: message = status_301; break;
677 0           case 302: message = status_302; break;
678 1           case 304: message = status_304; break;
679 0           case 400: message = status_400; break;
680 0           case 404: message = status_404; break;
681 0           case 500: message = status_500; break;
682 0           default:
683 0           ptr = http_code_to_msg(code);
684 0           message = sv_2mortal(newSVpvf("%"UVuf" %s",code,ptr));
685 0           break;
686             }
687             }
688              
689             // don't generate or strip Content-Length headers for responses that MUST NOT have a body
690             // RFC 7230: 1xx, 204, 205, 304 responses MUST NOT contain a message body
691 506 50         c->auto_cl = (code == 204 || code == 205 || code == 304 ||
    100          
692 1015 100         (100 <= code && code <= 199)) ? 0 : 1;
    50          
    50          
693              
694             // Build entire header block into a single buffer to minimize iovecs.
695             // Pre-calculate total size to avoid reallocation.
696             STRLEN msg_len;
697 509           const char *msg_ptr = SvPV(message, msg_len);
698              
699             // First pass: calculate total header size and detect Content-Length
700 509           bool has_content_length = 0;
701 509           STRLEN hdr_total = 9 + msg_len + 2; // "HTTP/1.x " + message + CRLF
702 509           SV **ary = AvARRAY(headers);
703 1168 100         for (i=0; i
704 659           SV *hdr = ary[i];
705 659           SV *val = ary[i+1];
706 659 50         if (unlikely(!hdr || !SvOK(hdr) || !val || !SvOK(val)))
    50          
    50          
    50          
    50          
    50          
707 68           continue;
708             STRLEN hlen, vlen;
709 659           const char *hp = SvPV(hdr, hlen);
710 659 100         if (unlikely(hlen == 14) && str_case_eq_fixed("content-length", hp, 14)) {
    50          
711 78 100         if (likely(c->auto_cl) && !streaming)
    100          
712 68           continue;
713 10           has_content_length = 1;
714             }
715 591           (void)SvPV(val, vlen);
716 591           hdr_total += hlen + 2 + vlen + 2; // key + ": " + value + CRLF
717             }
718             // HTTP/1.0 streaming with a body but no Content-Length: the only way to
719             // signal end-of-body is Connection: close. No-body statuses (auto_cl=0)
720             // are exempt — RFC 7230 §3.3.3 rule 1 says clients MUST NOT expect a body
721             // for 1xx/204/304 regardless of framing headers, so keepalive is safe.
722 509 100         if (streaming && c->is_keepalive && !has_content_length
    100          
    100          
723 6 100         && !c->is_http11 && c->auto_cl)
    50          
724 1           c->is_keepalive = 0;
725              
726             // Date, Connection, Transfer-Encoding, terminal CRLF
727 509 100         if (likely(c->is_http11))
728 493 100         hdr_total += DATE_HEADER_LENGTH + (!c->is_keepalive ? 19 : 0);
729 16 100         else if (c->is_keepalive)
730 1           hdr_total += 24; // "Connection: keep-alive\r\n"
731 509 100         if (streaming) {
732 59 100         if (c->is_http11 && !has_content_length && c->auto_cl)
    100          
    100          
733 40           hdr_total += 30; // "Transfer-Encoding: chunked\r\n\r\n"
734             else
735 19           hdr_total += 2; // terminal CRLF
736             }
737              
738             // Second pass: build the header buffer
739 509           SV *hdr_sv = newSV(hdr_total + 1);
740 509           SvPOK_on(hdr_sv);
741 509           char *p = SvPVX(hdr_sv);
742 509           STRLEN hdr_alloc = hdr_total;
743              
744             // Status line
745 509 100         const char *ver = c->is_http11 ? "HTTP/1.1 " : "HTTP/1.0 ";
746 509           memcpy(p, ver, 9); p += 9;
747 509           memcpy(p, msg_ptr, msg_len); p += msg_len;
748 509           *p++ = '\r'; *p++ = '\n';
749              
750             // Response headers
751 1168 100         for (i=0; i
752 659           SV *hdr = ary[i];
753 659           SV *val = ary[i+1];
754 659 50         if (unlikely(!hdr || !SvOK(hdr) || !val || !SvOK(val)))
    50          
    50          
    50          
    50          
    50          
755 68           continue;
756             STRLEN hlen, vlen;
757 659           const char *hp = SvPV(hdr, hlen);
758 659 100         if (unlikely(hlen == 14) && str_case_eq_fixed("content-length", hp, 14)) {
    50          
759 78 100         if (likely(c->auto_cl) && !streaming)
    100          
760 68           continue;
761             }
762 591           const char *vp = SvPV(val, vlen);
763 591           STRLEN need = hlen + 2 + vlen + 2;
764             // Guard against magical SVs returning longer strings than pass 1
765 591 50         if (unlikely((p - SvPVX(hdr_sv)) + need > hdr_alloc)) {
766 0           STRLEN pos = p - SvPVX(hdr_sv);
767 0           hdr_alloc = pos + need + 128; // slack covers tail (Date+Connection+TE ≤ 86 bytes)
768 0 0         SvGROW(hdr_sv, hdr_alloc + 1);
    0          
769 0           p = SvPVX(hdr_sv) + pos;
770             }
771 591           memcpy(p, hp, hlen); p += hlen;
772 591           *p++ = ':'; *p++ = ' ';
773 591           memcpy(p, vp, vlen); p += vlen;
774 591           *p++ = '\r'; *p++ = '\n';
775             }
776              
777             // Date and Connection headers
778 509 100         if (likely(c->is_http11)) {
779 493           memcpy(p, DATE_BUF, DATE_HEADER_LENGTH); p += DATE_HEADER_LENGTH;
780 493 100         if (!c->is_keepalive) {
781 352           memcpy(p, "Connection: close\r\n", 19); p += 19;
782             }
783 16 100         } else if (c->is_keepalive) {
784 1           memcpy(p, "Connection: keep-alive\r\n", 24); p += 24;
785             }
786              
787 509 100         if (streaming) {
788 59 100         if (c->is_http11 && !has_content_length && c->auto_cl) {
    100          
    100          
789 40           memcpy(p, "Transfer-Encoding: chunked\r\n\r\n", 30); p += 30;
790 40           c->use_chunked = 1;
791             }
792             else {
793 19           *p++ = '\r'; *p++ = '\n';
794 19           c->use_chunked = 0;
795             }
796             }
797              
798 509           SvCUR_set(hdr_sv, p - SvPVX(hdr_sv));
799 509           *p = '\0';
800              
801             // Add as a single iovec entry
802             {
803 509           struct iomatrix *m = next_iomatrix(c);
804 509           unsigned idx = m->count++;
805 509           m->iov[idx].iov_base = SvPVX(hdr_sv);
806 509           m->iov[idx].iov_len = SvCUR(hdr_sv);
807 509           m->sv[idx] = hdr_sv;
808 509           c->wbuf_len += SvCUR(hdr_sv);
809             }
810              
811             // For streaming responses, start writing headers immediately.
812             // For non-streaming (RESPOND_NORMAL), feersum_write_whole_body will
813             // call conn_write_ready after the body is buffered. This is critical
814             // because conn_write_ready triggers immediate writes and would
815             // prematurely finish the response before body is ready.
816 509 100         if (streaming)
817 59           conn_write_ready(c);
818 509           }
819              
820             static size_t
821 450           feersum_write_whole_body (pTHX_ struct feer_conn *c, SV *body)
822             {
823             size_t RETVAL;
824             I32 i;
825 450           bool body_is_string = 0;
826             STRLEN cur;
827              
828 450 50         if (c->responding != RESPOND_NORMAL)
829 0           croak("can't use write_whole_body when in streaming mode");
830              
831             #ifdef FEERSUM_HAS_H2
832             if (unlikely(c->is_h2_stream)) {
833             /* H2 streams use nghttp2 submit_response, not wbuf */
834             SV *body_sv;
835             if (!SvOK(body)) {
836             body_sv = sv_2mortal(newSVpvs(""));
837             } else if (SvROK(body)) {
838             SV *refd = SvRV(body);
839             if (SvOK(refd) && !SvROK(refd)) {
840             body_sv = refd;
841             } else if (SvTYPE(refd) == SVt_PVAV) {
842             AV *ab = (AV*)refd;
843             body_sv = sv_2mortal(newSVpvs(""));
844             I32 amax = av_len(ab);
845             for (i = 0; i <= amax; i++) {
846             SV *sv = fetch_av_normal(aTHX_ ab, i);
847             if (sv) sv_catsv(body_sv, sv);
848             }
849             } else {
850             croak("body must be a scalar, scalar reference or array reference");
851             }
852             } else {
853             body_sv = body;
854             }
855             return feersum_h2_write_whole_body(aTHX_ c, body_sv);
856             }
857             #endif
858              
859 450 50         if (!SvOK(body)) {
860 0           body = sv_2mortal(newSVpvs(""));
861 0           body_is_string = 1;
862             }
863 450 100         else if (SvROK(body)) {
864 331           SV *refd = SvRV(body);
865 331 100         if (SvOK(refd) && !SvROK(refd)) {
    50          
866 173           body = refd;
867 173           body_is_string = 1;
868             }
869 158 50         else if (SvTYPE(refd) != SVt_PVAV) {
870 0           croak("body must be a scalar, scalar reference or array reference");
871             }
872             }
873             else {
874 119           body_is_string = 1;
875             }
876              
877             // For array bodies with a single element, extract the scalar for the fast path
878 450           SV *body_scalar = NULL;
879 450 100         if (body_is_string) {
880 292           body_scalar = body;
881             } else {
882 158           AV *abody = (AV*)SvRV(body);
883 158 100         if (av_len(abody) == 0) {
884 139           SV *elem = fetch_av_normal(aTHX_ abody, 0);
885 139 50         if (elem) body_scalar = elem;
886             }
887             }
888              
889             // Optimization: for small scalar bodies with auto_cl, append Content-Length
890             // and body directly to the header iomatrix entry, producing a single iovec
891             // for the entire HTTP response → write() fast path.
892             // For large bodies (>4KB), keep them as a separate iovec to avoid memcpy;
893             // writev with 2-3 iovecs is fine and avoids duplicating the body buffer.
894 450 100         if (likely(c->auto_cl) && body_scalar && c->wbuf_rinq) {
    100          
    50          
895 430           struct iomatrix *m = (struct iomatrix *)c->wbuf_rinq->prev->ref;
896 430           unsigned last = m->count - 1;
897 430           SV *hdr_sv = m->sv[last];
898              
899             // Guard: verify the tail iomatrix entry is the writable header SV
900             // from feersum_start_response (not a const entry or NULL).
901 430 50         if (unlikely(!hdr_sv || SvREADONLY(hdr_sv)))
    50          
902 0           goto whole_body_slow_path;
903              
904             STRLEN body_len;
905 430           const char *body_ptr = SvPV(body_scalar, body_len);
906             char cl_buf[48];
907 430           int cl_len = format_content_length(cl_buf, body_len);
908 430           STRLEN hdr_cur = SvCUR(hdr_sv);
909              
910 430 100         if (likely(body_len <= 4096)) {
911             // Small body: merge headers + CL + body into one buffer → write()
912 428           STRLEN total = hdr_cur + cl_len + body_len;
913 428 50         SvGROW(hdr_sv, total + 1);
    50          
914 428           char *p = SvPVX(hdr_sv) + hdr_cur;
915 428           memcpy(p, cl_buf, cl_len); p += cl_len;
916 428           memcpy(p, body_ptr, body_len); p += body_len;
917 428           *p = '\0';
918 428           SvCUR_set(hdr_sv, total);
919 428           m->iov[last].iov_len = total;
920 428           c->wbuf_len += cl_len + body_len;
921             } else {
922             // Large body: append CL to header buffer, body as separate iovec
923 2 50         SvGROW(hdr_sv, hdr_cur + cl_len + 1);
    50          
924 2           memcpy(SvPVX(hdr_sv) + hdr_cur, cl_buf, cl_len);
925 2           SvCUR_set(hdr_sv, hdr_cur + cl_len);
926 2           SvPVX(hdr_sv)[hdr_cur + cl_len] = '\0';
927 2           m->iov[last].iov_len = hdr_cur + cl_len;
928 2           c->wbuf_len += cl_len;
929 2           add_sv_to_wbuf(c, body_scalar);
930             }
931 430           m->iov[last].iov_base = SvPVX(hdr_sv);
932 430           RETVAL = body_len;
933             }
934 20           else { whole_body_slow_path:;
935             SV *cl_sv; // content-length future
936             struct iovec *cl_iov;
937 20 100         if (likely(c->auto_cl))
938 16           add_placeholder_to_wbuf(c, &cl_sv, &cl_iov);
939             else
940 4           add_crlf_to_wbuf(c);
941              
942             // RFC 7230 §3.3: 1xx/204/205/304 MUST NOT have a message body.
943             // Skip body writes when auto_cl is 0 (no-body status codes).
944 20 100         if (unlikely(!c->auto_cl)) {
945 4           RETVAL = 0;
946             }
947 16 50         else if (body_is_string) {
948 0           cur = add_sv_to_wbuf(c,body);
949 0           RETVAL = cur;
950             }
951             else {
952 16           AV *abody = (AV*)SvRV(body);
953 16           I32 amax = av_len(abody);
954 16           RETVAL = 0;
955 51 100         for (i=0; i<=amax; i++) {
956 35           SV *sv = fetch_av_normal(aTHX_ abody, i);
957 35 100         if (unlikely(!sv)) continue;
958 34           cur = add_sv_to_wbuf(c,sv);
959             trace("body part i=%d sv=%p cur=%"Sz_uf"\n", i, sv, (Sz)cur);
960 34           RETVAL += cur;
961             }
962             }
963              
964 20 100         if (likely(c->auto_cl)) {
965             char cl_buf[48];
966 16           int cl_len = format_content_length(cl_buf, RETVAL);
967 16           sv_setpvn(cl_sv, cl_buf, cl_len);
968 16           update_wbuf_placeholder(c, cl_sv, cl_iov);
969             }
970             }
971              
972 450           change_responding_state(c, RESPOND_SHUTDOWN);
973 450           conn_write_ready(c);
974 450           return RETVAL;
975             }
976              
977             static void
978 3           call_died (pTHX_ struct feer_conn *c, const char *cb_type)
979             {
980 3           dSP;
981             #if DEBUG >= 1
982             trace("An error was thrown in the %s callback: %-p\n",cb_type,ERRSV);
983             #endif
984 3 50         PUSHMARK(SP);
985 3 50         mXPUSHs(newSVsv(ERRSV));
    50          
986 3           PUTBACK;
987 3           call_pv("Feersum::DIED", G_DISCARD|G_EVAL|G_VOID);
988 3           SPAGAIN;
989              
990 3           respond_with_server_error(c, "Request handler exception\n", 500);
991 3 50         sv_setsv(ERRSV, &PL_sv_undef);
992 3           }
993              
994             static void
995 16           feersum_start_psgi_streaming(pTHX_ struct feer_conn *c, SV *streamer)
996             {
997 16           dSP;
998 16           ENTER;
999 16           SAVETMPS;
1000 16 50         PUSHMARK(SP);
1001 16 50         mXPUSHs(feer_conn_2sv(c));
1002 16 50         XPUSHs(streamer);
1003 16           PUTBACK;
1004 16           call_method("_initiate_streaming_psgi", G_DISCARD|G_EVAL|G_VOID);
1005 16           SPAGAIN;
1006 16 50         if (unlikely(SvTRUE(ERRSV))) {
    50          
1007 0           call_died(aTHX_ c, "PSGI stream initiator");
1008             }
1009 16           PUTBACK;
1010 16 50         FREETMPS;
1011 16           LEAVE;
1012 16           }
1013              
1014             static void
1015 91           feersum_handle_psgi_response(
1016             pTHX_ struct feer_conn *c, SV *ret, bool can_recurse)
1017             {
1018 91 50         if (unlikely(!SvOK(ret) || !SvROK(ret))) {
    50          
1019 0 0         sv_setpvs(ERRSV, "Invalid PSGI response (expected reference)");
1020 0           call_died(aTHX_ c, "PSGI request");
1021 0           return;
1022             }
1023              
1024 91 50         if (unlikely(!IsArrayRef(ret))) {
    100          
1025 16 50         if (likely(can_recurse)) {
1026             trace("PSGI response non-array, c=%p ret=%p\n", c, ret);
1027 16           feersum_start_psgi_streaming(aTHX_ c, ret);
1028             }
1029             else {
1030 0 0         sv_setpvs(ERRSV, "PSGI attempt to recurse in a streaming callback");
1031 0           call_died(aTHX_ c, "PSGI request");
1032             }
1033 16           return;
1034             }
1035              
1036 75           AV *psgi_triplet = (AV*)SvRV(ret);
1037 75 50         if (unlikely(av_len(psgi_triplet)+1 != 3)) {
1038 0 0         sv_setpvs(ERRSV, "Invalid PSGI array response (expected triplet)");
1039 0           call_died(aTHX_ c, "PSGI request");
1040 0           return;
1041             }
1042              
1043             trace("PSGI response triplet, c=%p av=%p\n", c, psgi_triplet);
1044             // we know there's three elems so *should* be safe to de-ref
1045 75           SV **msg_p = av_fetch(psgi_triplet,0,0);
1046 75           SV **hdrs_p = av_fetch(psgi_triplet,1,0);
1047 75           SV **body_p = av_fetch(psgi_triplet,2,0);
1048 75 50         if (unlikely(!msg_p || !hdrs_p || !body_p)) {
    50          
    50          
    50          
1049 0 0         sv_setpvs(ERRSV, "Invalid PSGI array response (NULL element)");
1050 0           call_died(aTHX_ c, "PSGI request");
1051 0           return;
1052             }
1053 75           SV *msg = *msg_p;
1054 75           SV *hdrs = *hdrs_p;
1055 75           SV *body = *body_p;
1056              
1057             AV *headers;
1058 75 50         if (IsArrayRef(hdrs))
    50          
1059 75           headers = (AV*)SvRV(hdrs);
1060             else {
1061 0 0         sv_setpvs(ERRSV, "PSGI Headers must be an array-ref");
1062 0           call_died(aTHX_ c, "PSGI request");
1063 0           return;
1064             }
1065              
1066 75 50         if (likely(IsArrayRef(body))) {
    100          
1067 70           feersum_start_response(aTHX_ c, msg, headers, 0);
1068 70           feersum_write_whole_body(aTHX_ c, body);
1069             }
1070 5 50         else if (likely(SvROK(body))) { // probably an IO::Handle-like object
1071 5           feersum_start_response(aTHX_ c, msg, headers, 1);
1072             #ifdef FEERSUM_HAS_H2
1073             if (unlikely(c->is_h2_stream)) {
1074             /* H2: drain the IO::Handle synchronously since there is no
1075             * per-stream write watcher; nghttp2 handles flow control. */
1076             pump_h2_io_handle(aTHX_ c, body);
1077             } else
1078             #endif
1079             {
1080 5           c->poll_write_cb = newSVsv(body);
1081 5           c->poll_write_cb_is_io_handle = 1;
1082 5           conn_write_ready(c);
1083             }
1084             }
1085             else {
1086 0 0         sv_setpvs(ERRSV, "Expected PSGI array-ref or IO::Handle-like body");
1087 0           call_died(aTHX_ c, "PSGI request");
1088 0           return;
1089             }
1090             }
1091              
1092             static int
1093 61           feersum_close_handle (pTHX_ struct feer_conn *c, bool is_writer)
1094             {
1095             int RETVAL;
1096 61 100         if (is_writer) {
1097             trace("close writer fd=%d, c=%p, refcnt=%d\n", c->fd, c, SvREFCNT(c->self));
1098 54 100         if (c->poll_write_cb) {
1099 1           SV *tmp = c->poll_write_cb;
1100 1           c->poll_write_cb = NULL; // NULL before dec: prevents re-entrant double-free
1101 1           c->poll_write_cb_is_io_handle = 0;
1102 1           SvREFCNT_dec(tmp);
1103             }
1104             #ifdef FEERSUM_HAS_H2
1105             if (unlikely(c->is_h2_stream)) {
1106             if (c->responding < RESPOND_SHUTDOWN) {
1107             feersum_h2_close_write(aTHX_ c);
1108             change_responding_state(c, RESPOND_SHUTDOWN);
1109             }
1110             } else
1111             #endif
1112 54 50         if (c->responding < RESPOND_SHUTDOWN) {
1113 54           finish_wbuf(c); // only adds terminator if use_chunked is set
1114 54           change_responding_state(c, RESPOND_SHUTDOWN);
1115 54           conn_write_ready(c);
1116             }
1117 54           RETVAL = 1;
1118             }
1119             else {
1120             trace("close reader fd=%d, c=%p\n", c->fd, c);
1121 7 50         if (c->poll_read_cb) {
1122 0           SV *tmp = c->poll_read_cb;
1123 0           c->poll_read_cb = NULL;
1124 0           SvREFCNT_dec(tmp);
1125             }
1126 7 100         if (c->rbuf) {
1127 1           SvREFCNT_dec(c->rbuf);
1128 1           c->rbuf = NULL;
1129             }
1130 7 50         if (c->fd >= 0
1131             #ifdef FEERSUM_HAS_H2
1132             && !c->is_h2_stream
1133             #endif
1134             )
1135 7           RETVAL = shutdown(c->fd, SHUT_RD);
1136             else
1137 0           RETVAL = -1; // already closed or H2 stream
1138 7           change_receiving_state(c, RECEIVE_SHUTDOWN);
1139             }
1140              
1141             // disassociate the handle from the conn
1142 61           SvREFCNT_dec(c->self);
1143 61           return RETVAL;
1144             }
1145              
1146             static SV*
1147 6           feersum_conn_guard(pTHX_ struct feer_conn *c, SV *guard)
1148             {
1149 6 100         if (guard) {
1150 4 100         if (c->ext_guard) SvREFCNT_dec(c->ext_guard);
1151 4 50         c->ext_guard = SvOK(guard) ? newSVsv(guard) : NULL;
1152             }
1153 6 50         return c->ext_guard ? newSVsv(c->ext_guard) : &PL_sv_undef;
1154             }
1155              
1156             static void
1157 532           call_request_callback (struct feer_conn *c)
1158             {
1159             dTHX;
1160 532           dSP;
1161             int flags;
1162 532           struct feer_server *server = c->server;
1163 532           c->in_callback++;
1164 532           SvREFCNT_inc_void_NN(c->self);
1165 532           server->total_requests++;
1166              
1167             trace("request callback c=%p\n", c);
1168              
1169 532           ENTER;
1170 532           SAVETMPS;
1171 532 50         PUSHMARK(SP);
1172              
1173 532 100         if (server->request_cb_is_psgi) {
1174 88           HV *env = feersum_env(aTHX_ c);
1175 88 50         mXPUSHs(newRV_noinc((SV*)env));
1176 88           flags = G_EVAL|G_SCALAR;
1177             }
1178             else {
1179 444 50         mXPUSHs(feer_conn_2sv(c));
1180 444           flags = G_DISCARD|G_EVAL|G_VOID;
1181             }
1182              
1183 532           PUTBACK;
1184 532           int returned = call_sv(server->request_cb_cv, flags);
1185 532           SPAGAIN;
1186              
1187             trace("called request callback, errsv? %d\n", SvTRUE(ERRSV) ? 1 : 0);
1188              
1189 532 50         if (unlikely(SvTRUE(ERRSV))) {
    100          
1190 3           call_died(aTHX_ c, "request");
1191 3           returned = 0; // pretend nothing got returned
1192             }
1193              
1194 532           SV *psgi_response = NULL;
1195 532 100         if (server->request_cb_is_psgi && likely(returned >= 1)) {
    50          
1196 88           psgi_response = POPs;
1197 88           SvREFCNT_inc_void_NN(psgi_response);
1198             }
1199              
1200             trace("leaving request callback\n");
1201 532           PUTBACK;
1202              
1203 532 100         if (psgi_response) {
1204 88           feersum_handle_psgi_response(aTHX_ c, psgi_response, 1); // can_recurse
1205 88           SvREFCNT_dec(psgi_response);
1206             }
1207              
1208 532           c->in_callback--;
1209 532           SvREFCNT_dec(c->self);
1210              
1211 532 50         FREETMPS;
1212 532           LEAVE;
1213 532           }
1214              
1215             static void
1216 26           call_poll_callback (struct feer_conn *c, bool is_write)
1217             {
1218             dTHX;
1219 26           dSP;
1220              
1221 26 100         SV *cb = (is_write) ? c->poll_write_cb : c->poll_read_cb;
1222              
1223 26 50         if (unlikely(cb == NULL)) return;
1224              
1225 26           c->in_callback++;
1226              
1227             trace("%s poll callback c=%p cbrv=%p\n",
1228             is_write ? "write" : "read", c, cb);
1229              
1230 26           ENTER;
1231 26           SAVETMPS;
1232 26 50         PUSHMARK(SP);
1233 26           SV *hdl = new_feer_conn_handle(aTHX_ c, is_write);
1234 26 50         mXPUSHs(hdl);
1235 26           PUTBACK;
1236 26           call_sv(cb, G_DISCARD|G_EVAL|G_VOID);
1237 26           SPAGAIN;
1238              
1239             trace("called %s poll callback, errsv? %d\n",
1240             is_write ? "write" : "read", SvTRUE(ERRSV) ? 1 : 0);
1241              
1242 26 50         if (unlikely(SvTRUE(ERRSV))) {
    50          
1243 0 0         call_died(aTHX_ c, is_write ? "write poll" : "read poll");
1244             }
1245              
1246             // Neutralize the mortal handle before FREETMPS so its DESTROY doesn't
1247             // call feersum_close_handle and prematurely close the connection.
1248             // If the callback already called close(), SvUVX is already 0.
1249             {
1250 26           SV *inner = SvRV(hdl);
1251 26 50         if (SvUVX(inner)) {
1252 26           SvUVX(inner) = 0;
1253 26           SvREFCNT_dec(c->self); // balance new_feer_conn_handle's inc
1254             }
1255             }
1256              
1257             trace("leaving %s poll callback\n", is_write ? "write" : "read");
1258 26           PUTBACK;
1259 26 50         FREETMPS;
1260 26           LEAVE;
1261              
1262 26           c->in_callback--;
1263             }
1264              
1265             static void
1266 11           pump_io_handle (struct feer_conn *c)
1267             {
1268             dTHX;
1269 11           dSP;
1270              
1271 11 50         if (unlikely(c->poll_write_cb == NULL)) return;
1272              
1273 11           c->in_callback++;
1274              
1275             trace("pump io handle %d\n", c->fd);
1276              
1277 11           SV *old_rs = PL_rs;
1278 11 50         SvREFCNT_inc_simple_void(old_rs);
1279 11           PL_rs = sv_2mortal(newRV_noinc(newSViv(IO_PUMP_BUFSZ)));
1280 11           sv_setsv(get_sv("/", GV_ADD), PL_rs);
1281              
1282 11           ENTER;
1283 11           SAVETMPS;
1284              
1285 11 50         PUSHMARK(SP);
1286 11 50         XPUSHs(c->poll_write_cb);
1287 11           PUTBACK;
1288 11           int returned = call_method("getline", G_SCALAR|G_EVAL);
1289 11           SPAGAIN;
1290              
1291             trace("called getline on io handle fd=%d errsv=%d returned=%d\n",
1292             c->fd, SvTRUE(ERRSV) ? 1 : 0, returned);
1293              
1294 11 50         if (unlikely(SvTRUE(ERRSV))) {
    50          
1295             /* Restore PL_rs before call_died — it invokes Feersum::DIED which
1296             * is Perl code that (or whose callees) may read $/ */
1297 0           PL_rs = old_rs;
1298 0           sv_setsv(get_sv("/", GV_ADD), old_rs);
1299 0           call_died(aTHX_ c, "getline on io handle");
1300             // Clear poll callback to prevent re-invocation after error
1301 0 0         if (c->poll_write_cb) {
1302 0           SvREFCNT_dec(c->poll_write_cb);
1303 0           c->poll_write_cb = NULL;
1304             }
1305 0           c->poll_write_cb_is_io_handle = 0;
1306 0           goto done_pump_io;
1307             }
1308              
1309 11           SV *ret = NULL;
1310 11 50         if (returned > 0)
1311 11           ret = POPs;
1312 11 50         if (ret && SvMAGICAL(ret))
    50          
1313 0           ret = sv_2mortal(newSVsv(ret));
1314              
1315 11 50         if (unlikely(!ret || !SvOK(ret))) {
    100          
1316             // returned undef, so call the close method out of nicety
1317 5 50         PUSHMARK(SP);
1318 5 50         XPUSHs(c->poll_write_cb);
1319 5           PUTBACK;
1320 5           call_method("close", G_VOID|G_DISCARD|G_EVAL);
1321 5           SPAGAIN;
1322              
1323 5 50         if (unlikely(SvTRUE(ERRSV))) {
    50          
1324 0 0         trouble("Couldn't close body IO handle: %-p",ERRSV);
1325 0 0         sv_setsv(ERRSV, &PL_sv_undef);
1326             }
1327              
1328 5           SvREFCNT_dec(c->poll_write_cb);
1329 5           c->poll_write_cb = NULL;
1330 5           c->poll_write_cb_is_io_handle = 0;
1331 5           finish_wbuf(c);
1332 5           change_responding_state(c, RESPOND_SHUTDOWN);
1333              
1334 5           goto done_pump_io;
1335             }
1336              
1337 6 50         if (c->use_chunked)
1338 6           add_chunk_sv_to_wbuf(c, ret);
1339             else
1340 0           add_sv_to_wbuf(c, ret);
1341              
1342 11           done_pump_io:
1343             trace("leaving pump io handle %d\n", c->fd);
1344              
1345             // Restore PL_rs before FREETMPS — the mortal RV set at line 1245
1346             // would be freed by FREETMPS, leaving PL_rs as a dangling pointer.
1347 11           PL_rs = old_rs;
1348 11           sv_setsv(get_sv("/", GV_ADD), old_rs);
1349 11           SvREFCNT_dec(old_rs);
1350              
1351 11           PUTBACK;
1352 11 50         FREETMPS;
1353 11           LEAVE;
1354              
1355 11           c->in_callback--;
1356             }
1357              
1358             static int
1359 9           psgix_io_svt_get (pTHX_ SV *sv, MAGIC *mg)
1360             {
1361 9           dSP;
1362              
1363 9           struct feer_conn *c = sv_2feer_conn(mg->mg_obj);
1364             trace("invoking psgix.io magic for fd=%d\n", c->fd);
1365              
1366 9           sv_unmagic(sv, PERL_MAGIC_ext);
1367              
1368             #ifdef FEERSUM_HAS_H2
1369             /* H2 tunnel: create socketpair and expose sv[1] as the IO handle.
1370             * For PSGI: auto-send 200 HEADERS and enable response swallowing so
1371             * apps can use the same psgix.io code for H1 and H2 transparently. */
1372             if (c->is_h2_stream) {
1373             struct feer_h2_stream *stream = (struct feer_h2_stream *)c->read_ev_timer.data;
1374             if (!stream || !stream->is_tunnel) {
1375             trouble("psgix.io: not supported on regular H2 streams fd=%d\n", c->fd);
1376             return 0;
1377             }
1378             h2_tunnel_auto_accept(aTHX_ c, stream);
1379             /* Re-fetch stream: auto_accept → session_send may have freed it */
1380             stream = (struct feer_h2_stream *)c->read_ev_timer.data;
1381             if (!stream) {
1382             trouble("psgix.io: stream freed during auto_accept fd=%d\n", c->fd);
1383             return 0;
1384             }
1385             feer_h2_setup_tunnel(aTHX_ stream);
1386             if (!stream->tunnel_established) {
1387             trouble("psgix.io: tunnel setup failed fd=%d\n", c->fd);
1388             struct feer_conn *parent = stream->parent;
1389             SvREFCNT_inc_void_NN(parent->self);
1390             h2_submit_rst(parent->h2_session, stream->stream_id,
1391             NGHTTP2_INTERNAL_ERROR);
1392             feer_h2_session_send(parent);
1393             SvREFCNT_dec(parent->self);
1394             return 0;
1395             }
1396              
1397             ENTER;
1398             SAVETMPS;
1399             PUSHMARK(SP);
1400             XPUSHs(sv);
1401             mXPUSHs(newSViv(stream->tunnel_sv1));
1402             PUTBACK;
1403              
1404             call_pv("Feersum::Connection::_raw", G_VOID|G_DISCARD|G_EVAL);
1405             SPAGAIN;
1406              
1407             if (unlikely(SvTRUE(ERRSV))) {
1408             call_died(aTHX_ c, "psgix.io H2 tunnel magic");
1409             } else {
1410             SV *io_glob = SvRV(sv);
1411             GvSV(io_glob) = newRV_inc(c->self);
1412             stream->tunnel_sv1 = -1;
1413             }
1414              
1415             c->io_taken = 1;
1416             PUTBACK;
1417             FREETMPS;
1418             LEAVE;
1419             return 0;
1420             }
1421             #endif
1422              
1423             #ifdef FEERSUM_HAS_TLS
1424             /* TLS tunnel: create socketpair relay for psgix.io over TLS */
1425 9 50         if (c->tls) {
1426 0           feer_tls_setup_tunnel(c);
1427 0 0         if (!c->tls_tunnel) {
1428 0           trouble("psgix.io: TLS tunnel setup failed fd=%d\n", c->fd);
1429 0           return 0;
1430             }
1431              
1432 0           ENTER;
1433 0           SAVETMPS;
1434 0 0         PUSHMARK(SP);
1435 0 0         XPUSHs(sv);
1436 0 0         mXPUSHs(newSViv(c->tls_tunnel_sv1));
1437 0           PUTBACK;
1438              
1439 0           call_pv("Feersum::Connection::_raw", G_VOID|G_DISCARD|G_EVAL);
1440 0           SPAGAIN;
1441              
1442 0 0         if (unlikely(SvTRUE(ERRSV))) {
    0          
1443 0           call_died(aTHX_ c, "psgix.io TLS tunnel magic");
1444             /* fd NOT consumed by _raw on failure; conn cleanup will close it */
1445             } else {
1446 0           SV *io_glob = SvRV(sv);
1447 0           GvSV(io_glob) = newRV_inc(c->self);
1448 0           c->tls_tunnel_sv1 = -1;
1449             }
1450              
1451 0           c->io_taken = 1;
1452 0           stop_read_timer(c);
1453 0           stop_write_timer(c);
1454 0           PUTBACK;
1455 0 0         FREETMPS;
1456 0           LEAVE;
1457 0           return 0;
1458             }
1459             #endif
1460              
1461 9           ENTER;
1462 9           SAVETMPS;
1463              
1464 9 50         PUSHMARK(SP);
1465 9 50         XPUSHs(sv);
1466 9 50         mXPUSHs(newSViv(c->fd));
1467 9           PUTBACK;
1468              
1469 9           call_pv("Feersum::Connection::_raw", G_VOID|G_DISCARD|G_EVAL);
1470 9           SPAGAIN;
1471              
1472 9 50         if (unlikely(SvTRUE(ERRSV))) {
    50          
1473 0           call_died(aTHX_ c, "psgix.io magic");
1474             }
1475             else {
1476 9           SV *io_glob = SvRV(sv);
1477 9           GvSV(io_glob) = newRV_inc(c->self);
1478              
1479             // Put whatever remainder data into the socket buffer.
1480             // Optimizes for the websocket case.
1481             // Use return_from_psgix_io() to pull data back for keepalive.
1482 9 50         if (likely(c->rbuf && SvOK(c->rbuf) && SvCUR(c->rbuf))) {
    50          
    50          
    50          
1483             STRLEN rbuf_len;
1484 9           const char *rbuf_ptr = SvPV(c->rbuf, rbuf_len);
1485 9           IO *io = GvIOp(io_glob);
1486 9 50         if (unlikely(!io)) {
1487 0           trouble("psgix.io: GvIOp returned NULL fd=%d\n", c->fd);
1488             // Skip unread, data will remain in rbuf
1489             }
1490             else {
1491             // PerlIO_unread copies the data internally, so it's safe to
1492             // clear rbuf after. Use SvCUR_set to keep buffer allocated
1493             // (more efficient for potential reuse).
1494 9           SSize_t pushed = PerlIO_unread(IoIFP(io), (const void *)rbuf_ptr, rbuf_len);
1495 9 50         if (likely(pushed == (SSize_t)rbuf_len)) {
1496 9           SvCUR_set(c->rbuf, 0);
1497 0 0         } else if (pushed > 0) {
1498 0           sv_chop(c->rbuf, rbuf_ptr + pushed);
1499 0           trouble("PerlIO_unread partial: %zd of %"Sz_uf" bytes fd=%d\n",
1500             (size_t)pushed, (Sz)rbuf_len, c->fd);
1501             } else {
1502 0           trouble("PerlIO_unread failed in psgix.io magic fd=%d\n", c->fd);
1503             }
1504             }
1505             }
1506              
1507             // Stop Feersum's watchers - user now owns the socket
1508 9           stop_read_watcher(c);
1509 9           stop_read_timer(c);
1510 9           stop_write_timer(c);
1511             // don't stop write watcher in case there's outstanding data
1512              
1513 9           c->io_taken = 1;
1514             }
1515              
1516 9 50         FREETMPS;
1517 9           LEAVE;
1518              
1519 9           return 0;
1520             }
1521              
1522             #ifdef FEERSUM_HAS_H2
1523             static void
1524             h2_tunnel_auto_accept(pTHX_ struct feer_conn *c, struct feer_h2_stream *stream)
1525             {
1526             if (c->responding != RESPOND_NOT_STARTED)
1527             return;
1528             AV *empty_hdr = newAV();
1529             SV *status_sv = newSVpvs("200");
1530             feersum_start_response(aTHX_ c, status_sv, empty_hdr, 1);
1531             SvREFCNT_dec(status_sv);
1532             SvREFCNT_dec((SV *)empty_hdr);
1533             /* Re-fetch stream: feersum_start_response → feer_h2_session_send may
1534             * have freed it via h2_on_stream_close_cb (e.g. RST_STREAM queued) */
1535             stream = (struct feer_h2_stream *)c->read_ev_timer.data;
1536             if (stream)
1537             stream->tunnel_swallow_response = 1;
1538             }
1539              
1540             static void
1541             pump_h2_io_handle(pTHX_ struct feer_conn *c, SV *body)
1542             {
1543             dSP;
1544             SV *io = newSVsv(body);
1545             c->in_callback++;
1546             SV *old_rs = PL_rs;
1547             SvREFCNT_inc_simple_void(old_rs);
1548             PL_rs = sv_2mortal(newRV_noinc(newSViv(IO_PUMP_BUFSZ)));
1549             sv_setsv(get_sv("/", GV_ADD), PL_rs);
1550             ENTER;
1551             SAVETMPS;
1552             for (;;) {
1553             ENTER; SAVETMPS;
1554             PUSHMARK(SP);
1555             XPUSHs(io);
1556             PUTBACK;
1557             int returned = call_method("getline", G_SCALAR|G_EVAL);
1558             SPAGAIN;
1559             if (unlikely(SvTRUE(ERRSV))) {
1560             /* Restore PL_rs before call_died — it invokes Feersum::DIED which
1561             * is Perl code that (or whose callees) may read $/ */
1562             PL_rs = old_rs;
1563             sv_setsv(get_sv("/", GV_ADD), old_rs);
1564             call_died(aTHX_ c, "getline on H2 io handle");
1565             PUTBACK; FREETMPS; LEAVE;
1566             break;
1567             }
1568             SV *ret = NULL;
1569             if (returned > 0) ret = POPs;
1570             if (ret && SvMAGICAL(ret)) ret = sv_2mortal(newSVsv(ret));
1571             if (!ret || !SvOK(ret)) {
1572             PUSHMARK(SP); XPUSHs(io); PUTBACK;
1573             call_method("close", G_VOID|G_DISCARD|G_EVAL);
1574             SPAGAIN;
1575             if (unlikely(SvTRUE(ERRSV))) {
1576             trouble("Couldn't close body IO handle: %-p",ERRSV);
1577             sv_setsv(ERRSV, &PL_sv_undef);
1578             }
1579             PUTBACK; FREETMPS; LEAVE;
1580             feersum_h2_close_write(aTHX_ c);
1581             break;
1582             }
1583             feersum_h2_write_chunk(aTHX_ c, ret);
1584             PUTBACK; FREETMPS; LEAVE;
1585             }
1586             FREETMPS; LEAVE;
1587             PL_rs = old_rs;
1588             sv_setsv(get_sv("/", GV_ADD), old_rs);
1589             SvREFCNT_dec(old_rs);
1590             c->in_callback--;
1591             SvREFCNT_dec(io);
1592             }
1593              
1594             static SV*
1595             feersum_env_method_h2(pTHX_ struct feer_conn *c, struct feer_req *r)
1596             {
1597             if (unlikely(c->is_h2_stream)) {
1598             struct feer_h2_stream *stream = (struct feer_h2_stream *)c->read_ev_timer.data;
1599             if (stream && stream->is_tunnel && stream->h2_protocol)
1600             return SvREFCNT_inc_simple_NN(method_GET);
1601             }
1602             return feersum_env_method(aTHX_ r);
1603             }
1604             #endif