tls: bound the TLS handshake, and say when it timed out
connect-timeout-ms stops at the moment the peer accepts. Everything after that -- the whole handshake -- was an unbounded blocking read. A peer that accepts a connection and then never sends a ServerHello left the caller parked in read() forever, which under Sigil's cooperative scheduler freezes the entire process rather than failing one request.
tls-connect and tls-upgrade take an optional handshake-timeout-ms. When set, handshake reads go through mbedtls_net_recv_timeout under a deadline covering the whole handshake, not each read, so a peer dripping bytes cannot extend it. The read timeout is unwound before the connection is handed back, so an established connection reads exactly as before. Omitted or <= 0 keeps the original blocking handshake.
tls-connect/status and tls-upgrade/status report (status . conn-or-#f), so "handshake-timeout" (the peer went silent) stays distinguishable from "handshake-failed" (the peer refused us). Those are retried differently and a single flat #f cannot express the difference.
Also closes an fd leak on the tls-upgrade failure path: the socket object is marked closed before the handshake, so nothing else owned that descriptor. Previously unreachable in practice; a bounded handshake makes it reachable.
native/tls.c | 384 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
src/sigil/tls.sgl | 57 ++++++++++++++++++++++----
test/test-tls.sgl | 107 ++++++++++++++++++++++++++++++++++++++++++++++++-
3 files changed, 473 insertions(+), 75 deletions(-)native/tls.cmodified
#endif /* !_WIN32 *//* * tls-connect hostname port [connect-timeout-ms] -> tls-connection | #f * Establish a TLS connection to the specified host and port. * Returns a TLS connection object on success, #f on failure. * Handshake outcomes, shared by tls-connect and tls-upgrade. * * The optional connect-timeout-ms (a positive integer) bounds the TCP * connect phase via a non-blocking connect + select with try-next-address, * so a blackholed address can't hang on the OS SYN timeout. Omitted or <= 0 * keeps the original blocking mbedtls_net_connect (default behavior * unchanged). On Windows the timeout is ignored (blocking connect). * TIMEOUT is deliberately distinct from FAILED: "the peer never answered" * and "the peer refused us" are different facts, and a caller retries them * differently. Folding them together is what let a stalled handshake read * as an ordinary connection failure. */static Value native_tls_connect(SigilVM *vm, int argc, Value *args)#define SIGIL_TLS_HS_OK 0#define SIGIL_TLS_HS_TIMEOUT 1#define SIGIL_TLS_HS_FAILED 2/* * Run the TLS handshake on an already-connected conn->server_fd. * * With handshake_timeout_ms <= 0 (the default) this is byte-for-byte the * original blocking handshake: blocking mbedtls_net_recv as the BIO and a * loop that only retries on WANT_READ/WANT_WRITE. * * With a positive handshake_timeout_ms, handshake reads are driven through * mbedtls_net_recv_timeout, which selects on the socket and returns * MBEDTLS_ERR_SSL_TIMEOUT when nothing arrives. This is the segment that * connect-timeout-ms does NOT cover: once a peer has ACCEPTED the TCP * connection, the connect phase is over and the handshake read that follows * was previously unbounded. A peer that accepts and then never sends a * ServerHello wedged a production monitoring service for 55 days. * * The deadline is on the WHOLE handshake, not per read: each iteration * re-budgets the remaining time, so a peer dripping one byte just under the * interval cannot extend the wait indefinitely. * * The read timeout is unwound before returning, so an established * connection's subsequent tls-read calls behave exactly as before. Only the * handshake is bounded here. */static int sigil_tls_run_handshake(TlsConnectionData *conn, long handshake_timeout_ms){ (void)argc; int ret; if (!sigil_is_string(args[0])) { sigil__vm_error(vm, SIGIL_ERR_TYPE, "tls-connect: expected string for hostname"); return SIGIL_UNDEFINED;#ifndef _WIN32 if (handshake_timeout_ms > 0) { int outcome = SIGIL_TLS_HS_OK; long deadline = sigil_tls_now_ms() + handshake_timeout_ms; mbedtls_ssl_set_bio(&conn->ssl, &conn->server_fd, mbedtls_net_send, mbedtls_net_recv, mbedtls_net_recv_timeout); for (;;) { long remaining = deadline - sigil_tls_now_ms(); if (remaining <= 0) { outcome = SIGIL_TLS_HS_TIMEOUT; break; } mbedtls_ssl_conf_read_timeout(&conn->conf, (uint32_t)remaining); ret = mbedtls_ssl_handshake(&conn->ssl); if (ret == 0) { break; } if (ret == MBEDTLS_ERR_SSL_TIMEOUT) { outcome = SIGIL_TLS_HS_TIMEOUT; break; } if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) { outcome = SIGIL_TLS_HS_FAILED; break; } } /* Restore unbounded reads + the plain BIO for the session. */ mbedtls_ssl_conf_read_timeout(&conn->conf, 0); mbedtls_ssl_set_bio(&conn->ssl, &conn->server_fd, mbedtls_net_send, mbedtls_net_recv, NULL); return outcome; } if (!sigil_is_fixnum(args[1])) { sigil__vm_error(vm, SIGIL_ERR_TYPE, "tls-connect: expected integer for port"); return SIGIL_UNDEFINED;#else (void)handshake_timeout_ms;#endif mbedtls_ssl_set_bio(&conn->ssl, &conn->server_fd, mbedtls_net_send, mbedtls_net_recv, NULL); while ((ret = mbedtls_ssl_handshake(&conn->ssl)) != 0) { if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) { return SIGIL_TLS_HS_FAILED; } } return SIGIL_TLS_HS_OK;}/* * Build the (status . connection-or-#f) pair returned by the /status * variants. `status` is a short stable string, e.g. "connected", * "handshake-timeout". */static Value make_tls_status(SigilVM *vm, const char *status, Value conn){ Value status_val = sigil_make_string(vm, status, strlen(status)); sigil__gc_push_temp_root(vm, status_val); sigil__gc_push_temp_root(vm, conn); Value result = sigil_cons(vm, status_val, conn); sigil__gc_pop_temp_root(vm); sigil__gc_pop_temp_root(vm); return result;}/* * Core of tls-connect. Returns the connection value, or SIGIL_FALSE on * failure, and writes a short stable status string through *status_out * ("connected", "tcp-connect-failed", "handshake-timeout", ...). * * Both tls-connect (which discards the status) and tls-connect/status * (which surfaces it) run this, so the two can never drift apart. */static Value tls_connect_core(SigilVM *vm, int argc, Value *args, const char **status_out){ *status_out = "tls-init-failed"; if (ensure_tls_initialized() < 0) { return SIGIL_FALSE; connect_timeout_ms = (long)sigil_as_fixnum(args[2]); } /* Optional handshake timeout (milliseconds); <= 0 or absent = blocking. */ long handshake_timeout_ms = 0; if (argc >= 4 && sigil_is_fixnum(args[3])) { handshake_timeout_ms = (long)sigil_as_fixnum(args[3]); } /* Null-terminate hostname */ char *hostname = malloc(host_str->byte_length + 1); if (!hostname) return SIGIL_FALSE; conn->closed = 0; int ret; const char *error_stage = NULL; /* Initialize mbedTLS structures */ mbedtls_net_init(&conn->server_fd); MBEDTLS_NET_PROTO_TCP);#endif if (ret != 0) { error_stage = "TCP connect"; *status_out = "tcp-connect-failed"; goto cleanup_error; } MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT); if (ret != 0) { error_stage = "SSL config defaults"; *status_out = "ssl-config-failed"; goto cleanup_error; } /* Set up SSL context */ ret = mbedtls_ssl_setup(&conn->ssl, &conn->conf); if (ret != 0) { error_stage = "SSL setup"; *status_out = "ssl-setup-failed"; goto cleanup_error; } /* Set hostname for SNI (Server Name Indication) */ ret = mbedtls_ssl_set_hostname(&conn->ssl, hostname); if (ret != 0) { error_stage = "SSL set hostname"; *status_out = "ssl-set-hostname-failed"; goto cleanup_error; } /* Set I/O functions */ mbedtls_ssl_set_bio(&conn->ssl, &conn->server_fd, mbedtls_net_send, mbedtls_net_recv, NULL); /* Perform TLS handshake */ while ((ret = mbedtls_ssl_handshake(&conn->ssl)) != 0) { if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) { error_stage = "TLS handshake"; goto cleanup_error; } /* Perform TLS handshake (bounded when handshake_timeout_ms > 0). */ ret = sigil_tls_run_handshake(conn, handshake_timeout_ms); if (ret == SIGIL_TLS_HS_TIMEOUT) { *status_out = "handshake-timeout"; goto cleanup_error; } if (ret != SIGIL_TLS_HS_OK) { *status_out = "handshake-failed"; goto cleanup_error; } free(hostname); *status_out = "connected"; return make_tls_connection(vm, conn);cleanup_error: return SIGIL_FALSE;}/* Shared argument validation for tls-connect and tls-connect/status. * Returns 0 on success, -1 when a VM type error has been raised. */static int tls_connect_check_args(SigilVM *vm, Value *args, const char *who){ if (!sigil_is_string(args[0])) { char msg[96]; snprintf(msg, sizeof(msg), "%s: expected string for hostname", who); sigil__vm_error(vm, SIGIL_ERR_TYPE, msg); return -1; } if (!sigil_is_fixnum(args[1])) { char msg[96]; snprintf(msg, sizeof(msg), "%s: expected integer for port", who); sigil__vm_error(vm, SIGIL_ERR_TYPE, msg); return -1; } return 0;}/* * tls-connect hostname port [connect-timeout-ms [handshake-timeout-ms]] * -> tls-connection | #f * * Establish a TLS connection to the specified host and port. * Returns a TLS connection object on success, #f on failure. * * The optional connect-timeout-ms (a positive integer) bounds the TCP * connect phase via a non-blocking connect + select with try-next-address, * so a blackholed address can't hang on the OS SYN timeout. * * The optional handshake-timeout-ms bounds the TLS HANDSHAKE, which the * connect timeout does not reach: a peer that ACCEPTS the connection and * then never sends a ServerHello leaves the connect phase already complete * and the handshake read blocking forever. Both are omitted or <= 0 by * default, keeping the original blocking behavior. Ignored on Windows. * * `tls-connect` collapses every failure to #f. Use `tls-connect/status` * when you need to tell "the peer never answered" from "the peer refused". */static Value native_tls_connect(SigilVM *vm, int argc, Value *args){ const char *status = NULL; if (tls_connect_check_args(vm, args, "tls-connect") < 0) { return SIGIL_UNDEFINED; } return tls_connect_core(vm, argc, args, &status);}/* * tls-connect/status hostname port [connect-timeout-ms [handshake-timeout-ms]] * -> (status-string . tls-connection | #f) * * Same connection attempt as tls-connect, but reports WHY it failed: * * "connected" the pair's cdr is a live connection * "tcp-connect-failed" never reached the peer * "handshake-timeout" peer accepted, then went silent (bounded here) * "handshake-failed" peer rejected us (cert, version, alert) * "ssl-*-failed" local setup fault * * "handshake-timeout" is the case that has to stay distinguishable: a * caller retries a silent peer differently from one that refused it. */static Value native_tls_connect_status(SigilVM *vm, int argc, Value *args){ const char *status = "tls-init-failed"; if (tls_connect_check_args(vm, args, "tls-connect/status") < 0) { return SIGIL_UNDEFINED; } Value conn = tls_connect_core(vm, argc, args, &status); return make_tls_status(vm, status, conn);}/* * tls-read connection [max-bytes] -> string | #f | eof-object * Read data from TLS connection.}/* * tls-upgrade socket hostname -> tls-connection | #f * Upgrade an existing TCP socket to a TLS connection via STARTTLS. * Takes ownership of the socket's file descriptor. The original socket * object should not be used after this call. * Core of tls-upgrade, shared with tls-upgrade/status. Assumes the * arguments have already been validated. */static Value native_tls_upgrade(SigilVM *vm, int argc, Value *args)static Value tls_upgrade_core(SigilVM *vm, int argc, Value *args, const char **status_out){ (void)argc; /* Validate socket argument - must be a foreign object with "socket" type tag */ if (!sigil_is_foreign(args[0])) { sigil__vm_error(vm, SIGIL_ERR_TYPE, "tls-upgrade: expected socket"); return SIGIL_UNDEFINED; } Value socket_tag = sigil_intern_symbol(vm, "socket", 6); if (sigil_foreign_type(args[0]) != socket_tag) { sigil__vm_error(vm, SIGIL_ERR_TYPE, "tls-upgrade: expected socket"); return SIGIL_UNDEFINED; } if (!sigil_is_string(args[1])) { sigil__vm_error(vm, SIGIL_ERR_TYPE, "tls-upgrade: expected string for hostname"); return SIGIL_UNDEFINED; } *status_out = "tls-init-failed"; if (ensure_tls_initialized() < 0) { return SIGIL_FALSE; /* Mark the original socket as closed so it won't close the fd on GC */ socket_data[2] = 1; /* closed = 1 */ /* Optional handshake timeout (milliseconds); <= 0 or absent = blocking. */ long handshake_timeout_ms = 0; if (argc >= 3 && sigil_is_fixnum(args[2])) { handshake_timeout_ms = (long)sigil_as_fixnum(args[2]); } SigilString *host_str = (SigilString *)sigil_as_ptr(args[1]); /* Null-terminate hostname */ MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT); if (ret != 0) { *status_out = "ssl-config-failed"; goto cleanup_error; } /* Set up SSL context */ ret = mbedtls_ssl_setup(&conn->ssl, &conn->conf); if (ret != 0) { *status_out = "ssl-setup-failed"; goto cleanup_error; } /* Set hostname for SNI */ ret = mbedtls_ssl_set_hostname(&conn->ssl, hostname); if (ret != 0) { *status_out = "ssl-set-hostname-failed"; goto cleanup_error; } /* Set I/O functions */ mbedtls_ssl_set_bio(&conn->ssl, &conn->server_fd, mbedtls_net_send, mbedtls_net_recv, NULL); /* Perform TLS handshake on existing connection */ while ((ret = mbedtls_ssl_handshake(&conn->ssl)) != 0) { if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) { goto cleanup_error; } /* Perform TLS handshake on the existing connection (bounded when * handshake_timeout_ms > 0). An upgraded socket is ALREADY connected, * so every byte of its handshake sits past the connect phase — the * unbounded window here is strictly wider than tls-connect's. */ ret = sigil_tls_run_handshake(conn, handshake_timeout_ms); if (ret == SIGIL_TLS_HS_TIMEOUT) { *status_out = "handshake-timeout"; goto cleanup_error; } if (ret != SIGIL_TLS_HS_OK) { *status_out = "handshake-failed"; goto cleanup_error; } free(hostname); *status_out = "connected"; return make_tls_connection(vm, conn);cleanup_error: /* Don't call mbedtls_net_free - it would close the fd which we took ownership of */ conn->server_fd.fd = -1; /* Prevent double-close */ /* We took ownership of the fd (the socket object was marked closed * above), so we own closing it. Closing it directly rather than through * mbedtls_net_free keeps mbedTLS's own bookkeeping out of a path where * server_fd may never have been handed to it. */ if (conn->server_fd.fd >= 0) { close(conn->server_fd.fd); } conn->server_fd.fd = -1; /* Prevent double-close in mbedtls_net_free */ mbedtls_net_free(&conn->server_fd); mbedtls_ssl_free(&conn->ssl); mbedtls_ssl_config_free(&conn->conf); return SIGIL_FALSE;}/* Shared argument validation for tls-upgrade and tls-upgrade/status. * Returns 0 on success, -1 when a VM type error has been raised. */static int tls_upgrade_check_args(SigilVM *vm, Value *args, const char *who){ char msg[96]; if (!sigil_is_foreign(args[0])) { snprintf(msg, sizeof(msg), "%s: expected socket", who); sigil__vm_error(vm, SIGIL_ERR_TYPE, msg); return -1; } Value socket_tag = sigil_intern_symbol(vm, "socket", 6); if (sigil_foreign_type(args[0]) != socket_tag) { snprintf(msg, sizeof(msg), "%s: expected socket", who); sigil__vm_error(vm, SIGIL_ERR_TYPE, msg); return -1; } if (!sigil_is_string(args[1])) { snprintf(msg, sizeof(msg), "%s: expected string for hostname", who); sigil__vm_error(vm, SIGIL_ERR_TYPE, msg); return -1; } return 0;}/* * tls-upgrade socket hostname [handshake-timeout-ms] -> tls-connection | #f * Upgrade an existing TCP socket to a TLS connection via STARTTLS. * Takes ownership of the socket's file descriptor. The original socket * object should not be used after this call. * * The optional handshake-timeout-ms bounds the handshake. Omitted or <= 0 * keeps the original blocking handshake. Ignored on Windows. */static Value native_tls_upgrade(SigilVM *vm, int argc, Value *args){ const char *status = NULL; if (tls_upgrade_check_args(vm, args, "tls-upgrade") < 0) { return SIGIL_UNDEFINED; } return tls_upgrade_core(vm, argc, args, &status);}/* * tls-upgrade/status socket hostname [handshake-timeout-ms] * -> (status-string . tls-connection | #f) * * Same upgrade as tls-upgrade, reporting why it failed. Statuses match * tls-connect/status, minus the connect-phase ones. */static Value native_tls_upgrade_status(SigilVM *vm, int argc, Value *args){ const char *status = "tls-init-failed"; if (tls_upgrade_check_args(vm, args, "tls-upgrade/status") < 0) { return SIGIL_UNDEFINED; } Value conn = tls_upgrade_core(vm, argc, args, &status); return make_tls_status(vm, status, conn);}/* * Hook implementations for TLS integration with socket layer. * These are registered with sigil-lib during module init. /* Connection operations */ REGISTER_AND_EXPORT("tls-connect", native_tls_connect, SIGIL_ARITY_RANGE(2, 3), "Connect to TLS server"); SIGIL_ARITY_RANGE(2, 4), "Connect to TLS server"); REGISTER_AND_EXPORT("tls-connect/status", native_tls_connect_status,Showing the first 500 of 517 diff lines for this file. This diff is INCOMPLETE; read the file or clone the repository for the rest.
src/sigil/tls.sglmodified
(export tls-connection? tls-connect tls-connect/status tls-read tls-read-bytevector tls-write tls-close tls-closed? tls-set-non-blocking! tls-upgrade) tls-upgrade tls-upgrade/status) (begin ;;; the OS SYN-retransmit timeout. Omitted or <= 0 keeps the original ;;; blocking connect (default behavior unchanged). Ignored on Windows. ;;; ;;; A further optional `handshake-timeout-ms` bounds the TLS HANDSHAKE. ;;; The connect timeout does not reach it: once a peer has ACCEPTED the ;;; TCP connection the connect phase is over, and a peer that then never ;;; sends a ServerHello previously blocked the handshake read forever. ;;; Under Sigil's cooperative scheduler that freezes the whole process, ;;; so this is the difference between a failed request and a dead ;;; service. The bound covers the whole handshake, not each read. ;;; Omitted or <= 0 keeps the original blocking handshake. ;;; ;;; ```scheme ;;; (tls-connect "example.com" 443) ; => tls-connection | #f ;;; (tls-connect "example.com" 443 10000) ; 10s connect timeout ;;; (tls-connect "example.com" 443) ; => tls-connection | #f ;;; (tls-connect "example.com" 443 10000) ; 10s connect timeout ;;; (tls-connect "example.com" 443 10000 10000) ; + 10s handshake timeout ;;; ``` (define-native (tls-connect hostname port . connect-timeout-ms) (define-native (tls-connect hostname port . timeouts-ms) (: string? integer? -> any?)) ;;; Like `tls-connect`, but returns `(status . connection-or-#f)` so a ;;; caller can tell WHY the attempt failed instead of seeing one flat #f. ;;; ;;; Statuses: `"connected"`, `"tcp-connect-failed"`, ;;; `"handshake-timeout"`, `"handshake-failed"`, `"ssl-config-failed"`, ;;; `"ssl-setup-failed"`, `"ssl-set-hostname-failed"`, ;;; `"tls-init-failed"`. ;;; ;;; `"handshake-timeout"` (the peer accepted and then went silent) and ;;; `"handshake-failed"` (the peer rejected us) are different facts and ;;; are retried differently, so they are kept apart. ;;; ;;; ```scheme ;;; (let ((r (tls-connect/status "example.com" 443 10000 10000))) ;;; (if (cdr r) (use-connection (cdr r)) (log-failure (car r)))) ;;; ``` (define-native (tls-connect/status hostname port . timeouts-ms) (: string? integer? -> pair?)) ;;; Read data from a TLS connection. ;;; ;;; Returns a string with data, #f on error, or eof-object if the ;;; Takes ownership of the socket's file descriptor; the original socket ;;; should not be used after this call. ;;; ;;; The optional `handshake-timeout-ms` bounds the handshake. An upgraded ;;; socket is already connected, so ALL of its handshake sits past the ;;; connect phase — the unbounded window here is strictly wider than ;;; `tls-connect`'s. Omitted or <= 0 keeps the original blocking ;;; handshake. Ignored on Windows. ;;; ;;; ```scheme ;;; (tls-upgrade sock "mail.example.com") ; => tls-connection | #f ;;; (tls-upgrade sock "mail.example.com") ; => tls-connection | #f ;;; (tls-upgrade sock "mail.example.com" 10000) ; 10s handshake bound ;;; ``` (define-native (tls-upgrade socket hostname) (: any? string? -> any?)))) (define-native (tls-upgrade socket hostname . handshake-timeout-ms) (: any? string? -> any?)) ;;; Like `tls-upgrade`, but returns `(status . connection-or-#f)`. ;;; Statuses match `tls-connect/status` minus the connect-phase ones. (define-native (tls-upgrade/status socket hostname . handshake-timeout-ms) (: any? string? -> pair?))))test/test-tls.sglmodified
(import (sigil test) (sigil tls) (sigil socket)) (sigil socket) (sigil time) (only (sigil math) inexact));; ============================================================;; Type predicates (test "tls-close with non-connection raises error" (assert-error (tls-close "not-a-connection"))));; ============================================================;; Handshake timeout;; ============================================================;;;; THE STALL RIG. `tcp-listen` and then NEVER `tcp-accept`: the kernel;; completes the TCP three-way handshake from the listen backlog, so the;; client's connect() SUCCEEDS and the socket reaches ESTABLISHED. The;; client then blocks reading a ServerHello that will never arrive.;;;; That is the shape that wedged a production monitoring service for 55;; days, and the reason it matters here: because connect() has already;; succeeded, `connect-timeout-ms` provably CANNOT fire against this rig.;; Any bound observed below therefore comes from the handshake timeout and;; from nothing else.(define (elapsed-seconds thunk) (let* ((t0 (current-jiffy)) (result (thunk)) (dt (/ (inexact (- (current-jiffy) t0)) (jiffies-per-second)))) (cons dt result)))(define (stall-port sock) (cadr (socket-local-address sock)))(test-group "handshake timeout" ;; POSITIVE CONTROL for the status API: it can report a NON-timeout ;; status. Without this, "handshake-timeout" below could just be the only ;; string the function ever produces. (test "tls-connect/status reports tcp-connect-failed for a closed port" (let ((r (tls-connect/status "127.0.0.1" 1 1000 1000))) (assert-equal "tcp-connect-failed" (car r)) (assert-false (cdr r)))) (test "tls-connect/status reports connected only with a live peer" ;; Guards the other direction: "connected" is not produced by a failure. (let ((r (tls-connect/status "127.0.0.1" 1 1000 1000))) (assert-false (equal? "connected" (car r))))) (test "handshake timeout bounds an accept-and-stall peer" (let* ((server (tcp-listen 0)) (port (stall-port server))) (let* ((measured (elapsed-seconds (lambda () (tls-connect "127.0.0.1" port 1000 1000)))) (dt (car measured)) (conn (cdr measured))) (socket-close server) ;; It must FAIL (the peer never completed a handshake) ... (assert-false conn) ;; ... and it must fail on roughly the deadline, not merely ;; eventually. Pre-fix this call does not return at all; the upper ;; bound is what makes this a timeout assertion rather than an ;; "an error appeared" assertion. (assert-true (>= dt 0.5)) (assert-true (<= dt 5.0))))) (test "handshake timeout is reported as handshake-timeout, not a generic failure" (let* ((server (tcp-listen 0)) (port (stall-port server)) (r (tls-connect/status "127.0.0.1" port 1000 1000))) (socket-close server) (assert-equal "handshake-timeout" (car r)) (assert-false (cdr r)))) (test "the deadline scales with the argument" ;; A bound that ignores its argument would satisfy the test above. ;; Two different deadlines must produce two different waits. (let* ((server (tcp-listen 0)) (port (stall-port server)) (short (car (elapsed-seconds (lambda () (tls-connect "127.0.0.1" port 1000 300))))) (long (car (elapsed-seconds (lambda () (tls-connect "127.0.0.1" port 1000 2000)))))) (socket-close server) (assert-true (< short 1.0)) (assert-true (> long 1.5)) (assert-true (> long short)))) (test "tls-upgrade honours a handshake timeout on an already-connected socket" ;; tls-upgrade's handshake is strictly MORE exposed than tls-connect's: ;; the socket is connected before the call, so every handshake byte is ;; past the connect phase. Bound it on its own rig. (let* ((server (tcp-listen 0)) (port (stall-port server)) (client (tcp-connect "127.0.0.1" port))) (assert-true (socket? client)) (let* ((measured (elapsed-seconds (lambda () (tls-upgrade client "localhost" 1000)))) (dt (car measured)) (conn (cdr measured))) (socket-close server) (assert-false conn) (assert-true (>= dt 0.5)) (assert-true (<= dt 5.0))))) (test "tls-upgrade/status reports handshake-timeout" (let* ((server (tcp-listen 0)) (port (stall-port server)) (client (tcp-connect "127.0.0.1" port)) (r (tls-upgrade/status client "localhost" 1000))) (socket-close server) (assert-equal "handshake-timeout" (car r)) (assert-false (cdr r)))))(run-tests)