CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-ssh2

SSH2 client and server modules written in pure JavaScript for node.js

Pending
Overview
Eval results
Files

http-tunneling.mddocs/

HTTP Tunneling

HTTP and HTTPS agents that tunnel web traffic through SSH connections, enabling secure web browsing and API access through SSH tunnels.

Capabilities

HTTPAgent Class

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();

HTTPSAgent Class

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();

Advanced Usage Patterns

Web Scraping Through SSH Tunnel

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();
});

REST API Client with SSH Tunnel

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();

WebSocket Tunneling

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');
});

Configuration Options

SSH Connection Configuration

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;
}

HTTP Agent Options

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;
}

Error Handling

Connection Error Handling

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();
  });
}

Retry Logic

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();
  }
}

Performance Considerations

Connection Pooling

// 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());

Request Batching

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();
    });
  }
}

Type Definitions

HTTP Agent Types

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

docs

http-tunneling.md

index.md

key-management.md

port-forwarding.md

sftp-operations.md

ssh-agents.md

ssh-client.md

ssh-server.md

tile.json