ssh-bridge-ai
Version:
AI-Powered SSH Tool with Bulletproof Connections & Enterprise Sandbox Security + Cursor-like Confirmation - Enable AI assistants to securely SSH into your servers with persistent sessions, keepalive, automatic recovery, sandbox command testing, and user c
183 lines (158 loc) • 5.22 kB
JavaScript
const axios = require('axios');
const { Config } = require('./config');
const { ERROR_CODES, SECURITY, NETWORK } = require('./utils/constants');
const { ErrorHandler, NetworkError, SecurityError } = require('./utils/errors');
const logger = require('./utils/logger');
class APIClient {
constructor() {
// Use fallback URL from constants if environment variable not set
this.baseURL = process.env.SSHBRIDGE_API_URL || require('./utils/constants').API.BASE_URL;
if (!this.baseURL) {
throw new Error('SSHBRIDGE_API_URL environment variable must be set for security. No hardcoded endpoints allowed.');
}
// SECURITY: Validate URL format
try {
const url = new URL(this.baseURL);
if (!['http:', 'https:'].includes(url.protocol)) {
throw new Error('API URL must use HTTP or HTTPS protocol');
}
} catch (error) {
throw new Error(`Invalid API URL format: ${this.baseURL}`);
}
this.config = new Config();
}
// SECURITY: Enhanced request method with validation
async request(method, endpoint, data = null) {
// SECURITY: Validate endpoint to prevent SSRF
if (!this.isValidEndpoint(endpoint)) {
throw new Error('Invalid endpoint detected');
}
const headers = {
'Content-Type': 'application/json',
};
const apiKey = this.config.getApiKey();
if (apiKey) {
headers['Authorization'] = `Bearer ${apiKey}`;
}
try {
const response = await axios({
method,
url: `${this.baseURL}${endpoint}`,
headers,
data,
timeout: NETWORK.HTTP_TIMEOUT,
// SECURITY: Prevent redirects to external domains
maxRedirects: 0,
// SECURITY: Validate response status
validateStatus: (status) => status >= 200 && status < 300
});
return response.data;
} catch (error) {
if (error.response) {
throw new Error(error.response.data.message || 'API request failed');
} else if (error.request) {
throw new Error('Network error - please check your connection');
} else {
throw new Error('Request failed');
}
}
}
// SECURITY: Endpoint validation to prevent SSRF
isValidEndpoint(endpoint) {
if (!endpoint || typeof endpoint !== 'string') {
return false;
}
// Block dangerous endpoints
const dangerousEndpoints = [
/^\/\//, // Absolute URLs
/^https?:\/\//, // Full URLs
/\.\./, // Directory traversal
/\/etc\//, // System directories
/\/proc\//, // Process info
/\/sys\//, // System info
/\/dev\//, // Device files
/\/root\//, // Root directory
/\/var\//, // Variable data
/\/usr\// // User programs
];
return !dangerousEndpoints.some(pattern => pattern.test(endpoint));
}
async register(email) {
return this.request('POST', '/auth/register', { email });
}
async verifyEmail(email, code) {
return this.request('POST', '/auth/verify', { email, code });
}
async validateUsage() {
return this.request('GET', '/usage/validate');
}
async logUsage(server, command) {
return this.request('POST', '/usage/log', {
server,
command,
timestamp: new Date().toISOString()
});
}
async getUsage() {
return this.request('GET', '/usage');
}
async getUserStatus(email) {
try {
// First try to get usage stats (requires authentication)
const usage = await this.request('GET', '/usage');
return {
tier: usage.plan === 'pro' ? 'pro' : 'free',
serversUsed: usage.servers || 0,
commandsUsed: usage.used || 0,
serversLimit: usage.maxServers || 5,
commandsLimit: usage.limit || 50
};
} catch (error) {
// If user is not authenticated (no API key), return default free tier status
if (error.message.includes('401') ||
error.message.includes('Unauthorized') ||
error.message.includes('Network error') ||
!this.config.getApiKey()) {
return {
tier: 'free',
serversUsed: 0,
commandsUsed: 0,
serversLimit: 5,
commandsLimit: 50
};
}
// For other errors, re-throw
throw error;
}
}
async getServers() {
return this.request('GET', '/servers');
}
async upgradeAccount(plan) {
return this.request('POST', '/billing/upgrade', { plan });
}
async executeSSHCommand(host, username, command, options = {}) {
const payload = {
host,
username,
command,
port: options.port || 22,
private_key: options.private_key || null,
password: options.password || null
};
return this.request('POST', '/ssh/execute', payload);
}
async getExecutionToken(mode = 'hybrid') {
return this.request('POST', '/ssh/get-token', { mode });
}
async reportUsage(token, server, command, executionTime, success) {
return this.request('POST', '/ssh/report-usage', {
token,
server,
command,
execution_time: executionTime,
success
});
}
}
module.exports = { APIClient };