SSH2 client and server modules written in pure JavaScript for node.js
—
HTTP and HTTPS agents that tunnel web traffic through SSH connections, enabling secure web browsing and API access through SSH tunnels.
HTTP agent that tunnels HTTP requests through SSH connections.
/**
* HTTP agent that tunnels requests through SSH connection
* Extends Node.js http.Agent with SSH tunneling capability
*/
class HTTPAgent extends http.Agent {
/**
* Create HTTP agent with SSH tunneling
* @param connectCfg - SSH connection configuration
* @param agentOptions - Standard HTTP agent options
*/
constructor(connectCfg: ClientConfig, agentOptions?: http.AgentOptions);
/**
* Create connection through SSH tunnel
* @param options - HTTP request options
* @param callback - Callback receiving tunneled connection
*/
createConnection(options: http.RequestOptions, callback: ConnectCallback): void;
}
type ConnectCallback = (err: Error | null, socket?: Socket) => void;HTTP Agent Usage Examples:
const { HTTPAgent } = require('ssh2');
const http = require('http');
// Create HTTP agent with SSH tunnel
const agent = new HTTPAgent({
host: 'jump-server.com',
username: 'user',
privateKey: require('fs').readFileSync('/path/to/key')
}, {
keepAlive: true,
maxSockets: 10
});
// Use agent for HTTP requests
const options = {
hostname: 'internal-api.corp',
port: 80,
path: '/api/data',
method: 'GET',
agent: agent
};
const req = http.request(options, (res) => {
console.log(`statusCode: ${res.statusCode}`);
console.log(`headers:`, res.headers);
res.on('data', (chunk) => {
console.log(chunk.toString());
});
});
req.on('error', (error) => {
console.error(error);
});
req.end();HTTPS agent that tunnels HTTPS requests through SSH connections with TLS support.
/**
* HTTPS agent that tunnels requests through SSH connection
* Extends Node.js https.Agent with SSH tunneling capability
*/
class HTTPSAgent extends https.Agent {
/**
* Create HTTPS agent with SSH tunneling
* @param connectCfg - SSH connection configuration
* @param agentOptions - Standard HTTPS agent options
*/
constructor(connectCfg: ClientConfig, agentOptions?: https.AgentOptions);
/**
* Create TLS connection through SSH tunnel
* @param options - HTTPS request options
* @param callback - Callback receiving tunneled TLS connection
*/
createConnection(options: https.RequestOptions, callback: ConnectCallback): void;
}HTTPS Agent Usage Examples:
const { HTTPSAgent } = require('ssh2');
const https = require('https');
// Create HTTPS agent with SSH tunnel
const agent = new HTTPSAgent({
host: 'jump-server.com',
username: 'user',
agent: process.env.SSH_AUTH_SOCK, // Use SSH agent
agentForward: true
}, {
keepAlive: true,
maxSockets: 5,
// TLS options
rejectUnauthorized: false, // For self-signed certs
ca: [require('fs').readFileSync('/path/to/ca.pem')]
});
// Use agent for HTTPS requests
const options = {
hostname: 'secure-api.corp',
port: 443,
path: '/api/secure-data',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123'
},
agent: agent
};
const req = https.request(options, (res) => {
console.log(`statusCode: ${res.statusCode}`);
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
console.log('Response:', JSON.parse(data));
});
});
req.write(JSON.stringify({ query: 'data' }));
req.end();const { HTTPAgent, HTTPSAgent } = require('ssh2');
const http = require('http');
const https = require('https');
class TunneledScraper {
constructor(sshConfig) {
this.httpAgent = new HTTPAgent(sshConfig, {
keepAlive: true,
timeout: 30000
});
this.httpsAgent = new HTTPSAgent(sshConfig, {
keepAlive: true,
timeout: 30000,
rejectUnauthorized: false
});
}
async fetchPage(url) {
return new Promise((resolve, reject) => {
const urlObj = new URL(url);
const isHttps = urlObj.protocol === 'https:';
const lib = isHttps ? https : http;
const agent = isHttps ? this.httpsAgent : this.httpAgent;
const options = {
hostname: urlObj.hostname,
port: urlObj.port || (isHttps ? 443 : 80),
path: urlObj.pathname + urlObj.search,
method: 'GET',
agent: agent,
headers: {
'User-Agent': 'Mozilla/5.0 (compatible; SSH2-Scraper)'
}
};
const req = lib.request(options, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => resolve({
statusCode: res.statusCode,
headers: res.headers,
body: data
}));
});
req.on('error', reject);
req.setTimeout(30000, () => {
req.destroy();
reject(new Error('Request timeout'));
});
req.end();
});
}
async scrapeSite(urls) {
const results = [];
for (const url of urls) {
try {
console.log(`Fetching: ${url}`);
const result = await this.fetchPage(url);
results.push({ url, ...result });
} catch (error) {
console.error(`Failed to fetch ${url}:`, error.message);
results.push({ url, error: error.message });
}
}
return results;
}
destroy() {
this.httpAgent.destroy();
this.httpsAgent.destroy();
}
}
// Usage
const scraper = new TunneledScraper({
host: 'proxy-server.com',
username: 'user',
privateKey: privateKey
});
scraper.scrapeSite([
'http://internal-site1.corp/page1',
'https://internal-site2.corp/api/data',
'http://restricted-site.corp/info'
]).then(results => {
console.log('Scraping results:', results);
scraper.destroy();
});const { HTTPSAgent } = require('ssh2');
const https = require('https');
class TunneledAPIClient {
constructor(sshConfig, apiBaseUrl) {
this.baseUrl = apiBaseUrl;
this.agent = new HTTPSAgent(sshConfig, {
keepAlive: true,
maxSockets: 20
});
}
async request(method, endpoint, data = null, headers = {}) {
return new Promise((resolve, reject) => {
const url = new URL(endpoint, this.baseUrl);
const options = {
hostname: url.hostname,
port: url.port || 443,
path: url.pathname + url.search,
method: method.toUpperCase(),
agent: this.agent,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
...headers
}
};
if (data) {
const jsonData = JSON.stringify(data);
options.headers['Content-Length'] = Buffer.byteLength(jsonData);
}
const req = https.request(options, (res) => {
let responseData = '';
res.on('data', chunk => responseData += chunk);
res.on('end', () => {
const result = {
statusCode: res.statusCode,
headers: res.headers,
data: responseData
};
if (res.headers['content-type']?.includes('application/json')) {
try {
result.data = JSON.parse(responseData);
} catch (e) {
// Keep as string if not valid JSON
}
}
resolve(result);
});
});
req.on('error', reject);
if (data) {
req.write(JSON.stringify(data));
}
req.end();
});
}
async get(endpoint, headers) {
return this.request('GET', endpoint, null, headers);
}
async post(endpoint, data, headers) {
return this.request('POST', endpoint, data, headers);
}
async put(endpoint, data, headers) {
return this.request('PUT', endpoint, data, headers);
}
async delete(endpoint, headers) {
return this.request('DELETE', endpoint, null, headers);
}
destroy() {
this.agent.destroy();
}
}
// Usage
const apiClient = new TunneledAPIClient({
host: 'bastion.company.com',
username: 'developer',
privateKey: privateKey
}, 'https://internal-api.corp');
async function demonstrateAPI() {
try {
// GET request
const users = await apiClient.get('/users');
console.log('Users:', users.data);
// POST request
const newUser = await apiClient.post('/users', {
name: 'John Doe',
email: 'john@company.com'
});
console.log('Created user:', newUser.data);
// PUT request
const updated = await apiClient.put(`/users/${newUser.data.id}`, {
name: 'John Smith'
});
console.log('Updated user:', updated.data);
// DELETE request
await apiClient.delete(`/users/${newUser.data.id}`);
console.log('User deleted');
} catch (error) {
console.error('API error:', error);
} finally {
apiClient.destroy();
}
}
demonstrateAPI();While not directly supported, WebSocket connections can be tunneled using the underlying connection:
const { HTTPSAgent } = require('ssh2');
const WebSocket = require('ws');
function createTunneledWebSocket(sshConfig, wsUrl) {
const agent = new HTTPSAgent(sshConfig);
// Create WebSocket with custom agent
const ws = new WebSocket(wsUrl, {
agent: agent,
headers: {
'User-Agent': 'SSH2-WebSocket-Client'
}
});
return ws;
}
// Usage
const ws = createTunneledWebSocket({
host: 'tunnel-server.com',
username: 'user',
privateKey: privateKey
}, 'wss://internal-websocket.corp/socket');
ws.on('open', () => {
console.log('WebSocket connected through SSH tunnel');
ws.send(JSON.stringify({ type: 'hello', data: 'world' }));
});
ws.on('message', (data) => {
console.log('Received:', JSON.parse(data));
});
ws.on('close', () => {
console.log('WebSocket connection closed');
});interface ClientConfig {
// Connection details
host: string;
port?: number;
username: string;
// Authentication
password?: string;
privateKey?: Buffer | string;
passphrase?: string;
agent?: string | BaseAgent;
// Connection options
keepaliveInterval?: number;
readyTimeout?: number;
// Advanced options
algorithms?: AlgorithmList;
debug?: DebugFunction;
}interface HTTPAgentOptions extends http.AgentOptions {
// Connection pooling
keepAlive?: boolean;
keepAliveMsecs?: number;
maxSockets?: number;
maxFreeSockets?: number;
// Timeouts
timeout?: number;
// Other options
scheduling?: 'lifo' | 'fifo';
}
interface HTTPSAgentOptions extends https.AgentOptions {
// All HTTP options plus TLS options
rejectUnauthorized?: boolean;
ca?: string | Buffer | Array<string | Buffer>;
cert?: string | Buffer;
key?: string | Buffer;
passphrase?: string;
ciphers?: string;
secureProtocol?: string;
}const { HTTPAgent } = require('ssh2');
const agent = new HTTPAgent({
host: 'unreliable-server.com',
username: 'user',
privateKey: privateKey
}, {
timeout: 10000,
maxSockets: 5
});
// Handle agent errors
agent.on('error', (error) => {
console.error('Agent error:', error);
});
// Handle individual request errors
function makeRequest(options) {
return new Promise((resolve, reject) => {
const req = http.request({
...options,
agent: agent
}, (res) => {
// Handle response
resolve(res);
});
req.on('error', (error) => {
if (error.code === 'ECONNREFUSED') {
console.error('Target server refused connection');
} else if (error.code === 'ETIMEDOUT') {
console.error('Request timed out');
} else if (error.code === 'ENOTFOUND') {
console.error('Host not found');
}
reject(error);
});
req.on('timeout', () => {
req.destroy();
reject(new Error('Request timeout'));
});
req.end();
});
}class ResilientTunneledClient {
constructor(sshConfig, options = {}) {
this.sshConfig = sshConfig;
this.maxRetries = options.maxRetries || 3;
this.retryDelay = options.retryDelay || 1000;
this.recreateAgent();
}
recreateAgent() {
if (this.httpAgent) this.httpAgent.destroy();
if (this.httpsAgent) this.httpsAgent.destroy();
this.httpAgent = new HTTPAgent(this.sshConfig);
this.httpsAgent = new HTTPSAgent(this.sshConfig);
}
async requestWithRetry(options, retries = 0) {
try {
return await this.makeRequest(options);
} catch (error) {
if (retries < this.maxRetries && this.isRetryableError(error)) {
console.log(`Request failed, retrying in ${this.retryDelay}ms (attempt ${retries + 1}/${this.maxRetries})`);
// Recreate agents on connection errors
if (error.code === 'ECONNREFUSED' || error.code === 'ECONNRESET') {
this.recreateAgent();
}
await new Promise(resolve => setTimeout(resolve, this.retryDelay));
return this.requestWithRetry(options, retries + 1);
}
throw error;
}
}
isRetryableError(error) {
const retryableCodes = ['ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND'];
return retryableCodes.includes(error.code);
}
makeRequest(options) {
// Implementation using this.httpAgent or httpsAgent
}
destroy() {
if (this.httpAgent) this.httpAgent.destroy();
if (this.httpsAgent) this.httpsAgent.destroy();
}
}// Configure agents for optimal performance
const httpAgent = new HTTPAgent(sshConfig, {
keepAlive: true, // Reuse connections
keepAliveMsecs: 30000, // Keep alive for 30 seconds
maxSockets: 50, // Allow up to 50 concurrent connections
maxFreeSockets: 10, // Keep 10 idle connections
timeout: 60000 // 60 second timeout
});
// Monitor connection usage
console.log('Current connections:', httpAgent.getCurrentConnections());
console.log('Free connections:', httpAgent.getFreeSockets());class BatchedTunneledClient {
constructor(sshConfig) {
this.agent = new HTTPSAgent(sshConfig, {
keepAlive: true,
maxSockets: 20
});
this.requestQueue = [];
this.processing = false;
}
async batchRequest(requests) {
// Process multiple requests concurrently
const promises = requests.map(req => this.makeRequest(req));
return Promise.allSettled(promises);
}
async makeRequest(options) {
return new Promise((resolve, reject) => {
const req = https.request({
...options,
agent: this.agent
}, resolve);
req.on('error', reject);
req.end();
});
}
}import { Agent as HttpAgent } from 'http';
import { Agent as HttpsAgent } from 'https';
import { Socket } from 'net';
interface ClientConfig {
host: string;
port?: number;
username: string;
password?: string;
privateKey?: Buffer | string;
passphrase?: string;
agent?: string | BaseAgent;
[key: string]: any;
}Install with Tessl CLI
npx tessl i tessl/npm-ssh2