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.
426 lines (366 loc) • 10.6 kB
JavaScript
const crypto = require('crypto');
/**
* Rate Limiter for Security Operations
*
* Implements rate limiting for:
* - SSH connection attempts
* - Vault unlock attempts
* - Command execution
* - General API operations
*/
class RateLimiter {
constructor(options = {}) {
this.defaultWindow = options.defaultWindow || 60 * 1000; // 1 minute
this.maxAttempts = options.maxAttempts || 5;
this.blockDuration = options.blockDuration || 15 * 60 * 1000; // 15 minutes
// Storage for tracking attempts
this.attempts = new Map();
this.blocked = new Map();
// Cleanup interval - only create in production to avoid test interference
if (process.env.NODE_ENV !== 'test') {
this.cleanupInterval = setInterval(() => {
this.cleanup();
}, 5 * 60 * 1000); // Clean up every 5 minutes
}
}
/**
* Check if an operation is allowed
*/
isAllowed(identifier, operation = 'default', options = {}) {
const key = this.getKey(identifier, operation);
const now = Date.now();
// Check if currently blocked
if (this.isBlocked(key, now)) {
return {
allowed: false,
reason: 'rate_limited',
remainingTime: this.getRemainingBlockTime(key, now),
retryAfter: new Date(now + this.getRemainingBlockTime(key, now))
};
}
// Get current attempts
const attempts = this.attempts.get(key) || [];
const window = options.window || this.defaultWindow;
const maxAttempts = options.maxAttempts || this.maxAttempts;
// Filter attempts within current window
const recentAttempts = attempts.filter(timestamp =>
now - timestamp < window
);
// Check if limit exceeded
if (recentAttempts.length >= maxAttempts) {
// Block this identifier
this.block(key, now, options.blockDuration || this.blockDuration);
return {
allowed: false,
reason: 'limit_exceeded',
remainingTime: options.blockDuration || this.blockDuration,
retryAfter: new Date(now + (options.blockDuration || this.blockDuration))
};
}
return {
allowed: true,
remainingAttempts: maxAttempts - recentAttempts.length,
resetTime: new Date(now + window)
};
}
/**
* Record an attempt
*/
recordAttempt(identifier, operation = 'default', success = false) {
const key = this.getKey(identifier, operation);
const now = Date.now();
// Get or create attempts array
if (!this.attempts.has(key)) {
this.attempts.set(key, []);
}
const attempts = this.attempts.get(key);
// Add current attempt
attempts.push(now);
// If successful, clear failed attempts
if (success) {
this.clearAttempts(key);
}
// Clean up old attempts
this.cleanupAttempts(key, now);
}
/**
* Record a successful operation
*/
recordSuccess(identifier, operation = 'default') {
this.recordAttempt(identifier, operation, true);
}
/**
* Record a failed operation
*/
recordFailure(identifier, operation = 'default') {
this.recordAttempt(identifier, operation, false);
}
/**
* Check if an identifier is currently blocked
*/
isBlocked(key, now = Date.now()) {
const blockedInfo = this.blocked.get(key);
if (!blockedInfo) {
return false;
}
// Check if block has expired
if (now >= blockedInfo.until) {
this.blocked.delete(key);
return false;
}
return true;
}
/**
* Block an identifier
*/
block(key, now = Date.now(), duration = null) {
const blockDuration = duration || this.blockDuration;
this.blocked.set(key, {
until: now + blockDuration,
blockedAt: now,
duration: blockDuration
});
}
/**
* Unblock an identifier
*/
unblock(identifier, operation = 'default') {
const key = this.getKey(identifier, operation);
this.blocked.delete(key);
this.clearAttempts(key);
}
/**
* Get remaining block time
*/
getRemainingBlockTime(key, now = Date.now()) {
const blockedInfo = this.blocked.get(key);
if (!blockedInfo) {
return 0;
}
return Math.max(0, blockedInfo.until - now);
}
/**
* Clear attempts for an identifier
*/
clearAttempts(key) {
this.attempts.delete(key);
}
/**
* Clean up old attempts
*/
cleanupAttempts(key, now = Date.now()) {
const attempts = this.attempts.get(key);
if (!attempts) {
return;
}
// Remove attempts older than 1 hour
const cutoff = now - (60 * 60 * 1000);
const recentAttempts = attempts.filter(timestamp => timestamp > cutoff);
if (recentAttempts.length === 0) {
this.attempts.delete(key);
} else {
this.attempts.set(key, recentAttempts);
}
}
/**
* Clean up expired blocks and old attempts
*/
cleanup() {
const now = Date.now();
// Clean up expired blocks
for (const [key, blockedInfo] of this.blocked.entries()) {
if (now >= blockedInfo.until) {
this.blocked.delete(key);
}
}
// Clean up old attempts
for (const key of this.attempts.keys()) {
this.cleanupAttempts(key, now);
}
}
/**
* Generate a unique key for an identifier and operation
*/
getKey(identifier, operation) {
return `${operation}:${identifier}`;
}
/**
* Get statistics for an identifier
*/
getStats(identifier, operation = 'default') {
const key = this.getKey(identifier, operation);
const now = Date.now();
const attempts = this.attempts.get(key) || [];
const blockedInfo = this.blocked.get(key);
const recentAttempts = attempts.filter(timestamp =>
now - timestamp < this.defaultWindow
);
return {
identifier,
operation,
totalAttempts: attempts.length,
recentAttempts: recentAttempts.length,
isBlocked: this.isBlocked(key, now),
remainingBlockTime: this.getRemainingBlockTime(key, now),
resetTime: new Date(now + this.defaultWindow),
remainingAttempts: Math.max(0, this.maxAttempts - recentAttempts.length)
};
}
/**
* Get all statistics
*/
getAllStats() {
const stats = [];
// Get unique identifiers
const identifiers = new Set();
for (const key of this.attempts.keys()) {
const [, identifier] = key.split(':', 2);
identifiers.add(identifier);
}
for (const key of this.blocked.keys()) {
const [, identifier] = key.split(':', 2);
identifiers.add(identifier);
}
// Get stats for each identifier
for (const identifier of identifiers) {
stats.push(this.getStats(identifier));
}
return stats;
}
/**
* Reset all rate limiting for an identifier
*/
reset(identifier, operation = 'default') {
const key = this.getKey(identifier, operation);
this.blocked.delete(key);
this.clearAttempts(key);
}
/**
* Reset all rate limiting
*/
resetAll() {
this.attempts.clear();
this.blocked.clear();
}
/**
* Update configuration
*/
updateConfig(newConfig) {
if (newConfig.defaultWindow) {
this.defaultWindow = newConfig.defaultWindow;
}
if (newConfig.maxAttempts) {
this.maxAttempts = newConfig.maxAttempts;
}
if (newConfig.blockDuration) {
this.blockDuration = newConfig.blockDuration;
}
}
/**
* Get current configuration
*/
getConfig() {
return {
defaultWindow: this.defaultWindow,
maxAttempts: this.maxAttempts,
blockDuration: this.blockDuration,
totalTracked: this.attempts.size + this.blocked.size
};
}
/**
* Destroy the rate limiter and cleanup resources
*/
destroy() {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = null;
}
this.resetAll();
}
}
/**
* Specialized rate limiter for SSH operations
*/
class SSHRateLimiter extends RateLimiter {
constructor(options = {}) {
super({
defaultWindow: 5 * 60 * 1000, // 5 minutes for SSH
maxAttempts: 3, // 3 attempts per 5 minutes
blockDuration: 30 * 60 * 1000, // 30 minutes block
...options
});
this.sshOperations = ['connect', 'auth', 'command'];
}
/**
* Check if SSH connection is allowed
*/
isSSHAllowed(hostname, username, operation = 'connect') {
const identifier = `${username}@${hostname}`;
return this.isAllowed(identifier, `ssh_${operation}`, {
window: 5 * 60 * 1000, // 5 minutes
maxAttempts: 3, // 3 attempts
blockDuration: 30 * 60 * 1000 // 30 minutes block
});
}
/**
* Record SSH connection attempt
*/
recordSSHAttempt(hostname, username, operation = 'connect', success = false) {
const identifier = `${username}@${hostname}`;
this.recordAttempt(identifier, `ssh_${operation}`, success);
}
/**
* Record successful SSH connection
*/
recordSSHSuccess(hostname, username, operation = 'connect') {
this.recordSSHAttempt(hostname, username, operation, true);
}
/**
* Record failed SSH connection
*/
recordSSHFailure(hostname, username, operation = 'connect') {
this.recordSSHAttempt(hostname, username, operation, false);
}
}
/**
* Specialized rate limiter for vault operations
*/
class VaultRateLimiter extends RateLimiter {
constructor(options = {}) {
super({
defaultWindow: 15 * 60 * 1000, // 15 minutes for vault
maxAttempts: 5, // 5 attempts per 15 minutes
blockDuration: 60 * 60 * 1000, // 1 hour block
...options
});
}
/**
* Check if vault unlock is allowed
*/
isVaultUnlockAllowed(identifier = 'default') {
return this.isAllowed(identifier, 'vault_unlock', {
window: 15 * 60 * 1000, // 15 minutes
maxAttempts: 5, // 5 attempts
blockDuration: 60 * 60 * 1000 // 1 hour block
});
}
/**
* Record vault unlock attempt
*/
recordVaultUnlockAttempt(identifier = 'default', success = false) {
this.recordAttempt(identifier, 'vault_unlock', success);
}
/**
* Record successful vault unlock
*/
recordVaultUnlockSuccess(identifier = 'default') {
this.recordVaultUnlockAttempt(identifier, true);
}
/**
* Record failed vault unlock
*/
recordVaultUnlockFailure(identifier = 'default') {
this.recordVaultUnlockAttempt(identifier, false);
}
}
module.exports = { RateLimiter, SSHRateLimiter, VaultRateLimiter };