File Coverage

feersum_psgi.c.inc
Criterion Covered Total %
statement 569 698 81.5
branch 374 630 59.3
condition n/a
subroutine n/a
pod n/a
total 943 1328 71.0


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