Add TLS support (upgrade-after-INFO) - #57
Conversation
Adds client TLS so the driver can connect to TLS-required servers such as
Synadia NGS (tls://connect.ngs.global). Previously the client only spoke
plaintext (CONNECT always sent tls_required:false) and any TLS-required
server reset the connection.
Design:
- Config: new optional `?ClientTlsContext $tls` (null = current plaintext
behavior, fully backward compatible). `fromURI` enables TLS from a
tls:// / nats+tls:// / ssl:// scheme (peer name defaults to the host for
SNI/verification); `fromArray` accepts a `tls` key.
- SocketConnectionFactory: attaches the TLS context to the ConnectContext so
the socket can be upgraded (does NOT connect TLS-first).
- Framer: when TLS is configured, the single reader fiber reads the plaintext
INFO line, runs setupTls(), THEN pushes INFO to the parser. Doing the
upgrade inside the sole socket-reader fiber preserves the single-reader
invariant (amphp forbids concurrent reads); pushing INFO only after the
handshake completes prevents startup() from writing CONNECT mid-handshake.
- SocketConnection::startup() is unchanged (no TLS logic).
Scope/limitation: implements the standard NATS upgrade-after-INFO flow
(server sends plaintext INFO advertising tls_required, client then upgrades).
Servers configured with `tls { handshake_first: true }` are not covered.
Non-TLS code path is byte-for-byte unchanged; existing tests unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- fromURI tls://, nats+tls://, ssl:// -> ClientTlsContext with host as peer name - non-tls scheme leaves tls null (backward compat) - fromArray accepts a tls key Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| $scheme = strtolower($components['scheme'] ?? ''); | ||
| if (($scheme === 'tls' || $scheme === 'nats+tls' || $scheme === 'ssl') && ($components['host'] ?? '') !== '') { | ||
| $firstHost = explode(':', explode(',', $components['host'])[0])[0]; | ||
| $tls = (new \Amp\Socket\ClientTlsContext($firstHost)); |
There was a problem hiding this comment.
| $tls = (new \Amp\Socket\ClientTlsContext($firstHost)); | |
| $tls = new ClientTlsContext($firstHost); |
There was a problem hiding this comment.
Done imported it. Also caught a third FQCN in the fromArray docblock.
| // set, the connection upgrades to TLS after the server's plaintext INFO | ||
| // (the standard NATS tls_required flow, e.g. Synadia NGS). The upgrade is | ||
| // performed by the socket's single reader fiber — see Framer. | ||
| public ?\Amp\Socket\ClientTlsContext $tls = null, |
There was a problem hiding this comment.
| public ?\Amp\Socket\ClientTlsContext $tls = null, | |
| public ?ClientTlsContext $tls = null, |
| // SBC firmware fetcher) can't read an object whose digest we wrote | ||
| // unpadded. base64encode() strips padding (correct for subject/name | ||
| // segments), so pad the digest explicitly here. | ||
| $encodedDigest = strtr(base64_encode($digest->finish()), '+/', '-_'); |
There was a problem hiding this comment.
These changes should be made in another PR.
| */ | ||
| public function startup(): void | ||
| { | ||
| // The TLS upgrade (when configured) happens inside the Framer's reader |
There was a problem hiding this comment.
Unnecessary AI comment, imho.
- import ClientTlsContext instead of using the FQCN inline - drop the explanatory comment in SocketConnection::startup() Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
23b85d6 to
67cf895
Compare
Co-authored-by: Vadim Zanfir <vadimzanfir@gmail.com>
The accepted suggestion was authored against the pre-import file and added a second identical use statement, which is a fatal error. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
| // below reads the encrypted stream exactly as normal. | ||
| if ($upgradeTls) { | ||
| $preamble = ''; | ||
| while (!str_contains($preamble, "\r\n")) { |
There was a problem hiding this comment.
The read loop stops as soon as $preamble contains \r\n, but nothing guarantees a single read() ends exactly at the delimiter: TCP can hand us more than the INFO line. If any bytes land after the first \r\n, we push them to the parser as plaintext (which then tries to parse post-upgrade bytes as a NATS frame) and setupTls() never sees them. Should we guard this explicitly (fail if there's anything after the first \r\n) rather than rely on it silently? Or is there a reason the tail can't happen that I'm missing?
|
@ChrisFrizza6969, run |
Add TLS support (upgrade-after-INFO)
Summary
Adds client TLS so the driver can connect to TLS-required servers such as
Synadia NGS (
tls://connect.ngs.global). Currentlythe client only speaks plaintext — it always sends
CONNECTwithtls_required: falseand never performs a TLS handshake — so any server thatadvertises
tls_required: trueresets the connection.Motivation
NGS (and any NATS server with
tls: {...}) sends a plaintextINFOadvertisingtls_required: true, then expects the client to run the TLS handshake beforeCONNECT. Without TLS there is no way to use this driver against those servers.What changed
Config: new optional?Amp\Socket\ClientTlsContext $tls(defaultnull= existing plaintext behavior, fully backward compatible).
fromURIenablesTLS from a
tls:///nats+tls:///ssl://scheme, defaulting the peer nameto the host for SNI/verification;
fromArrayaccepts atlskey.SocketConnectionFactory: attaches the TLS context to theConnectContextso the socket can be upgraded (it does not connect TLS-first).
Framer: when TLS is configured, the single reader fiber reads theplaintext
INFOline, runssetupTls(), and then pushesINFOto theparser. Doing the upgrade inside the sole socket-reader fiber preserves the
single-reader invariant (amphp forbids concurrent reads on a socket); pushing
INFOonly after the handshake completes preventsstartup()(a differentfiber) from writing
CONNECTmid-handshake and corrupting the stream.SocketConnection::startup()is unchanged (no TLS logic).Compatibility
The non-TLS code path is byte-for-byte unchanged: when
$tlsisnull,$upgradeTlsisfalseand theFramerbehaves exactly as before. Existingtests are unaffected.
Scope / limitation
Implements the standard NATS upgrade-after-
INFOflow. Servers configured withtls { handshake_first: true }(TLS handshake beforeINFO) are not covered bythis PR.
Tests
ConfigTestcases:tls:///nats+tls:///ssl://produce aClientTlsContextwith the host as peer name; a non-TLS scheme leavestlsnull;
fromArrayaccepts atlskey.php-cs-fixer(no diff),phpunitConfigTestgreen,phpstanlevel max / strict clean on the changed files.INFO→setupTls()→CONNECT→PONG, plus JetStream object store list/get/putover the resulting TLS connection.