codinget
  • Joined on 2022-10-19
codinget commented on pull request webnet/webnet#64 2026-07-07 09:50:43 +02:00
feat: add @webnet/sftp package (SFTP v3 client and server)

On a mid-stream write error the loop breaks and releaseLock()s but never cancels the source stream, so the producer feeding it is left hanging with no signal to stop (same pattern I flagged on the FTP client). A reader.cancel(writeError) before releaseLock() on the error path would propagate cancellation. Minor.

codinget commented on pull request webnet/webnet#64 2026-07-07 09:50:43 +02:00
feat: add @webnet/sftp package (SFTP v3 client and server)

Read pipelining assumes reads are never short except at EOF — a short mid-file READ silently drops data. fill() advances nextOffset/requested by the requested want, and the pull loop advances the stream by whatever each READ returns, without reconciling the two. If a server returns fewer than want bytes for a non-final READ (legal in the protocol; happens with pipes/special files and some non-OpenSSH servers), the already-pipelined next request sits at offset + want, so the want - returned bytes in between are never requested and the delivered file is corrupt (short, with a gap), with no error. This is safe against the bundled server (its readFromHandle always fills len until EOF) and against regular files on spec-compliant servers, which is why loopback + OpenSSH tests pass. To be robust, treat data.length < want as authoritative: set nextOffset from the actual bytes returned and discard/re-issue the outstanding pipelined requests, rather than trusting the optimistic offsets. At minimum, document the regular-files-only assumption.

codinget commented on pull request webnet/webnet#63 2026-07-07 09:44:59 +02:00
feat(transport): add explicit TLS upgrade (STARTTLS / AUTH TLS) to RawTransport

Server-side STARTTLS race: an early ClientHello makes the upgrade hard-fail. After a completed read() the socket stays in flowing mode (read() resumes it and #onData with a waiting callback never re-pauses). So in the server dance — read STARTTLS, write("220 …"), upgradeTls({isServer}) — a fast client's ClientHello can arrive in the event-loop gap after the write, land in #buffers, and the upgrade then throws Cannot upgrade with buffered data. Loopback tests won't hit this, but a real peer that pipelines the handshake right behind reading the 220 will, intermittently.

codinget commented on pull request webnet/webnet#63 2026-07-07 09:44:59 +02:00
feat(transport): add explicit TLS upgrade (STARTTLS / AUTH TLS) to RawTransport

Autonomous review (Claude Fable 5). The API shape is good (quiescence requirement, close-on-handshake-failure, in-flight guards), the node implementation's detach/attach listener handling is careful, and the test suite covers the edge cases well. Two real findings below — a timing race on the server side of the node transport, and an over-eager close on the worker path. The Go side of the submodule bump (webnet/tailscale#14) was not reviewed here.

codinget commented on pull request webnet/webnet#63 2026-07-07 09:44:59 +02:00
feat(transport): add explicit TLS upgrade (STARTTLS / AUTH TLS) to RawTransport

upgradeError conflates validation failures with handshake failures. Conn.upgradeTls on the worker side throws for plenty of non-fatal reasons — missing serverName, options type errors, the "rebuild main.wasm" feature check — none of which close the underlying Go conn. But this branch unconditionally marks the WorkerConn closed and closes the port, so: (a) a caller who passes bad options loses a perfectly healthy connection (unlike the direct Conn, which stays usable), and (b) the worker-side conn is never actually closed — it leaks. Either mirror the cheap validations client-side before sending the message, or split the protocol reply into "failed, conn still open" vs "failed, conn closed" and only tear down on the latter.

codinget commented on pull request webnet/webnet#61 2026-07-07 09:43:07 +02:00
feat: add @webnet/ftp package (FTP/FTPS client and server)

Two small lifecycle nits: (1) if the connect promise rejected, await (await conn) makes close() reject too — worth a catch; (2) #control() only resets #conn on connect-time failure — if the established control connection later dies (server idle timeout is common in FTP), every subsequent call fails on the dead connection until the caller makes a new client. Resetting #conn when a command fails with a transport-level error would make long-lived clients self-heal.

codinget commented on pull request webnet/webnet#61 2026-07-07 09:43:07 +02:00
feat: add @webnet/ftp package (FTP/FTPS client and server)

REST is sent before EPSV/PASV. RFC 3659 §5.3 says REST must be the command immediately preceding the transfer command. vsftpd and proftpd happen to keep the restart offset across an intervening EPSV/PASV, so this works against the named targets (and your own server), but stricter servers are allowed to reset it. Moving the REST exchange to after openDataUnlocked() (right before RETR) is a one-line reorder that removes the interop risk.

codinget commented on pull request webnet/webnet#61 2026-07-07 09:43:07 +02:00
feat: add @webnet/ftp package (FTP/FTPS client and server)

550 is mapped unconditionally to not-found, but servers also use 550 for permission errors (vsftpd: "550 Permission denied." on DELE/STOR). A `/(permission

codinget commented on pull request webnet/webnet#61 2026-07-07 09:43:07 +02:00
feat: add @webnet/ftp package (FTP/FTPS client and server)

A non-VFSError thrown by a VFS implementation (or a handler bug) propagates out of run() and kills the whole control connection instead of producing a reply. Catching the generic case with a 451 Requested action aborted: local error keeps one bad operation from dropping the session; onError can still be notified.

codinget commented on pull request webnet/webnet#61 2026-07-07 09:43:07 +02:00
feat: add @webnet/ftp package (FTP/FTPS client and server)

type (and prot) are tracked in session state but never read: TYPE A is accepted and then the transfer is sent as binary, and PROT C on an implicit-TLS server still serves TLS data connections. Both are fine behaviors in practice (modern clients use TYPE I / PROT P), but accepting-and-ignoring is the worst of both options for ASCII mode — either honor TYPE A (CRLF translation on RETR/STOR) or reply 504 so the client knows. If you keep them as accepted-but-ignored, dropping the dead state fields would at least make that explicit.

codinget commented on pull request webnet/webnet#61 2026-07-07 09:43:07 +02:00
feat: add @webnet/ftp package (FTP/FTPS client and server)

Autonomous review (Claude Fable 5). This is a well-put-together package: the control-channel mutex design (lock held across verb+data, released when the returned stream settles) is clean, CRLF injection is blocked in the codec, resolvePath prevents .. escapes on the server, and the 150-before-dial dance is correctly implemented on both sides. No blocking correctness bugs found; comments below are interop robustness and small hardening items.

codinget commented on pull request webnet/webnet#60 2026-07-07 09:40:42 +02:00
feat(smb2): SMB2/3 client with async VFS interface

Failed connect is cached forever. If #doConnect() rejects (transient dial failure, wrong password once, server briefly down), #connecting keeps holding the rejected promise and #tree stays unset, so every subsequent operation on this client rejects with the same stale error until disconnect() is called manually. Clear #connecting when the promise rejects, e.g. this.#connecting = this.#doConnect().catch((e) => { this.#connecting = undefined; throw e }).

codinget commented on pull request webnet/webnet#60 2026-07-07 09:40:42 +02:00
feat(smb2): SMB2/3 client with async VFS interface

Minor: if a write fails mid-stream, the source stream is left locked and un-cancelled (the finally only closes the SMB handle). Consider reader.cancel()/releaseLock() on the error path so the producer is told to stop.

codinget commented on pull request webnet/webnet#60 2026-07-07 09:40:42 +02:00
feat(smb2): SMB2/3 client with async VFS interface

parseFileIdBothDirInfo throws RangeError on an empty buffer (the first r.u32() reads past the end). Tree.queryDirectory checks batch.length === 0 as a loop exit, which suggests a zero-length success buffer is considered possible — if so, guard with if (buffer.length === 0) return [].

codinget commented on pull request webnet/webnet#60 2026-07-07 09:40:42 +02:00
feat(smb2): SMB2/3 client with async VFS interface

Autonomous review (Claude Fable 5). Solid, readable implementation — the crypto (CMAC subkey generation, SP800-108 KDF, NTLMv2 + MIC ordering) and the wire codecs all check out against the specs, and the preauth-integrity ingest/derive ordering in #doRequest is correct. Findings below: one real availability bug, one error-mapping inconsistency, one security gap worth an explicit decision, plus minor notes.

codinget commented on pull request webnet/webnet#60 2026-07-07 09:40:42 +02:00
feat(smb2): SMB2/3 client with async VFS interface

Connect-time SMB errors bypass the VFSError mapping. #ensure() is awaited outside the try, so Smb2Errors thrown during connect — LOGON_FAILURE from session setup, BAD_NETWORK_NAME from tree connect — escape as raw Smb2Error instead of going through statusToVFSError. The BAD_NETWORK_NAME/LOGON_FAILURE/ACCOUNT_DISABLED entries in the status map in status.ts are effectively dead code today. Either move #ensure() inside the try, or map in #doConnect.

codinget commented on pull request webnet/webnet#60 2026-07-07 09:40:42 +02:00
feat(smb2): SMB2/3 client with async VFS interface

Response signatures are never verified. Requests are signed, but the signature on received messages is never checked, so an on-path attacker can tamper with or inject responses even on a "signed" session — which defeats most of the point of mandatory signing. [MS-SMB2] §3.2.5.1.3 requires the client to verify. If this is deliberately deferred like encryption, it deserves the same explicit call-out in the PR description / a TODO; otherwise verification is cheap to add here (recompute over the message with the signature field zeroed, compare constant-time-ish).

codinget pushed to feat/sftp at webnet/webnet 2026-07-07 08:55:38 +02:00
251eb6862d docs(sftp): note the code-review hardening in the AI disclosure
c03d0f4c0d fix(sftp): address code-review findings in the server and channel layers
Compare 2 commits »
codinget created pull request webnet/webnet#64 2026-07-07 01:27:36 +02:00
WIP: feat: add @webnet/sftp package (SFTP v3 client and server)
codinget created branch feat/sftp in webnet/webnet 2026-07-07 01:26:52 +02:00