| index 1dd846f..61a08a4 100644 |
| --- a/apps/spinel/patches/2-tep-bakeoff.patch |
| +++ b/apps/spinel/patches/2-tep-bakeoff.patch |
| @@ -1,39 +1,78 @@ |
| --- a/bin/tep |
| +++ b/bin/tep |
| -@@ -899,6 +899,7 @@ |
| +@@ -899,6 +899,8 @@ |
| __port = #{config[:port] || 4567} |
| __workers = #{config[:workers] || 1} |
| __sockpath = "" |
| + __listen_host = "" |
| ++ __listen_bad = false |
| __quiet = false |
| __scheduled = #{scheduled ? 'true' : 'false'} |
| __i = 0 |
| -@@ -915,6 +916,16 @@ |
| +@@ -915,6 +917,21 @@ |
| # argv deployment platforms like CARLOS spawn instances with). |
| __sockpath = ARGV[__i + 1] |
| __i += 2 |
| + elsif __a == "--listen" && __i + 1 < ARGV.length |
| -+ # host:port -- bind TCP on exactly that address (127.0.0.1 for |
| -+ # local runs) instead of -p's INADDR_ANY. |
| ++ # host:port -- bind TCP on exactly that address. Loopback only |
| ++ # (127.0.0.0/8, ::1, localhost): anything else is refused below. |
| + __hp = ARGV[__i + 1] |
| + __colon = __hp.rindex(":") |
| -+ if __colon != nil |
| ++ if __colon == nil |
| ++ __listen_bad = true |
| ++ else |
| + __listen_host = __hp[0, __colon] |
| + __port = __hp[__colon + 1, __hp.length - __colon - 1].to_i |
| ++ if !Tep.loopback_host?(__listen_host) || __port < 1 || __port > 65535 |
| ++ __listen_bad = true |
| ++ end |
| + end |
| + __i += 2 |
| elsif __a == "--db" && __i + 1 < ARGV.length |
| # Platform-supplied DB path; tolerated so a platform can pass it |
| # unconditionally. Apps that want it read it from ARGV themselves. |
| -@@ -936,6 +947,8 @@ |
| +@@ -934,10 +951,19 @@ |
| + __i += 1 |
| + end |
| end |
| ++ # Never fall back to tep's every-interface listener: the process |
| ++ # serves a unix socket (--socket) or a loopback address (--listen). |
| ++ if __listen_bad |
| ++ puts "tep: --listen needs a loopback address and a port, e.g. 127.0.0.1:8080" |
| ++ exit(2) |
| ++ end |
| if __sockpath.length > 0 |
| Tep.run_unix!(__sockpath, __quiet) |
| + elsif __listen_host.length > 0 |
| + Tep.run_host!(__listen_host, __port, __quiet) |
| else |
| - Tep.run!(__port, __workers, __quiet, __scheduled) |
| +- Tep.run!(__port, __workers, __quiet, __scheduled) |
| ++ puts "tep: give --socket <path> or --listen 127.0.0.1:<port>" |
| ++ exit(2) |
| end |
| + RB |
| + out << "" |
| +--- a/native/sqlite/tep_sqlite.c |
| ++++ b/native/sqlite/tep_sqlite.c |
| +@@ -44,7 +44,7 @@ |
| + #include <sqlite3.h> |
| + |
| + #define TEP_SQLITE_MAX_HANDLES 16 |
| +-#define TEP_SQLITE_COL_BUFSIZE 65536 |
| ++#define TEP_SQLITE_COL_BUFSIZE (256 * 1024 + 1) /* a whole request body (SPHTTP_BODY_MAX) fits */ |
| + #define TEP_SQLITE_COL_BUF_SLOTS 16 |
| + #define TEP_SQLITE_CACHE_SLOTS 64 /* prepare-statement cache */ |
| + #define TEP_SQLITE_CACHE_SQL_MAX 512 /* longest cached SQL */ |
| +--- a/sig/tep/request.rbs |
| ++++ b/sig/tep/request.rbs |
| +@@ -14,6 +14,7 @@ |
| + # an ivar slot with Response. |
| + attr_accessor req_headers: Hash[String, String] |
| + attr_accessor raw_body: String |
| ++ attr_accessor body_status: Integer |
| + attr_accessor cookies: Hash[String, String] |
| + attr_accessor session: Tep::Session |
| + attr_accessor remote_host: String |
| --- a/tep/assets.rb |
| +++ b/tep/assets.rb |
| @@ -25,8 +25,11 @@ |
| @@ -50,7 +89,7 @@ |
| def self.has?(path) |
| --- a/tep/net.rb |
| +++ b/tep/net.rb |
| -@@ -27,6 +27,10 @@ |
| +@@ -27,6 +27,14 @@ |
| # Unix-domain listener: bind+listen on a filesystem path (see |
| # sphttp.c). -1 on failure; accept/read/write reuse the same fd paths. |
| ffi_func :sphttp_listen_unix, [:str], :int |
| @@ -58,6 +97,10 @@ |
| + ffi_func :sphttp_listen_host, [:str, :int], :int |
| + # Form-rules URL decoding in C (see sphttp.c and Tep.parse_form). |
| + ffi_func :sphttp_url_unescape, [:str], :str |
| ++ # Whole-request deadline + complete-body reads (see sphttp.c). |
| ++ ffi_func :sphttp_set_deadline, [:int, :int], :int |
| ++ ffi_func :sphttp_body_max, [], :int |
| ++ ffi_func :sphttp_body_incomplete, [], :int |
| ffi_func :sphttp_accept, [:int], :int |
| # Non-blocking accept variant used by Tep::Server::Scheduled. |
| # Listen fd must be in non-blocking mode (sphttp_set_nonblock). |
| @@ -84,7 +127,59 @@ |
| end |
| --- a/tep/request.rb |
| +++ b/tep/request.rb |
| -@@ -190,7 +190,7 @@ |
| +@@ -4,6 +4,8 @@ |
| + attr_accessor :verb, :path, :raw_path, :http_version |
| + attr_accessor :params, :query, :req_headers, :raw_body, :cookies, :session |
| + attr_accessor :remote_host |
| ++ # 0 when the body was read whole; else the status to refuse it with. |
| ++ attr_accessor :body_status |
| + attr_accessor :ivars |
| + # Set by the auth-filter (Tep::AuthFilter, run before the user's |
| + # before-filter -- see Tep::App#auth_filter). Always populated: |
| +@@ -29,6 +31,7 @@ |
| + @session = Session.new # signed cookie store |
| + @raw_body = "" # same reasoning as req_headers |
| + @remote_host = "" |
| ++ @body_status = 0 |
| + @passed = false # `pass` flag: skip to the next matching route |
| + @ivars = Tep.str_hash # per-request bag for `@name = ...` |
| + # set by handlers and `before` filters, |
| +@@ -136,12 +139,33 @@ |
| + # Other content types leave @raw_body intact for handlers that |
| + # want to consume it directly. |
| + def consume_body(client_fd) |
| ++ if @req_headers.key?("transfer-encoding") |
| ++ @body_status = 400 |
| ++ return 0 |
| ++ end |
| + cl = content_length |
| +- already = @raw_body.length |
| ++ if cl < 0 |
| ++ @body_status = 400 |
| ++ return 0 |
| ++ end |
| ++ if cl > Sock.sphttp_body_max |
| ++ @body_status = 413 |
| ++ return 0 |
| ++ end |
| ++ already = @raw_body.bytesize |
| + if cl > already |
| + rest = Sock.sphttp_drain_body(client_fd, cl - already) |
| ++ if Sock.sphttp_body_incomplete != 0 |
| ++ @body_status = 400 |
| ++ return 0 |
| ++ end |
| + @raw_body = @raw_body + rest |
| + end |
| ++ # Short (or carrying a NUL, which the C string boundary cuts off). |
| ++ if @raw_body.bytesize < cl |
| ++ @body_status = 400 |
| ++ return 0 |
| ++ end |
| + parse_form_body |
| + 0 |
| + end |
| +@@ -190,7 +214,7 @@ |
| # @raw_body to Content-Length. |
| def parse_form_body |
| if form? |
| @@ -107,7 +202,7 @@ |
| i += 1 |
| --- a/tep/server.rb |
| +++ b/tep/server.rb |
| -@@ -8,6 +8,7 @@ |
| +@@ -8,11 +8,13 @@ |
| if status == 204; return "No Content"; end |
| if status == 301; return "Moved Permanently"; end |
| if status == 302; return "Found"; end |
| @@ -115,7 +210,13 @@ |
| if status == 304; return "Not Modified"; end |
| if status == 400; return "Bad Request"; end |
| if status == 401; return "Unauthorized"; end |
| -@@ -121,6 +122,25 @@ |
| + if status == 403; return "Forbidden"; end |
| + if status == 404; return "Not Found"; end |
| ++ if status == 413; return "Content Too Large"; end |
| + if status == 500; return "Internal Server Error"; end |
| + "OK" |
| + end |
| +@@ -121,6 +123,25 @@ |
| end |
| end |
| |
| @@ -141,19 +242,32 @@ |
| def worker_loop(sfd) |
| loop do |
| client = Sock.sphttp_accept(sfd) |
| -@@ -152,6 +172,12 @@ |
| +@@ -152,6 +173,12 @@ |
| end |
| Sock.sphttp_set_recv_timeout(client, 0) |
| end |
| + # Close-per-request mode serves one connection at a time, so a |
| -+ # client that connects and then sends nothing (a browser's |
| -+ # speculative preconnect) must not hold the worker: bound reads. |
| ++ # client that connects and then sends nothing, or trickles bytes, |
| ++ # must not hold the worker: the whole request gets 10 seconds. |
| + if Tep::APP.close_per_request |
| -+ Sock.sphttp_set_recv_timeout(client, 5000) |
| ++ Sock.sphttp_set_deadline(client, 10000) |
| + end |
| handle_connection(client) |
| end |
| # Explicit Integer return: the `loop` falls through as nil (boxed), |
| +@@ -178,6 +205,12 @@ |
| + end |
| + |
| + req.consume_body(client) |
| ++ # A body that is too large, short of its Content-Length, or chunked |
| ++ # (unsupported here) never reaches a handler, so nothing mutates. |
| ++ if req.body_status != 0 |
| ++ send_simple(client, req.body_status, "the body was refused") |
| ++ break |
| ++ end |
| + |
| + res = Response.new |
| + @app.dispatch(req, res) |
| --- a/tep/sphttp.c |
| +++ b/tep/sphttp.c |
| @@ -85,6 +85,7 @@ |
| @@ -164,7 +278,7 @@ |
| extern int sp_net_accept(int sfd); |
| extern int sp_net_accept_nb(int sfd); |
| extern int sp_net_connect(const char *host, int port); |
| -@@ -147,7 +148,61 @@ |
| +@@ -147,19 +148,111 @@ |
| return fd; |
| } |
| |
| @@ -226,9 +340,113 @@ |
| int sphttp_accept(int sfd) { return sp_net_accept(sfd); } |
| int sphttp_accept_nb(int sfd) { return sp_net_accept_nb(sfd); } |
| |
| + /* Read until end-of-headers ("\r\n\r\n") or the buffer fills. Subsequent |
| + * recv()s for the body are the caller's job (we expose a length helper). |
| + * Returns the parsed length (>0), 0 on clean EOF, -1 on error. */ |
| ++/* Whole-request deadline (close-per-request mode). Before every blocking |
| ++ * read or write on the connection, SO_RCVTIMEO/SO_SNDTIMEO are set to the |
| ++ * time left, so a client trickling one byte at a time can hold the single |
| ++ * worker for at most the deadline, not a timeout per read. */ |
| ++static struct timespec sphttp_deadline; |
| ++static int sphttp_deadline_on = 0; |
| ++ |
| ++int sphttp_set_deadline(int fd, int ms) { |
| ++ (void)fd; |
| ++ if (ms <= 0) { sphttp_deadline_on = 0; return 0; } |
| ++ clock_gettime(CLOCK_MONOTONIC, &sphttp_deadline); |
| ++ sphttp_deadline.tv_sec += ms / 1000; |
| ++ sphttp_deadline.tv_nsec += (long)(ms % 1000) * 1000000L; |
| ++ if (sphttp_deadline.tv_nsec >= 1000000000L) { |
| ++ sphttp_deadline.tv_sec += 1; |
| ++ sphttp_deadline.tv_nsec -= 1000000000L; |
| ++ } |
| ++ sphttp_deadline_on = 1; |
| ++ return 0; |
| ++} |
| ++ |
| ++/* Arms the socket timeouts with the time left; -1 once it has run out. */ |
| ++static int sphttp_arm_deadline(int fd) { |
| ++ if (!sphttp_deadline_on) return 0; |
| ++ struct timespec now; |
| ++ clock_gettime(CLOCK_MONOTONIC, &now); |
| ++ long long left_ms = (long long)(sphttp_deadline.tv_sec - now.tv_sec) * 1000LL + |
| ++ (sphttp_deadline.tv_nsec - now.tv_nsec) / 1000000L; |
| ++ if (left_ms <= 0) { errno = ETIMEDOUT; return -1; } |
| ++ struct timeval tv; |
| ++ tv.tv_sec = (time_t)(left_ms / 1000); |
| ++ tv.tv_usec = (suseconds_t)((left_ms % 1000) * 1000); |
| ++ setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); |
| ++ setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); |
| ++ return 0; |
| ++} |
| ++ |
| + int sphttp_read_request(int fd) { |
| + SSL *ssl = sphttp_ssl_for(fd); /* inbound TLS: decrypt via SSL_read */ |
| + sphttp_req_len = 0; |
| + sphttp_req_buf[0] = '\0'; |
| + while (sphttp_req_len < SPHTTP_BUFSIZE - 1) { |
| + int want = SPHTTP_BUFSIZE - 1 - sphttp_req_len; |
| ++ if (!ssl && sphttp_arm_deadline(fd) < 0) return -1; |
| + ssize_t n = ssl |
| + ? (ssize_t)SSL_read(ssl, sphttp_req_buf + sphttp_req_len, want) |
| + : recv(fd, sphttp_req_buf + sphttp_req_len, (size_t)want, 0); |
| +@@ -190,23 +283,42 @@ |
| + * computes remaining = content_length - already_in_buf; this gulps |
| + * those into a Ruby-visible string buffer. We round-trip via a |
| + * static buffer to avoid hand-rolling write_str FFI. */ |
| +-static char sphttp_body_buf[SPHTTP_BUFSIZE]; |
| ++/* Bodies are read whole, up to SPHTTP_BODY_MAX, into a growing buffer |
| ++ * (this used to stop at 64 KiB, silently truncating longer bodies). |
| ++ * sphttp_body_incomplete() reports whether the last drain came up short: |
| ++ * the peer closed or the deadline passed before total_len bytes arrived. */ |
| ++#define SPHTTP_BODY_MAX (256 * 1024) |
| ++static char *sphttp_body_buf = NULL; |
| ++static size_t sphttp_body_cap = 0; |
| ++static int sphttp_body_short = 0; |
| ++ |
| ++int sphttp_body_max(void) { return SPHTTP_BODY_MAX; } |
| ++int sphttp_body_incomplete(void) { return sphttp_body_short; } |
| + |
| + const char *sphttp_drain_body(int fd, int total_len) { |
| + SSL *ssl = sphttp_ssl_for(fd); |
| + int n = total_len; |
| ++ sphttp_body_short = 0; |
| + if (n < 0) n = 0; |
| +- if (n >= SPHTTP_BUFSIZE) n = SPHTTP_BUFSIZE - 1; |
| ++ if (n > SPHTTP_BODY_MAX) { sphttp_body_short = 1; return ""; } |
| ++ if ((size_t)n + 1 > sphttp_body_cap) { |
| ++ char *nb = (char *)realloc(sphttp_body_buf, (size_t)n + 1); |
| ++ if (!nb) { sphttp_body_short = 1; return ""; } |
| ++ sphttp_body_buf = nb; |
| ++ sphttp_body_cap = (size_t)n + 1; |
| ++ } |
| + int got = 0; |
| + while (got < n) { |
| ++ if (!ssl && sphttp_arm_deadline(fd) < 0) break; |
| + ssize_t r = ssl ? (ssize_t)SSL_read(ssl, sphttp_body_buf + got, n - got) |
| + : recv(fd, sphttp_body_buf + got, n - got, 0); |
| + if (r <= 0) { |
| +- if (!ssl && errno == EINTR) continue; |
| ++ if (!ssl && r < 0 && errno == EINTR) continue; |
| + break; |
| + } |
| + got += (int)r; |
| + } |
| ++ if (got < n) sphttp_body_short = 1; |
| + sphttp_body_buf[got] = '\0'; |
| + return sphttp_body_buf; |
| + } |
| +@@ -221,6 +333,7 @@ |
| + if (w <= 0) return -1; |
| + off += (size_t)w; |
| + } else { |
| ++ if (sphttp_arm_deadline(fd) < 0) return -1; |
| + ssize_t n = send(fd, s + off, len - off, 0); |
| + if (n <= 0) { |
| + if (errno == EINTR) continue; |
| --- a/tep.rb |
| +++ b/tep.rb |
| -@@ -166,6 +166,37 @@ |
| +@@ -166,6 +166,68 @@ |
| # narrowing model). Removing it would require a mechanical |
| # `< 0` -> `.nil?` refactor across http.rb / parser.rb / url.rb / |
| # jwt.rb / app.rb. Worth doing eventually; not urgent. |
| @@ -262,11 +480,42 @@ |
| + end |
| + h |
| + end |
| ++ |
| ++ # True for the addresses `--listen` may bind: localhost, ::1 and |
| ++ # dotted-quad 127.0.0.0/8. |
| ++ def self.loopback_host?(h) |
| ++ if h == "localhost" || h == "::1" |
| ++ return true |
| ++ end |
| ++ parts = h.split(".") |
| ++ if parts.length != 4 || parts[0] != "127" |
| ++ return false |
| ++ end |
| ++ i = 0 |
| ++ while i < 4 |
| ++ p = parts[i] |
| ++ if p.length == 0 || p.length > 3 |
| ++ return false |
| ++ end |
| ++ j = 0 |
| ++ while j < p.length |
| ++ if p[j] < "0" || p[j] > "9" |
| ++ return false |
| ++ end |
| ++ j += 1 |
| ++ end |
| ++ if p.to_i > 255 |
| ++ return false |
| ++ end |
| ++ i += 1 |
| ++ end |
| ++ true |
| ++ end |
| + |
| def self.str_find(s, needle, start) |
| nlen = needle.length |
| slen = s.length |
| -@@ -300,6 +331,11 @@ |
| +@@ -300,6 +362,11 @@ |
| Server.new(APP).run_unix(path, quiet) |
| end |
| |