Implements StateTransferable<SMB2TransferState> and SMB2Client.adopt()
per issue #99. An idle client detaches and exports the parked-transport
token plus the protocol state needed to continue the authenticated
session: dialect, negotiated sizes, message-id/credit counters, session
and tree ids, preauth hash, and the derived signing key. Credentials are
never serialized; the adopter supplies them and may opt into reconnect
fallback, which never reuses transferred signing state on a new dial.
Transfer rejects unless quiescent (no request in flight, no pending
connect, no stream owning a file handle).
Closes#99
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Channel.send() no longer deadlocks when the peer closes while the
sender is window-blocked: _deliverClose wakes #windowWaiters and send()
aborts on a remotely-closed channel.
- pipe()'s copy loop guards writes (not just reads) and both call sites
swallow, so a write failure on a forwarded socket can't become an
unhandledRejection that crashes the process.
- Connect-phase failures close the transport/connection instead of
leaking the socket (SSHClientConnection.connect on auth failure;
SFTPClient.#doConnect on openSubsystem/init failure).
- authenticateServer accepts a falsy auth context; #matchForward fails
closed on an ambiguous port; acceptSession() rejects once the accept
loop dies; duplicate tcpip-forward closes the superseded listener;
RemoteForward teardown closes queued transports.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Key remote forwards by bind host and port (not port alone) so two
forwards sharing a port stay distinct; guard the accept loops against a
failing CHANNEL_OPEN_CONFIRMATION send (close the upstream, keep the loop
alive); reject pending RemoteForward.accept() waiters when the connection
closes or its mux fails.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Env-gated (SSH_TEST_*) suite exercising direct-tcpip and a remote-forward
loop against a real sshd; verified against a local OpenSSH server.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SFTPClient and the server Session now use SSHClientConnection/
SSHServerConnection instead of driving the transport, auth, and mux
directly; behaviour is unchanged and the full suite passes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SSHClientConnection/SSHServerConnection wrap the transport, auth, and
channel mux; expose direct-tcpip local forwarding (dialer()) and
tcpip-forward remote forwarding (RemoteForward as a RawListener).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
channelTransport() wraps a Channel as a @webnet/transport RawTransport
(EOF as throw + readEnded, halfClose as CHANNEL_EOF); pipe() bridges two
transports for onward forwarding.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ConnectionMux gains configurable accept types and a global-request
handler; adds direct-tcpip/forwarded-tcpip channel opens, tcpip-forward
global requests, and closed/readEnded/whenClosed on Channel.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
authenticateServer is now generic over an authentication context and
auth failures throw SSHAuthError; @webnet/sftp maps those back to
VFSError("forbidden") at its boundary.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pure file moves of the transport, kex, cipher, auth, and channel
modules out of @webnet/sftp into a standalone package; @webnet/sftp
now consumes them via @webnet/ssh and @webnet/ssh/_internals.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
From the neutral review: the pipelined reader advanced offsets by the
requested length, so a spec-legal short mid-file READ (pipes, special files,
some non-OpenSSH servers) left an un-requested gap and silently delivered a
corrupt, short file. Track each requests offset and requested size, and on a
short non-EOF read reset to just past the returned bytes and re-issue,
discarding the now-misaligned in-flight requests (with a catch so they never
become unhandled rejections). Also cancel the source stream with the write
error on the writeFile error path so the producer is signalled to stop.
Regression test drives a server that short-reads mid-file (verified failing
without the fix). 203 tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3bV2ANs8WMHxefD5pF9LN
From the autonomous review of the package:
- server/session: wrap the whole session (auth, channel accept, subsystem
request) in try/finally so a malformed post-handshake packet closes the
transport instead of leaking it and its read loop.
- server/handles: WriteQueue.fail() releases a writer blocked on backpressure,
and a failed vfs.writeFile now fails the queue, so a WRITE stuck behind a
broken VFS consumer rejects instead of orphaning the request and its handle
mutex forever.
- ssh/channel: enforce the advertised receive window, failing the channel if a
peer sends more CHANNEL_DATA than the credit granted (bounded buffering).
- sftp/packets: reject a zero-length SFTP packet instead of reading a byte past
the validated length.
Adds regression tests for each. 202 tests; interop with the OpenSSH sftp CLI
re-verified.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3bV2ANs8WMHxefD5pF9LN
Export SFTPClient/SFTPServer and their option types from the package root
and subpath barrels. Add the real client-against-real-server loopback suite
(18 tests over encrypted transport: round-trips incl. a 4 MiB transfer,
ranged reads, recursive delete, posix-rename overwrite, utf8 names,
pipelined concurrency, cancel-then-continue, password/publickey/per-user
auth, and host-key verification) plus an env-gated suite against a real
OpenSSH sshd. Interop-tested against the OpenSSH sftp CLI driving our server,
which surfaced one fix: acceptSubsystem now denies benign pre-subsystem
channel requests (the CLI env request) with CHANNEL_FAILURE and keeps waiting
for the subsystem request instead of tearing the session down.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3bV2ANs8WMHxefD5pF9LN
SFTPServer.listen runs an accept loop mirroring the FTP server; each
connection runs the SSH server transport, authenticates via the pluggable
callback (per-user AsyncVFS), accepts the sftp subsystem channel, and serves
an FXP dispatch loop. Handlers map OPEN/READ/WRITE/CLOSE, OPENDIR/READDIR
(100 entries per batch with ls-style longnames), STAT/LSTAT/FSTAT,
REMOVE/RMDIR/MKDIR, RENAME (v3 fail-on-exists) and posix-rename@openssh.com
overwrite, and REALPATH onto the VFS; SETSTAT/FSETSTAT are accepted as no-ops
so OpenSSH put succeeds. Reads stream from the VFS with per-handle
serialization; writes stream sequentially into vfs.writeFile with a
backpressured queue and reject non-sequential offsets. Responses are
serialized on one send chain so split DATA frames never interleave, and a
handler error replies a status without tearing down the session. 17 tests
against an in-suite mock client.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3bV2ANs8WMHxefD5pF9LN
SFTPClient dials through a RawDialer, runs the SSH transport/auth/channel
stack, opens the sftp subsystem, and maps AsyncVFS verbs onto SFTP v3
operations. SftpSession pipelines requests over one channel with a
request-id dispatch map, serializing only the wire writes so a WRITE packet
that splits across channel frames stays contiguous. readFile/readFileRange
return pull-driven ReadableStreams keeping up to 8 outstanding 32 KiB READs
at sequential offsets; writeFile pumps with the same in-flight bound and
fails fast. delete recurses client-side; move uses posix-rename@openssh.com
for overwrite when advertised. 12 tests against an in-suite mock server incl.
a 4 MiB round-trip, ranged reads, concurrent ops, and cancel-then-continue.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3bV2ANs8WMHxefD5pF9LN
Client and server halves of ssh-userauth: client tries a direct signed
Ed25519 publickey request then falls back to password; server drives the
none/publickey(PK_OK)/password flow through a pluggable authenticate
callback returning a per-user AsyncVFS, capping failed attempts. ConnectionMux
runs the post-auth dispatch loop and Channel implements the session channel
with the sftp subsystem request and bidirectional window flow control:
outbound sends block on WINDOW_ADJUST, inbound window is replenished only as
the consumer reads, giving end-to-end backpressure bounded at the 1 MiB
window. 15 tests incl. a 4 MiB windowed transfer and stalled-reader
backpressure.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3bV2ANs8WMHxefD5pF9LN
Role-shared SSHTransport over RawTransport: version-banner exchange with
pre-banner-line tolerance, binary packet framing with per-direction sequence
numbers, curve25519-sha256 key exchange with Ed25519/RSA host-key
verification, RFC 4253 key derivation, and transparent peer- or
self-initiated rekey via a send gate. Data-driven PacketCipher covers
aes128/256-gcm@openssh.com, aes128/256-ctr with hmac-sha2-256 and
hmac-sha2-256-etm@openssh.com; continuous CTR counter and GCM invocation
nonce tracked across packets. 39 tests incl. a 1000-packet encrypted echo
and mid-stream rekey over loopback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3bV2ANs8WMHxefD5pF9LN
New @webnet/sftp package skeleton: big-endian SSH cursor (string/nameList/
mpint), SSH message constants, openssh-key-v1 Ed25519 key parse/serialize
with PKCS#8 seed wrapping and generateHostKey(), host-key/signature blob
codecs with Ed25519/RSA verification and SHA256 fingerprints, SFTP v3
packet/ATTRS codecs with Stat mapping and packet reassembly, SSH_FX status
to VFSError mapping, unix ls longname formatter, and lexical path helpers.
95 unit tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3bV2ANs8WMHxefD5pF9LN
Reflect the final state in AI_CHANGES.md: the client verifies inbound
response signatures (not just signs requests), advertises no cipher, and
the mock server does a real signed round-trip with tamper/unsigned
rejection tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014AcGBx7EjVaByiHcBcsSfP
Address the second-pass review of PR #60:
- Verify inbound signatures. Once a signed session is established, the
client now requires every response to carry the SIGNED flag and a valid
signature (recomputed over the message with the signature field zeroed,
compared constant-time), rejecting otherwise, per [MS-SMB2] 3.2.5.1.3 —
signing requests without verifying responses left signed sessions open
to response tampering/injection. Also sanity-check the response
message id. The mock server now derives the real session/signing key and
signs its responses, so the end-to-end tests exercise the signed
round-trip, with new tests asserting tampered and unsigned responses are
rejected.
- Don't cache a rejected connect: a transient dial/auth failure no longer
wedges every later operation until disconnect().
- Map connect-time Smb2Error to VFSError in #doConnect, so LOGON_FAILURE /
BAD_NETWORK_NAME / ACCOUNT_DISABLED surface as proper VFSErrors.
- writeFile cancels the source stream on a mid-stream error.
- parseFileIdBothDirInfo returns [] on an empty buffer instead of throwing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014AcGBx7EjVaByiHcBcsSfP
Address autonomous review of PR #60:
- NEGOTIATE no longer advertises the ENCRYPTION capability or any cipher,
so a server never selects encryption. A share that mandates encryption
now fails cleanly at TREE_CONNECT (ACCESS_DENIED -> forbidden) instead of
sending a transform header the client cannot decrypt. The encryption
negotiate context is omitted entirely when no cipher is offered.
- Guard against a received SMB2 TRANSFORM (encrypted) message with a clear
error rather than an opaque "bad SMB2 magic" parse failure.
- deriveSigningKey now handles only the dialects actually offered
(SMB 3.1.1 via SP800-108, <=2.1 via the session key) and throws for any
other 3.x dialect instead of silently using the 3.1.1 label.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014AcGBx7EjVaByiHcBcsSfP
Add @webnet/smb2, an SMB2/3 client that runs over the existing transport
abstraction and exposes a remote Windows/Samba share as an AsyncVFS, the
same way @webnet/drive exposes WebDAV. Targets default Windows 10/11
(including 24H2 mandatory signing) and supported Samba, negotiating
dialects {2.0.2, 2.1, 3.1.1}.
Runs unchanged in browser and Node with no Node APIs and no new
dependencies: MD4/MD5/HMAC-MD5 are hand-written (NTLM only), AES-CMAC is
built on Web Crypto AES-CBC, SP800-108 KDF on HMAC-SHA256, and SHA-512
preauth and HMAC-SHA256 signing use Web Crypto. NTLMv2 over NTLMSSP/SPNEGO
with MIC; SMB signing implemented, SMB3 encryption deferred.
Includes crypto/auth vector tests, protocol codec round-trips, an in-repo
mock SMB2 server for end-to-end coverage over the loopback transport, and
an env-gated integration test. Wired into test-app as window.smb2.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014AcGBx7EjVaByiHcBcsSfP
Transferred conns keep their TLS state: claim replies and conn
ResourceMeta carry isTls, upgradeTls is serialized through the resource
chan lock (never overlapping an in-flight read, reply dropped on detach
mid-upgrade), and transferState refuses while an upgrade is in progress.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
transfer() only recognized a bare TransferToken, so composite states like
DavTransferState were sent with an empty tokens list, skipping worker-side
validation and queue-expiry cleanup. Handshake-buffered envelopes are now
replayed before awaiting the client lock so channel ordering is preserved,
and the main-thread promise-returning stubs reject instead of throwing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Results of reads/accepts that were in flight when a resource was detached
were delivered to the old owner's dead port, and accepted conns were
registered on (and leaked with) the old client entry. Each resource now
carries a generation-tagged channel; stale completions are deposited into
a backlog the next owner's bridge drains first, serialized so stream order
is preserved. TTL expiry and cancel close backlogged conns.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Envelopes flushed by the worker when a client key registers arrive before
ready and were dropped by the temporary handshake handler.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>