| 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 | |