Skip to content

Repository files navigation

sshclient-wasm

WebAssembly-based SSH client for the browser with transport-agnostic architecture. Built with Go's golang.org/x/crypto/ssh package and compiled to WASM for browser usage.

Features

  • 🔐 SSH client running entirely in the browser via WebAssembly
  • 🔌 Transport-agnostic architecture supporting multiple protocols
  • 🌐 Built-in support for WebSocket and AWS IoT Secure Tunneling
  • 📦 Packet-level send/receive hooks for monitoring and transformation
  • 🔄 Support for custom packet transformations (e.g., Protobuf encoding)
  • 🔑 Password and private key authentication
  • 📘 TypeScript support with full type definitions
  • 🚀 ES Module compatible for modern frontend frameworks

Architecture

graph TB
subgraph "Browser Environment"
A["ES Module<br/>(TypeScript)"]
B["Transport Translation Layer"]
C["WASM Layer<br/>(Go SSH Client)"]
A --> B
B <--> C
end
subgraph "Transport Implementations"
D["WebSocket Transport"]
E["AWS IoT Secure Tunnel<br/>Transport"]
F["Custom Transport<br/>(User-defined)"]
B --> D
B --> E
B --> F
end
subgraph "Network Layer"
G["Direct WebSocket<br/>Connection"]
H["AWS IoT<br/>Secure Tunneling"]
I["Custom Protocol<br/>Endpoint"]
D <--> G
E <--> H
F <--> I
end
subgraph "Destination"
J["SSH Server"]
G --> J
H --> J
I --> J
end
style A fill:#0277bd,color:#fff
style B fill:#7b1fa2,color:#fff
style C fill:#ef6c00,color:#fff
style D fill:#388e3c,color:#fff
style E fill:#388e3c,color:#fff
style F fill:#388e3c,color:#fff
style G fill:#d32f2f,color:#fff
style H fill:#d32f2f,color:#fff
style I fill:#d32f2f,color:#fff
style J fill:#424242,color:#fff
Loading

Installation

npm install sshclient-wasm

Quick Start

Installation & Setup

npm install sshclient-wasm

Copy WASM files to your public directory:

# Copy these files to your public/ directory:# - sshclient.wasm# - wasm_exec.js

Framework-Specific Imports (Recommended)

// Next.js optimizedimport{initializeSSHClient,SSHClient,NextJSConfig}from"sshclient-wasm/next";// Vite optimizedimport{initializeSSHClient,SSHClient,ViteConfig}from"sshclient-wasm/vite";// React hooks and utilitiesimport{useSSHClient,SSHClient}from"sshclient-wasm/react";// Generic/universal importimport{SSHClient}from"sshclient-wasm";

Quick Initialization

// Framework-specific (auto-optimized)import{initializeSSHClient}from"sshclient-wasm/next";awaitinitializeSSHClient();// Or generic with auto-detectionimport{SSHClient}from"sshclient-wasm";awaitSSHClient.initialize();

Usage Examples

Basic WebSocket Connection

import{SSHClient,WebSocketTransport}from"sshclient-wasm";// Simple initialization - auto-detects assets in public directoryawaitSSHClient.initialize();// Create a WebSocket transportconsttransport=newWebSocketTransport("transport-1","wss://ssh-gateway.example.com",["ssh"]);// Connect to SSH serverconstsession=awaitSSHClient.connect({host: "example.com",port: 22,user: "username",password: "password",},transport);// Send dataawaitsession.send(newUint8Array([0x01,0x02,0x03]));// Disconnectawaitsession.disconnect();

Next.js Example

import{initializeSSHClient,NextJSConfig}from"sshclient-wasm/next";import{useEffect,useState}from"react";functionSSHComponent(){const[isReady,setIsReady]=useState(false);useEffect(()=>{// Initialize with Next.js optimizationsinitializeSSHClient().then(()=>setIsReady(true)).catch((error)=>{console.error("Failed to initialize SSH client:",error);// Error messages will guide you to fix asset placement});},[]);if(!isReady)return<div>Loading SSH client...</div>;// Your SSH logic herereturn<div>SSH client ready!</div>;}// Alternative: Use the React hookimport{useSSHClient}from"sshclient-wasm/react";functionSSHComponentWithHook(){const{ isInitialized, initError, isLoading }=useSSHClient();if(isLoading)return<div>Loading SSH client...</div>;if(initError)return<div>Error: {initError.message}</div>;if(!isInitialized)return<div>SSH client not ready</div>;return<div>SSH client ready!</div>;}

Vite/React Example

import{initializeSSHClient,ViteConfig}from"sshclient-wasm/vite";import{useSSHConnection}from"sshclient-wasm/react";functionViteSSHComponent(){const{ connect, disconnect, session, connectionState }=useSSHConnection();useEffect(()=>{initializeSSHClient();},[]);consthandleConnect=async()=>{consttransport=newWebSocketTransport("transport-1","wss://example.com");awaitconnect({host: "example.com",port: 22,user: "username",password: "password"},transport);};return(<div><p>Status: {connectionState}</p><buttononClick={handleConnect}>Connect SSH</button><buttononClick={disconnect}>Disconnect</button></div>);}

AWS IoT Secure Tunnel Connection

import{SSHClient,SecureTunnelTransport}from"sshclient-wasm";// Auto-initializeawaitSSHClient.initialize();// Create AWS IoT Secure Tunnel transportconsttransport=newSecureTunnelTransport("tunnel-1",{region: "us-east-1",accessToken: "your-tunnel-access-token",clientMode: "source",serviceId: "SSH",protocol: "V3",});// Connect through the secure tunnelconstsession=awaitSSHClient.connect({host: "internal-server",port: 22,user: "username",privateKey: "ssh-private-key",},transport);// Use the SSH sessionawaitsession.send(encoder.encode("ls -la\n"));// Disconnectawaitsession.disconnect();

With Packet Hooks

consttransport=newWebSocketTransport("transport-1","wss://example.com");constsession=awaitSSHClient.connect({host: "example.com",port: 22,user: "username",password: "password",},transport,{onPacketSend: (data,metadata)=>{console.log("Sending packet:",data,metadata);},onPacketReceive: (data,metadata)=>{console.log("Received packet:",data,metadata);},onStateChange: (state)=>{console.log("Connection state:",state);},});

Packet Transformation

import{PacketTransformer}from"sshclient-wasm";// Transform to/from Base64constbase64=PacketTransformer.toBase64(data);constbinary=PacketTransformer.fromBase64(base64);// Custom Protobuf transformation (implement your own logic)constprotobuf=PacketTransformer.toProtobuf(data,schema);

Framework-Specific Setup

Next.js

1. Copy WASM files to public/:

cp node_modules/sshclient-wasm/dist/sshclient.wasm public/
cp node_modules/sshclient-wasm/dist/wasm_exec.js public/

2. Use the Next.js configuration helper:

// next.config.jsimport{NextJSConfig}from"sshclient-wasm/next";/** @type {import('next').NextConfig} */constnextConfig=NextJSConfig.getNextConfig({// Your custom Next.js config here});module.exports=nextConfig;

Or configure manually:

/** @type {import('next').NextConfig} */constnextConfig={webpack: (config)=>{config.experiments={ ...config.experiments,asyncWebAssembly: true};returnconfig;},asyncheaders(){return[{source: "/(.*)",headers: [{key: "Cross-Origin-Embedder-Policy",value: "require-corp"},{key: "Cross-Origin-Opener-Policy",value: "same-origin"},],},];},};module.exports=nextConfig;

3. Use in your components:

import{initializeSSHClient,useSSHClient}from"sshclient-wasm/next";// Method 1: Direct initializationuseEffect(()=>{initializeSSHClient().then(()=>console.log("SSH client ready")).catch(console.error);},[]);// Method 2: Using the hook (if using sshclient-wasm/react)const{ isInitialized, initError, isLoading }=useSSHClient();

Vite/React

1. Copy WASM files to public/:

cp node_modules/sshclient-wasm/dist/sshclient.wasm public/
cp node_modules/sshclient-wasm/dist/wasm_exec.js public/

2. Use the Vite configuration helper:

// vite.config.tsimport{defineConfig}from"vite";importreactfrom"@vitejs/plugin-react";import{ViteConfig}from"sshclient-wasm/vite";exportdefaultdefineConfig(ViteConfig.getViteConfig({plugins: [react()],// Your custom Vite config here}));

Or configure manually:

import{defineConfig}from"vite";importreactfrom"@vitejs/plugin-react";exportdefaultdefineConfig({plugins: [react()],server: {headers: {"Cross-Origin-Embedder-Policy": "require-corp","Cross-Origin-Opener-Policy": "same-origin",},},});

3. Use in your components:

import{initializeSSHClient}from"sshclient-wasm/vite";import{useSSHClient}from"sshclient-wasm/react";// Method 1: Direct initializationuseEffect(()=>{initializeSSHClient().then(()=>console.log("SSH client ready")).catch(console.error);},[]);// Method 2: Using React hooksconst{ isInitialized, initError, isLoading }=useSSHClient();

React Hooks & Utilities

The sshclient-wasm/react module provides React-specific hooks and utilities:

import{useSSHClient,useSSHConnection,SSHClientProvider,withSSHClient}from"sshclient-wasm/react";// Hook for initializationconst{ isInitialized, initError, isLoading }=useSSHClient();// Hook for connection managementconst{ connect, disconnect, send, session, connectionState }=useSSHConnection();// Provider for context<SSHClientProvideroptions={{cacheBusting: false}}><YourApp/></SSHClientProvider>// HOC wrapperconstWrappedComponent=withSSHClient(YourComponent);

Webpack/Generic

1. Copy WASM files to your public directory

2. Initialize with custom options:

import{SSHClient}from"sshclient-wasm";awaitSSHClient.initialize({publicDir: "/assets/",// Your public directory pathautoDetect: true,cacheBusting: process.env.NODE_ENV==="development",});

Advanced Initialization Options

Custom Paths

import{SSHClient}from"sshclient-wasm";awaitSSHClient.initialize({wasmPath: "/custom/path/sshclient.wasm",wasmExecPath: "/custom/path/wasm_exec.js",autoDetect: false,cacheBusting: false,timeout: 15000,});

Asset Availability Testing

import{SSHClientHelpers}from"sshclient-wasm";// Test if assets are properly placedconst{ wasmAvailable, wasmExecAvailable }=awaitSSHClientHelpers.testAssetAvailability("/sshclient.wasm","/wasm_exec.js");if(!wasmAvailable){console.error("❌ Please copy sshclient.wasm to your public directory");}if(!wasmExecAvailable){console.error("❌ Please copy wasm_exec.js to your public directory");}

Error Handling & Troubleshooting

try{awaitSSHClient.initialize();}catch(error){if(error.message.includes("WASM file not found")){console.error("Copy sshclient.wasm to public/ directory");}elseif(error.message.includes("wasm_exec.js not found")){console.error("Copy wasm_exec.js to public/ directory");}elseif(error.message.includes("Failed to fetch WASM")){console.error("Check network connection and CORS headers");}else{console.error("Initialization failed:",error.message);}}

Development

Prerequisites

  • Go 1.21+
  • Node.js 16+
  • Make

Building from Source

# Clone the repository
git clone https://github.com/andrew/sshclient-wasm.git
cd sshclient-wasm
# Install dependencies
go mod download
npm install
# Build WASM and TypeScript
make build
# Run example
make dev

Project Structure

sshclient-wasm/
├── main.go # WASM entry point
├── pkg/sshclient/ # Go SSH client implementation
│ ├── client.go # Main client logic
│ └── interceptor.go # Packet interception
├── lib/ # TypeScript/JavaScript bindings
│ └── index.ts # Main TypeScript API
├── example/ # Example application
└── dist/ # Build output

TypeScript API Reference

Main Classes

SSHClient

The main entry point for establishing SSH connections through WebAssembly.

classSSHClient{/** * Initialize the WASM module. Must be called before any other methods. * @param options - Initialization options or legacy string path */staticasyncinitialize(options?: InitializationOptions|string): Promise<void>;/** * Connect to an SSH server through a transport * @param options - SSH connection configuration * @param transport - Transport implementation for network communication * @param callbacks - Optional callbacks for monitoring packet flow * @returns SSH session handle for sending data and disconnecting */staticasyncconnect(options: ConnectionOptions,transport: Transport,callbacks?: SSHClientCallbacks): Promise<SSHSession>;/** * Disconnect a specific SSH session * @param sessionId - The ID of the session to disconnect */staticasyncdisconnect(sessionId: string): Promise<void>;/** * Get the library version * @returns Version string */staticgetVersion(): string;}

PacketTransformer

Utilities for transforming SSH packet data.

classPacketTransformer{/** * Convert binary data to Base64 encoding * @param data - Binary data to encode * @returns Base64 encoded string */statictoBase64(data: Uint8Array): string;/** * Convert Base64 string to binary data * @param base64 - Base64 encoded string * @returns Decoded binary data */staticfromBase64(base64: string): Uint8Array;/** * Transform binary data to Protobuf format (user-implemented) * @param data - Binary data to transform * @param schema - Protobuf schema definition * @returns Protobuf encoded data */statictoProtobuf(data: Uint8Array,schema?: any): Uint8Array;/** * Transform Protobuf data to binary format (user-implemented) * @param data - Protobuf encoded data * @param schema - Protobuf schema definition * @returns Decoded binary data */staticfromProtobuf(data: Uint8Array,schema?: any): Uint8Array;}

Transport Implementations

WebSocketTransport

Direct WebSocket connection to SSH servers.

classWebSocketTransportimplementsTransport{/** * Create a WebSocket transport * @param id - Unique identifier for this transport * @param url - WebSocket URL (e.g., 'wss://ssh-gateway.example.com') * @param protocols - Optional WebSocket subprotocols */constructor(id: string,url: string,protocols?: string[]);asyncconnect(): Promise<void>;asyncdisconnect(): Promise<void>;asyncsend(data: Uint8Array): Promise<void>;}

SecureTunnelTransport

AWS IoT Secure Tunneling transport for end-to-end encrypted connections.

classSecureTunnelTransportimplementsTransport{/** * Create an AWS IoT Secure Tunnel transport * @param id - Unique identifier for this transport * @param config - Tunnel configuration */constructor(id: string,config: SecureTunnelConfig);asyncconnect(): Promise<void>;asyncdisconnect(): Promise<void>;asyncsend(data: Uint8Array): Promise<void>;}

CustomTransport

User-defined transport for custom protocols.

classCustomTransportimplementsTransport{/** * Create a custom transport with user-provided implementations * @param id - Unique identifier for this transport * @param connectImpl - Custom connection implementation * @param disconnectImpl - Custom disconnection implementation * @param sendImpl - Custom send implementation */constructor(id: string,connectImpl?: ()=>Promise<void>,disconnectImpl?: ()=>Promise<void>,sendImpl?: (data: Uint8Array)=>Promise<void>);/** * Inject received data into the transport * @param data - Data received from the custom protocol */injectData(data: Uint8Array): void;}

SSHClientHelpers

Framework-specific helpers and utilities.

classSSHClientHelpers{/** * Get recommended asset paths for the detected framework */staticgetAssetPaths: (publicDir?: string)=>{wasmPath: string;wasmExecPath: string;};/** * Detect the current framework */staticdetectFramework: ()=>"nextjs"|"vite"|"webpack"|"generic";/** * Test if WASM assets are available at the given paths */statictestAssetAvailability: (wasmPath: string,wasmExecPath: string)=>Promise<{wasmAvailable: boolean;wasmExecAvailable: boolean;}>;/** * Next.js specific initialization helper */staticinitializeForNextJS: (options?: Partial<InitializationOptions>)=>Promise<void>;/** * Vite specific initialization helper */staticinitializeForVite: (options?: Partial<InitializationOptions>)=>Promise<void>;/** * Generic initialization with sensible defaults */staticinitializeWithDefaults: (customOptions?: Partial<InitializationOptions>)=>Promise<void>;}

Interfaces

InitializationOptions

Configuration options for initializing the SSH client.

interfaceInitializationOptions{/** Path to the WASM file (default: auto-detected) */wasmPath?: string;/** Path to the wasm_exec.js file (default: auto-detected) */wasmExecPath?: string;/** Enable automatic path detection (default: true) */autoDetect?: boolean;/** Public directory path for auto-detection (default: '/') */publicDir?: string;/** Enable cache busting for development (default: true) */cacheBusting?: boolean;/** Timeout for loading assets in milliseconds (default: 10000) */timeout?: number;}

Transport

Base interface for all transport implementations.

interfaceTransport{id: string;connect(): Promise<void>;disconnect(): Promise<void>;send(data: Uint8Array): Promise<void>;// Event handlers (set by the library)onData?: (data: Uint8Array)=>void;onError?: (error: Error)=>void;onClose?: ()=>void;}

ConnectionOptions

SSH connection configuration.

interfaceConnectionOptions{/** Target SSH server hostname */host: string;/** SSH server port (default: 22) */port: number;/** SSH username */user: string;/** Password for authentication (optional) */password?: string;/** Private key for authentication (optional) */privateKey?: string;/** Connection timeout in milliseconds (optional) */timeout?: number;}

SecureTunnelConfig

AWS IoT Secure Tunnel configuration.

interfaceSecureTunnelConfig{/** AWS region where the tunnel is created */region: string;/** Tunnel access token */accessToken: string;/** Client mode: 'source' or 'destination' */clientMode: "source"|"destination";/** Service identifier for multiplexed tunnels (optional) */serviceId?: string;/** Protocol version: 'V2' or 'V3' (default: 'V2') */protocol?: "V2"|"V3";}

SSHSession

SSH session handle returned from connect().

interfaceSSHSession{/** Unique session identifier */sessionId: string;/** * Send data through the SSH connection * @param data - Binary data to send */send(data: Uint8Array): Promise<void>;/** * Close the SSH connection */disconnect(): Promise<void>;}

SSHClientCallbacks

Optional callbacks for monitoring SSH packet flow.

interfaceSSHClientCallbacks{/** * Called when sending an SSH packet * @param data - Raw packet data being sent * @param metadata - Packet metadata (type, size, etc.) */onPacketSend?: (data: Uint8Array,metadata: PacketMetadata)=>void;/** * Called when receiving an SSH packet * @param data - Raw packet data received * @param metadata - Packet metadata (type, size, etc.) */onPacketReceive?: (data: Uint8Array,metadata: PacketMetadata)=>void;/** * Called when SSH connection state changes * @param state - New connection state */onStateChange?: (state: SSHConnectionState)=>void;}

PacketMetadata

Metadata about SSH packets.

interfacePacketMetadata{/** Packet type identifier */packetType: string;/** Packet type code */packetTypeCode: number;/** Packet size in bytes */size: number;/** Timestamp of packet */timestamp?: number;}

SSHConnectionState

SSH connection states.

typeSSHConnectionState=|"connecting"|"connected"|"authenticating"|"authenticated"|"ready"|"disconnecting"|"disconnected"|"error";

Enums

TunnelMessageType

AWS IoT Secure Tunnel message types.

enumTunnelMessageType{UNKNOWN=0,DATA=1,STREAM_START=2,STREAM_RESET=3,SESSION_RESET=4,SERVICE_IDS=5,CONNECTION_START=6,CONNECTION_RESET=7,}

Troubleshooting

Common Issues

❌ "WASM file not found"

# Solution: Copy WASM files to public directory
cp node_modules/sshclient-wasm/dist/sshclient.wasm public/
cp node_modules/sshclient-wasm/dist/wasm_exec.js public/

❌ "Failed to fetch WASM file: 404"

  • Verify files are in your public directory
  • Check that your web server serves static files from public/
  • Ensure file paths are correct (case-sensitive on some servers)

❌ "Go runtime not loaded"

  • Ensure wasm_exec.js is accessible and loads successfully
  • Check browser console for script loading errors
  • Verify CORS headers allow script loading

❌ "Cross-origin requests blocked"

Next.js users: Add CORS headers to next.config.js:

asyncheaders(){return[{source: '/(.*)',headers: [{key: 'Cross-Origin-Embedder-Policy',value: 'require-corp'},{key: 'Cross-Origin-Opener-Policy',value: 'same-origin'},],}];}

Vite users: Add to vite.config.ts:

server: {headers: {'Cross-Origin-Embedder-Policy': 'require-corp','Cross-Origin-Opener-Policy': 'same-origin',},}

❌ "WebAssembly.instantiate failed"

  • Your browser may not support WebAssembly
  • WASM file may be corrupted or incompatible
  • Try clearing browser cache

Debug Helpers

Test Asset Availability

import{SSHClientHelpers}from"sshclient-wasm";constresult=awaitSSHClientHelpers.testAssetAvailability("/sshclient.wasm","/wasm_exec.js");console.log("WASM available:",result.wasmAvailable);console.log("Exec available:",result.wasmExecAvailable);

Check Framework Detection

import{SSHClientHelpers}from"sshclient-wasm";console.log("Detected framework:",SSHClientHelpers.detectFramework());console.log("Recommended paths:",SSHClientHelpers.getAssetPaths());

Error Handling

All async methods throw errors that can be caught:

try{awaitSSHClient.initialize();awaitSSHClient.connect(options,transport);}catch(error){if(error.message.includes("WASM file not found")){console.error("📁 Copy sshclient.wasm to public/ directory");}elseif(error.message.includes("wasm_exec.js not found")){console.error("📁 Copy wasm_exec.js to public/ directory");}elseif(error.message.includes("Failed to fetch WASM")){console.error("🌐 Check network connection and CORS headers");}elseif(error.message.includes("authentication")){console.error("🔐 Check SSH credentials");}elseif(error.message.includes("timeout")){console.error("⏰ Connection or initialization timeout");}else{console.error("❌ Unknown error:",error.message);}}

Performance Tips

  • Cache Busting: Disable in production for better performance
  • Asset Preloading: Consider preloading WASM files for faster initialization
  • Memory Usage: Monitor memory usage for long-running sessions
  • Connection Pooling: Reuse transports when possible

Examples

Custom Packet Transformation

constsession=awaitSSHClient.connect(connectionOptions,transport,{onPacketSend: (data,metadata)=>{// Transform outgoing packetsconsttransformed=PacketTransformer.toProtobuf(data,mySchema);console.log(`Sending ${metadata.packetType} packet (${metadata.size} bytes)`);returntransformed;},onPacketReceive: (data,metadata)=>{// Process incoming packetsconstdecoded=PacketTransformer.fromProtobuf(data,mySchema);console.log(`Received ${metadata.packetType} packet`);returndecoded;},});

Custom Transport Implementation

classMyCustomTransportextendsCustomTransport{constructor(id: string,config: MyConfig){super(id,async()=>{// Custom connection logicawaitthis.establishConnection(config);},async()=>{// Custom disconnection logicawaitthis.closeConnection();},async(data: Uint8Array)=>{// Custom send logicawaitthis.sendData(data);});}privateasyncestablishConnection(config: MyConfig){// Implementation specific to your protocol}// Call this.injectData() when receiving data from your protocolhandleIncomingData(data: Uint8Array){this.injectData(data);}}

License

BSD-3-Clause

About

WebAssembly SSH client that runs directly in the browser over WebSockets

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages