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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/Makefile +134 -0
- data/README.md +247 -0
- data/SINATRA_COMPAT.md +376 -0
- data/bin/tep +2156 -0
- data/examples/agentic_chat/README.md +103 -0
- data/examples/agentic_chat/app.rb +310 -0
- data/examples/api_gateway/README.md +49 -0
- data/examples/api_gateway/app.rb +66 -0
- data/examples/blog/app.rb +367 -0
- data/examples/blog/views/index.erb +36 -0
- data/examples/blog/views/login.erb +28 -0
- data/examples/blog/views/new_post.erb +25 -0
- data/examples/blog/views/show.erb +16 -0
- data/examples/chat/app.rb +278 -0
- data/examples/chat/assets/logo.svg +13 -0
- data/examples/chat/assets/style.css +209 -0
- data/examples/chat/views/index.erb +142 -0
- data/examples/chatbot/README.md +111 -0
- data/examples/chatbot/app.rb +1024 -0
- data/examples/chatbot/assets/chat.js +249 -0
- data/examples/chatbot/assets/compare.js +93 -0
- data/examples/chatbot/assets/markdown.js +84 -0
- data/examples/chatbot/assets/style.css +215 -0
- data/examples/chatbot/schema.sql +25 -0
- data/examples/chatbot/views/compare.erb +43 -0
- data/examples/chatbot/views/index.erb +42 -0
- data/examples/chatbot/views/login.erb +22 -0
- data/examples/chatbot/views/setup.erb +23 -0
- data/examples/counter/README.md +68 -0
- data/examples/counter/app.rb +85 -0
- data/examples/experiments/AGENTS.md +91 -0
- data/examples/experiments/README.md +99 -0
- data/examples/experiments/app.rb +225 -0
- data/examples/geohash/Gemfile +11 -0
- data/examples/geohash/Gemfile.lock +17 -0
- data/examples/geohash/README.md +58 -0
- data/examples/geohash/app.rb +33 -0
- data/examples/hello.rb +120 -0
- data/examples/llm_gateway/README.md +73 -0
- data/examples/llm_gateway/app.rb +91 -0
- data/examples/maidenhead/Gemfile +7 -0
- data/examples/maidenhead/Gemfile.lock +17 -0
- data/examples/maidenhead/README.md +47 -0
- data/examples/maidenhead/app.rb +46 -0
- data/examples/pg_hello.rb +76 -0
- data/examples/qdrant/Gemfile +11 -0
- data/examples/qdrant/Gemfile.lock +29 -0
- data/examples/qdrant/README.md +54 -0
- data/examples/sinatra_style.rb +32 -0
- data/examples/websocket_echo.rb +37 -0
- data/lib/tep/agent_delegation.rb +35 -0
- data/lib/tep/app.rb +291 -0
- data/lib/tep/assets.rb +52 -0
- data/lib/tep/auth.rb +78 -0
- data/lib/tep/auth_bearer_token.rb +126 -0
- data/lib/tep/auth_oauth2.rb +189 -0
- data/lib/tep/auth_oauth2_client.rb +29 -0
- data/lib/tep/auth_oauth2_code.rb +40 -0
- data/lib/tep/auth_session_cookie.rb +132 -0
- data/lib/tep/broadcast.rb +265 -0
- data/lib/tep/broadcast_subscription.rb +42 -0
- data/lib/tep/cache.rb +49 -0
- data/lib/tep/events.rb +257 -0
- data/lib/tep/filter.rb +21 -0
- data/lib/tep/handler.rb +35 -0
- data/lib/tep/http.rb +599 -0
- data/lib/tep/identity.rb +67 -0
- data/lib/tep/job.rb +186 -0
- data/lib/tep/json.rb +572 -0
- data/lib/tep/jwt.rb +126 -0
- data/lib/tep/live_view.rb +219 -0
- data/lib/tep/llm.rb +505 -0
- data/lib/tep/logger.rb +85 -0
- data/lib/tep/mcp.rb +203 -0
- data/lib/tep/multipart.rb +98 -0
- data/lib/tep/net.rb +155 -0
- data/lib/tep/openai_server.rb +725 -0
- data/lib/tep/parallel.rb +168 -0
- data/lib/tep/parser.rb +81 -0
- data/lib/tep/password.rb +102 -0
- data/lib/tep/pg.rb +1128 -0
- data/lib/tep/presence.rb +589 -0
- data/lib/tep/presence_entry.rb +52 -0
- data/lib/tep/proxy.rb +801 -0
- data/lib/tep/request.rb +194 -0
- data/lib/tep/response.rb +134 -0
- data/lib/tep/router.rb +137 -0
- data/lib/tep/scheduler.rb +342 -0
- data/lib/tep/security.rb +140 -0
- data/lib/tep/server.rb +276 -0
- data/lib/tep/server_scheduled.rb +375 -0
- data/lib/tep/session.rb +98 -0
- data/lib/tep/shell.rb +62 -0
- data/lib/tep/sphttp.c +858 -0
- data/lib/tep/sqlite.rb +215 -0
- data/lib/tep/streamer.rb +31 -0
- data/lib/tep/tep_pg.c +769 -0
- data/lib/tep/tep_sqlite.c +320 -0
- data/lib/tep/url.rb +161 -0
- data/lib/tep/version.rb +3 -0
- data/lib/tep/websocket/connection.rb +171 -0
- data/lib/tep/websocket/driver.rb +169 -0
- data/lib/tep/websocket/frame.rb +238 -0
- data/lib/tep/websocket/handshake.rb +159 -0
- data/lib/tep/websocket.rb +68 -0
- data/lib/tep.rb +981 -0
- data/public/hello.txt +1 -0
- data/public/style.css +4 -0
- data/spinel-ext.json +33 -0
- data/test/helper.rb +248 -0
- data/test/real_world/01_simple.rb +5 -0
- data/test/real_world/02_lifecycle.rb +20 -0
- data/test/real_world/03_chat.rb +75 -0
- data/test/real_world/04_health_api.rb +25 -0
- data/test/real_world/05_todo_api.rb +57 -0
- data/test/real_world/06_basic_auth.rb +25 -0
- data/test/real_world/07_bbc_rest_api.rb +228 -0
- data/test/real_world/07_sklise_things.rb +109 -0
- data/test/real_world/08_jwd83_helloworld.rb +56 -0
- data/test/run_all.rb +7 -0
- data/test/run_parallel.rb +89 -0
- data/test/spinel_scheduled_burst_segv_repro.rb +33 -0
- data/test/test_api_gateway.rb +76 -0
- data/test/test_auth.rb +223 -0
- data/test/test_auth_oauth2.rb +208 -0
- data/test/test_auth_session_cookie.rb +198 -0
- data/test/test_broadcast.rb +197 -0
- data/test/test_broadcast_pg.rb +135 -0
- data/test/test_cache.rb +98 -0
- data/test/test_cache_static.rb +48 -0
- data/test/test_cookies.rb +52 -0
- data/test/test_erb.rb +53 -0
- data/test/test_erb_ivars.rb +58 -0
- data/test/test_events.rb +114 -0
- data/test/test_filters.rb +41 -0
- data/test/test_geohash_example.rb +89 -0
- data/test/test_http.rb +137 -0
- data/test/test_http_pool.rb +122 -0
- data/test/test_http_pool_send.rb +57 -0
- data/test/test_identity.rb +165 -0
- data/test/test_inbound_tls.rb +101 -0
- data/test/test_inbound_tls_scheduled.rb +101 -0
- data/test/test_job.rb +108 -0
- data/test/test_json.rb +168 -0
- data/test/test_jwt.rb +143 -0
- data/test/test_live_view.rb +324 -0
- data/test/test_llm.rb +250 -0
- data/test/test_llm_gateway.rb +95 -0
- data/test/test_logger.rb +101 -0
- data/test/test_maidenhead_example.rb +86 -0
- data/test/test_mcp.rb +264 -0
- data/test/test_misc_v02.rb +54 -0
- data/test/test_modular.rb +43 -0
- data/test/test_multi_filters.rb +40 -0
- data/test/test_mustache.rb +57 -0
- data/test/test_openai_server.rb +598 -0
- data/test/test_optional_segments.rb +45 -0
- data/test/test_parallel.rb +102 -0
- data/test/test_params.rb +99 -0
- data/test/test_pass.rb +42 -0
- data/test/test_password.rb +101 -0
- data/test/test_pg.rb +673 -0
- data/test/test_presence.rb +374 -0
- data/test/test_presence_pg.rb +309 -0
- data/test/test_proxy.rb +556 -0
- data/test/test_proxy_dsl.rb +119 -0
- data/test/test_proxy_streaming.rb +146 -0
- data/test/test_real_world.rb +397 -0
- data/test/test_regex_routes.rb +52 -0
- data/test/test_request_methods.rb +102 -0
- data/test/test_response.rb +123 -0
- data/test/test_routing.rb +109 -0
- data/test/test_scheduler.rb +153 -0
- data/test/test_security.rb +72 -0
- data/test/test_server_scheduled.rb +56 -0
- data/test/test_sessions.rb +59 -0
- data/test/test_shell.rb +54 -0
- data/test/test_sqlite.rb +148 -0
- data/test/test_sqlite_cached.rb +171 -0
- data/test/test_static.rb +57 -0
- data/test/test_streaming.rb +96 -0
- data/test/test_unsupported.rb +32 -0
- data/test/test_websocket.rb +152 -0
- data/test/test_websocket_echo.rb +138 -0
- data/test/views/greet.erb +5 -0
- data/test/views/hello.erb +5 -0
- data/test/views/list.erb +5 -0
- data/test/views/m_ivars.mustache +3 -0
- data/test/views/m_simple.mustache +4 -0
- data/test/views/mixed.erb +3 -0
- 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 */
|