File Coverage

feersum_psgi.c.inc
Criterion Covered Total %
statement 584 714 81.7
branch 398 672 59.2
condition n/a
subroutine n/a
pod n/a
total 982 1386 70.8


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