AtlatestRepositorysigil-tls
1
;;; (sigil tls) - TLS/SSL Connections2
;;;3
;;; Secure TCP connections using mbedTLS. Supports TLS 1.2 and TLS 1.3 client4
;;; connections with system CA certificate verification.5
;;;6
;;; ```scheme7
;;; (import (sigil tls))8
;;;9
;;; (let ((conn (tls-connect "example.com" 443)))10
;;; (tls-write conn "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")11
;;; (display (tls-read conn))12
;;; (tls-close conn))13
;;; ```15
(define-library (sigil tls)16
(export17
tls-connection?18
tls-connect19
tls-connect/status20
tls-connect/details21
tls-read22
tls-read-bytevector23
tls-write24
tls-close25
tls-closed?26
tls-set-non-blocking!27
tls-upgrade28
tls-upgrade/status29
tls-upgrade/details)31
(begin33
;;; Check if value is a TLS connection object.34
(define-native (tls-connection? value)35
(: any? -> boolean?))37
;;; Establish a TLS connection to the specified host and port.38
;;;39
;;; Returns a TLS connection object on success, #f on failure.40
;;; Certificates are verified against system CA certificates by default.41
;;; Set SIGIL_TLS_INSECURE=1 to skip verification (testing only).42
;;;43
;;; An optional `connect-timeout-ms` (positive integer milliseconds)44
;;; bounds the TCP connect phase: the underlying connect is made45
;;; non-blocking and each resolved address is tried with `select`46
;;; under a shared deadline, so a blackholed address cannot hang on47
;;; the OS SYN-retransmit timeout. Omitted or <= 0 keeps the original48
;;; blocking connect (default behavior unchanged). Ignored on Windows.49
;;;50
;;; A further optional `handshake-timeout-ms` bounds the TLS HANDSHAKE.51
;;; The connect timeout does not reach it: once a peer has ACCEPTED the52
;;; TCP connection the connect phase is over, and a peer that then never53
;;; sends a ServerHello previously blocked the handshake read forever.54
;;; Under Sigil's cooperative scheduler that freezes the whole process,55
;;; so this is the difference between a failed request and a dead56
;;; service.57
;;;58
;;; The bound is a TOTAL over the handshake, not a per-read one. That59
;;; distinction is the whole value: a per-read bound is defeated by a60
;;; peer that DRIPS rather than one that goes silent, because mbedTLS61
;;; restarts its read timeout on every partial read. Measured against an62
;;; earlier build, one byte every 600 ms made a 1000 ms per-read bound63
;;; take 13 s to fire, and never fire at all while the drip continued.64
;;;65
;;; Omitted or <= 0 keeps the original blocking handshake.66
;;;67
;;; ```scheme68
;;; (tls-connect "example.com" 443) ; => tls-connection | #f69
;;; (tls-connect "example.com" 443 10000) ; 10s connect timeout70
;;; (tls-connect "example.com" 443 10000 10000) ; + 10s handshake timeout71
;;; ```72
(define-native (tls-connect hostname port . timeouts-ms)73
(: string? integer? -> any?))75
;;; Like `tls-connect`, but returns `(status . connection-or-#f)` so a76
;;; caller can tell WHY the attempt failed instead of seeing one flat #f.77
;;;78
;;; Statuses: `"connected"`, `"tcp-connect-failed"`,79
;;; `"handshake-timeout"`, `"handshake-failed"`, `"ssl-config-failed"`,80
;;; `"ssl-setup-failed"`, `"ssl-set-hostname-failed"`,81
;;; `"tls-init-failed"`.82
;;;83
;;; `"handshake-timeout"` (the peer accepted and then went silent) and84
;;; `"handshake-failed"` (the peer rejected us) are different facts and85
;;; are retried differently, so they are kept apart.86
;;;87
;;; ```scheme88
;;; (let ((r (tls-connect/status "example.com" 443 10000 10000)))89
;;; (if (cdr r) (use-connection (cdr r)) (log-failure (car r))))90
;;; ```91
(define-native (tls-connect/status hostname port . timeouts-ms)92
(: string? integer? -> pair?))94
;;; Connect with per-attempt handshake diagnostics.95
;;;96
;;; Takes the same arguments and deadlines as tls-connect/status.97
;;; Returns a dictionary with status:, connection:, error-code:,98
;;; error-message:, handshake-state:, protocol:, cipher:, verify-flags:.99
;;; The connection is #f on failure; close a successful one with tls-close.100
;;; Error codes/messages are mbedTLS handshake errors, with 0/#f when no101
;;; handshake error is available. Consult status: for pre-handshake failures.102
;;; Handshake state is #f before a handshake, otherwise a diagnostic state103
;;; name (not a stable application error category). Protocol and cipher are104
;;; populated only after success. Verify flags are mbedTLS X.509 flags;105
;;; they do not establish verification when insecure mode was requested.106
;;; Each result owns its details; later attempts cannot overwrite it.107
(define-native (tls-connect/details hostname port . timeouts-ms)108
(: string? integer? -> dict?))110
;;; Read data from a TLS connection.111
;;;112
;;; Returns a string with data, #f on error, or eof-object if the113
;;; connection was closed by the peer. An optional max-bytes argument114
;;; controls the buffer size (default 4096).115
;;;116
;;; ```scheme117
;;; (tls-read conn) ; => string | #f | eof-object118
;;; (tls-read conn 8192) ; read up to 8192 bytes119
;;; ```120
(define-native (tls-read connection . max-bytes)121
(: any? -> any?))123
;;; Read raw bytes from a TLS connection into a bytevector.124
;;;125
;;; Like `tls-read` but returns a bytevector instead of a string,126
;;; preserving raw bytes without encoding interpretation.127
;;;128
;;; ```scheme129
;;; (tls-read-bytevector conn) ; => bytevector | #f | eof-object130
;;; (tls-read-bytevector conn 8192) ; read up to 8192 bytes131
;;; ```132
(define-native (tls-read-bytevector connection . max-bytes)133
(: any? -> any?))135
;;; Write data to a TLS connection.136
;;;137
;;; Accepts a string or bytevector. Returns the number of bytes138
;;; written, or #f on error.139
;;;140
;;; On a BLOCKING connection (the default) this writes the whole buffer141
;;; before returning, exactly as before.142
;;;143
;;; On a NON-BLOCKING connection (see `tls-set-non-blocking!`) it returns144
;;; the number of bytes actually committed, which may be `0`, so a caller145
;;; can poll against a deadline. Previously a non-blocking connection made146
;;; this spin in a hot loop that never returned, so a peer that stopped147
;;; reading could pin a CPU forever. A `0` return means retry the SAME148
;;; slice; a positive return means those bytes are committed and the next149
;;; call should start after them.150
;;;151
;;; The optional `start` and `end` byte offsets let a write loop resend152
;;; the tail of a buffer without copying it, keeping a large body O(n)153
;;; rather than O(n^2). This mirrors `socket-write`.154
;;;155
;;; ```scheme156
;;; (tls-write conn "GET / HTTP/1.1\r\n\r\n") ; => integer | #f157
;;; (tls-write conn body 4096) ; write from byte 4096 on158
;;; ```159
(define-native (tls-write connection data . start-end)160
(: any? (any-of string? bytevector?) -> (any-of integer? boolean?)))162
;;; Close a TLS connection.163
;;;164
;;; Sends a close notification and frees resources. Returns #t.165
(define-native (tls-close connection)166
(: any? -> boolean?))168
;;; Check if a TLS connection is closed.169
(define-native (tls-closed? connection)170
(: any? -> boolean?))172
;;; Set the underlying socket to non-blocking mode.173
;;;174
;;; Enable defaults to #t if not provided. Returns #t on success.175
;;;176
;;; ```scheme177
;;; (tls-set-non-blocking! conn) ; enable non-blocking178
;;; (tls-set-non-blocking! conn #f) ; disable non-blocking179
;;; ```180
(define-native (tls-set-non-blocking! connection . enable)181
(: any? -> boolean?))183
;;; Upgrade an existing TCP socket to a TLS connection.184
;;;185
;;; Performs a TLS handshake on an existing socket connection (STARTTLS).186
;;; Takes ownership of the socket's file descriptor; the original socket187
;;; should not be used after this call.188
;;;189
;;; The optional `handshake-timeout-ms` bounds the handshake. An upgraded190
;;; socket is already connected, so ALL of its handshake sits past the191
;;; connect phase — the unbounded window here is strictly wider than192
;;; `tls-connect`'s. Omitted or <= 0 keeps the original blocking193
;;; handshake. Ignored on Windows.194
;;;195
;;; ```scheme196
;;; (tls-upgrade sock "mail.example.com") ; => tls-connection | #f197
;;; (tls-upgrade sock "mail.example.com" 10000) ; 10s handshake bound198
;;; ```199
(define-native (tls-upgrade socket hostname . handshake-timeout-ms)200
(: any? string? -> any?))202
;;; Like `tls-upgrade`, but returns `(status . connection-or-#f)`.203
;;; Statuses match `tls-connect/status` minus the connect-phase ones.204
(define-native (tls-upgrade/status socket hostname . handshake-timeout-ms)205
(: any? string? -> pair?))207
;;; Upgrade an existing socket, returning the same diagnostic dictionary208
;;; as tls-connect/details. Takes the same arguments as tls-upgrade/status209
;;; and owns the socket even when the handshake fails.210
(define-native (tls-upgrade/details socket hostname . handshake-timeout-ms)211
(: any? string? -> dict?))))