@scriptc/runtime 0.0.29 → 0.0.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scriptc/runtime",
3
- "version": "0.0.29",
3
+ "version": "0.0.31",
4
4
  "description": "scriptc native runtime — C sources, compiled into every scriptc binary",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://scriptc.dev",
package/src/scr_http.c CHANGED
@@ -112,6 +112,10 @@ static const char *scr_http_reason(int code) {
112
112
  * typedef lives in scr_runtime.h (both units name it). */
113
113
  static const ScrHttpH2Ops *scr_http_h2_ops = NULL;
114
114
  static void (*scr_http_h2_request_hook)(void *h2ctx, ScrClosure *cb /*moves*/,
115
+ void *fn, bool once) = NULL;
116
+ static void (*scr_http_h2_connect_hook)(void *h2ctx, ScrClosure *cb /*moves*/,
117
+ void *h1_fn, void *h2_fn, bool once) = NULL;
118
+ static void (*scr_http_h2_upgrade_hook)(void *h2ctx, ScrClosure *cb /*moves*/,
115
119
  void *fn, bool once) = NULL;
116
120
 
117
121
  void scr_http_set_h2_ops(const ScrHttpH2Ops *ops) { scr_http_h2_ops = ops; }
@@ -119,6 +123,13 @@ void scr_http_set_h2_ops(const ScrHttpH2Ops *ops) { scr_http_h2_ops = ops; }
119
123
  void scr_http_set_h2_request_hook(void (*hook)(void *h2ctx, ScrClosure *cb, void *fn, bool once)) {
120
124
  scr_http_h2_request_hook = hook;
121
125
  }
126
+ void scr_http_set_h2_connect_hook(void (*hook)(void *h2ctx, ScrClosure *cb,
127
+ void *h1_fn, void *h2_fn, bool once)) {
128
+ scr_http_h2_connect_hook = hook;
129
+ }
130
+ void scr_http_set_h2_upgrade_hook(void (*hook)(void *h2ctx, ScrClosure *cb, void *fn, bool once)) {
131
+ scr_http_h2_upgrade_hook = hook;
132
+ }
122
133
 
123
134
  /* ── the request handle ──────────────────────────────────────────────── */
124
135
 
@@ -762,6 +773,27 @@ static void scr_http_buf_append(ScrHttpBuf *b, const char *s, size_t n) {
762
773
 
763
774
  static void scr_http_buf_str(ScrHttpBuf *b, const char *s) { scr_http_buf_append(b, s, strlen(s)); }
764
775
 
776
+ static bool scr_http_header_has_token(const ScrStr *value, const char *token) {
777
+ size_t token_len = strlen(token);
778
+ for (size_t off = 0; off < value->len;) {
779
+ while (off < value->len &&
780
+ (value->data[off] == ',' || value->data[off] == ' ' || value->data[off] == '\t')) off++;
781
+ size_t end = off;
782
+ while (end < value->len && value->data[end] != ',' &&
783
+ value->data[end] != ' ' && value->data[end] != '\t') end++;
784
+ if (end - off == token_len) {
785
+ bool equal = true;
786
+ for (size_t i = 0; i < token_len && equal; i++) {
787
+ if (tolower((unsigned char)value->data[off + i]) != token[i]) equal = false;
788
+ }
789
+ if (equal) return true;
790
+ }
791
+ while (end < value->len && value->data[end] != ',') end++;
792
+ off = end;
793
+ }
794
+ return false;
795
+ }
796
+
765
797
  /* Serializes and sends the head. `body_len` >= 0 fixes Content-Length
766
798
  * (the end(data)-before-head path); -1 means chunked unless the user set
767
799
  * Content-Length or Transfer-Encoding (Node's useChunkedEncodingByDefault
@@ -788,12 +820,19 @@ static void scr_http_res_send_head(ScrHttpRes *r, long long body_len) {
788
820
  if (r->status_msg) scr_http_buf_append(&b, r->status_msg->data, r->status_msg->len);
789
821
  else scr_http_buf_str(&b, scr_http_reason(status));
790
822
  scr_http_buf_str(&b, "\r\n");
823
+ bool user_chunked = false;
791
824
  for (size_t i = 0; i < r->nheaders; i++) {
792
825
  scr_http_buf_append(&b, r->hnames[i]->data, r->hnames[i]->len);
793
826
  scr_http_buf_str(&b, ": ");
794
827
  scr_http_buf_append(&b, r->hvalues[i]->data, r->hvalues[i]->len);
795
828
  scr_http_buf_str(&b, "\r\n");
829
+ bool transfer = r->hnames[i]->len == 17;
830
+ for (size_t j = 0; j < 17 && transfer; j++) {
831
+ if (tolower((unsigned char)r->hnames[i]->data[j]) != "transfer-encoding"[j]) transfer = false;
832
+ }
833
+ if (transfer && scr_http_header_has_token(r->hvalues[i], "chunked")) user_chunked = true;
796
834
  }
835
+ r->chunked = user_chunked;
797
836
  if (!r->no_date && !scr_http_res_has_header(r, "date")) {
798
837
  /* Node's utcDate: "Date: Wed, 16 Jul 2026 04:20:00 GMT" */
799
838
  time_t now = time(NULL);
@@ -1406,6 +1445,50 @@ static void scr_http_srv_ctx_settle(void *p) {
1406
1445
  scr_net_ls_drop(&ctx->connect_ls);
1407
1446
  }
1408
1447
 
1448
+ /* Combined HTTP/2 allowHTTP1 servers retain the HTTP/1 parser ctx behind
1449
+ * their h2-facing alias. These narrow hooks let scr_http2.c mirror request
1450
+ * listeners and settle both protocol lists without exposing the ctx shape. */
1451
+ void scr_http1_ctx_on_request(void *p, ScrClosure *cb /*moves*/, ScrHttpReqFn fn, bool once,
1452
+ ScrNetOnce *shared_once /*moves, nullable*/) {
1453
+ ScrHttpSrvCtx *ctx = (ScrHttpSrvCtx *)p;
1454
+ if (ctx == NULL || ctx->proto != SCR_NET_PROTO_HTTP1) {
1455
+ scr_closure_release(cb);
1456
+ scr_net_once_release(shared_once);
1457
+ return;
1458
+ }
1459
+ if (shared_once != NULL) {
1460
+ scr_net_ls_add_shared_once(&ctx->request_ls, cb, (void *)fn, shared_once);
1461
+ } else {
1462
+ scr_net_ls_add(&ctx->request_ls, cb, (void *)fn, once);
1463
+ }
1464
+ }
1465
+
1466
+ void scr_http1_ctx_on_connect(void *p, ScrClosure *cb /*moves*/, ScrHttpUpgradeFn fn, bool once,
1467
+ ScrNetOnce *shared_once /*moves, nullable*/) {
1468
+ ScrHttpSrvCtx *ctx = (ScrHttpSrvCtx *)p;
1469
+ if (ctx == NULL || ctx->proto != SCR_NET_PROTO_HTTP1) {
1470
+ scr_closure_release(cb);
1471
+ scr_net_once_release(shared_once);
1472
+ return;
1473
+ }
1474
+ if (shared_once != NULL) {
1475
+ scr_net_ls_add_shared_once(&ctx->connect_ls, cb, (void *)fn, shared_once);
1476
+ } else {
1477
+ scr_net_ls_add(&ctx->connect_ls, cb, (void *)fn, once);
1478
+ }
1479
+ }
1480
+
1481
+ void scr_http1_ctx_on_upgrade(void *p, ScrClosure *cb /*moves*/, ScrHttpUpgradeFn fn, bool once) {
1482
+ ScrHttpSrvCtx *ctx = (ScrHttpSrvCtx *)p;
1483
+ if (ctx == NULL || ctx->proto != SCR_NET_PROTO_HTTP1) {
1484
+ scr_closure_release(cb);
1485
+ return;
1486
+ }
1487
+ scr_net_ls_add(&ctx->upgrade_ls, cb, (void *)fn, once);
1488
+ }
1489
+
1490
+ void scr_http1_ctx_settle(void *p) { scr_http_srv_ctx_settle(p); }
1491
+
1409
1492
  static void scr_http_conn_drop_request(ScrHttpConn *conn, bool fire_end) {
1410
1493
  /* the deferred 'close' emits: res before req, Node's order */
1411
1494
  if (conn->res) scr_http_queue_res_close(conn->res);
@@ -1626,13 +1709,12 @@ static bool scr_http_conn_parse_head(ScrHttpConn *conn, size_t head_len) {
1626
1709
  /* A CONNECT request: 'connect' fires INSTEAD of 'request' — the
1627
1710
  * upgrade machinery's exact shape ((req, socket, head); the parser
1628
1711
  * steps aside; no listener destroys the socket, Node's default). The
1629
- * h2 compat server's HTTP/1.1 arm lands here too under the
1630
- * allowHTTP1 lowering it is the ONLY arm (SEMANTICS.md divergence
1631
- * 57), so a portless-style listener always takes the raw socket. */
1712
+ * h2 compat server's HTTP/1.1 arm lands here too; its HTTP/2 arm is
1713
+ * dispatched separately with the compatibility response handle. */
1632
1714
  if (req->method->len == 7 && memcmp(req->method->data, "CONNECT", 7) == 0) {
1633
1715
  memmove(conn->buf, conn->buf + head_len, conn->len - head_len);
1634
1716
  conn->len -= head_len;
1635
- if (conn->srv->connect_ls.n == 0) {
1717
+ if (!scr_net_ls_has_live(&conn->srv->connect_ls)) {
1636
1718
  scr_http_req_release(req);
1637
1719
  conn->len = 0;
1638
1720
  scr_net_sock_destroy(conn->sock);
@@ -2082,6 +2164,16 @@ ScrHttpReq *scr_http_h2_req_new(ScrNetSocket *sock /*borrowed, nullable*/,
2082
2164
  return req;
2083
2165
  }
2084
2166
 
2167
+ void *scr_http_req_h2_stream(ScrHttpReq *r) {
2168
+ return r->h2_stream != NULL ? scr_http_h2_ops->retain(r->h2_stream) : NULL;
2169
+ }
2170
+
2171
+ void *scr_http_req_h2_stream_or_throw(ScrHttpReq *r, ScrStr *member /*borrowed*/) {
2172
+ if (r->h2_stream != NULL) return scr_http_h2_ops->retain(r->h2_stream);
2173
+ scr_http2_stream_undef_call(member);
2174
+ return NULL;
2175
+ }
2176
+
2085
2177
  void scr_http_h2_req_line(ScrHttpReq *r, ScrStr *method /*borrowed, nullable*/,
2086
2178
  ScrStr *url /*borrowed, nullable*/) {
2087
2179
  if (method != NULL) {
@@ -2148,23 +2240,36 @@ bool scr_http_h2_res_finished(ScrHttpRes *r) { return r->finished; }
2148
2240
  * server.on("upgrade", ...) below: the registration twins of on_request.
2149
2241
  * On a server with no HTTP parser neither list ever fires — honest dead
2150
2242
  * weight, Node's shape. */
2151
- void scr_http_server_on_connect(ScrNetServer *s, ScrClosure *cb /*moves*/, ScrHttpUpgradeFn fn,
2243
+ void scr_http_server_on_connect(ScrNetServer *s, ScrClosure *cb /*moves*/,
2244
+ ScrHttpUpgradeFn h1_fn, ScrHttpConnectH2Fn h2_fn,
2152
2245
  bool once) {
2153
2246
  ScrHttpSrvCtx *ctx = (ScrHttpSrvCtx *)scr_net_server_get_http_ctx(s);
2154
- if (ctx != NULL && ctx->proto != SCR_NET_PROTO_HTTP1) ctx = NULL; /* an h2 server's ctx */
2247
+ if (ctx != NULL && ctx->proto != SCR_NET_PROTO_HTTP1) {
2248
+ if (scr_http_h2_connect_hook != NULL && !scr_net_server_settled(s)) {
2249
+ scr_http_h2_connect_hook(ctx, cb, (void *)h1_fn, (void *)h2_fn, once);
2250
+ return;
2251
+ }
2252
+ ctx = NULL;
2253
+ }
2155
2254
  if (ctx == NULL) {
2156
2255
  /* no HTTP parser on this server: the event can never fire (Node's
2157
2256
  * net server never emits 'connect' either) — honest dead weight */
2158
2257
  scr_closure_release(cb);
2159
2258
  return;
2160
2259
  }
2161
- scr_net_ls_add(&ctx->connect_ls, cb, (void *)fn, once);
2260
+ scr_net_ls_add(&ctx->connect_ls, cb, (void *)h1_fn, once);
2162
2261
  }
2163
2262
 
2164
2263
  void scr_http_server_on_upgrade(ScrNetServer *s, ScrClosure *cb /*moves*/, ScrHttpUpgradeFn fn,
2165
- bool once) {
2264
+ bool once) {
2166
2265
  ScrHttpSrvCtx *ctx = (ScrHttpSrvCtx *)scr_net_server_get_http_ctx(s);
2167
- if (ctx != NULL && ctx->proto != SCR_NET_PROTO_HTTP1) ctx = NULL; /* an h2 server's ctx */
2266
+ if (ctx != NULL && ctx->proto != SCR_NET_PROTO_HTTP1) {
2267
+ if (scr_http_h2_upgrade_hook != NULL && !scr_net_server_settled(s)) {
2268
+ scr_http_h2_upgrade_hook(ctx, cb, (void *)fn, once);
2269
+ return;
2270
+ }
2271
+ ctx = NULL;
2272
+ }
2168
2273
  if (ctx == NULL) {
2169
2274
  scr_closure_release(cb);
2170
2275
  return;
package/src/scr_http2.c CHANGED
@@ -688,7 +688,11 @@ typedef struct ScrH2SrvCtx {
688
688
  ScrNetLs stream_ls;
689
689
  ScrNetLs session_ls;
690
690
  ScrNetLs request_ls; /* the compat (req, res) listeners — createServer's
691
- * eager handler and server.on("request", ...) */
691
+ * eager handler and server.on("request", ...) */
692
+ ScrNetLs connect_ls; /* traditional/RFC 8441 CONNECT (req, res) listeners */
693
+ ScrNetLs session_error_ls; /* server 'sessionError' (err, session) */
694
+ void *http1_ctx; /* borrowed: owned by the TLS wrapper's inner hook */
695
+ bool enable_connect_protocol;
692
696
  } ScrH2SrvCtx;
693
697
 
694
698
  /* The SETTINGS the API observes (session.localSettings/remoteSettings —
@@ -750,6 +754,7 @@ struct ScrH2Session {
750
754
  /* the API-visible settings records + the in-flight local ack */
751
755
  ScrH2Settings local_settings, remote_settings;
752
756
  bool pending_settings_ack;
757
+ bool error_emitted;
753
758
  ScrNetLs stream_ls, close_ls, err_ls, connect_ls, goaway_ls,
754
759
  remote_settings_ls, local_settings_ls;
755
760
  };
@@ -802,6 +807,8 @@ static void scr_h2_srv_ctx_release(ScrH2SrvCtx *ctx) {
802
807
  scr_net_ls_drop(&ctx->stream_ls);
803
808
  scr_net_ls_drop(&ctx->session_ls);
804
809
  scr_net_ls_drop(&ctx->request_ls);
810
+ scr_net_ls_drop(&ctx->connect_ls);
811
+ scr_net_ls_drop(&ctx->session_error_ls);
805
812
  free(ctx);
806
813
  }
807
814
 
@@ -949,6 +956,11 @@ static void scr_h2_send_settings(ScrH2Session *s) {
949
956
  scr_h2_send_frame(s, SCR_H2_F_SETTINGS, 0, 0, no_push, 6);
950
957
  return;
951
958
  }
959
+ if (s->local_settings.enable_connect_protocol) {
960
+ static const uint8_t enable_connect[6] = { 0x0, 0x8, 0, 0, 0, 1 };
961
+ scr_h2_send_frame(s, SCR_H2_F_SETTINGS, 0, 0, enable_connect, 6);
962
+ return;
963
+ }
952
964
  scr_h2_send_frame(s, SCR_H2_F_SETTINGS, 0, 0, NULL, 0);
953
965
  }
954
966
 
@@ -1233,6 +1245,8 @@ static void scr_h2_cleanup_atexit(void) {
1233
1245
 
1234
1246
  static const ScrHttpH2Ops scr_h2_compat_ops;
1235
1247
  static void scr_h2_request_hook(void *ctx, ScrClosure *cb, void *fn, bool once);
1248
+ static void scr_h2_connect_hook(void *ctx, ScrClosure *cb, void *h1_fn, void *h2_fn, bool once);
1249
+ static void scr_h2_upgrade_hook(void *ctx, ScrClosure *cb, void *fn, bool once);
1236
1250
 
1237
1251
  static void scr_http2_install(void) {
1238
1252
  static bool installed = false;
@@ -1244,6 +1258,8 @@ static void scr_http2_install(void) {
1244
1258
  * ops vtable and h2-tagged 'request' registrations through the hook */
1245
1259
  scr_http_set_h2_ops(&scr_h2_compat_ops);
1246
1260
  scr_http_set_h2_request_hook(&scr_h2_request_hook);
1261
+ scr_http_set_h2_connect_hook(&scr_h2_connect_hook);
1262
+ scr_http_set_h2_upgrade_hook(&scr_h2_upgrade_hook);
1247
1263
  }
1248
1264
 
1249
1265
  /* ── session lifecycle ───────────────────────────────────────────────── */
@@ -1281,6 +1297,31 @@ static void scr_h2_session_abort_streams(ScrH2Session *s) {
1281
1297
  }
1282
1298
  }
1283
1299
 
1300
+ static void scr_h2_session_fail(ScrH2Session *s, const char *text) {
1301
+ if (s->error_emitted) return;
1302
+ s->error_emitted = true;
1303
+ ScrStr *msg = scr_str_new(text, strlen(text));
1304
+ if (s->srv != NULL && s->srv->session_error_ls.n > 0) {
1305
+ ScrNetL *snap;
1306
+ size_t n = scr_net_ls_snapshot(&s->srv->session_error_ls, &snap);
1307
+ ScrNetServer *server = s->sock ? scr_net_sock_server(s->sock) : NULL;
1308
+ scr_dyn_this_push(server, SCR_DYNH_NET_SERVER);
1309
+ for (size_t i = 0; i < n; i++) {
1310
+ if (!scr_exc_pending()) {
1311
+ ((ScrH2SessionErrorFn)snap[i].fn)(snap[i].cb, msg,
1312
+ scr_http2_session_retain(s));
1313
+ }
1314
+ scr_closure_release(snap[i].cb);
1315
+ }
1316
+ scr_dyn_this_pop();
1317
+ free(snap);
1318
+ }
1319
+ if (!scr_exc_pending() && s->err_ls.n > 0) {
1320
+ scr_net_fire_err_this(&s->err_ls, msg, s, SCR_DYNH_H2_SESSION);
1321
+ }
1322
+ scr_str_release(msg);
1323
+ }
1324
+
1284
1325
  void scr_http2_session_destroy(ScrH2Session *s) {
1285
1326
  if (s->destroyed) return;
1286
1327
  s->destroyed = true;
@@ -1303,7 +1344,7 @@ static void scr_h2_dispatch_headers(ScrH2Session *s, int32_t stream_id, uint8_t
1303
1344
  ScrH2Headers *h);
1304
1345
  static void scr_h2_stream_flush(ScrH2Stream *st);
1305
1346
  static void scr_h2_compat_dispatch(ScrH2Session *s, ScrH2Stream *st, const ScrH2Headers *h,
1306
- bool end_stream);
1347
+ bool end_stream, ScrNetLs *listeners);
1307
1348
 
1308
1349
  /* The settings record the API observes (Node's key order; maxHeaderSize
1309
1350
  * aliases maxHeaderListSize; customSettings not modeled — empty). +1. */
@@ -1342,8 +1383,7 @@ static bool scr_h2_on_frame(ScrH2Session *s, uint8_t type, uint8_t flags, int32_
1342
1383
  const uint8_t *p, size_t n) {
1343
1384
  /* An open header block admits only its CONTINUATIONs. */
1344
1385
  if (s->hblock_active && type != SCR_H2_F_CONTINUATION) {
1345
- scr_http2_session_destroy(s);
1346
- return false;
1386
+ goto proto_err;
1347
1387
  }
1348
1388
  switch (type) {
1349
1389
  case SCR_H2_F_HEADERS: {
@@ -1583,6 +1623,7 @@ static bool scr_h2_on_frame(ScrH2Session *s, uint8_t type, uint8_t flags, int32_
1583
1623
  return true;
1584
1624
  proto_err:
1585
1625
  scr_h2_send_goaway(s, 1 /* NGHTTP2_PROTOCOL_ERROR */);
1626
+ scr_h2_session_fail(s, "Protocol error");
1586
1627
  scr_http2_session_destroy(s);
1587
1628
  return false;
1588
1629
  }
@@ -1605,6 +1646,7 @@ static void scr_h2_on_data(void *ctx, const char *buf, size_t n) {
1605
1646
  if (!s->client && !s->preface_done) {
1606
1647
  if (s->rlen - off < 24) goto done;
1607
1648
  if (memcmp(s->rbuf + off, SCR_H2_PREFACE, 24) != 0) {
1649
+ scr_h2_session_fail(s, "Protocol error");
1608
1650
  scr_http2_session_destroy(s);
1609
1651
  goto done;
1610
1652
  }
@@ -1659,8 +1701,10 @@ static void scr_h2_on_closed(void *ctx) {
1659
1701
 
1660
1702
  static bool scr_h2_on_err(void *ctx, ScrStr *msg) {
1661
1703
  ScrH2Session *s = ctx;
1662
- if (s->err_ls.n == 0) return false; /* the socket's error story applies */
1663
- scr_net_fire_err_this(&s->err_ls, msg, s, SCR_DYNH_H2_SESSION);
1704
+ if (s->err_ls.n == 0 && (s->srv == NULL || s->srv->session_error_ls.n == 0)) {
1705
+ return false; /* the socket's error story applies */
1706
+ }
1707
+ scr_h2_session_fail(s, (const char *)msg->data);
1664
1708
  return true;
1665
1709
  }
1666
1710
 
@@ -2043,6 +2087,15 @@ static void scr_h2_dispatch_headers(ScrH2Session *s, int32_t stream_id, uint8_t
2043
2087
  st->end_recv = true;
2044
2088
  st->state = SCR_H2_S_CLOSED_REMOTE;
2045
2089
  }
2090
+ bool connect_request = false;
2091
+ for (size_t i = 0; i < h->n; i++) {
2092
+ ScrStr *name = h->names[i];
2093
+ ScrStr *value = h->values[i];
2094
+ if (name->len == 7 && memcmp(name->data, ":method", 7) == 0 &&
2095
+ value->len == 7 && memcmp(value->data, "CONNECT", 7) == 0) {
2096
+ connect_request = true;
2097
+ }
2098
+ }
2046
2099
  ScrArr *pairs = scr_h2_headers_pairs(h, false, NULL);
2047
2100
  ScrNetServer *server = s->sock ? scr_net_sock_server(s->sock) : NULL;
2048
2101
  if (s->srv) scr_h2_fire_stream_list(&s->srv->stream_ls, st, pairs, fl, server, SCR_DYNH_NET_SERVER);
@@ -2050,9 +2103,12 @@ static void scr_h2_dispatch_headers(ScrH2Session *s, int32_t stream_id, uint8_t
2050
2103
  scr_h2_fire_stream_list(&s->stream_ls, st, pairs, fl, s, SCR_DYNH_H2_SESSION);
2051
2104
  }
2052
2105
  scr_arr_release(pairs);
2053
- /* the compat layer: 'request' listeners get the (req, res) pair */
2054
- if (!scr_exc_pending() && s->srv != NULL && s->srv->request_ls.n > 0 && !st->destroyed) {
2055
- scr_h2_compat_dispatch(s, st, h, end_stream);
2106
+ /* The stream events precede their compatibility event, Node's order.
2107
+ * Both traditional and extended CONNECT emit 'connect', never
2108
+ * 'request'; ordinary methods retain the request path. */
2109
+ if (!scr_exc_pending() && s->srv != NULL && !st->destroyed) {
2110
+ ScrNetLs *listeners = connect_request ? &s->srv->connect_ls : &s->srv->request_ls;
2111
+ if (scr_net_ls_has_live(listeners)) scr_h2_compat_dispatch(s, st, h, end_stream, listeners);
2056
2112
  }
2057
2113
  scr_http2_stream_release(st);
2058
2114
  return;
@@ -2171,13 +2227,57 @@ static const ScrHttpH2Ops scr_h2_compat_ops = {
2171
2227
  * ctx carries the h2 proto tag (the compat registration seam). */
2172
2228
  static void scr_h2_request_hook(void *ctx, ScrClosure *cb /*moves*/, void *fn, bool once) {
2173
2229
  ScrH2SrvCtx *srv = (ScrH2SrvCtx *)ctx;
2174
- scr_net_ls_add(&srv->request_ls, cb, fn, once);
2230
+ ScrNetOnce *shared_once = srv->http1_ctx != NULL && once ? scr_net_once_new() : NULL;
2231
+ if (srv->http1_ctx != NULL) {
2232
+ scr_http1_ctx_on_request(srv->http1_ctx, scr_closure_retain(cb), (ScrHttpReqFn)fn, once,
2233
+ shared_once != NULL ? scr_net_once_retain(shared_once) : NULL);
2234
+ }
2235
+ if (shared_once != NULL) {
2236
+ scr_net_ls_add_shared_once(&srv->request_ls, cb, fn, shared_once);
2237
+ } else {
2238
+ scr_net_ls_add(&srv->request_ls, cb, fn, once);
2239
+ }
2240
+ }
2241
+
2242
+ static void scr_h2_connect_hook(void *ctx, ScrClosure *cb /*moves*/,
2243
+ void *h1_fn, void *h2_fn, bool once) {
2244
+ ScrH2SrvCtx *srv = (ScrH2SrvCtx *)ctx;
2245
+ bool have_h1 = srv->http1_ctx != NULL;
2246
+ bool have_h2 = h2_fn != NULL;
2247
+ ScrNetOnce *shared_once = have_h1 && have_h2 && once ? scr_net_once_new() : NULL;
2248
+ if (!have_h1 && !have_h2) {
2249
+ scr_closure_release(cb);
2250
+ return;
2251
+ }
2252
+ if (have_h1 && !have_h2) {
2253
+ scr_http1_ctx_on_connect(srv->http1_ctx, cb, (ScrHttpUpgradeFn)h1_fn, once, NULL);
2254
+ return;
2255
+ }
2256
+ if (srv->http1_ctx != NULL) {
2257
+ scr_http1_ctx_on_connect(srv->http1_ctx, scr_closure_retain(cb),
2258
+ (ScrHttpUpgradeFn)h1_fn, once,
2259
+ shared_once != NULL ? scr_net_once_retain(shared_once) : NULL);
2260
+ }
2261
+ if (shared_once != NULL) {
2262
+ scr_net_ls_add_shared_once(&srv->connect_ls, cb, h2_fn, shared_once);
2263
+ } else {
2264
+ scr_net_ls_add(&srv->connect_ls, cb, h2_fn, once);
2265
+ }
2266
+ }
2267
+
2268
+ static void scr_h2_upgrade_hook(void *ctx, ScrClosure *cb /*moves*/, void *fn, bool once) {
2269
+ ScrH2SrvCtx *srv = (ScrH2SrvCtx *)ctx;
2270
+ if (srv->http1_ctx != NULL) {
2271
+ scr_http1_ctx_on_upgrade(srv->http1_ctx, cb, (ScrHttpUpgradeFn)fn, once);
2272
+ } else {
2273
+ scr_closure_release(cb);
2274
+ }
2175
2275
  }
2176
2276
 
2177
2277
  /* Build the (req, res) pair over a fresh server stream and fire the
2178
2278
  * 'request' listeners (this = the net server, the http/1 shape). */
2179
2279
  static void scr_h2_compat_dispatch(ScrH2Session *s, ScrH2Stream *st, const ScrH2Headers *h,
2180
- bool end_stream) {
2280
+ bool end_stream, ScrNetLs *listeners) {
2181
2281
  ScrHttpReq *req = scr_http_h2_req_new(s->sock, st);
2182
2282
  for (size_t i = 0; i < h->n; i++) {
2183
2283
  ScrStr *name = h->names[i];
@@ -2199,7 +2299,7 @@ static void scr_h2_compat_dispatch(ScrH2Session *s, ScrH2Stream *st, const ScrH2
2199
2299
  * already satisfied (settle conditions see a delivered read side) */
2200
2300
  if (end_stream) st->end_emitted = true;
2201
2301
  ScrNetL *snap;
2202
- size_t n = scr_net_ls_snapshot(&s->srv->request_ls, &snap);
2302
+ size_t n = scr_net_ls_snapshot(listeners, &snap);
2203
2303
  ScrNetServer *server = s->sock ? scr_net_sock_server(s->sock) : NULL;
2204
2304
  scr_dyn_this_push(server, SCR_DYNH_NET_SERVER);
2205
2305
  for (size_t i = 0; i < n; i++) {
@@ -2226,6 +2326,9 @@ static void scr_h2_srv_ctx_settle(void *p) {
2226
2326
  scr_net_ls_drop(&ctx->stream_ls);
2227
2327
  scr_net_ls_drop(&ctx->session_ls);
2228
2328
  scr_net_ls_drop(&ctx->request_ls);
2329
+ scr_net_ls_drop(&ctx->connect_ls);
2330
+ scr_net_ls_drop(&ctx->session_error_ls);
2331
+ if (ctx->http1_ctx != NULL) scr_http1_ctx_settle(ctx->http1_ctx);
2229
2332
  }
2230
2333
 
2231
2334
  static void scr_h2_on_connection(void *ctx, ScrNetSocket *sock) {
@@ -2234,6 +2337,7 @@ static void scr_h2_on_connection(void *ctx, ScrNetSocket *sock) {
2234
2337
  sess->srv = scr_h2_srv_ctx_retain(srv);
2235
2338
  sess->sock = scr_net_sock_retain(sock);
2236
2339
  sess->next_stream_id = 2; /* server-initiated ids are even (push, unused) */
2340
+ sess->local_settings.enable_connect_protocol = srv->enable_connect_protocol;
2237
2341
  scr_net_sock_set_native_reader(sock, &scr_h2_on_data, &scr_h2_on_eof, &scr_h2_on_closed,
2238
2342
  sess, &scr_h2_session_ctx_free);
2239
2343
  scr_net_sock_set_native_events(sock, NULL, &scr_h2_on_err);
@@ -2279,19 +2383,43 @@ ScrNetServer *scr_http2_create_server_req(ScrClosure *handler /*moves*/, ScrHttp
2279
2383
  * tears down at establishment (Node parks it as 'unknownProtocol' and
2280
2384
  * destroys at the timeout — this surface destroys now). */
2281
2385
  ScrNetServer *scr_http2_create_secure_server(const char *cert, size_t cert_len,
2282
- const char *key, size_t key_len) {
2386
+ const char *key, size_t key_len,
2387
+ bool enable_connect_protocol) {
2283
2388
  scr_http2_install();
2284
2389
  ScrNetServer *s = scr_net_create_server(NULL, NULL);
2285
2390
  ScrH2SrvCtx *ctx = calloc(1, sizeof *ctx);
2286
2391
  if (!ctx) scr_h2_oom();
2287
2392
  ctx->proto = SCR_NET_PROTO_H2;
2288
2393
  ctx->rc = 1;
2394
+ ctx->enable_connect_protocol = enable_connect_protocol;
2289
2395
  scr_net_server_set_http_ctx(s, ctx); /* borrowed alias: 'stream'/'session' registration */
2290
2396
  scr_net_server_set_proto_settle(s, &scr_h2_srv_ctx_settle);
2291
2397
  /* the TLS wrap owns the ctx reference (+1 moved in) and dispatches
2292
2398
  * established h2 connections back into scr_h2_on_connection */
2293
2399
  scr_tls_server_wrap_h2(s, cert, cert_len, key, key_len, &scr_h2_on_connection, ctx,
2294
- &scr_h2_srv_ctx_free_v);
2400
+ &scr_h2_srv_ctx_free_v, NULL, NULL);
2401
+ return s;
2402
+ }
2403
+
2404
+ ScrNetServer *scr_http2_create_secure_server_allow_http1(
2405
+ const char *cert, size_t cert_len, const char *key, size_t key_len,
2406
+ ScrClosure *handler /*moves, nullable*/, ScrHttpReqFn fn,
2407
+ ScrClosure *sni_cb /*moves, nullable*/, void *sni_answer_fn,
2408
+ bool enable_connect_protocol) {
2409
+ scr_http2_install();
2410
+ ScrNetServer *s = scr_http_create_server(NULL, NULL);
2411
+ void *http1_ctx = scr_net_server_get_http_ctx(s);
2412
+ ScrH2SrvCtx *ctx = calloc(1, sizeof *ctx);
2413
+ if (!ctx) scr_h2_oom();
2414
+ ctx->proto = SCR_NET_PROTO_H2;
2415
+ ctx->rc = 1;
2416
+ ctx->http1_ctx = http1_ctx;
2417
+ ctx->enable_connect_protocol = enable_connect_protocol;
2418
+ scr_net_server_set_http_ctx(s, ctx);
2419
+ scr_net_server_set_proto_settle(s, &scr_h2_srv_ctx_settle);
2420
+ if (handler != NULL) scr_h2_request_hook(ctx, handler, (void *)fn, false);
2421
+ scr_tls_server_wrap_h2(s, cert, cert_len, key, key_len, &scr_h2_on_connection, ctx,
2422
+ &scr_h2_srv_ctx_free_v, sni_cb, sni_answer_fn);
2295
2423
  return s;
2296
2424
  }
2297
2425
 
@@ -2307,10 +2435,12 @@ static ScrH2SrvCtx *scr_h2_server_ctx(ScrNetServer *s) {
2307
2435
  * first 'request' listener over the ALPN=h2 server — the
2308
2436
  * create_server_req route, secured. */
2309
2437
  ScrNetServer *scr_http2_create_secure_server_req(const char *cert, size_t cert_len,
2310
- const char *key, size_t key_len,
2311
- ScrClosure *handler /*moves, nullable*/,
2312
- ScrHttpReqFn fn) {
2313
- ScrNetServer *s = scr_http2_create_secure_server(cert, cert_len, key, key_len);
2438
+ const char *key, size_t key_len,
2439
+ ScrClosure *handler /*moves, nullable*/,
2440
+ ScrHttpReqFn fn,
2441
+ bool enable_connect_protocol) {
2442
+ ScrNetServer *s = scr_http2_create_secure_server(cert, cert_len, key, key_len,
2443
+ enable_connect_protocol);
2314
2444
  if (handler != NULL) {
2315
2445
  ScrH2SrvCtx *ctx = scr_h2_server_ctx(s);
2316
2446
  if (ctx != NULL) scr_net_ls_add(&ctx->request_ls, handler, (void *)fn, false);
@@ -2346,7 +2476,8 @@ ScrNetServer *scr_http2_create_secure_server_dyn(const ScrDyn *opts /*borrowed*/
2346
2476
  ? scr_https_create_server((const char *)cert->data, cert->len,
2347
2477
  (const char *)key->data, key->len, handler, fn)
2348
2478
  : scr_http2_create_secure_server_req((const char *)cert->data, cert->len,
2349
- (const char *)key->data, key->len, handler, fn);
2479
+ (const char *)key->data, key->len, handler, fn,
2480
+ false);
2350
2481
  scr_bytes_release(cert);
2351
2482
  scr_bytes_release(key);
2352
2483
  return s;
@@ -2531,6 +2662,33 @@ void scr_http2_session_on_error(ScrH2Session *s, ScrClosure *cb /*moves*/, ScrCh
2531
2662
  scr_net_ls_add(&s->err_ls, cb, (void *)fn, once);
2532
2663
  }
2533
2664
 
2665
+ void scr_http2_server_on_session_error(ScrNetServer *s, ScrClosure *cb /*moves*/,
2666
+ ScrH2SessionErrorFn fn, bool once) {
2667
+ ScrH2SrvCtx *ctx = scr_h2_server_ctx(s);
2668
+ if (ctx == NULL || scr_net_server_settled(s)) {
2669
+ scr_closure_release(cb);
2670
+ return;
2671
+ }
2672
+ scr_net_ls_add(&ctx->session_error_ls, cb, (void *)fn, once);
2673
+ }
2674
+
2675
+ void scr_http2_session_error_thunk0(ScrClosure *cb, ScrStr *msg, ScrH2Session *session) {
2676
+ (void)msg;
2677
+ scr_http2_session_release(session);
2678
+ ((void (*)(ScrClosure *))cb->fn)(cb);
2679
+ }
2680
+
2681
+ void scr_http2_session_error_thunk1(ScrClosure *cb, ScrStr *msg, ScrH2Session *session) {
2682
+ scr_http2_session_release(session);
2683
+ ScrError *err = scr_error_new(0, msg);
2684
+ ((void (*)(ScrClosure *, ScrError *))cb->fn)(cb, err);
2685
+ }
2686
+
2687
+ void scr_http2_session_error_thunk2(ScrClosure *cb, ScrStr *msg, ScrH2Session *session) {
2688
+ ScrError *err = scr_error_new(0, msg);
2689
+ ((void (*)(ScrClosure *, ScrError *, ScrH2Session *))cb->fn)(cb, err, session);
2690
+ }
2691
+
2534
2692
  void scr_http2_session_on_connect(ScrH2Session *s, ScrClosure *cb /*moves*/, ScrH2ConnectFn fn, bool once) {
2535
2693
  if (s->close_emitted) {
2536
2694
  scr_closure_release(cb);
package/src/scr_lib.c CHANGED
@@ -106,6 +106,7 @@ extern char **environ; /* env snapshot (scr_env_pairs) */
106
106
 
107
107
  static SCR_TL int scr_lib_argc = 0;
108
108
  static SCR_TL char **scr_lib_argv = NULL;
109
+ static SCR_TL bool scr_lib_skip_reexec_arg = false;
109
110
  static SCR_TL ScrArr *scr_argv_arr = NULL; /* interned process.argv */
110
111
  static SCR_TL ScrStr *scr_platform_str = NULL; /* interned process.platform */
111
112
  static SCR_TL ScrStr *scr_exec_path_str = NULL; /* interned process.execPath */
@@ -133,9 +134,28 @@ static void scr_lib_cleanup(void) {
133
134
  * atexit handlers (the emitted library init never calls this; keeping it out
134
135
  * the archive's objects free of any atexit reference — the K8 ambient
135
136
  * audit's bar). */
137
+ static bool scr_lib_same_executable_arg(const char *a, const char *b) {
138
+ if (strcmp(a, b) == 0) return true;
139
+ char resolved_a[PATH_MAX], resolved_b[PATH_MAX];
140
+ #ifdef _WIN32
141
+ const char *use_a = _fullpath(resolved_a, a, sizeof resolved_a) != NULL ? resolved_a : a;
142
+ const char *use_b = _fullpath(resolved_b, b, sizeof resolved_b) != NULL ? resolved_b : b;
143
+ return _stricmp(use_a, use_b) == 0;
144
+ #else
145
+ const char *use_a = realpath(a, resolved_a) != NULL ? resolved_a : a;
146
+ const char *use_b = realpath(b, resolved_b) != NULL ? resolved_b : b;
147
+ return strcmp(use_a, use_b) == 0;
148
+ #endif
149
+ }
150
+
136
151
  void scr_lib_init(int argc, char **argv) {
137
152
  scr_lib_argc = argc;
138
153
  scr_lib_argv = argv;
154
+ /* Node self-reexecution spells `spawn(process.execPath,
155
+ * [process.argv[1], ...args])`. In a native binary argv[0] already IS the
156
+ * entry executable, so collapse that repeated first argument before
157
+ * exposing Node's [exec, script, ...args] process.argv shape. */
158
+ scr_lib_skip_reexec_arg = argc >= 2 && scr_lib_same_executable_arg(argv[0], argv[1]);
139
159
  atexit(scr_lib_cleanup);
140
160
  }
141
161
  #endif /* !SCR_LIB */
@@ -151,18 +171,21 @@ void scr_lib_session_cleanup(void) { scr_lib_cleanup(); }
151
171
  /* Raw argv accessors for the island's process shim (scr_island.c): the
152
172
  * island's process.argv must match the static world's ["scriptc",
153
173
  * argv[0], ...] shape exactly, so both build from the same stash. */
154
- int scr_lib_arg_count(void) { return scr_lib_argc; }
155
- const char *scr_lib_arg(int i) { return scr_lib_argv[i]; }
174
+ int scr_lib_arg_count(void) { return scr_lib_argc - (scr_lib_skip_reexec_arg ? 1 : 0); }
175
+ const char *scr_lib_arg(int i) {
176
+ return scr_lib_argv[i + (scr_lib_skip_reexec_arg && i > 0 ? 1 : 0)];
177
+ }
156
178
 
157
179
  ScrArr *scr_process_argv(void) {
158
180
  if (!scr_argv_arr) {
159
181
  /* ["scriptc", argv[0], argv[1], ...]: positions and length line up
160
182
  * with Node's [node-path, script-path, ...args]; the argv[0]/argv[1]
161
183
  * VALUES diverge (SEMANTICS.md). */
162
- scr_argv_arr = scr_arr_new(SCR_ELEM_STR, (size_t)scr_lib_argc + 1);
184
+ int argc = scr_lib_arg_count();
185
+ scr_argv_arr = scr_arr_new(SCR_ELEM_STR, (size_t)argc + 1);
163
186
  scr_arr_push_ref(scr_argv_arr, scr_str_new("scriptc", 7));
164
- for (int i = 0; i < scr_lib_argc; i++) {
165
- const char *a = scr_lib_argv[i];
187
+ for (int i = 0; i < argc; i++) {
188
+ const char *a = scr_lib_arg(i);
166
189
  scr_arr_push_ref(scr_argv_arr, scr_str_new(a, strlen(a)));
167
190
  }
168
191
  }
package/src/scr_library.c CHANGED
@@ -53,6 +53,35 @@ void scr_library_set_sink(ScrLibSinkFn fn, void *ctx) {
53
53
  scr_library_sink_ctx = ctx;
54
54
  }
55
55
 
56
+ /* ── host-callback channels (scr_runtime.h's contract) ────────────────────
57
+ * Fixed slot storage, one pair per profile-declared channel (declaration
58
+ * order = slot index, assigned by the generated registration symbol's
59
+ * dispatch). SCR_TL gives thread-instanced archives per-instance
60
+ * registration; localization gives multi-instance processes per-archive
61
+ * slots — exactly the sink's story. The generated program TU is the only
62
+ * caller, with slot indices proven in range at compile time (the profile
63
+ * caps channels at SCR_LIB_MAX_CALLBACKS). */
64
+
65
+ static SCR_TL ScrLibCbFn scr_library_cb_fns[SCR_LIB_MAX_CALLBACKS];
66
+ static SCR_TL void *scr_library_cb_ctxs[SCR_LIB_MAX_CALLBACKS];
67
+
68
+ void scr_library_cb_set(size_t slot, ScrLibCbFn fn, void *ctx) {
69
+ /* A pure store, the sink registration's rule: latest wins, NULL clears,
70
+ * not poison-guarded, persists across init/reset. */
71
+ scr_library_cb_fns[slot] = fn;
72
+ scr_library_cb_ctxs[slot] = ctx;
73
+ }
74
+
75
+ ScrLibCbFn scr_library_cb_require(size_t slot, const char *trap_msg) {
76
+ /* trap_msg is the call site's constant ("scriptc: library callback
77
+ * '<name>' invoked before registration\n") — a DETECTED trap the funnel
78
+ * classifies SC4025 and assembles with the entry the host called. */
79
+ if (scr_library_cb_fns[slot] == NULL) scr_trap(trap_msg);
80
+ return scr_library_cb_fns[slot];
81
+ }
82
+
83
+ void *scr_library_cb_ctx(size_t slot) { return scr_library_cb_ctxs[slot]; }
84
+
56
85
  /* ── the trap funnel, library expansion ───────────────────────────────────
57
86
  * Poison first (the sink may longjmp to a host frame below the entry — the
58
87
  * conforming survival pattern), then deliver exactly once, then abort:
@@ -104,6 +133,7 @@ static const struct {
104
133
  {"scriptc: SyntaxError: ", "SC4016"}, /* syntax trap (regex compile) */
105
134
  {"scriptc: out of memory", "SC4017"}, /* allocation failure */
106
135
  {"scriptc: internal error: ", "SC4018"}, /* internal invariant failure */
136
+ {"scriptc: library callback ", "SC4025"}, /* unregistered host callback */
107
137
  };
108
138
 
109
139
  static const char *scr_library_trap_code(const char *msg, size_t len) {
package/src/scr_net.c CHANGED
@@ -283,6 +283,27 @@ static const char *scr_net_errname(int err) {
283
283
  * The types live in scr_runtime.h: scr_http.c reuses the whole family
284
284
  * for its request-body listener lists. */
285
285
 
286
+ struct ScrNetOnce {
287
+ size_t rc;
288
+ bool fired;
289
+ };
290
+
291
+ ScrNetOnce *scr_net_once_new(void) {
292
+ ScrNetOnce *once = calloc(1, sizeof *once);
293
+ if (!once) scr_net_oom();
294
+ once->rc = 1;
295
+ return once;
296
+ }
297
+
298
+ ScrNetOnce *scr_net_once_retain(ScrNetOnce *once) {
299
+ once->rc++;
300
+ return once;
301
+ }
302
+
303
+ void scr_net_once_release(ScrNetOnce *once) {
304
+ if (once != NULL && --once->rc == 0) free(once);
305
+ }
306
+
286
307
  void scr_net_ls_add(ScrNetLs *l, ScrClosure *cb, void *fn, bool once) {
287
308
  if (l->n == l->cap) {
288
309
  l->cap = l->cap ? l->cap * 2 : 2;
@@ -292,11 +313,28 @@ void scr_net_ls_add(ScrNetLs *l, ScrClosure *cb, void *fn, bool once) {
292
313
  l->ls[l->n].cb = cb;
293
314
  l->ls[l->n].fn = fn;
294
315
  l->ls[l->n].once = once;
316
+ l->ls[l->n].shared_once = NULL;
295
317
  l->n++;
296
318
  }
297
319
 
320
+ void scr_net_ls_add_shared_once(ScrNetLs *l, ScrClosure *cb, void *fn,
321
+ ScrNetOnce *once /*moves*/) {
322
+ scr_net_ls_add(l, cb, fn, true);
323
+ l->ls[l->n - 1].shared_once = once;
324
+ }
325
+
326
+ bool scr_net_ls_has_live(const ScrNetLs *l) {
327
+ for (size_t i = 0; i < l->n; i++) {
328
+ if (l->ls[i].shared_once == NULL || !l->ls[i].shared_once->fired) return true;
329
+ }
330
+ return false;
331
+ }
332
+
298
333
  void scr_net_ls_drop(ScrNetLs *l) {
299
- for (size_t i = 0; i < l->n; i++) scr_closure_release(l->ls[i].cb);
334
+ for (size_t i = 0; i < l->n; i++) {
335
+ scr_closure_release(l->ls[i].cb);
336
+ scr_net_once_release(l->ls[i].shared_once);
337
+ }
300
338
  free(l->ls);
301
339
  l->ls = NULL;
302
340
  l->n = l->cap = 0;
@@ -314,22 +352,32 @@ size_t scr_net_ls_snapshot(ScrNetLs *l, ScrNetL **out) {
314
352
  }
315
353
  ScrNetL *snap = malloc(n * sizeof *snap);
316
354
  if (!snap) scr_net_oom();
317
- for (size_t i = 0; i < n; i++) {
318
- snap[i] = l->ls[i];
319
- scr_closure_retain(snap[i].cb);
320
- }
321
- /* remove the once entries from the live list */
355
+ size_t nsnap = 0;
322
356
  size_t w = 0;
323
357
  for (size_t i = 0; i < l->n; i++) {
358
+ ScrNetL entry = l->ls[i];
359
+ bool emit = entry.shared_once == NULL || !entry.shared_once->fired;
360
+ if (emit) {
361
+ if (entry.shared_once != NULL) entry.shared_once->fired = true;
362
+ snap[nsnap] = entry;
363
+ snap[nsnap].shared_once = NULL;
364
+ scr_closure_retain(snap[nsnap].cb);
365
+ nsnap++;
366
+ }
324
367
  if (l->ls[i].once) {
325
368
  scr_closure_release(l->ls[i].cb);
369
+ scr_net_once_release(l->ls[i].shared_once);
326
370
  } else {
327
371
  l->ls[w++] = l->ls[i];
328
372
  }
329
373
  }
330
374
  l->n = w;
375
+ if (nsnap == 0) {
376
+ free(snap);
377
+ snap = NULL;
378
+ }
331
379
  *out = snap;
332
- return n;
380
+ return nsnap;
333
381
  }
334
382
 
335
383
  /* Fire a zero-payload event list (end/close/connect/listening) with the
package/src/scr_runtime.h CHANGED
@@ -103,8 +103,9 @@ typedef struct ScrBytes ScrBytes;
103
103
  *
104
104
  * Every trap the runtime DETECTS arrives structured: the funnel assembles
105
105
  * the baseline human line into field 0 unchanged, a stable code for the
106
- * trap kind (the compiler registry's SC4013–SC4019 runtime family,
107
- * classified in scr_library.c), the entry symbol recorded by the trapping
106
+ * trap kind (the compiler registry's runtime family — SC4013–SC4019 plus
107
+ * the SC4025 unregistered-callback trap, classified in scr_library.c), the
108
+ * entry symbol recorded by the trapping
108
109
  * entry's prologue, and the profile's remediation for that code when the
109
110
  * program TU's overlay table declares one (the whole fourth field is
110
111
  * absent otherwise). A message that already begins with the marker — a
@@ -114,6 +115,39 @@ typedef void (*ScrLibSinkFn)(void *ctx, const uint8_t *msg, size_t msg_len,
114
115
  uint64_t address);
115
116
  void scr_library_set_sink(ScrLibSinkFn fn, void *ctx); /* latest wins */
116
117
 
118
+ /* ── host-callback channels ───────────────────────────────────────────────
119
+ * The panic sink's registration pattern generalized into a synchronous
120
+ * outbound seam: the profile declares named channels (bytes/scalar
121
+ * signatures), the generated registration symbol maps a channel name to a
122
+ * slot index here, and compiled call sites fetch the slot through
123
+ * scr_library_cb_require — which returns the host's pointer or delivers
124
+ * the call site's trap message through the funnel (the SC4025 detected
125
+ * trap) when the host never registered. The typed shape of each stored
126
+ * pointer is the channel's, with the opaque context first:
127
+ *
128
+ * <ret> (*)(void *ctx, <params...>)
129
+ *
130
+ * Registration is a pure store like the sink's (no entry prologue, no
131
+ * poison guard, legal before init); latest wins, NULL clears, and
132
+ * registrations persist across init/reset. Slots are per-copy of this
133
+ * state, exactly the sink's story: per-archive under abi.localize_runtime,
134
+ * per-thread instance under abi.instance_per_thread (SCR_TL) — a callback
135
+ * registered on thread T fires only for T's instance. The host's callback
136
+ * runs on the calling thread inside the entry's dynamic extent and must
137
+ * NOT call back into any library entry (registration symbols included) or
138
+ * unwind/longjmp across library frames: read the borrowed buffers, copy
139
+ * what outlives the call, return. Buffer parameters are borrowed for the
140
+ * duration of the call only. */
141
+ #define SCR_LIB_MAX_CALLBACKS 32 /* keep in step with LIB_MAX_CALLBACKS (library/profile.ts) */
142
+ /* The stored shape: generated call sites cast a slot's pointer to the
143
+ * channel's typed shape before calling. */
144
+ typedef void (*ScrLibCbFn)(void);
145
+ void scr_library_cb_set(size_t slot, ScrLibCbFn fn, void *ctx);
146
+ /* The call-site fetch: the registered pointer, or the funnel trap with
147
+ * trap_msg (never returns NULL). */
148
+ ScrLibCbFn scr_library_cb_require(size_t slot, const char *trap_msg);
149
+ void *scr_library_cb_ctx(size_t slot);
150
+
117
151
  /* Entry prologue: aborts deterministically when the library is poisoned (a
118
152
  * trap already fired — no profile entry may run again; recovery is process
119
153
  * restart). reset_arena additionally drops the result arena (the
@@ -162,8 +196,8 @@ _Noreturn void scr_trap_len(const char *msg, size_t len);
162
196
  * (both emissions emit identical data) and consumed by the funnel when it
163
197
  * assembles a detected trap's structured message: flat triples of
164
198
  * (code, teaching-or-NULL, remediation-or-NULL), one per runtime trap code
165
- * (SC4013–SC4019 family) the profile declares text for; _len counts
166
- * triples. A declared teaching replaces the baseline human line as field 0;
199
+ * (the SC4013–SC4019 family plus SC4025) the profile declares text for;
200
+ * _len counts triples. A declared teaching replaces the baseline human line as field 0;
167
201
  * a declared remediation becomes the optional fourth field. */
168
202
  extern const char *const scr_library_trap_overlays[];
169
203
  extern const size_t scr_library_trap_overlays_len;
@@ -5072,10 +5106,13 @@ typedef void (*ScrNetDataFn)(ScrClosure *cb, ScrBytes *chunk); /* borrowed */
5072
5106
 
5073
5107
  /* The listener-list family (snapshot firing, once-before-run): owned by
5074
5108
  * scr_net.c, reused by scr_http.c for the request-body event lists. */
5109
+ typedef struct ScrNetOnce ScrNetOnce;
5110
+
5075
5111
  typedef struct {
5076
5112
  ScrClosure *cb; /* owned */
5077
5113
  void *fn; /* adapter with the event's firing ABI */
5078
5114
  bool once;
5115
+ ScrNetOnce *shared_once; /* owned, nullable: one once registration mirrored across lists */
5079
5116
  } ScrNetL;
5080
5117
 
5081
5118
  typedef struct {
@@ -5084,6 +5121,12 @@ typedef struct {
5084
5121
  } ScrNetLs;
5085
5122
 
5086
5123
  void scr_net_ls_add(ScrNetLs *l, ScrClosure *cb, void *fn, bool once);
5124
+ ScrNetOnce *scr_net_once_new(void); /* +1 */
5125
+ ScrNetOnce *scr_net_once_retain(ScrNetOnce *once); /* +1 */
5126
+ void scr_net_once_release(ScrNetOnce *once);
5127
+ void scr_net_ls_add_shared_once(ScrNetLs *l, ScrClosure *cb, void *fn,
5128
+ ScrNetOnce *once /*moves*/);
5129
+ bool scr_net_ls_has_live(const ScrNetLs *l);
5087
5130
  void scr_net_ls_drop(ScrNetLs *l);
5088
5131
  size_t scr_net_ls_snapshot(ScrNetLs *l, ScrNetL **out);
5089
5132
  void scr_net_fire0(ScrNetLs *l);
@@ -5410,7 +5453,7 @@ void scr_tls_sock_on_session(ScrNetSocket *s, ScrClosure *cb /*moves*/, void *fn
5410
5453
  * the https-authority client wrap. scr_http2.c calls both directly —
5411
5454
  * every http2-using binary links the TLS unit (the moduleUsesTls
5412
5455
  * switch), so no registration indirection is needed. */
5413
- void scr_tls_server_wrap_h2(ScrNetServer *s, const char *cert, size_t cert_len, const char *key, size_t key_len, ScrNetNativeConnFn h2_conn, void *h2_ctx /*moves*/, void (*h2_ctx_free)(void *));
5456
+ void scr_tls_server_wrap_h2(ScrNetServer *s, const char *cert, size_t cert_len, const char *key, size_t key_len, ScrNetNativeConnFn h2_conn, void *h2_ctx /*moves*/, void (*h2_ctx_free)(void *), ScrClosure *sni_cb /*moves, nullable*/, void *sni_answer_fn);
5414
5457
  void scr_tls_h2_client_wrap(ScrNetSocket *sock, ScrStr *host /*borrowed*/, bool reject_unauthorized, const char *ca /*borrowed, len 0 = none*/, size_t ca_len);
5415
5458
 
5416
5459
  /* ── tls.SecureContext + the SNI callback (scr_tls.c) ─────────────────
@@ -5483,6 +5526,7 @@ bool scr_tls_ca_default_override(const char **pem, size_t *len, uint64_t *gen);
5483
5526
  typedef struct ScrHttpReq ScrHttpReq;
5484
5527
  typedef struct ScrHttpRes ScrHttpRes;
5485
5528
  typedef void (*ScrHttpReqFn)(ScrClosure *cb, ScrHttpReq *req, ScrHttpRes *res); /* both +1 */
5529
+ typedef void (*ScrHttpConnectH2Fn)(ScrClosure *cb, ScrHttpReq *req, ScrHttpRes *res); /* both +1 */
5486
5530
  /* 'upgrade' listeners, both sides: (req-or-res, socket, head) — all +1.
5487
5531
  * The parser steps aside (native reader cleared) and the socket is the
5488
5532
  * listener's raw stream; `head` carries bytes read past the 101/request
@@ -5525,10 +5569,16 @@ typedef struct ScrHttpH2Ops {
5525
5569
  void scr_http_set_h2_ops(const ScrHttpH2Ops *ops);
5526
5570
  void scr_http_set_h2_request_hook(void (*hook)(void *h2ctx, ScrClosure *cb /*moves*/,
5527
5571
  void *fn, bool once));
5572
+ void scr_http_set_h2_connect_hook(void (*hook)(void *h2ctx, ScrClosure *cb /*moves*/,
5573
+ void *h1_fn, void *h2_fn, bool once));
5574
+ void scr_http_set_h2_upgrade_hook(void (*hook)(void *h2ctx, ScrClosure *cb /*moves*/,
5575
+ void *fn, bool once));
5528
5576
 
5529
5577
  /* The compat pair, built by scr_http2.c per server stream. */
5530
5578
  ScrHttpReq *scr_http_h2_req_new(ScrNetSocket *sock /*borrowed, nullable*/,
5531
5579
  void *stream /*borrowed, nullable*/); /* +1 */
5580
+ void *scr_http_req_h2_stream(ScrHttpReq *r); /* +1 or NULL */
5581
+ void *scr_http_req_h2_stream_or_throw(ScrHttpReq *r, ScrStr *member /*borrowed*/); /* +1 */
5532
5582
  void scr_http_h2_req_line(ScrHttpReq *r, ScrStr *method /*borrowed, nullable*/,
5533
5583
  ScrStr *url /*borrowed, nullable*/);
5534
5584
  void scr_http_h2_req_header(ScrHttpReq *r, ScrStr *name /*borrowed*/, ScrStr *value /*borrowed*/);
@@ -5563,6 +5613,12 @@ void scr_http2_stream_undef_call(ScrStr *member);
5563
5613
  * server) the closure releases unregistered: 'request' never fires
5564
5614
  * there, like Node. */
5565
5615
  void scr_http_server_on_request(ScrNetServer *s, ScrClosure *cb /*moves*/, ScrHttpReqFn fn, bool once);
5616
+ void scr_http1_ctx_on_request(void *ctx, ScrClosure *cb /*moves*/, ScrHttpReqFn fn, bool once,
5617
+ ScrNetOnce *shared_once /*moves, nullable*/);
5618
+ void scr_http1_ctx_on_connect(void *ctx, ScrClosure *cb /*moves*/, ScrHttpUpgradeFn fn, bool once,
5619
+ ScrNetOnce *shared_once /*moves, nullable*/);
5620
+ void scr_http1_ctx_on_upgrade(void *ctx, ScrClosure *cb /*moves*/, ScrHttpUpgradeFn fn, bool once);
5621
+ void scr_http1_ctx_settle(void *ctx);
5566
5622
  ScrStr *scr_http_req_url(ScrHttpReq *r); /* +1 */
5567
5623
  ScrStr *scr_http_req_method(ScrHttpReq *r); /* +1 */
5568
5624
  ScrStr *scr_http_req_header(ScrHttpReq *r, ScrStr *name /*borrowed*/); /* +1 or NULL (undefined arm) */
@@ -5610,10 +5666,10 @@ void scr_http_server_join_duplicate_headers(ScrNetServer *s);
5610
5666
  void scr_http_handler_thunk0(ScrClosure *cb, ScrHttpReq *req, ScrHttpRes *res);
5611
5667
  void scr_http_handler_thunk1(ScrClosure *cb, ScrHttpReq *req, ScrHttpRes *res);
5612
5668
  void scr_http_handler_thunk2(ScrClosure *cb, ScrHttpReq *req, ScrHttpRes *res);
5613
- /* server.on("connect", ...) — HTTP CONNECT, the upgrade twin: fired
5614
- * INSTEAD of 'request' for CONNECT-method requests with (req, socket,
5615
- * head); no listener destroys the socket. */
5616
- void scr_http_server_on_connect(ScrNetServer *s, ScrClosure *cb /*moves*/, ScrHttpUpgradeFn fn,
5669
+ /* server.on("connect", ...) — fired instead of 'request' for CONNECT.
5670
+ * HTTP/1 uses (req, socket, head); HTTP/2 compatibility uses (req, res). */
5671
+ void scr_http_server_on_connect(ScrNetServer *s, ScrClosure *cb /*moves*/,
5672
+ ScrHttpUpgradeFn h1_fn, ScrHttpConnectH2Fn h2_fn,
5617
5673
  bool once);
5618
5674
  void scr_http_server_on_upgrade(ScrNetServer *s, ScrClosure *cb /*moves*/, ScrHttpUpgradeFn fn, bool once);
5619
5675
  ScrArr *scr_http_req_raw_headers(ScrHttpReq *r); /* +1 — [name, value, ...], arrival order/case */
@@ -5758,6 +5814,8 @@ enum { SCR_NET_PROTO_HTTP1 = 1, SCR_NET_PROTO_H2 = 2 };
5758
5814
 
5759
5815
  typedef struct ScrH2Session ScrH2Session;
5760
5816
  typedef struct ScrH2Stream ScrH2Stream;
5817
+ typedef void (*ScrH2SessionErrorFn)(ScrClosure *cb, ScrStr *msg,
5818
+ ScrH2Session *session); /* session +1 */
5761
5819
 
5762
5820
  typedef void (*ScrH2StreamFn)(ScrClosure *cb, ScrH2Stream *st, ScrArr *pairs, double flags); /* st, pairs +1 */
5763
5821
  typedef void (*ScrH2RespFn)(ScrClosure *cb, ScrArr *pairs, double status, double flags); /* pairs +1 */
@@ -5784,11 +5842,12 @@ ScrNetServer *scr_http2_create_server_req(ScrClosure *handler /*moves*/, ScrHttp
5784
5842
  /* The REAL h2-over-TLS server (http2.createSecureServer({ cert, key })):
5785
5843
  * the same session machinery behind an mbedTLS handshake whose ALPN
5786
5844
  * advertises h2 alone — protocol-identical after establishment. */
5787
- ScrNetServer *scr_http2_create_secure_server(const char *cert, size_t cert_len, const char *key, size_t key_len); /* +1 */
5845
+ ScrNetServer *scr_http2_create_secure_server(const char *cert, size_t cert_len, const char *key, size_t key_len, bool enable_connect_protocol); /* +1 */
5846
+ ScrNetServer *scr_http2_create_secure_server_allow_http1(const char *cert, size_t cert_len, const char *key, size_t key_len, ScrClosure *handler /*moves, nullable*/, ScrHttpReqFn fn, ScrClosure *sni_cb /*moves, nullable*/, void *sni_answer_fn, bool enable_connect_protocol); /* +1 */
5788
5847
  /* createSecureServer(options, handler): the eager COMPAT handler as the
5789
5848
  * first 'request' listener over the ALPN=h2 server (the createServerReq
5790
5849
  * route, secured). */
5791
- ScrNetServer *scr_http2_create_secure_server_req(const char *cert, size_t cert_len, const char *key, size_t key_len, ScrClosure *handler /*moves, nullable*/, ScrHttpReqFn fn); /* +1 */
5850
+ ScrNetServer *scr_http2_create_secure_server_req(const char *cert, size_t cert_len, const char *key, size_t key_len, ScrClosure *handler /*moves, nullable*/, ScrHttpReqFn fn, bool enable_connect_protocol); /* +1 */
5792
5851
  /* createSecureServer with a RUNTIME options record (the divergence-66
5793
5852
  * stance): allowHTTP1 picks the flavor at runtime, cert/key ride the
5794
5853
  * shared TLS server walk (out-of-bounds members throw the catchable
@@ -5818,6 +5877,11 @@ void scr_http2_session_close(ScrH2Session *s, ScrClosure *cb /*moves, nullable*/
5818
5877
  void scr_http2_session_destroy(ScrH2Session *s);
5819
5878
  void scr_http2_session_on_close(ScrH2Session *s, ScrClosure *cb /*moves*/, bool once);
5820
5879
  void scr_http2_session_on_error(ScrH2Session *s, ScrClosure *cb /*moves*/, ScrChildErrFn fn, bool once);
5880
+ void scr_http2_server_on_session_error(ScrNetServer *s, ScrClosure *cb /*moves*/,
5881
+ ScrH2SessionErrorFn fn, bool once);
5882
+ void scr_http2_session_error_thunk0(ScrClosure *cb, ScrStr *msg, ScrH2Session *session);
5883
+ void scr_http2_session_error_thunk1(ScrClosure *cb, ScrStr *msg, ScrH2Session *session);
5884
+ void scr_http2_session_error_thunk2(ScrClosure *cb, ScrStr *msg, ScrH2Session *session);
5821
5885
  void scr_http2_session_on_connect(ScrH2Session *s, ScrClosure *cb /*moves*/, ScrH2ConnectFn fn, bool once);
5822
5886
  void scr_http2_session_on_stream(ScrH2Session *s, ScrClosure *cb /*moves*/, ScrH2StreamFn fn, bool once);
5823
5887
  void scr_http2_session_on_goaway(ScrH2Session *s, ScrClosure *cb /*moves*/, ScrH2GoawayFn fn, bool once);
package/src/scr_tls.c CHANGED
@@ -276,6 +276,7 @@ typedef struct ScrTlsSrv {
276
276
  * the handshake with no_application_protocol — Node's h2-only split). */
277
277
  static const char *SCR_TLS_ALPN_HTTP11[] = {"http/1.1", NULL};
278
278
  static const char *SCR_TLS_ALPN_H2[] = {"h2", NULL};
279
+ static const char *SCR_TLS_ALPN_H2_HTTP11[] = {"h2", "http/1.1", NULL};
279
280
 
280
281
  static ScrTlsSrv *scr_tls_srv_new(const char *cert, size_t cert_len, const char *key,
281
282
  size_t key_len, bool alpn_http11) {
@@ -864,7 +865,8 @@ static void scr_tls_on_established(void *tctx) {
864
865
  const char *alpn = mbedtls_ssl_get_alpn_protocol(&t->ssl);
865
866
  if (alpn != NULL && strcmp(alpn, "h2") == 0) {
866
867
  t->srv->h2_conn(t->srv->h2_ctx, t->sock);
867
- } else if (alpn != NULL && t->srv->inner_conn != NULL && strcmp(alpn, "http/1.1") == 0) {
868
+ } else if (t->srv->inner_conn != NULL &&
869
+ (alpn == NULL || strcmp(alpn, "http/1.1") == 0)) {
868
870
  t->srv->inner_conn(t->srv->inner_ctx, t->sock);
869
871
  } else {
870
872
  scr_net_sock_transport_error(t->sock, NULL);
@@ -1593,13 +1595,20 @@ ScrNetSocket *scr_tls_connect_dyn(double port, ScrStr *host /*borrowed, nullable
1593
1595
  void scr_tls_server_wrap_h2(ScrNetServer *s, const char *cert, size_t cert_len,
1594
1596
  const char *key, size_t key_len,
1595
1597
  ScrNetNativeConnFn h2_conn, void *h2_ctx /*moves*/,
1596
- void (*h2_ctx_free)(void *)) {
1598
+ void (*h2_ctx_free)(void *),
1599
+ ScrClosure *sni_cb /*moves, nullable*/, void *sni_answer_fn) {
1597
1600
  ScrTlsSrv *srv = scr_tls_srv_new(cert, cert_len, key, key_len, false);
1598
- mbedtls_ssl_conf_alpn_protocols(&srv->conf, SCR_TLS_ALPN_H2);
1599
1601
  srv->h2_conn = h2_conn;
1600
1602
  srv->h2_ctx = h2_ctx;
1601
1603
  srv->h2_ctx_free = h2_ctx_free;
1602
1604
  scr_net_server_get_native_conn(s, &srv->inner_conn, &srv->inner_ctx, &srv->inner_ctx_free);
1605
+ mbedtls_ssl_conf_alpn_protocols(
1606
+ &srv->conf, srv->inner_conn != NULL ? SCR_TLS_ALPN_H2_HTTP11 : SCR_TLS_ALPN_H2);
1607
+ if (sni_cb != NULL) {
1608
+ srv->sni_cb = sni_cb;
1609
+ srv->sni_answer_fn = sni_answer_fn;
1610
+ mbedtls_ssl_conf_sni(&srv->conf, &scr_tls_f_sni, NULL);
1611
+ }
1603
1612
  scr_net_server_set_native_conn(s, &scr_tls_srv_on_conn, srv, &scr_tls_srv_release_v);
1604
1613
  scr_net_server_defer_connections(s); /* 'connection'/'secureConnection' wait for the handshake */
1605
1614
  }