ssh-bridge-ai
Version:
One Command Magic SSH with Invisible Analytics - Connect to any server instantly with 'sshbridge user@server'. Zero setup, zero friction, pure magic. Industry-standard security with behind-the-scenes business intelligence.
217 lines (186 loc) • 6.32 kB
JavaScript
const axios = require('axios');
const { Config } = require('./config');
const { ErrorHandler, NetworkError, ValidationError } = require('./utils/errors');
const { ValidationUtils } = require('./utils/validation');
const logger = require('./utils/logger');
const { API, AUTH, NETWORK } = require('./utils/constants');
class APIClient {
constructor() {
// Use environment variable for API URL
this.baseURL = process.env.SSHBRIDGE_API_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) {
let errorMessage = 'API request failed';
if (error.response.data) {
if (typeof error.response.data === 'string') {
errorMessage = error.response.data;
} else if (error.response.data.detail) {
errorMessage = error.response.data.detail;
} else if (error.response.data.message) {
errorMessage = error.response.data.message;
} else if (error.response.status === 422) {
errorMessage = 'Validation error - please check your input';
} else if (error.response.status === 400) {
errorMessage = 'Bad request - please check your input';
}
}
throw new Error(errorMessage);
} else if (error.request) {
throw new Error('Network error - please check your connection');
} else {
throw new Error(`Request failed: ${error.message}`);
}
}
}
// 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;
}
}
// New authentication methods
async checkUserExists(email) {
return this.request('POST', '/auth/check-user', { email });
}
async sendVerificationCode(email) {
return this.request('POST', '/auth/send-code', { email });
}
async createAccount(userData) {
return this.request('POST', '/auth/create-account', userData);
}
async verifyEmail(email, code) {
return this.request('POST', '/auth/verify', { email, code });
}
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 };