carlos / todomvc-bakeoff Public

Clone
git clone https://amadan.net/carlos/todomvc-bakeoff

Plain git — no account needed to clone.

Download

Download this file

1--- a/bin/tep
2+++ b/bin/tep
3@@ -899,6 +899,8 @@
4 __port = #{config[:port] || 4567}
5 __workers = #{config[:workers] || 1}
6 __sockpath = ""
7+ __listen_host = ""
8+ __listen_bad = false
9 __quiet = false
10 __scheduled = #{scheduled ? 'true' : 'false'}
11 __i = 0
12@@ -915,6 +917,21 @@
13 # argv deployment platforms like CARLOS spawn instances with).
14 __sockpath = ARGV[__i + 1]
15 __i += 2
16+ elsif __a == "--listen" && __i + 1 < ARGV.length
17+ # host:port -- bind TCP on exactly that address. Loopback only
18+ # (127.0.0.0/8, ::1, localhost): anything else is refused below.
19+ __hp = ARGV[__i + 1]
20+ __colon = __hp.rindex(":")
21+ if __colon == nil
22+ __listen_bad = true
23+ else
24+ __listen_host = __hp[0, __colon]
25+ __port = __hp[__colon + 1, __hp.length - __colon - 1].to_i
26+ if !Tep.loopback_host?(__listen_host) || __port < 1 || __port > 65535
27+ __listen_bad = true
28+ end
29+ end
30+ __i += 2
31 elsif __a == "--db" && __i + 1 < ARGV.length
32 # Platform-supplied DB path; tolerated so a platform can pass it
33 # unconditionally. Apps that want it read it from ARGV themselves.
34@@ -934,10 +951,19 @@
35 __i += 1
36 end
37 end
38+ # Never fall back to tep's every-interface listener: the process
39+ # serves a unix socket (--socket) or a loopback address (--listen).
40+ if __listen_bad
41+ puts "tep: --listen needs a loopback address and a port, e.g. 127.0.0.1:8080"
42+ exit(2)
43+ end
44 if __sockpath.length > 0
45 Tep.run_unix!(__sockpath, __quiet)
46+ elsif __listen_host.length > 0
47+ Tep.run_host!(__listen_host, __port, __quiet)
48 else
49- Tep.run!(__port, __workers, __quiet, __scheduled)
50+ puts "tep: give --socket <path> or --listen 127.0.0.1:<port>"
51+ exit(2)
52 end
53 RB
54 out << ""
55--- a/native/sqlite/tep_sqlite.c
56+++ b/native/sqlite/tep_sqlite.c
57@@ -44,7 +44,7 @@
58 #include <sqlite3.h>
59
60 #define TEP_SQLITE_MAX_HANDLES 16
61-#define TEP_SQLITE_COL_BUFSIZE 65536
62+#define TEP_SQLITE_COL_BUFSIZE (256 * 1024 + 1) /* a whole request body (SPHTTP_BODY_MAX) fits */
63 #define TEP_SQLITE_COL_BUF_SLOTS 16
64 #define TEP_SQLITE_CACHE_SLOTS 64 /* prepare-statement cache */
65 #define TEP_SQLITE_CACHE_SQL_MAX 512 /* longest cached SQL */
66--- a/sig/tep/request.rbs
67+++ b/sig/tep/request.rbs
68@@ -14,6 +14,7 @@
69 # an ivar slot with Response.
70 attr_accessor req_headers: Hash[String, String]
71 attr_accessor raw_body: String
72+ attr_accessor body_status: Integer
73 attr_accessor cookies: Hash[String, String]
74 attr_accessor session: Tep::Session
75 attr_accessor remote_host: String
76--- a/tep/assets.rb
77+++ b/tep/assets.rb
78@@ -25,8 +25,11 @@
79 # `Tep.public_dir` instead.
80 module Tep
81 class Assets
82+ # Returns 0, as sig/tep/assets.rbs declares (add_asset returns the
83+ # ETag String, which otherwise leaks out as this method's value).
84 def self._add(path, body, mime)
85 Tep::APP.add_asset(path, body, mime)
86+ 0
87 end
88
89 def self.has?(path)
90--- a/tep/net.rb
91+++ b/tep/net.rb
92@@ -27,6 +27,14 @@
93 # Unix-domain listener: bind+listen on a filesystem path (see
94 # sphttp.c). -1 on failure; accept/read/write reuse the same fd paths.
95 ffi_func :sphttp_listen_unix, [:str], :int
96+ # TCP listener on one address (`--listen 127.0.0.1:<port>`).
97+ ffi_func :sphttp_listen_host, [:str, :int], :int
98+ # Form-rules URL decoding in C (see sphttp.c and Tep.parse_form).
99+ ffi_func :sphttp_url_unescape, [:str], :str
100+ # Whole-request deadline + complete-body reads (see sphttp.c).
101+ ffi_func :sphttp_set_deadline, [:int, :int], :int
102+ ffi_func :sphttp_body_max, [], :int
103+ ffi_func :sphttp_body_incomplete, [], :int
104 ffi_func :sphttp_accept, [:int], :int
105 # Non-blocking accept variant used by Tep::Server::Scheduled.
106 # Listen fd must be in non-blocking mode (sphttp_set_nonblock).
107--- a/tep/parser.rb
108+++ b/tep/parser.rb
109@@ -33,7 +33,7 @@
110 else
111 req.path = req.raw_path[0, qmark]
112 qstring = req.raw_path[qmark + 1, req.raw_path.length - qmark - 1]
113- req.query = SpinelKit::Url.parse_query(qstring)
114+ req.query = Tep.parse_form(qstring)
115 end
116
117 i = 1
118@@ -66,7 +66,8 @@
119 if eq > 0
120 cname = pair[0, eq].strip
121 cvalue = pair[eq + 1, pair.length - eq - 1].strip
122- req.cookies[cname] = SpinelKit::Url.unescape(cvalue)
123+ cdecoded = Sock.sphttp_url_unescape(cvalue)
124+ req.cookies[cname] = cdecoded
125 end
126 end
127 end
128--- a/tep/request.rb
129+++ b/tep/request.rb
130@@ -4,6 +4,8 @@
131 attr_accessor :verb, :path, :raw_path, :http_version
132 attr_accessor :params, :query, :req_headers, :raw_body, :cookies, :session
133 attr_accessor :remote_host
134+ # 0 when the body was read whole; else the status to refuse it with.
135+ attr_accessor :body_status
136 attr_accessor :ivars
137 # Set by the auth-filter (Tep::AuthFilter, run before the user's
138 # before-filter -- see Tep::App#auth_filter). Always populated:
139@@ -29,6 +31,7 @@
140 @session = Session.new # signed cookie store
141 @raw_body = "" # same reasoning as req_headers
142 @remote_host = ""
143+ @body_status = 0
144 @passed = false # `pass` flag: skip to the next matching route
145 @ivars = Tep.str_hash # per-request bag for `@name = ...`
146 # set by handlers and `before` filters,
147@@ -136,12 +139,33 @@
148 # Other content types leave @raw_body intact for handlers that
149 # want to consume it directly.
150 def consume_body(client_fd)
151+ if @req_headers.key?("transfer-encoding")
152+ @body_status = 400
153+ return 0
154+ end
155 cl = content_length
156- already = @raw_body.length
157+ if cl < 0
158+ @body_status = 400
159+ return 0
160+ end
161+ if cl > Sock.sphttp_body_max
162+ @body_status = 413
163+ return 0
164+ end
165+ already = @raw_body.bytesize
166 if cl > already
167 rest = Sock.sphttp_drain_body(client_fd, cl - already)
168+ if Sock.sphttp_body_incomplete != 0
169+ @body_status = 400
170+ return 0
171+ end
172 @raw_body = @raw_body + rest
173 end
174+ # Short (or carrying a NUL, which the C string boundary cuts off).
175+ if @raw_body.bytesize < cl
176+ @body_status = 400
177+ return 0
178+ end
179 parse_form_body
180 0
181 end
182@@ -190,7 +214,7 @@
183 # @raw_body to Content-Length.
184 def parse_form_body
185 if form?
186- SpinelKit::Url.parse_query(@raw_body).each do |k, v|
187+ Tep.parse_form(@raw_body).each do |k, v|
188 @params[k] = v
189 end
190 elsif multipart?
191--- a/tep/router.rb
192+++ b/tep/router.rb
193@@ -77,7 +77,8 @@
194 pp = pat[i]
195 if pp.length > 0 && pp[0] == ":"
196 name = @r_params[pi]
197- req.params[name] = SpinelKit::Url.unescape(rp[i])
198+ pdecoded = Sock.sphttp_url_unescape(rp[i])
199+ req.params[name] = pdecoded
200 pi += 1
201 end
202 i += 1
203--- a/tep/server.rb
204+++ b/tep/server.rb
205@@ -8,11 +8,13 @@
206 if status == 204; return "No Content"; end
207 if status == 301; return "Moved Permanently"; end
208 if status == 302; return "Found"; end
209+ if status == 303; return "See Other"; end
210 if status == 304; return "Not Modified"; end
211 if status == 400; return "Bad Request"; end
212 if status == 401; return "Unauthorized"; end
213 if status == 403; return "Forbidden"; end
214 if status == 404; return "Not Found"; end
215+ if status == 413; return "Content Too Large"; end
216 if status == 500; return "Internal Server Error"; end
217 "OK"
218 end
219@@ -121,6 +123,25 @@
220 end
221 end
222
223+ # Serve TCP on one address (`--listen 127.0.0.1:<port>`), with the
224+ # same single-worker, close-per-request shape as run_unix.
225+ def run_host(host, port, quiet)
226+ if !quiet
227+ puts "[tep " + VERSION + "] listening on http://" + host + ":" + port.to_s
228+ end
229+ Tep::APP.close_per_request = true
230+ Sock.sphttp_install_term_handlers
231+ sfd = Sock.sphttp_listen_host(host, port)
232+ if sfd < 0
233+ puts "tep: bind failed on " + host + ":" + port.to_s
234+ exit(1)
235+ end
236+ worker_loop(sfd)
237+ if Sock.sphttp_shutdown_requested != 0
238+ Tep.on_shutdown
239+ end
240+ end
241+
242 def worker_loop(sfd)
243 loop do
244 client = Sock.sphttp_accept(sfd)
245@@ -152,6 +173,12 @@
246 end
247 Sock.sphttp_set_recv_timeout(client, 0)
248 end
249+ # Close-per-request mode serves one connection at a time, so a
250+ # client that connects and then sends nothing, or trickles bytes,
251+ # must not hold the worker: the whole request gets 10 seconds.
252+ if Tep::APP.close_per_request
253+ Sock.sphttp_set_deadline(client, 10000)
254+ end
255 handle_connection(client)
256 end
257 # Explicit Integer return: the `loop` falls through as nil (boxed),
258@@ -178,6 +205,12 @@
259 end
260
261 req.consume_body(client)
262+ # A body that is too large, short of its Content-Length, or chunked
263+ # (unsupported here) never reaches a handler, so nothing mutates.
264+ if req.body_status != 0
265+ send_simple(client, req.body_status, "the body was refused")
266+ break
267+ end
268
269 res = Response.new
270 @app.dispatch(req, res)
271--- a/tep/sphttp.c
272+++ b/tep/sphttp.c
273@@ -85,6 +85,7 @@
274 extern int sp_net_install_term_handlers(void);
275 extern int sp_net_shutdown_requested(void);
276 extern int sp_net_listen(int port, int reuseport);
277+extern int sp_net_listen_host(const char *host, int port, int backlog);
278 extern int sp_net_accept(int sfd);
279 extern int sp_net_accept_nb(int sfd);
280 extern int sp_net_connect(const char *host, int port);
281@@ -147,19 +148,111 @@
282 return fd;
283 }
284
285+/* TCP listener bound to one address (e.g. 127.0.0.1) rather than
286+ * INADDR_ANY, for `--listen host:port`. -1 on failure. */
287+int sphttp_listen_host(const char *host, int port) {
288+ int fd = sp_net_listen_host(host, port, 1024);
289+ if (fd >= 0) {
290+ int one = 1;
291+ setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
292+ }
293+ return fd;
294+}
295+
296 int sphttp_listen(int port, int reuseport) { return sp_net_listen(port, reuseport); }
297+
298+/* URL-decode with form rules ('+' is a space). Decoding byte by byte in
299+ * Ruby allocates a string per input byte, and SpinelKit::Url.parse_query
300+ * holds the decoded key in an unrooted temporary while it decodes the
301+ * value, so a collection in between loses the key (seen on aarch64 with
302+ * long form values, and on x86-64 under SPINEL_GC_STRESS). Returns a
303+ * static buffer; the FFI copies it into a Ruby string. */
304+static char *sphttp_unesc_buf = NULL;
305+static size_t sphttp_unesc_cap = 0;
306+
307+static int sphttp_hexval(int c) {
308+ if (c >= '0' && c <= '9') return c - '0';
309+ if (c >= 'a' && c <= 'f') return c - 'a' + 10;
310+ if (c >= 'A' && c <= 'F') return c - 'A' + 10;
311+ return -1;
312+}
313+
314+const char *sphttp_url_unescape(const char *s) {
315+ size_t n = strlen(s);
316+ if (n + 1 > sphttp_unesc_cap) {
317+ char *nb = (char *)realloc(sphttp_unesc_buf, n + 1);
318+ if (!nb) return "";
319+ sphttp_unesc_buf = nb;
320+ sphttp_unesc_cap = n + 1;
321+ }
322+ size_t o = 0;
323+ for (size_t i = 0; i < n; i++) {
324+ unsigned char c = (unsigned char)s[i];
325+ if (c == '+') {
326+ sphttp_unesc_buf[o++] = ' ';
327+ } else if (c == '%' && i + 2 < n &&
328+ sphttp_hexval((unsigned char)s[i + 1]) >= 0 &&
329+ sphttp_hexval((unsigned char)s[i + 2]) >= 0) {
330+ sphttp_unesc_buf[o++] = (char)(sphttp_hexval((unsigned char)s[i + 1]) * 16 +
331+ sphttp_hexval((unsigned char)s[i + 2]));
332+ i += 2;
333+ } else {
334+ sphttp_unesc_buf[o++] = (char)c;
335+ }
336+ }
337+ sphttp_unesc_buf[o] = '\0';
338+ return sphttp_unesc_buf;
339+}
340 int sphttp_accept(int sfd) { return sp_net_accept(sfd); }
341 int sphttp_accept_nb(int sfd) { return sp_net_accept_nb(sfd); }
342
343 /* Read until end-of-headers ("\r\n\r\n") or the buffer fills. Subsequent
344 * recv()s for the body are the caller's job (we expose a length helper).
345 * Returns the parsed length (>0), 0 on clean EOF, -1 on error. */
346+/* Whole-request deadline (close-per-request mode). Before every blocking
347+ * read or write on the connection, SO_RCVTIMEO/SO_SNDTIMEO are set to the
348+ * time left, so a client trickling one byte at a time can hold the single
349+ * worker for at most the deadline, not a timeout per read. */
350+static struct timespec sphttp_deadline;
351+static int sphttp_deadline_on = 0;
352+
353+int sphttp_set_deadline(int fd, int ms) {
354+ (void)fd;
355+ if (ms <= 0) { sphttp_deadline_on = 0; return 0; }
356+ clock_gettime(CLOCK_MONOTONIC, &sphttp_deadline);
357+ sphttp_deadline.tv_sec += ms / 1000;
358+ sphttp_deadline.tv_nsec += (long)(ms % 1000) * 1000000L;
359+ if (sphttp_deadline.tv_nsec >= 1000000000L) {
360+ sphttp_deadline.tv_sec += 1;
361+ sphttp_deadline.tv_nsec -= 1000000000L;
362+ }
363+ sphttp_deadline_on = 1;
364+ return 0;
365+}
366+
367+/* Arms the socket timeouts with the time left; -1 once it has run out. */
368+static int sphttp_arm_deadline(int fd) {
369+ if (!sphttp_deadline_on) return 0;
370+ struct timespec now;
371+ clock_gettime(CLOCK_MONOTONIC, &now);
372+ long long left_ms = (long long)(sphttp_deadline.tv_sec - now.tv_sec) * 1000LL +
373+ (sphttp_deadline.tv_nsec - now.tv_nsec) / 1000000L;
374+ if (left_ms <= 0) { errno = ETIMEDOUT; return -1; }
375+ struct timeval tv;
376+ tv.tv_sec = (time_t)(left_ms / 1000);
377+ tv.tv_usec = (suseconds_t)((left_ms % 1000) * 1000);
378+ setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
379+ setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
380+ return 0;
381+}
382+
383 int sphttp_read_request(int fd) {
384 SSL *ssl = sphttp_ssl_for(fd); /* inbound TLS: decrypt via SSL_read */
385 sphttp_req_len = 0;
386 sphttp_req_buf[0] = '\0';
387 while (sphttp_req_len < SPHTTP_BUFSIZE - 1) {
388 int want = SPHTTP_BUFSIZE - 1 - sphttp_req_len;
389+ if (!ssl && sphttp_arm_deadline(fd) < 0) return -1;
390 ssize_t n = ssl
391 ? (ssize_t)SSL_read(ssl, sphttp_req_buf + sphttp_req_len, want)
392 : recv(fd, sphttp_req_buf + sphttp_req_len, (size_t)want, 0);
393@@ -190,23 +283,42 @@
394 * computes remaining = content_length - already_in_buf; this gulps
395 * those into a Ruby-visible string buffer. We round-trip via a
396 * static buffer to avoid hand-rolling write_str FFI. */
397-static char sphttp_body_buf[SPHTTP_BUFSIZE];
398+/* Bodies are read whole, up to SPHTTP_BODY_MAX, into a growing buffer
399+ * (this used to stop at 64 KiB, silently truncating longer bodies).
400+ * sphttp_body_incomplete() reports whether the last drain came up short:
401+ * the peer closed or the deadline passed before total_len bytes arrived. */
402+#define SPHTTP_BODY_MAX (256 * 1024)
403+static char *sphttp_body_buf = NULL;
404+static size_t sphttp_body_cap = 0;
405+static int sphttp_body_short = 0;
406+
407+int sphttp_body_max(void) { return SPHTTP_BODY_MAX; }
408+int sphttp_body_incomplete(void) { return sphttp_body_short; }
409
410 const char *sphttp_drain_body(int fd, int total_len) {
411 SSL *ssl = sphttp_ssl_for(fd);
412 int n = total_len;
413+ sphttp_body_short = 0;
414 if (n < 0) n = 0;
415- if (n >= SPHTTP_BUFSIZE) n = SPHTTP_BUFSIZE - 1;
416+ if (n > SPHTTP_BODY_MAX) { sphttp_body_short = 1; return ""; }
417+ if ((size_t)n + 1 > sphttp_body_cap) {
418+ char *nb = (char *)realloc(sphttp_body_buf, (size_t)n + 1);
419+ if (!nb) { sphttp_body_short = 1; return ""; }
420+ sphttp_body_buf = nb;
421+ sphttp_body_cap = (size_t)n + 1;
422+ }
423 int got = 0;
424 while (got < n) {
425+ if (!ssl && sphttp_arm_deadline(fd) < 0) break;
426 ssize_t r = ssl ? (ssize_t)SSL_read(ssl, sphttp_body_buf + got, n - got)
427 : recv(fd, sphttp_body_buf + got, n - got, 0);
428 if (r <= 0) {
429- if (!ssl && errno == EINTR) continue;
430+ if (!ssl && r < 0 && errno == EINTR) continue;
431 break;
432 }
433 got += (int)r;
434 }
435+ if (got < n) sphttp_body_short = 1;
436 sphttp_body_buf[got] = '\0';
437 return sphttp_body_buf;
438 }
439@@ -221,6 +333,7 @@
440 if (w <= 0) return -1;
441 off += (size_t)w;
442 } else {
443+ if (sphttp_arm_deadline(fd) < 0) return -1;
444 ssize_t n = send(fd, s + off, len - off, 0);
445 if (n <= 0) {
446 if (errno == EINTR) continue;
447--- a/tep.rb
448+++ b/tep.rb
449@@ -166,6 +166,68 @@
450 # narrowing model). Removing it would require a mechanical
451 # `< 0` -> `.nil?` refactor across http.rb / parser.rb / url.rb /
452 # jwt.rb / app.rb. Worth doing eventually; not urgent.
453+ # Parse an application/x-www-form-urlencoded string (a body or a query
454+ # string). Replaces SpinelKit::Url.parse_query, which can lose keys to
455+ # the GC (see sphttp_url_unescape); every intermediate here is a local.
456+ def self.parse_form(s)
457+ h = Tep.str_hash
458+ if s.length == 0
459+ return h
460+ end
461+ pairs = s.split("&")
462+ i = 0
463+ while i < pairs.length
464+ pair = pairs[i]
465+ if pair.length > 0
466+ k = ""
467+ v = ""
468+ eq = Tep.str_find(pair, "=", 0)
469+ if eq < 0
470+ k = Sock.sphttp_url_unescape(pair)
471+ else
472+ kraw = pair[0, eq]
473+ vraw = pair[eq + 1, pair.length - eq - 1]
474+ k = Sock.sphttp_url_unescape(kraw)
475+ v = Sock.sphttp_url_unescape(vraw)
476+ end
477+ h[k] = v
478+ end
479+ i += 1
480+ end
481+ h
482+ end
483+
484+ # True for the addresses `--listen` may bind: localhost, ::1 and
485+ # dotted-quad 127.0.0.0/8.
486+ def self.loopback_host?(h)
487+ if h == "localhost" || h == "::1"
488+ return true
489+ end
490+ parts = h.split(".")
491+ if parts.length != 4 || parts[0] != "127"
492+ return false
493+ end
494+ i = 0
495+ while i < 4
496+ p = parts[i]
497+ if p.length == 0 || p.length > 3
498+ return false
499+ end
500+ j = 0
501+ while j < p.length
502+ if p[j] < "0" || p[j] > "9"
503+ return false
504+ end
505+ j += 1
506+ end
507+ if p.to_i > 255
508+ return false
509+ end
510+ i += 1
511+ end
512+ true
513+ end
514+
515 def self.str_find(s, needle, start)
516 nlen = needle.length
517 slen = s.length
518@@ -300,6 +362,11 @@
519 Server.new(APP).run_unix(path, quiet)
520 end
521
522+ # TCP twin bound to one host address -- see Server#run_host.
523+ def self.run_host!(host, port, quiet)
524+ Server.new(APP).run_host(host, port, quiet)
525+ end
526+
527 def self.run!(port, workers, quiet, scheduled = false)
528 if scheduled
529 Server::Scheduled.new(APP).run(port, workers, quiet)
530