tep 0.11.0

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.
Files changed (193) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/Makefile +134 -0
  4. data/README.md +247 -0
  5. data/SINATRA_COMPAT.md +376 -0
  6. data/bin/tep +2156 -0
  7. data/examples/agentic_chat/README.md +103 -0
  8. data/examples/agentic_chat/app.rb +310 -0
  9. data/examples/api_gateway/README.md +49 -0
  10. data/examples/api_gateway/app.rb +66 -0
  11. data/examples/blog/app.rb +367 -0
  12. data/examples/blog/views/index.erb +36 -0
  13. data/examples/blog/views/login.erb +28 -0
  14. data/examples/blog/views/new_post.erb +25 -0
  15. data/examples/blog/views/show.erb +16 -0
  16. data/examples/chat/app.rb +278 -0
  17. data/examples/chat/assets/logo.svg +13 -0
  18. data/examples/chat/assets/style.css +209 -0
  19. data/examples/chat/views/index.erb +142 -0
  20. data/examples/chatbot/README.md +111 -0
  21. data/examples/chatbot/app.rb +1024 -0
  22. data/examples/chatbot/assets/chat.js +249 -0
  23. data/examples/chatbot/assets/compare.js +93 -0
  24. data/examples/chatbot/assets/markdown.js +84 -0
  25. data/examples/chatbot/assets/style.css +215 -0
  26. data/examples/chatbot/schema.sql +25 -0
  27. data/examples/chatbot/views/compare.erb +43 -0
  28. data/examples/chatbot/views/index.erb +42 -0
  29. data/examples/chatbot/views/login.erb +22 -0
  30. data/examples/chatbot/views/setup.erb +23 -0
  31. data/examples/counter/README.md +68 -0
  32. data/examples/counter/app.rb +85 -0
  33. data/examples/experiments/AGENTS.md +91 -0
  34. data/examples/experiments/README.md +99 -0
  35. data/examples/experiments/app.rb +225 -0
  36. data/examples/geohash/Gemfile +11 -0
  37. data/examples/geohash/Gemfile.lock +17 -0
  38. data/examples/geohash/README.md +58 -0
  39. data/examples/geohash/app.rb +33 -0
  40. data/examples/hello.rb +120 -0
  41. data/examples/llm_gateway/README.md +73 -0
  42. data/examples/llm_gateway/app.rb +91 -0
  43. data/examples/maidenhead/Gemfile +7 -0
  44. data/examples/maidenhead/Gemfile.lock +17 -0
  45. data/examples/maidenhead/README.md +47 -0
  46. data/examples/maidenhead/app.rb +46 -0
  47. data/examples/pg_hello.rb +76 -0
  48. data/examples/qdrant/Gemfile +11 -0
  49. data/examples/qdrant/Gemfile.lock +29 -0
  50. data/examples/qdrant/README.md +54 -0
  51. data/examples/sinatra_style.rb +32 -0
  52. data/examples/websocket_echo.rb +37 -0
  53. data/lib/tep/agent_delegation.rb +35 -0
  54. data/lib/tep/app.rb +291 -0
  55. data/lib/tep/assets.rb +52 -0
  56. data/lib/tep/auth.rb +78 -0
  57. data/lib/tep/auth_bearer_token.rb +126 -0
  58. data/lib/tep/auth_oauth2.rb +189 -0
  59. data/lib/tep/auth_oauth2_client.rb +29 -0
  60. data/lib/tep/auth_oauth2_code.rb +40 -0
  61. data/lib/tep/auth_session_cookie.rb +132 -0
  62. data/lib/tep/broadcast.rb +265 -0
  63. data/lib/tep/broadcast_subscription.rb +42 -0
  64. data/lib/tep/cache.rb +49 -0
  65. data/lib/tep/events.rb +257 -0
  66. data/lib/tep/filter.rb +21 -0
  67. data/lib/tep/handler.rb +35 -0
  68. data/lib/tep/http.rb +599 -0
  69. data/lib/tep/identity.rb +67 -0
  70. data/lib/tep/job.rb +186 -0
  71. data/lib/tep/json.rb +572 -0
  72. data/lib/tep/jwt.rb +126 -0
  73. data/lib/tep/live_view.rb +219 -0
  74. data/lib/tep/llm.rb +505 -0
  75. data/lib/tep/logger.rb +85 -0
  76. data/lib/tep/mcp.rb +203 -0
  77. data/lib/tep/multipart.rb +98 -0
  78. data/lib/tep/net.rb +155 -0
  79. data/lib/tep/openai_server.rb +725 -0
  80. data/lib/tep/parallel.rb +168 -0
  81. data/lib/tep/parser.rb +81 -0
  82. data/lib/tep/password.rb +102 -0
  83. data/lib/tep/pg.rb +1128 -0
  84. data/lib/tep/presence.rb +589 -0
  85. data/lib/tep/presence_entry.rb +52 -0
  86. data/lib/tep/proxy.rb +801 -0
  87. data/lib/tep/request.rb +194 -0
  88. data/lib/tep/response.rb +134 -0
  89. data/lib/tep/router.rb +137 -0
  90. data/lib/tep/scheduler.rb +342 -0
  91. data/lib/tep/security.rb +140 -0
  92. data/lib/tep/server.rb +276 -0
  93. data/lib/tep/server_scheduled.rb +375 -0
  94. data/lib/tep/session.rb +98 -0
  95. data/lib/tep/shell.rb +62 -0
  96. data/lib/tep/sphttp.c +858 -0
  97. data/lib/tep/sqlite.rb +215 -0
  98. data/lib/tep/streamer.rb +31 -0
  99. data/lib/tep/tep_pg.c +769 -0
  100. data/lib/tep/tep_sqlite.c +320 -0
  101. data/lib/tep/url.rb +161 -0
  102. data/lib/tep/version.rb +3 -0
  103. data/lib/tep/websocket/connection.rb +171 -0
  104. data/lib/tep/websocket/driver.rb +169 -0
  105. data/lib/tep/websocket/frame.rb +238 -0
  106. data/lib/tep/websocket/handshake.rb +159 -0
  107. data/lib/tep/websocket.rb +68 -0
  108. data/lib/tep.rb +981 -0
  109. data/public/hello.txt +1 -0
  110. data/public/style.css +4 -0
  111. data/spinel-ext.json +33 -0
  112. data/test/helper.rb +248 -0
  113. data/test/real_world/01_simple.rb +5 -0
  114. data/test/real_world/02_lifecycle.rb +20 -0
  115. data/test/real_world/03_chat.rb +75 -0
  116. data/test/real_world/04_health_api.rb +25 -0
  117. data/test/real_world/05_todo_api.rb +57 -0
  118. data/test/real_world/06_basic_auth.rb +25 -0
  119. data/test/real_world/07_bbc_rest_api.rb +228 -0
  120. data/test/real_world/07_sklise_things.rb +109 -0
  121. data/test/real_world/08_jwd83_helloworld.rb +56 -0
  122. data/test/run_all.rb +7 -0
  123. data/test/run_parallel.rb +89 -0
  124. data/test/spinel_scheduled_burst_segv_repro.rb +33 -0
  125. data/test/test_api_gateway.rb +76 -0
  126. data/test/test_auth.rb +223 -0
  127. data/test/test_auth_oauth2.rb +208 -0
  128. data/test/test_auth_session_cookie.rb +198 -0
  129. data/test/test_broadcast.rb +197 -0
  130. data/test/test_broadcast_pg.rb +135 -0
  131. data/test/test_cache.rb +98 -0
  132. data/test/test_cache_static.rb +48 -0
  133. data/test/test_cookies.rb +52 -0
  134. data/test/test_erb.rb +53 -0
  135. data/test/test_erb_ivars.rb +58 -0
  136. data/test/test_events.rb +114 -0
  137. data/test/test_filters.rb +41 -0
  138. data/test/test_geohash_example.rb +89 -0
  139. data/test/test_http.rb +137 -0
  140. data/test/test_http_pool.rb +122 -0
  141. data/test/test_http_pool_send.rb +57 -0
  142. data/test/test_identity.rb +165 -0
  143. data/test/test_inbound_tls.rb +101 -0
  144. data/test/test_inbound_tls_scheduled.rb +101 -0
  145. data/test/test_job.rb +108 -0
  146. data/test/test_json.rb +168 -0
  147. data/test/test_jwt.rb +143 -0
  148. data/test/test_live_view.rb +324 -0
  149. data/test/test_llm.rb +250 -0
  150. data/test/test_llm_gateway.rb +95 -0
  151. data/test/test_logger.rb +101 -0
  152. data/test/test_maidenhead_example.rb +86 -0
  153. data/test/test_mcp.rb +264 -0
  154. data/test/test_misc_v02.rb +54 -0
  155. data/test/test_modular.rb +43 -0
  156. data/test/test_multi_filters.rb +40 -0
  157. data/test/test_mustache.rb +57 -0
  158. data/test/test_openai_server.rb +598 -0
  159. data/test/test_optional_segments.rb +45 -0
  160. data/test/test_parallel.rb +102 -0
  161. data/test/test_params.rb +99 -0
  162. data/test/test_pass.rb +42 -0
  163. data/test/test_password.rb +101 -0
  164. data/test/test_pg.rb +673 -0
  165. data/test/test_presence.rb +374 -0
  166. data/test/test_presence_pg.rb +309 -0
  167. data/test/test_proxy.rb +556 -0
  168. data/test/test_proxy_dsl.rb +119 -0
  169. data/test/test_proxy_streaming.rb +146 -0
  170. data/test/test_real_world.rb +397 -0
  171. data/test/test_regex_routes.rb +52 -0
  172. data/test/test_request_methods.rb +102 -0
  173. data/test/test_response.rb +123 -0
  174. data/test/test_routing.rb +109 -0
  175. data/test/test_scheduler.rb +153 -0
  176. data/test/test_security.rb +72 -0
  177. data/test/test_server_scheduled.rb +56 -0
  178. data/test/test_sessions.rb +59 -0
  179. data/test/test_shell.rb +54 -0
  180. data/test/test_sqlite.rb +148 -0
  181. data/test/test_sqlite_cached.rb +171 -0
  182. data/test/test_static.rb +57 -0
  183. data/test/test_streaming.rb +96 -0
  184. data/test/test_unsupported.rb +32 -0
  185. data/test/test_websocket.rb +152 -0
  186. data/test/test_websocket_echo.rb +138 -0
  187. data/test/views/greet.erb +5 -0
  188. data/test/views/hello.erb +5 -0
  189. data/test/views/list.erb +5 -0
  190. data/test/views/m_ivars.mustache +3 -0
  191. data/test/views/m_simple.mustache +4 -0
  192. data/test/views/mixed.erb +3 -0
  193. metadata +264 -0
data/lib/tep/sphttp.c ADDED
@@ -0,0 +1,858 @@
1
+ /* _GNU_SOURCE: expose strptime(3) + timegm(3) (used by the HTTP-date
2
+ * helpers below). Must precede any system header include. */
3
+ #define _GNU_SOURCE 1
4
+
5
+ /* sphttp.c - POSIX HTTP plumbing for Tep, called from Spinel via FFI.
6
+ *
7
+ * Scope: socket server + client + poll + fork + shell/file helpers.
8
+ * Crypto (SHA-256/HMAC/PBKDF2/B64URL/random) lives in tep_crypto.c.
9
+ *
10
+ * The MVP stays single-threaded blocking; perf primitives (SO_REUSEPORT
11
+ * for prefork, keep-alive friendly recv, and a "accept after fork" path)
12
+ * are exposed so the Ruby side can do the rest. */
13
+
14
+ #include <stdio.h>
15
+ #include <stdlib.h>
16
+ #include <string.h>
17
+ #include <unistd.h>
18
+ #include <errno.h>
19
+ #include <fcntl.h>
20
+ #include <poll.h>
21
+ #include <netdb.h>
22
+ #include <sys/socket.h>
23
+ #include <sys/types.h>
24
+ #include <sys/wait.h>
25
+ #include <sys/stat.h>
26
+ #include <netinet/in.h>
27
+ #include <netinet/tcp.h>
28
+ #include <arpa/inet.h>
29
+ #include <signal.h>
30
+ #include <time.h>
31
+
32
+ #define SPHTTP_BUFSIZE 65536
33
+ #define SPHTTP_RESP_MAX (4 * 1024 * 1024)
34
+
35
+ /* ---------- TLS (libssl) binding -- outbound client; see tep#148 ----------
36
+ *
37
+ * The socket layer stays fd-based: sphttp_connect_tls returns a normal
38
+ * socket fd and registers an SSL* for it in sphttp_ssl_tab, keyed by fd.
39
+ * The read/write/close helpers consult the table and route through
40
+ * SSL_read/SSL_write/SSL_shutdown when an SSL* is present, else plain
41
+ * send/recv/close. So the FFI surface gains exactly one function
42
+ * (sphttp_connect_tls) and everything downstream is TLS-transparent.
43
+ * Sockets created via sphttp_connect_tls are BLOCKING, so SSL_read/
44
+ * SSL_write either complete or hard-error (no WANT_READ/WANT_WRITE
45
+ * churn). Inbound/server TLS (SSL_accept) is a later phase reusing
46
+ * this same table + a server-side SSL_CTX. */
47
+ #include <openssl/ssl.h>
48
+ #include <openssl/err.h>
49
+ #include <openssl/x509v3.h>
50
+
51
+ #define SPHTTP_FD_MAX 4096
52
+ static SSL *sphttp_ssl_tab[SPHTTP_FD_MAX]; /* fd -> SSL*, NULL = plaintext */
53
+ static SSL_CTX *sphttp_ssl_ctx = NULL;
54
+
55
+ static SSL *sphttp_ssl_for(int fd) {
56
+ if (fd < 0 || fd >= SPHTTP_FD_MAX) return NULL;
57
+ return sphttp_ssl_tab[fd];
58
+ }
59
+
60
+ /* Lazily build the shared client SSL_CTX: TLS 1.2+, peer verification
61
+ * on, system CA bundle. NULL on failure (callers fall back to -1). */
62
+ static SSL_CTX *sphttp_ssl_ctx_get(void) {
63
+ if (sphttp_ssl_ctx) return sphttp_ssl_ctx;
64
+ SSL_CTX *ctx = SSL_CTX_new(TLS_client_method());
65
+ if (!ctx) return NULL;
66
+ SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION);
67
+ SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
68
+ SSL_CTX_set_default_verify_paths(ctx);
69
+ sphttp_ssl_ctx = ctx;
70
+ return ctx;
71
+ }
72
+
73
+ static char sphttp_req_buf[SPHTTP_BUFSIZE];
74
+ static int sphttp_req_len = 0;
75
+
76
+ /* The POSIX TCP / poll / prefork / shell / signal primitives below now
77
+ * live in spinel's sp_net (libspinel_rt.a, matz/spinel#1055). tep keeps
78
+ * the sphttp_* names as thin delegating wrappers so lib/tep/net.rb and
79
+ * every Sock.sphttp_* call site stay unchanged (tep#12). sp_net symbols
80
+ * are auto-linked from libspinel_rt.a; we declare the surface we use
81
+ * here rather than coupling to a shared header (same spirit as
82
+ * sp_crypto, declared via ffi_func). HTTP framing, WebSocket accessors,
83
+ * TLS, and the SSL-aware I/O all stay tep-side below. */
84
+ extern int sp_net_install_term_handlers(void);
85
+ extern int sp_net_shutdown_requested(void);
86
+ extern int sp_net_listen(int port, int reuseport);
87
+ extern int sp_net_accept(int sfd);
88
+ extern int sp_net_accept_nb(int sfd);
89
+ extern int sp_net_connect(const char *host, int port);
90
+ extern int sp_net_set_nonblock(int fd);
91
+ extern int sp_net_poll_reset(void);
92
+ extern int sp_net_poll_add(int fd, int mode_bits);
93
+ extern int sp_net_poll_run(int timeout_ms);
94
+ extern int sp_net_poll_ready(int slot);
95
+ extern int sp_net_fork(void);
96
+ extern int sp_net_exit(int status);
97
+ extern int sp_net_getpid(void);
98
+ extern int sp_net_wait_any(void);
99
+ extern const char *sp_net_shell_capture(const char *cmd, int max_bytes);
100
+
101
+ int sphttp_install_term_handlers(void) { return sp_net_install_term_handlers(); }
102
+ int sphttp_shutdown_requested(void) { return sp_net_shutdown_requested(); }
103
+
104
+ /* Sub-second sleep, granular to a millisecond. spinel's Time / sleep
105
+ * surface deals in integer epoch-seconds only; this helper exposes
106
+ * usleep for callers that need finer-grained pacing (e.g. Tep::Proxy's
107
+ * retry backoff loop). Returns 0 on success, -1 on EINTR (the caller
108
+ * decides whether to retry). ms <= 0 returns immediately. */
109
+ int sphttp_sleep_ms(int ms) {
110
+ if (ms <= 0) return 0;
111
+ /* usleep accepts useconds_t but is deprecated on some BSDs; the
112
+ * portable shape is nanosleep, which we use here. */
113
+ struct timespec ts;
114
+ ts.tv_sec = ms / 1000;
115
+ ts.tv_nsec = (long)(ms % 1000) * 1000000L;
116
+ if (nanosleep(&ts, NULL) < 0) {
117
+ return -1;
118
+ }
119
+ return 0;
120
+ }
121
+
122
+ /* Bind & listen on 0.0.0.0:port. If `reuseport` != 0 we set
123
+ * SO_REUSEPORT so multiple worker processes can listen on the same
124
+ * port and the kernel will load-balance accept() across them. */
125
+ /* All three delegate to sp_net (#12). sp_net_accept carries the same
126
+ * term-flag-aware pre-check + EINTR handling as the old body; the
127
+ * shutdown flag is now sp_net's (set via sp_net_install_term_handlers,
128
+ * which sphttp_install_term_handlers delegates to). */
129
+ int sphttp_listen(int port, int reuseport) { return sp_net_listen(port, reuseport); }
130
+ int sphttp_accept(int sfd) { return sp_net_accept(sfd); }
131
+ int sphttp_accept_nb(int sfd) { return sp_net_accept_nb(sfd); }
132
+
133
+ /* Read until end-of-headers ("\r\n\r\n") or the buffer fills. Subsequent
134
+ * recv()s for the body are the caller's job (we expose a length helper).
135
+ * Returns the parsed length (>0), 0 on clean EOF, -1 on error. */
136
+ int sphttp_read_request(int fd) {
137
+ SSL *ssl = sphttp_ssl_for(fd); /* inbound TLS: decrypt via SSL_read */
138
+ sphttp_req_len = 0;
139
+ sphttp_req_buf[0] = '\0';
140
+ while (sphttp_req_len < SPHTTP_BUFSIZE - 1) {
141
+ int want = SPHTTP_BUFSIZE - 1 - sphttp_req_len;
142
+ ssize_t n = ssl
143
+ ? (ssize_t)SSL_read(ssl, sphttp_req_buf + sphttp_req_len, want)
144
+ : recv(fd, sphttp_req_buf + sphttp_req_len, (size_t)want, 0);
145
+ if (n == 0) {
146
+ if (sphttp_req_len == 0) return 0;
147
+ break;
148
+ }
149
+ if (n < 0) {
150
+ if (!ssl && errno == EINTR) continue;
151
+ return -1;
152
+ }
153
+ sphttp_req_len += (int)n;
154
+ sphttp_req_buf[sphttp_req_len] = '\0';
155
+ if (strstr(sphttp_req_buf, "\r\n\r\n") != NULL) break;
156
+ }
157
+ return sphttp_req_len;
158
+ }
159
+
160
+ const char *sphttp_request_buf(void) {
161
+ return sphttp_req_buf;
162
+ }
163
+
164
+ int sphttp_request_len(void) {
165
+ return sphttp_req_len;
166
+ }
167
+
168
+ /* Drain the body bytes we still owe past the buffered chunk. Tep
169
+ * computes remaining = content_length - already_in_buf; this gulps
170
+ * those into a Ruby-visible string buffer. We round-trip via a
171
+ * static buffer to avoid hand-rolling write_str FFI. */
172
+ static char sphttp_body_buf[SPHTTP_BUFSIZE];
173
+
174
+ const char *sphttp_drain_body(int fd, int total_len) {
175
+ SSL *ssl = sphttp_ssl_for(fd);
176
+ int n = total_len;
177
+ if (n < 0) n = 0;
178
+ if (n >= SPHTTP_BUFSIZE) n = SPHTTP_BUFSIZE - 1;
179
+ int got = 0;
180
+ while (got < n) {
181
+ ssize_t r = ssl ? (ssize_t)SSL_read(ssl, sphttp_body_buf + got, n - got)
182
+ : recv(fd, sphttp_body_buf + got, n - got, 0);
183
+ if (r <= 0) {
184
+ if (!ssl && errno == EINTR) continue;
185
+ break;
186
+ }
187
+ got += (int)r;
188
+ }
189
+ sphttp_body_buf[got] = '\0';
190
+ return sphttp_body_buf;
191
+ }
192
+
193
+ int sphttp_write_str(int fd, const char *s) {
194
+ SSL *ssl = sphttp_ssl_for(fd);
195
+ size_t len = strlen(s);
196
+ size_t off = 0;
197
+ while (off < len) {
198
+ if (ssl) {
199
+ int w = SSL_write(ssl, s + off, (int)(len - off));
200
+ if (w <= 0) return -1;
201
+ off += (size_t)w;
202
+ } else {
203
+ ssize_t n = send(fd, s + off, len - off, 0);
204
+ if (n <= 0) {
205
+ if (errno == EINTR) continue;
206
+ return -1;
207
+ }
208
+ off += (size_t)n;
209
+ }
210
+ }
211
+ return 0;
212
+ }
213
+
214
+ /* Binary write -- explicit length, no strlen. Required for any
215
+ * caller that needs to send bytes that may contain 0x00 (WebSocket
216
+ * frames, raw protocol bodies). Returns 0 on success, -1 on send
217
+ * failure. */
218
+ int sphttp_write_bytes(int fd, const char *data, int n) {
219
+ SSL *ssl = sphttp_ssl_for(fd);
220
+ size_t total = (n < 0) ? 0 : (size_t)n;
221
+ size_t off = 0;
222
+ while (off < total) {
223
+ if (ssl) {
224
+ int w = SSL_write(ssl, data + off, (int)(total - off));
225
+ if (w <= 0) return -1;
226
+ off += (size_t)w;
227
+ } else {
228
+ ssize_t w = send(fd, data + off, total - off, 0);
229
+ if (w <= 0) {
230
+ if (errno == EINTR) continue;
231
+ return -1;
232
+ }
233
+ off += (size_t)w;
234
+ }
235
+ }
236
+ return 0;
237
+ }
238
+
239
+ /* Binary recv accessor pair, mechanically identical to
240
+ * sphttp_request_buf / _len above but on a separate static buffer
241
+ * so callers that interleave HTTP request reads with arbitrary
242
+ * frame reads don't trample each other. Use case: WebSocket frame
243
+ * codec drives a `recv_into_frame -> _buf + _len` loop.
244
+ *
245
+ * The frame buffer is NOT NUL-terminated and may contain arbitrary
246
+ * bytes including 0x00. Always read exactly `sphttp_recv_frame_len()`
247
+ * bytes from the buffer; don't rely on strlen-style scanning. */
248
+ static char sphttp_frame_buf[SPHTTP_BUFSIZE];
249
+ static int sphttp_frame_len = 0;
250
+
251
+ /* Single non-blocking recv into the frame buffer. Returns the
252
+ * number of bytes received (also reflected in sphttp_recv_frame_len),
253
+ * 0 on EOF, -1 on error. Calling this overwrites the prior buffer
254
+ * contents. For EAGAIN-style "would block" the caller is expected
255
+ * to have parked on a poll/io_wait beforehand -- this fn does NOT
256
+ * retry. */
257
+ int sphttp_recv_into_frame(int fd) {
258
+ SSL *ssl = sphttp_ssl_for(fd);
259
+ sphttp_frame_len = 0;
260
+ ssize_t n = ssl ? (ssize_t)SSL_read(ssl, sphttp_frame_buf, SPHTTP_BUFSIZE)
261
+ : recv(fd, sphttp_frame_buf, SPHTTP_BUFSIZE, 0);
262
+ if (n < 0) {
263
+ if (!ssl && errno == EINTR) {
264
+ /* one retry on EINTR for ergonomics; further EINTRs surface */
265
+ n = recv(fd, sphttp_frame_buf, SPHTTP_BUFSIZE, 0);
266
+ if (n < 0) return -1;
267
+ } else {
268
+ return -1;
269
+ }
270
+ }
271
+ sphttp_frame_len = (int)n;
272
+ return (int)n;
273
+ }
274
+
275
+ const char *sphttp_recv_frame_buf(void) {
276
+ return sphttp_frame_buf;
277
+ }
278
+
279
+ int sphttp_recv_frame_len(void) {
280
+ return sphttp_frame_len;
281
+ }
282
+
283
+ /* Send a file's contents straight from disk -- used for static
284
+ * file serving. Returns -1 on open/read failure (caller falls back
285
+ * to 404), 0 on success. */
286
+ int sphttp_sendfile(int fd, const char *path) {
287
+ int src = open(path, O_RDONLY);
288
+ if (src < 0) return -1;
289
+ char buf[16384];
290
+ for (;;) {
291
+ ssize_t r = read(src, buf, sizeof(buf));
292
+ if (r <= 0) break;
293
+ ssize_t off = 0;
294
+ while (off < r) {
295
+ ssize_t w = send(fd, buf + off, r - off, 0);
296
+ if (w <= 0) {
297
+ if (errno == EINTR) continue;
298
+ close(src);
299
+ return -1;
300
+ }
301
+ off += w;
302
+ }
303
+ }
304
+ close(src);
305
+ return 0;
306
+ }
307
+
308
+ /* Returns the file size at `path`, or -1 if missing / not a regular file.
309
+ * Used by static serving to compute Content-Length. */
310
+ int sphttp_filesize(const char *path) {
311
+ struct stat st;
312
+ if (stat(path, &st) < 0) return -1;
313
+ if ((st.st_mode & S_IFMT) != S_IFREG) return -1;
314
+ if (st.st_size > 0x7fffffff) return -1;
315
+ return (int)st.st_size;
316
+ }
317
+
318
+ /* mtime (Unix epoch seconds) of a regular file, or -1 if it doesn't
319
+ * stat / isn't a regular file. Used for send_file's Last-Modified +
320
+ * the size-mtime ETag (cache revalidation, #152). */
321
+ int sphttp_file_mtime(const char *path) {
322
+ struct stat st;
323
+ if (stat(path, &st) < 0) return -1;
324
+ if ((st.st_mode & S_IFMT) != S_IFREG) return -1;
325
+ return (int)st.st_mtime;
326
+ }
327
+
328
+ int sphttp_close(int fd) {
329
+ SSL *ssl = sphttp_ssl_for(fd);
330
+ if (ssl) {
331
+ SSL_shutdown(ssl);
332
+ SSL_free(ssl);
333
+ sphttp_ssl_tab[fd] = NULL; /* fd < SPHTTP_FD_MAX guaranteed by sphttp_ssl_for */
334
+ }
335
+ return close(fd);
336
+ }
337
+
338
+ /* Chunked Transfer-Encoding frame: write `<hex-size>\r\n<bytes>\r\n`.
339
+ * Returns 0 on success, -1 on partial write / EOF. */
340
+ int sphttp_write_chunk(int fd, const char *s) {
341
+ size_t len = strlen(s);
342
+ if (len == 0) return 0;
343
+ char hdr[32];
344
+ int n = snprintf(hdr, sizeof(hdr), "%zx\r\n", len);
345
+ if (n <= 0) return -1;
346
+ if (sphttp_write_str(fd, hdr) < 0) return -1;
347
+ size_t off = 0;
348
+ while (off < len) {
349
+ ssize_t w = send(fd, s + off, len - off, 0);
350
+ if (w <= 0) {
351
+ if (errno == EINTR) continue;
352
+ return -1;
353
+ }
354
+ off += (size_t)w;
355
+ }
356
+ return sphttp_write_str(fd, "\r\n");
357
+ }
358
+
359
+ /* End-of-chunked-stream marker. */
360
+ int sphttp_write_chunk_end(int fd) {
361
+ return sphttp_write_str(fd, "0\r\n\r\n");
362
+ }
363
+
364
+ /* SHA-256 / HMAC / PBKDF2 / Base64URL / CSPRNG live in tep_crypto.c */
365
+
366
+ /* Pre-fork support. Returns child pid in parent, 0 in child, -1 on fail. */
367
+ /* Prefork primitives -- delegate to sp_net (#12). */
368
+ int sphttp_fork(void) { return sp_net_fork(); }
369
+ int sphttp_exit(int status) { return sp_net_exit(status); } /* never returns */
370
+ int sphttp_getpid(void) { return sp_net_getpid(); }
371
+ int sphttp_wait_any(void) { return sp_net_wait_any(); }
372
+
373
+ /* ---------- Non-blocking I/O + poll(2) plumbing ----------
374
+ *
375
+ * The scheduler parks a fiber on (fd, mode) via Sock.sphttp_poll_add;
376
+ * tick() then calls sphttp_poll_run with a timeout and walks the
377
+ * slots to see who got ready. Mode bits: 1=READ, 2=WRITE.
378
+ *
379
+ * Storage is process-static. The Ruby side owns the "reset between
380
+ * tick rounds" discipline -- safe because the scheduler is single-
381
+ * threaded inside one worker. */
382
+
383
+ /* poll(2) + set_nonblock delegate to sp_net (#12). The poll set is
384
+ * now sp_net's process-static storage; all four functions route to it,
385
+ * so the "reset between tick rounds" discipline still holds. */
386
+ int sphttp_poll_reset(void) { return sp_net_poll_reset(); }
387
+ int sphttp_poll_add(int fd, int mode_bits) { return sp_net_poll_add(fd, mode_bits); }
388
+ int sphttp_poll_run(int timeout_ms) { return sp_net_poll_run(timeout_ms); }
389
+ int sphttp_poll_ready(int slot) { return sp_net_poll_ready(slot); }
390
+ int sphttp_set_nonblock(int fd) { return sp_net_set_nonblock(fd); }
391
+
392
+ /* Bound a blocking recv with SO_RCVTIMEO (milliseconds; <=0 clears the
393
+ * timeout). Used by the pooled outbound client (6.7b): a keep-alive
394
+ * response with no Content-Length and no Connection: close (e.g. a
395
+ * chunked upstream) would otherwise read-until-an-EOF-that-never-comes
396
+ * and hang the worker. With a timeout the recv returns -1/EAGAIN and
397
+ * the caller bails with what it has. Returns 0 on success, -1 on
398
+ * setsockopt failure. */
399
+ int sphttp_set_recv_timeout(int fd, int ms) {
400
+ struct timeval tv;
401
+ if (ms <= 0) {
402
+ tv.tv_sec = 0;
403
+ tv.tv_usec = 0;
404
+ } else {
405
+ tv.tv_sec = ms / 1000;
406
+ tv.tv_usec = (long)(ms % 1000) * 1000L;
407
+ }
408
+ if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0) return -1;
409
+ return 0;
410
+ }
411
+
412
+ /* Outbound TCP connect. Resolves `host` via getaddrinfo (so both
413
+ * IP literals and DNS names work). Returns the connected fd or -1.
414
+ * Blocking connect for now -- a future variant can do non-blocking
415
+ * connect + poll(POLLOUT) for fully-async outbound. */
416
+ /* Plaintext outbound connect -- delegate to sp_net (#12). sphttp_connect_tls
417
+ * builds on this for the TLS path. */
418
+ int sphttp_connect(const char *host, int port) { return sp_net_connect(host, port); }
419
+
420
+ /* Outbound TLS connect: TCP connect to host:port, then a TLS 1.2+
421
+ * handshake with SNI + peer-cert verification + hostname check.
422
+ * Registers the SSL* against the returned fd so subsequent
423
+ * write/recv/close route through it transparently. Returns the fd on
424
+ * success, -1 on connect/handshake/verification failure. */
425
+ int sphttp_connect_tls(const char *host, int port) {
426
+ int fd = sphttp_connect(host, port);
427
+ if (fd < 0) return -1;
428
+ if (fd >= SPHTTP_FD_MAX) { close(fd); return -1; }
429
+
430
+ SSL_CTX *ctx = sphttp_ssl_ctx_get();
431
+ if (!ctx) { close(fd); return -1; }
432
+ SSL *ssl = SSL_new(ctx);
433
+ if (!ssl) { close(fd); return -1; }
434
+
435
+ SSL_set_fd(ssl, fd);
436
+ /* SNI -- required by virtually every multi-tenant TLS endpoint. */
437
+ SSL_set_tlsext_host_name(ssl, host);
438
+ /* Verify the presented cert actually matches `host`. */
439
+ SSL_set_hostflags(ssl, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS);
440
+ if (SSL_set1_host(ssl, host) != 1) { SSL_free(ssl); close(fd); return -1; }
441
+
442
+ if (SSL_connect(ssl) != 1) {
443
+ /* Handshake or verification failed (incl. bad/expired cert,
444
+ * hostname mismatch, untrusted CA). */
445
+ SSL_free(ssl);
446
+ close(fd);
447
+ return -1;
448
+ }
449
+ sphttp_ssl_tab[fd] = ssl;
450
+ return fd;
451
+ }
452
+
453
+ /* ---- inbound (server) TLS -- tep#148 phase 2 ----
454
+ *
455
+ * A separate server-side SSL_CTX (cert + key). sphttp_tls_server_init
456
+ * loads them once (before the prefork, so workers inherit the CTX);
457
+ * sphttp_accept_tls wraps an already-accepted plain-TCP fd in a TLS
458
+ * handshake and registers the SSL* so read/write/close are transparent
459
+ * (same fd->SSL* table as the client path). Blocking fd, so SSL_accept
460
+ * completes or hard-errors. */
461
+ static SSL_CTX *sphttp_ssl_server_ctx = NULL;
462
+
463
+ /* Load cert chain + private key into the server CTX. Returns 0 on
464
+ * success, -1 on any failure (missing/unreadable files, key mismatch). */
465
+ int sphttp_tls_server_init(const char *cert_path, const char *key_path) {
466
+ SSL_CTX *ctx = SSL_CTX_new(TLS_server_method());
467
+ if (!ctx) return -1;
468
+ SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION);
469
+ if (SSL_CTX_use_certificate_chain_file(ctx, cert_path) != 1) { SSL_CTX_free(ctx); return -1; }
470
+ if (SSL_CTX_use_PrivateKey_file(ctx, key_path, SSL_FILETYPE_PEM) != 1) { SSL_CTX_free(ctx); return -1; }
471
+ if (SSL_CTX_check_private_key(ctx) != 1) { SSL_CTX_free(ctx); return -1; }
472
+ sphttp_ssl_server_ctx = ctx;
473
+ return 0;
474
+ }
475
+
476
+ /* Server-side TLS handshake over an accepted fd. Returns 0 on success
477
+ * (SSL* registered for fd), -1 on failure -- the caller closes the fd. */
478
+ int sphttp_accept_tls(int fd) {
479
+ if (!sphttp_ssl_server_ctx) return -1;
480
+ if (fd < 0 || fd >= SPHTTP_FD_MAX) return -1;
481
+ SSL *ssl = SSL_new(sphttp_ssl_server_ctx);
482
+ if (!ssl) return -1;
483
+ SSL_set_fd(ssl, fd);
484
+ if (SSL_accept(ssl) != 1) {
485
+ SSL_free(ssl);
486
+ return -1;
487
+ }
488
+ sphttp_ssl_tab[fd] = ssl;
489
+ return 0;
490
+ }
491
+
492
+ /* ---- non-blocking TLS -- tep#150 (outbound coop) + scheduled inbound ----
493
+ *
494
+ * The blocking connect_tls / accept_tls above run the handshake inline,
495
+ * which is correct for the prefork-blocking server but would wedge a
496
+ * fiber under Tep::Server::Scheduled (the fd is non-blocking there, so
497
+ * the handshake -- and later SSL_read -- can return WANT_READ/WANT_WRITE
498
+ * instead of completing). These helpers split SSL setup from stepping so
499
+ * the Ruby fiber can park on Tep::Scheduler.io_wait for the indicated
500
+ * direction and retry. sphttp_io_status() exposes the last want-state so
501
+ * the cooperative recv loops can tell "no full TLS record yet" from a
502
+ * real EOF (both surface as an empty sphttp_recv_some result). */
503
+
504
+ /* Last I/O want-state, set by sphttp_recv_some and the handshake
505
+ * stepper: 0 = ok (data / handshake done), 1 = want-read, 2 = want-write,
506
+ * 3 = clean EOF (peer close / TLS close_notify), -1 = hard error. */
507
+ static int sphttp_io_last = 0;
508
+ int sphttp_io_status(void) { return sphttp_io_last; }
509
+
510
+ /* Map SSL_get_error for a <=0 SSL_read/SSL_do_handshake return to our
511
+ * want-state vocabulary. */
512
+ static int sphttp_ssl_want(SSL *ssl, int ret) {
513
+ int e = SSL_get_error(ssl, ret);
514
+ if (e == SSL_ERROR_WANT_READ) return 1;
515
+ if (e == SSL_ERROR_WANT_WRITE) return 2;
516
+ if (e == SSL_ERROR_ZERO_RETURN) return 3; /* clean close_notify */
517
+ return -1;
518
+ }
519
+
520
+ /* Begin an outbound TLS connection on a NON-BLOCKING socket: TCP
521
+ * connect, set non-blocking, build the client SSL (SNI + peer-cert +
522
+ * hostname verification), register it -- but DO NOT run the handshake.
523
+ * The caller drives it via sphttp_tls_handshake_step. Returns the fd, or
524
+ * -1 on any setup failure (fd closed). */
525
+ int sphttp_tls_connect_start(const char *host, int port) {
526
+ int fd = sphttp_connect(host, port);
527
+ if (fd < 0) return -1;
528
+ if (fd >= SPHTTP_FD_MAX) { close(fd); return -1; }
529
+ if (sp_net_set_nonblock(fd) < 0) { close(fd); return -1; }
530
+ SSL_CTX *ctx = sphttp_ssl_ctx_get();
531
+ if (!ctx) { close(fd); return -1; }
532
+ SSL *ssl = SSL_new(ctx);
533
+ if (!ssl) { close(fd); return -1; }
534
+ SSL_set_fd(ssl, fd);
535
+ SSL_set_tlsext_host_name(ssl, host);
536
+ SSL_set_hostflags(ssl, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS);
537
+ if (SSL_set1_host(ssl, host) != 1) { SSL_free(ssl); close(fd); return -1; }
538
+ SSL_set_connect_state(ssl);
539
+ sphttp_ssl_tab[fd] = ssl;
540
+ return fd;
541
+ }
542
+
543
+ /* Begin an inbound TLS handshake on an already-accepted, NON-BLOCKING
544
+ * fd. Registers the server SSL* but does not run the handshake. Returns
545
+ * 0 on success, -1 on setup failure (caller closes the fd). */
546
+ int sphttp_tls_accept_start(int fd) {
547
+ if (!sphttp_ssl_server_ctx) return -1;
548
+ if (fd < 0 || fd >= SPHTTP_FD_MAX) return -1;
549
+ SSL *ssl = SSL_new(sphttp_ssl_server_ctx);
550
+ if (!ssl) return -1;
551
+ SSL_set_fd(ssl, fd);
552
+ SSL_set_accept_state(ssl);
553
+ sphttp_ssl_tab[fd] = ssl;
554
+ return 0;
555
+ }
556
+
557
+ /* Drive one step of a non-blocking handshake (client or server -- the
558
+ * SSL's connect/accept state set above picks the direction). Returns
559
+ * 0 = complete, 1 = want-read, 2 = want-write, -1 = hard failure. On
560
+ * hard failure the SSL* is freed + unregistered (caller closes fd). */
561
+ int sphttp_tls_handshake_step(int fd) {
562
+ SSL *ssl = sphttp_ssl_for(fd);
563
+ if (!ssl) return -1;
564
+ int r = SSL_do_handshake(ssl);
565
+ if (r == 1) { sphttp_io_last = 0; return 0; }
566
+ int w = sphttp_ssl_want(ssl, r);
567
+ if (w == 1 || w == 2) { sphttp_io_last = w; return w; }
568
+ sphttp_ssl_tab[fd] = NULL; /* incl. cert/hostname verify fail */
569
+ SSL_free(ssl);
570
+ sphttp_io_last = -1;
571
+ return -1;
572
+ }
573
+
574
+ /* Best-effort recv() that returns the bytes as a static buffer.
575
+ * Pairs with sphttp_set_nonblock + sphttp_poll_run for the scheduler
576
+ * loop. Returns "" on EAGAIN/empty so callers can branch on
577
+ * .length == 0; "<EOF>" sentinel is the empty-string + closed fd
578
+ * pattern (use sphttp_close + state machine on the caller side). */
579
+ static char sphttp_recv_buf[SPHTTP_BUFSIZE];
580
+ const char *sphttp_recv_some(int fd, int maxlen) {
581
+ if (maxlen <= 0 || maxlen >= SPHTTP_BUFSIZE) maxlen = SPHTTP_BUFSIZE - 1;
582
+ SSL *ssl = sphttp_ssl_for(fd);
583
+ ssize_t n = ssl ? (ssize_t)SSL_read(ssl, sphttp_recv_buf, maxlen)
584
+ : recv(fd, sphttp_recv_buf, (size_t)maxlen, 0);
585
+ if (n <= 0) {
586
+ /* Record WHY we got nothing so the cooperative recv loops
587
+ * (send_req_coop / read_request_blob) can park-and-retry on a
588
+ * TLS partial record or a non-blocking EAGAIN instead of
589
+ * mistaking either for EOF. The blocking callers ignore it. */
590
+ if (ssl) {
591
+ sphttp_io_last = sphttp_ssl_want(ssl, (int)n);
592
+ } else if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
593
+ sphttp_io_last = 1; /* would block -> read */
594
+ } else {
595
+ sphttp_io_last = (n == 0) ? 3 : -1; /* EOF vs error */
596
+ }
597
+ sphttp_recv_buf[0] = '\0';
598
+ return sphttp_recv_buf;
599
+ }
600
+ sphttp_io_last = 0;
601
+ sphttp_recv_buf[n] = '\0';
602
+ return sphttp_recv_buf;
603
+ }
604
+
605
+ /* Read from `fd` until EOF (peer close) or `max_bytes`, whichever
606
+ * comes first. Used by Tep::Http for the HTTP/1.0 + Connection:
607
+ * close response shape. Returns the bytes in a static buffer
608
+ * (length encoded as the C strlen, which is fine because HTTP
609
+ * responses don't carry NUL bytes in their headers/body for the
610
+ * formats this client targets). */
611
+ static char sphttp_recv_all_buf[SPHTTP_BUFSIZE];
612
+ const char *sphttp_recv_all(int fd, int max_bytes) {
613
+ if (max_bytes <= 0 || max_bytes >= SPHTTP_BUFSIZE) max_bytes = SPHTTP_BUFSIZE - 1;
614
+ SSL *ssl = sphttp_ssl_for(fd);
615
+ int total = 0;
616
+ while (total < max_bytes) {
617
+ ssize_t n = ssl
618
+ ? (ssize_t)SSL_read(ssl, sphttp_recv_all_buf + total, max_bytes - total)
619
+ : recv(fd, sphttp_recv_all_buf + total, (size_t)(max_bytes - total), 0);
620
+ if (n <= 0) break;
621
+ total += (int)n;
622
+ }
623
+ sphttp_recv_all_buf[total] = '\0';
624
+ return sphttp_recv_all_buf;
625
+ }
626
+
627
+ /* popen-based shell-out. Captures stdout (up to SPHTTP_BUFSIZE-1)
628
+ * into a static buffer and returns it. Stderr is left to the
629
+ * inherited fd. WARNING: cmd is passed verbatim to /bin/sh -c, so
630
+ * NEVER interpolate untrusted input. The Ruby side (Tep::Shell)
631
+ * enforces this discipline at the API level. */
632
+ /* Shell capture -- delegate to sp_net (#12). */
633
+ const char *sphttp_shell_capture(const char *cmd, int max_bytes) {
634
+ return sp_net_shell_capture(cmd, max_bytes);
635
+ }
636
+
637
+ /* ISO-8601 UTC timestamp ("2026-05-27T13:40:01Z") for the given
638
+ * Unix epoch seconds. Used by Tep::Events for the run_start /
639
+ * run_end wall-clock fields -- spinel's Time.now only exposes
640
+ * integer epoch seconds, and hand-rolling the calendar math (leap
641
+ * years, etc.) in Ruby isn't worth it. gmtime_r + strftime do it
642
+ * in one line. Returns a pointer to a static buffer (single-
643
+ * threaded server model; copy on the Ruby side if retained). */
644
+ static char sphttp_iso8601_buf[32];
645
+ const char *sphttp_iso8601_utc(int epoch_secs) {
646
+ time_t t = (time_t)epoch_secs;
647
+ struct tm tmv;
648
+ gmtime_r(&t, &tmv);
649
+ strftime(sphttp_iso8601_buf, sizeof(sphttp_iso8601_buf),
650
+ "%Y-%m-%dT%H:%M:%SZ", &tmv);
651
+ return sphttp_iso8601_buf;
652
+ }
653
+
654
+ /* RFC 1123 GMT date ("Sun, 06 Nov 1994 08:49:37 GMT") for the given
655
+ * Unix epoch seconds -- the format HTTP Date / Last-Modified / Expires
656
+ * use. Static buffer; copy on the Ruby side if retained. The "C"
657
+ * locale day/month abbreviations are what HTTP requires (strftime here
658
+ * runs under the process default locale, which spinel programs don't
659
+ * change). */
660
+ static char sphttp_http_date_buf[40];
661
+ const char *sphttp_http_date(int epoch_secs) {
662
+ time_t t = (time_t)epoch_secs;
663
+ struct tm tmv;
664
+ gmtime_r(&t, &tmv);
665
+ strftime(sphttp_http_date_buf, sizeof(sphttp_http_date_buf),
666
+ "%a, %d %b %Y %H:%M:%S GMT", &tmv);
667
+ return sphttp_http_date_buf;
668
+ }
669
+
670
+ /* Parse an RFC 1123 HTTP date back to epoch seconds, or -1 if it
671
+ * doesn't parse. Only the modern fixed-length form is handled (what
672
+ * browsers send in If-Modified-Since); the legacy RFC 850 / asctime
673
+ * forms are intentionally not supported. timegm interprets the parsed
674
+ * struct tm as UTC (HTTP dates are always GMT). */
675
+ int sphttp_parse_http_date(const char *s) {
676
+ struct tm tmv;
677
+ memset(&tmv, 0, sizeof(tmv));
678
+ if (strptime(s, "%a, %d %b %Y %H:%M:%S GMT", &tmv) == NULL) return -1;
679
+ time_t t = timegm(&tmv);
680
+ if (t == (time_t)-1) return -1;
681
+ return (int)t;
682
+ }
683
+
684
+ /* uname-based host introspection for toy/v1's host:{name,os,arch}
685
+ * envelope (Tep::Events.run_start). One static buffer per field;
686
+ * we lowercase the os field to match the schema's "linux"/"darwin"
687
+ * convention (uname returns "Linux"/"Darwin" with leading caps).
688
+ * arch is returned as-is ("aarch64", "x86_64", ...). On uname()
689
+ * failure we return "unknown" so the field is always populated. */
690
+ #include <sys/utsname.h>
691
+ #include <ctype.h>
692
+ static char sphttp_os_buf[32];
693
+ static char sphttp_arch_buf[32];
694
+ const char *sphttp_os_kind(void) {
695
+ struct utsname u;
696
+ if (uname(&u) != 0) {
697
+ return "unknown";
698
+ }
699
+ size_t i;
700
+ for (i = 0; i < sizeof(sphttp_os_buf) - 1 && u.sysname[i]; i++) {
701
+ sphttp_os_buf[i] = (char)tolower((unsigned char)u.sysname[i]);
702
+ }
703
+ sphttp_os_buf[i] = '\0';
704
+ return sphttp_os_buf;
705
+ }
706
+ const char *sphttp_arch_kind(void) {
707
+ struct utsname u;
708
+ if (uname(&u) != 0) {
709
+ return "unknown";
710
+ }
711
+ size_t i;
712
+ for (i = 0; i < sizeof(sphttp_arch_buf) - 1 && u.machine[i]; i++) {
713
+ sphttp_arch_buf[i] = u.machine[i];
714
+ }
715
+ sphttp_arch_buf[i] = '\0';
716
+ return sphttp_arch_buf;
717
+ }
718
+
719
+ /* ---------- HTTP/1.1 outbound connection pool (chunk 6.7) ----------
720
+ *
721
+ * Per-process pool keyed by (host, port). Each slot caches one idle
722
+ * keep-alive socket; checkout() removes the matching idle slot,
723
+ * checkin() registers an idle slot, sweep() closes slots older than
724
+ * an idle-timeout. Pure C state -- single-threaded server model
725
+ * (each prefork worker has its own copy of these statics). The Ruby
726
+ * wrapper (Tep::Http::Pool) provides the ergonomic API + ms-grained
727
+ * stats.
728
+ *
729
+ * Fixed-size array with a basic LRU-by-last-used eviction when full.
730
+ * 256 slots is enough for any realistic gateway shape (each slot
731
+ * holds one host+port+fd triple), and the hot path is O(N) over
732
+ * slots -- acceptable for N=256 with cache-line locality. */
733
+ #define SPHTTP_POOL_MAX 256
734
+ #define SPHTTP_POOL_HOST 96
735
+ struct sphttp_pool_slot {
736
+ int fd; /* -1 = empty */
737
+ int port;
738
+ long last_used_secs; /* epoch seconds */
739
+ char host[SPHTTP_POOL_HOST];
740
+ };
741
+ static struct sphttp_pool_slot sphttp_pool[SPHTTP_POOL_MAX];
742
+ static int sphttp_pool_inited = 0;
743
+ static long sphttp_pool_checkouts = 0;
744
+ static long sphttp_pool_checkins = 0;
745
+ static long sphttp_pool_hits = 0;
746
+ static long sphttp_pool_misses = 0;
747
+
748
+ static void sphttp_pool_init_once(void) {
749
+ if (sphttp_pool_inited) return;
750
+ int i;
751
+ for (i = 0; i < SPHTTP_POOL_MAX; i++) {
752
+ sphttp_pool[i].fd = -1;
753
+ sphttp_pool[i].port = 0;
754
+ sphttp_pool[i].last_used_secs = 0;
755
+ sphttp_pool[i].host[0] = '\0';
756
+ }
757
+ sphttp_pool_inited = 1;
758
+ }
759
+
760
+ /* Try to claim an idle fd for (host, port). Returns the fd (>=0)
761
+ * on hit, -1 on miss. Caller owns the fd on hit -- it's removed
762
+ * from the pool atomically. checkouts + hits/misses are bumped for
763
+ * observability. */
764
+ int sphttp_pool_checkout(const char *host, int port) {
765
+ sphttp_pool_init_once();
766
+ sphttp_pool_checkouts++;
767
+ int i;
768
+ for (i = 0; i < SPHTTP_POOL_MAX; i++) {
769
+ if (sphttp_pool[i].fd >= 0 &&
770
+ sphttp_pool[i].port == port &&
771
+ strncmp(sphttp_pool[i].host, host, SPHTTP_POOL_HOST) == 0) {
772
+ int fd = sphttp_pool[i].fd;
773
+ sphttp_pool[i].fd = -1;
774
+ sphttp_pool[i].host[0] = '\0';
775
+ sphttp_pool_hits++;
776
+ return fd;
777
+ }
778
+ }
779
+ sphttp_pool_misses++;
780
+ return -1;
781
+ }
782
+
783
+ /* Register `fd` as an idle keep-alive socket for (host, port).
784
+ * Returns 0 on success, -1 on failure (pool full -- in that case
785
+ * the caller should close the fd; we do NOT close it for them so
786
+ * the call stays side-effect-light). LRU-evict the oldest entry
787
+ * when full: sweep finds the slot with the smallest last_used,
788
+ * closes its fd, reuses the slot. */
789
+ int sphttp_pool_checkin(int fd, const char *host, int port) {
790
+ sphttp_pool_init_once();
791
+ if (fd < 0) return -1;
792
+ int i, free_slot = -1;
793
+ long now = (long)time(NULL);
794
+ /* First pass: find an empty slot. */
795
+ for (i = 0; i < SPHTTP_POOL_MAX; i++) {
796
+ if (sphttp_pool[i].fd < 0) {
797
+ free_slot = i;
798
+ break;
799
+ }
800
+ }
801
+ /* Second pass: evict the LRU if no empty slot. Seed `oldest`
802
+ * with slot 0 + scan from i=1 to avoid needing LONG_MAX (which
803
+ * would pull in limits.h for a single sentinel). */
804
+ if (free_slot < 0) {
805
+ long oldest = sphttp_pool[0].last_used_secs;
806
+ free_slot = 0;
807
+ for (i = 1; i < SPHTTP_POOL_MAX; i++) {
808
+ if (sphttp_pool[i].last_used_secs < oldest) {
809
+ oldest = sphttp_pool[i].last_used_secs;
810
+ free_slot = i;
811
+ }
812
+ }
813
+ if (sphttp_pool[free_slot].fd >= 0) {
814
+ close(sphttp_pool[free_slot].fd);
815
+ }
816
+ }
817
+ if (free_slot < 0) return -1;
818
+ sphttp_pool[free_slot].fd = fd;
819
+ sphttp_pool[free_slot].port = port;
820
+ sphttp_pool[free_slot].last_used_secs = now;
821
+ /* strncpy WITHOUT trailing NUL guarantee on overflow -- but
822
+ * SPHTTP_POOL_HOST is bigger than any realistic hostname; we
823
+ * NUL-terminate manually for safety. */
824
+ strncpy(sphttp_pool[free_slot].host, host, SPHTTP_POOL_HOST - 1);
825
+ sphttp_pool[free_slot].host[SPHTTP_POOL_HOST - 1] = '\0';
826
+ sphttp_pool_checkins++;
827
+ return 0;
828
+ }
829
+
830
+ /* Close any pooled idle fd whose last_used is older than now_secs
831
+ * minus idle_seconds. Returns the count of slots closed. Callers
832
+ * sweep periodically (e.g. the server's main loop) to bound the
833
+ * idle-socket count under sustained low traffic. */
834
+ int sphttp_pool_close_idle(int idle_seconds) {
835
+ sphttp_pool_init_once();
836
+ long now = (long)time(NULL);
837
+ long cutoff = now - (long)idle_seconds;
838
+ int i, closed = 0;
839
+ for (i = 0; i < SPHTTP_POOL_MAX; i++) {
840
+ if (sphttp_pool[i].fd >= 0 &&
841
+ sphttp_pool[i].last_used_secs < cutoff) {
842
+ close(sphttp_pool[i].fd);
843
+ sphttp_pool[i].fd = -1;
844
+ sphttp_pool[i].host[0] = '\0';
845
+ closed++;
846
+ }
847
+ }
848
+ return closed;
849
+ }
850
+
851
+ /* Stats getters -- callers (Tep::Http::Pool.stats) read each one
852
+ * via separate FFI calls to avoid a struct-return shape over FFI. */
853
+ int sphttp_pool_stat_checkouts(void) { return (int)sphttp_pool_checkouts; }
854
+ int sphttp_pool_stat_checkins(void) { return (int)sphttp_pool_checkins; }
855
+ int sphttp_pool_stat_hits(void) { return (int)sphttp_pool_hits; }
856
+ int sphttp_pool_stat_misses(void) { return (int)sphttp_pool_misses; }
857
+
858
+ /* File read/write moved to spinel's built-in File.read / File.write */