From fd0078a9c397602588e7fe9459d1f9676501a0f9 Mon Sep 17 00:00:00 2001 From: "Victor C." <34163765+vicajilau@users.noreply.github.com> Date: Fri, 4 Sep 2026 07:40:44 +0100 Subject: [PATCH 1/2] docs: lead the web section with what works The section still read as a warning: sockets are unavailable, bring your own transport, and by the way here is what used to be broken before 4.0.0. That framing was right when web support was a caveat. It is not any more, now that pub.dev lists the platform and a browser runs a handshake, a command, an aes256-gcm session and an SFTP round trip against a real OpenSSH server on every pull request. So it now says what works first, then the one thing a caller has to supply, then the two things a browser genuinely cannot do. The archaeology about getUint64 moves out: that belongs in the changelog, which has it. Also points at tool/ws_bridge.dart and the test socket as a worked example of the transport, since writing one is the only real work left for a web user, and mentions the platform in the features list. --- README.md | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 33c7cce..c95a0f5 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ SSH and SFTP client written in pure Dart, aiming to be feature-rich as well as e ## ✨ Features -- **Pure Dart**: Working with both Dart VM and Flutter. +- **Pure Dart**: Runs on the Dart VM, Flutter and the web. Browser support is checked in CI against a real OpenSSH server, see [Web support](#web-support). - **SSH Session**: Executing commands, spawning shells, setting environment variables, pseudo terminals, etc. - **Authentication**: Supports password, in-memory private keys (`SSHKeyPair`), external asynchronous identities (`SSHIdentity` for Secure Enclave, YubiKey/FIDO2, smart cards, OS agents), RFC 4252 §7.8 public-key probing, RFC 4252 §9 hostbased authentication, and keyboard-interactive authentication. - **Forwarding**: Supports local forwarding, remote forwarding, and dynamic forwarding (SOCKS5 CONNECT). @@ -167,30 +167,34 @@ already trust end to end. ### Web support -Direct native TCP sockets are not available in browsers, so this will fail on -Flutter Web / Dart Web: +dartssh2 runs on Flutter Web and Dart Web. The protocol, authentication, +sessions, channel forwarding and SFTP all work compiled to JavaScript, and CI +checks it rather than assuming it: on every pull request a browser completes a +handshake, runs a command, holds an `aes256-gcm` session and round-trips a file +over SFTP against a real OpenSSH server. + +**You have to supply the transport.** Browsers cannot open TCP sockets, so this +fails: ```dart await SSHSocket.connect('host', 22); ``` -For web apps, use a custom `SSHSocket` transport over a browser-supported -channel (for example, a WebSocket tunnel/proxy to your SSH endpoint). - -Before 4.0.0 that was all it said, and it was not enough: every AEAD cipher and -the whole of SFTP went through `ByteData.getUint64`/`setUint64`, which throw -under dart2js, so a browser connection died at the first encrypted packet even -with a correct transport. That is fixed, and a `dart test -p chrome` job now -guards it. +`SSHSocket` is an interface, so give it something a browser does have, normally +a WebSocket to a proxy that bridges to your SSH endpoint. For a minimal working +pair of both ends, see [`tool/ws_bridge.dart`](tool/ws_bridge.dart) and the +socket in +[`test/src/integration/web_interop_test.dart`](test/src/integration/web_interop_test.dart). -Two caveats remain. `chacha20-poly1305@openssh.com` cannot be used on the web, -because PointyCastle's Poly1305 requires full-width 64-bit integers. It sits -third in the default cipher list, so AES-GCM or AES-CTR is normally negotiated -and nothing needs doing. Only pinning ChaCha20-Poly1305 explicitly will fail. +Two things are not available in a browser: -And `SftpFile.downloadToRandomAccess` is not available, since it takes a -`dart:io` `RandomAccessFile` and there is no local file to hand it. Use -`SftpFile.downloadTo`, which takes a sink, or `SftpFile.read`. +- `chacha20-poly1305@openssh.com`, because PointyCastle's Poly1305 needs + full-width 64-bit integers. It sits third in the default cipher list, so + AES-GCM or AES-CTR is negotiated instead and nothing needs doing. Only + pinning it explicitly will fail. +- `SftpFile.downloadToRandomAccess`, which takes a `dart:io` + `RandomAccessFile`, and there is no local file to hand it. Use + `SftpFile.downloadTo`, which takes a sink, or `SftpFile.read`. ### Customize client SSH identification From 3500b8b2b01d20bcc3a7465c859f27c71ef965b6 Mon Sep 17 00:00:00 2001 From: "Victor C." <34163765+vicajilau@users.noreply.github.com> Date: Fri, 4 Sep 2026 07:42:33 +0100 Subject: [PATCH 2/2] chore: release 4.1.0 Dates the section and bumps pubspec from 4.0.1. Nothing is left open, and the web platform work is invisible to anyone until this is published: pub.dev still serves 4.0.1 without platform:web, so a search filtered to web still does not find the package. --- CHANGELOG.md | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77a42d4..4c8d1ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## [4.1.0] - Unreleased +## [4.1.0] - 2026-09-04 - Added `SSHClient.pipelineChannelRequests`, off by default, which sends all of a session's channel requests before reading any reply instead of waiting for each one in turn. `execute` and `shell` send `env`, agent forwarding, `pty-req` and `x11-req` ahead of `exec` or `shell`, and each of them cost a round trip: against a server 40 ms away, `execute` with a pty measured 246 ms before and 206 ms after. RFC 4254 §5.4 permits sending further messages without waiting and §4 requires the peer to answer a channel's requests in the order it received them, which is what `ssh(1)` relies on when it does the same thing. Setting it also adopts OpenSSH's reporting, because it has to: the command is on the wire before a refusal can come back, so a refused `pty-req`, `env`, agent forwarding or `x11-req` is reported through `printDebug` and the command runs, the way `ssh(1)` prints `PTY allocation request failed on channel 0` and carries on, rather than throwing an error that means the command has already run. Only a refused `exec` or `shell` still throws `SSHChannelRequestError`, because nothing has run when that one fails. Leaving it unset changes nothing, down to the order the requests go out and the message of every error [#243]. - Added a `dartssh2-nopty` account to the interop server, which `PermitTTY no` applies to, so both sides of a refused `pty-req` are exercised against a real OpenSSH: by default the command does not run, and with pipelining it does [#243]. - Fixed the package reading as web-incompatible on pub.dev. `SftpFile.downloadToRandomAccess` takes a `dart:io` `RandomAccessFile`, and naming that type from the SFTP library was enough for pub.dev to drop `platform:web` from the whole package, which also keeps it out of any search filtered to web. Everything else already compiled and ran there, and 4.0.0 made the ciphers and SFTP work. The method moves to an `SftpFileDownload` extension in its own library, exported conditionally the way `SSHSocket` and dynamic forwarding already are, so nothing changes for a caller on the VM: same import, same call. On the web the extension is simply absent, as is the `RandomAccessFile` it would need. Confirmed with pana, the tool pub.dev scores with: `platform:web` is present after and absent before [#248]. diff --git a/pubspec.yaml b/pubspec.yaml index 36da4bb..3791839 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: dartssh2 -version: 4.0.1 +version: 4.1.0 description: SSH and SFTP client written in pure Dart, aiming to be feature-rich as well as easy to use. homepage: https://github.com/vicajilau/dartssh2 repository: https://github.com/vicajilau/dartssh2