| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
SSH and SFTP client written in pure Dart, aiming to be feature-rich as well as easy to use.
dartssh2 is now a complete rewrite of dartssh.
| ServerBox | NoPorts | DartShell | Naviterm | TealKit |
|
|
|
|
|
Feel free to add your own app here by opening a pull request.
# Install the `dartssh` command.
dart pub global activate dartssh2_cli
# Then use `dartssh` as regular `ssh` command.
dartssh user@example.com
# Example: execute a command on remote host.
dartssh user@example.com ls -al
# Example: connect to a non-standard port.
dartssh user@example.com:<port>
# Transfer files via SFTP.
dartsftp user@example.comIf the dartssh command can't be found after installation, you might need to set up your path.
void main() async {
final client = SSHClient(
await SSHSocket.connect('localhost', 22),
username: '<username>',
onPasswordRequest: () => '<password>',
);
}Note: SSHSocket.connect() uses native TCP sockets (dart:io) and is not available on Flutter Web / Dart Web. See Web support below for browser-compatible transport options.
SSHSocket is an interface and it's possible to implement your own SSHSocket if you want to use a different underlying transport rather than standard TCP socket. For example WebSocket or Unix domain socket.
Direct native TCP sockets are not available in browsers, so this will fail on Flutter Web / Dart Web:
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).
If your jump host or SSH gateway restricts client versions, you can customize the software version part of the identification string (SSH-2.0-<ident>):
void main() async {
final client = SSHClient(
await SSHSocket.connect('localhost', 22),
username: '<username>',
onPasswordRequest: () => '<password>',
ident: 'MyClient_1.0',
);
}ident defaults to DartSSH_2.0.
void main() async {
final shell = await client.shell();
// Attach local terminal streams only when a terminal is available.
// GUI apps on Windows may not have stdin/stdout/stderr attached.
final hasTerminal = stdin.hasTerminal && stdout.hasTerminal && stderr.hasTerminal;
if (hasTerminal) {
stdout.addStream(shell.stdout); // listening for stdout
stderr.addStream(shell.stderr); // listening for stderr
stdin.cast<Uint8List>().listen(shell.write); // writing to stdin
}
await shell.done; // wait for shell to exit
client.close();
}Note: The stdin/stdout bridging above is for CLI apps. If your app is launched without a terminal (for example, double-clicking a Windows .exe), skip the local stdio wiring and use your own UI/input pipeline.
void main() async {
final uptime = await client.run('uptime');
print(utf8.decode(uptime));
}Ignoring stderr:
void main() async {
final uptime = await client.run('uptime', stderr: false);
print(utf8.decode(uptime));
}client.run() is a convenience method that returns combined output bytes. Use client.runWithResult() when you need separate stdout / stderr streams and command exit metadata (exitCode / exitSignal).
To also access command exit metadata:
void main() async {
final result = await client.runWithResult('echo hello');
print('exitCode: ${result.exitCode}');
print('stdout: ${utf8.decode(result.stdout)}');
print('stderr: ${utf8.decode(result.stderr)}');
}Use example/run_flows.dart to test the main execution flows in one run:
Run it with environment variables:
SSH_HOST=test.rebex.net SSH_PORT=22 SSH_USERNAME=demo SSH_PASSWORD=password dart run example/run_flows.dartRun shell flow too:
SSH_HOST=test.rebex.net SSH_PORT=22 SSH_USERNAME=demo SSH_PASSWORD=password dart run example/run_flows.dart --shellOn Windows PowerShell:
$env:SSH_HOST = 'test.rebex.net'
$env:SSH_PORT = '22'
$env:SSH_USERNAME = 'demo'
$env:SSH_PASSWORD = 'password'
dart run example/run_flows.dart --shellvoid main() async {
final session = await client.execute('cat > file.txt');
await session.stdin.addStream(File('local_file.txt').openRead().cast());
await session.stdin.close(); // Close the sink to send EOF to the remote process.
await session.done; // Wait for session to exit to ensure all data is flushed to the remote process.
print(session.exitCode); // You can get the exit code after the session is done
}session.write() is a shorthand for session.stdin.add(). It's recommended to use session.stdin.addStream() instead of session.write() when you want to stream large amount of data to the remote process.
Killing a remote process by sending signal
void main() async {
session.kill(SSHSignal.KILL);
await session.done;
print('exitCode: ${session.exitCode}'); // -> exitCode: null
print('signal: ${session.exitSignal?.signalName}'); // -> signal: KILL
}Processes killed by signals do not have an exit code, instead they have an exit signal property.
void main() async {
final serverSocket = await ServerSocket.bind('localhost', 8080);
await for (final socket in serverSocket) {
final forward = await client.forwardLocal('httpbin.org', 80);
forward.stream.cast<List<int>>().pipe(socket);
socket.pipe(forward.sink);
}
}void main() async {
final forward = await client.forwardRemote(port: 2222);
if (forward == null) {
print('Failed to forward remote port');
return;
}
await for (final connection in forward.connections) {
final socket = await Socket.connect('localhost', 22);
connection.stream.cast<List<int>>().pipe(socket);
socket.pipe(connection.sink);
}
}void main() async {
final dynamicForward = await client.forwardDynamic(
bindHost: '127.0.0.1',
bindPort: 1080,
options: const SSHDynamicForwardOptions(
handshakeTimeout: Duration(seconds: 10),
connectTimeout: Duration(seconds: 15),
maxConnections: 128,
),
filter: (host, port) {
// Optional allow/deny policy.
return true;
},
);
print('SOCKS5 proxy at ${dynamicForward.host}:${dynamicForward.port}');
}This currently supports SOCKS5 NO AUTH + CONNECT. It requires dart:io and is not available on web runtimes.
Quick verification from your terminal:
curl --proxy socks5h://127.0.0.1:1080 https://ifconfig.meIf the proxy is working, this command returns the public egress IP seen through the SSH tunnel.
void main() async {
final client = SSHClient(
socket,
username: '<username>',
identities: [
// A single private key file may contain multiple keys.
...SSHKeyPair.fromPem(await File('path/to/id_rsa').readAsString())
],
);
}void main() async {
// Test whether the private key is encrypted.
final encrypted = SSHKeyPair.isEncrypted(await File('path/to/id_rsa').readAsString());
print(encrypted);
// If the private key is encrypted, you need to provide the passphrase.
final keys = SSHKeyPair.fromPem('<pem text>', '<passphrase>');
print(keys);
}Decrypt PEM file with compute in Flutter
void main() async {
List<SSHKeyPair> decryptKeyPairs(List<String> args) {
return SSHKeyPair.fromPem(args[0], args[1]);
}
final keypairs = await compute(decryptKeyPairs, ['<pem text>', '<passphrase>']);
}void main() async {
await client.authenticated;
print(client.remoteVersion); // SSH-2.0-OpenSSH_7.4p1
}void main() async {
final jumpServer = SSHClient(
await SSHSocket.connect('<jump server>', 22),
username: '...',
onPasswordRequest: () => '...',
);
final client = SSHClient(
await jumpServer.forwardLocal('<target server>', 22),
username: '...',
onPasswordRequest: () => '...',
);
print(utf8.decode(await client.run('hostname'))); // -> hostname of <target server>
}}
void main() async {
final sftp = await client.sftp();
final items = await sftp.listdir('/');
for (final item in items) {
print(item.longname);
}
}void main() async {
final sftp = await client.sftp();
final file = await sftp.open('/etc/passwd');
final content = await file.readBytes();
print(latin1.decode(content));
}void main() async {
final sftp = await client.sftp();
final output = File('local_file.txt').openWrite();
final bytes = await sftp.download(
'/remote/file.txt',
output,
onProgress: (bytesRead) => print('downloaded: $bytesRead bytes'),
closeDestination: true,
);
print('download complete: $bytes bytes');
}download() and downloadTo() are opt-in convenience APIs built on top of the existing stream-based behavior, so existing code remains fully compatible.
When to use each API:
void main() async {
final sftp = await client.sftp();
final file = await sftp.open('/remote/file.txt');
final output = File('local_partial.bin').openWrite();
try {
// Download bytes [1024, 1024 + 4096) using an existing open handle.
await file.downloadTo(
output,
offset: 1024,
length: 4096,
closeDestination: true,
);
} finally {
await file.close();
}
}For high-latency links or large files, you can tune pipelining:
void main() async {
final sftp = await client.sftp();
final output = File('local_file.txt').openWrite();
await sftp.download(
'/remote/file.txt',
output,
chunkSize: 64 * 1024,
maxPendingRequests: 128,
closeDestination: true,
);
}void main() async {
final sftp = await client.sftp();
final file = await sftp.open('file.txt', mode: SftpFileOpenMode.write);
await file.writeBytes(utf8.encode('hello there!') as Uint8List);
}Write at specific offset
void main() async {
final data = utf8.encode('world') as Uint8List;
await file.writeBytes(data, offset: 6);
}void main() async {
final sftp = await client.sftp();
final file = await sftp.open('file.txt', mode: SftpFileOpenMode.create | SftpFileOpenMode.write);
await file.write(File('local_file.txt').openRead().cast());
}void main() async {
final uploader = await file.write(File('local_file.txt').openRead().cast());
// ...
await uploader.pause();
// ...
await uploader.resume();
await uploader.done;
}Clear the remote file before opening it
void main() async {
final file = await sftp.open('file.txt',
mode: SftpFileOpenMode.create | SftpFileOpenMode.truncate | SftpFileOpenMode.write
);
}void main() async {
final sftp = await client.sftp();
await sftp.mkdir('/path/to/dir');
await sftp.rmdir('/path/to/dir');
}void main() async {
await sftp.stat('/path/to/file');
await sftp.setStat(
'/path/to/file',
SftpFileAttrs(mode: SftpFileMode(userRead: true)),
);
}void main() async {
final stat = await sftp.stat('/path/to/file');
print(stat.type);
// or
print(stat.isDirectory);
print(stat.isSocket);
print(stat.isSymbolicLink);
// ...
}void main() async {
final sftp = await client.sftp();
sftp.link('/from', '/to');
}void main() async {
final sftp = await client.sftp();
final statvfs = await sftp.statvfs('/root');
print('total: ${statvfs.blockSize * statvfs.totalBlocks}');
print('free: ${statvfs.blockSize * statvfs.freeBlocks}');
}Host key:
Key exchange:
Cipher:
AES-GCM is currently available as opt-in via SSHAlgorithms(cipher: ...), and is not enabled in the default cipher preference list yet.
Example (opt-in AES-GCM with explicit fallback ciphers):
void main() async {
final client = SSHClient(
await SSHSocket.connect('localhost', 22),
username: '<username>',
onPasswordRequest: () => '<password>',
algorithms: const SSHAlgorithms(
cipher: [
SSHCipherType.aes256gcm,
SSHCipherType.aes128gcm,
SSHCipherType.aes256ctr,
SSHCipherType.aes128ctr,
SSHCipherType.aes256cbc,
SSHCipherType.aes128cbc,
],
),
);
// Use the client...
client.close();
}chacha20-poly1305@openssh.com is not supported yet.
Integrity:
Private key:
| Type | Decode | Decrypt | Encode | Encrypt |
|---|---|---|---|---|
| RSA | ✔️ | ✔️ | ✔️ | WIP |
| OpenSSH RSA | ✔️ | ✔️ | ✔️ | WIP |
| OpenSSH ECDSA | ✔️ | ✔️ | ✔️ | WIP |
| OpenSSH Ed25519 | ✔️ | ✔️ | ✔️ | WIP |
dartssh is released under the terms of the MIT license. See LICENSE.
| Back | FazBrowse Home | New Git URL |