Files
drone-detector/server/middleware/ip-restriction.js
2025-09-15 07:53:10 +02:00

271 lines
8.5 KiB
JavaScript

/**
* IP Restriction Middleware
* Checks if the client IP is allowed access based on tenant configuration
*/
const { Tenant } = require('../models');
const MultiTenantAuth = require('./multi-tenant-auth');
class IPRestrictionMiddleware {
constructor() {
this.multiAuth = new MultiTenantAuth();
this.models = null; // For dependency injection in tests
}
/**
* Set models for dependency injection (used in tests)
* @param {Object} models - Models object
*/
setModels(models) {
this.models = models;
this.multiAuth.setModels && this.multiAuth.setModels(models);
}
/**
* Check if an IP address matches any pattern in the whitelist
* @param {string} clientIP - The client IP address
* @param {Array} whitelist - Array of IP addresses and CIDR blocks
* @returns {boolean} - True if IP is allowed
*/
isIPAllowed(clientIP, whitelist) {
if (!whitelist || !Array.isArray(whitelist) || whitelist.length === 0) {
return false; // Block access if no IPs are whitelisted
}
// Normalize IPv6-mapped IPv4 addresses
const normalizedIP = clientIP.replace(/^::ffff:/, '');
for (const allowedIP of whitelist) {
if (this.matchesPattern(normalizedIP, allowedIP.trim())) {
return true;
}
}
return false;
}
/**
* Check if an IP matches a pattern (IP address or CIDR block)
* @param {string} ip - The IP to check
* @param {string} pattern - IP address or CIDR block
* @returns {boolean} - True if IP matches pattern
*/
matchesPattern(ip, pattern) {
// Exact IP match
if (ip === pattern) {
return true;
}
// CIDR block matching
if (pattern.includes('/')) {
return this.isIPInCIDR(ip, pattern);
}
// Wildcard matching (e.g., 192.168.1.*)
if (pattern.includes('*')) {
const regex = new RegExp('^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '\\d+') + '$');
return regex.test(ip);
}
return false;
}
/**
* Check if an IP is within a CIDR block
* @param {string} ip - The IP to check
* @param {string} cidr - CIDR block (e.g., "192.168.1.0/24")
* @returns {boolean} - True if IP is in CIDR block
*/
isIPInCIDR(ip, cidr) {
try {
const [subnet, prefixLength] = cidr.split('/');
const prefix = parseInt(prefixLength, 10);
if (prefix < 0 || prefix > 32) {
return false;
}
const ipNum = this.ipToNumber(ip);
const subnetNum = this.ipToNumber(subnet);
const mask = (-1 << (32 - prefix)) >>> 0;
return (ipNum & mask) === (subnetNum & mask);
} catch (error) {
console.error('Error checking CIDR:', error);
return false;
}
}
/**
* Convert IP address to number
* @param {string} ip - IP address
* @returns {number} - IP as number
*/
ipToNumber(ip) {
return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) >>> 0;
}
/**
* Get client IP from request, considering proxy headers
* @param {Object} req - Express request object
* @returns {string} - Client IP address
*/
getClientIP(req) {
// Check various headers for real IP (in order of preference)
const possibleHeaders = [
'x-forwarded-for',
'x-real-ip',
'x-client-ip',
'cf-connecting-ip', // Cloudflare
'x-cluster-client-ip',
'x-forwarded',
'forwarded-for',
'forwarded'
];
for (const header of possibleHeaders) {
const ip = req.headers[header];
if (ip) {
// x-forwarded-for can contain multiple IPs, get the first one
const firstIP = ip.split(',')[0].trim();
if (this.isValidIP(firstIP)) {
return firstIP;
}
}
}
// Fallback to connection IP
return req.connection?.remoteAddress || req.socket?.remoteAddress || req.ip || 'unknown';
}
/**
* Basic IP validation
* @param {string} ip - IP address to validate
* @returns {boolean} - True if valid IP
*/
isValidIP(ip) {
const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
const ipv6Regex = /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
return ipv4Regex.test(ip) || ipv6Regex.test(ip);
}
/**
* Express middleware to check IP restrictions
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @param {Function} next - Next middleware function
*/
async checkIPRestriction(req, res, next) {
try {
// Ensure req.path exists
const path = req.path || req.url || '';
// Skip IP checking for health checks and internal requests
if (path === '/health' || path === '/api/health') {
return next();
}
// Skip IP restrictions for management routes - they have their own access controls
if (path.startsWith('/api/management/')) {
console.log('🔍 IP Restriction - Skipping for management route:', path);
return next();
}
// Skip IP restrictions for auth config - users need to see login form and get proper error
if (path === '/api/auth/config') {
console.log('🔍 IP Restriction - Skipping for auth config route');
return next();
}
console.log('🔍 IP Restriction Check - Path:', req.path, 'Method:', req.method);
// Determine tenant (check req.tenant first for test contexts)
let tenantId = req.tenant;
if (!tenantId) {
tenantId = await this.multiAuth.determineTenant(req);
}
console.log('🔍 IP Restriction - Determined tenant:', tenantId);
if (!tenantId) {
console.log('🔍 IP Restriction - No tenant found, skipping IP check');
// No tenant found, continue without IP checking
return next();
}
// Get tenant configuration
const TenantModel = this.models ? this.models.Tenant : Tenant;
const tenant = await TenantModel.findOne({
where: { slug: tenantId },
attributes: ['id', 'slug', 'ip_restriction_enabled', 'ip_whitelist', 'ip_restriction_message', 'updated_at']
});
if (!tenant) {
console.log('🔍 IP Restriction - Tenant not found in database:', tenantId);
return next();
}
console.log('🔍 IP Restriction - Tenant config (fresh from DB):', {
id: tenant.id,
slug: tenant.slug,
ip_restriction_enabled: tenant.ip_restriction_enabled,
ip_whitelist: tenant.ip_whitelist,
updated_at: tenant.updated_at
});
// Check if IP restrictions are enabled
if (!tenant.ip_restriction_enabled) {
console.log('🔍 IP Restriction - Restrictions disabled for tenant');
return next();
}
// Get client IP
const clientIP = this.getClientIP(req);
console.log('🔍 IP Restriction - Client IP:', clientIP);
console.log('🔍 IP Restriction - Request headers:', {
'x-forwarded-for': req.headers['x-forwarded-for'],
'x-real-ip': req.headers['x-real-ip'],
'remote-address': req.connection?.remoteAddress
});
// Parse allowed IPs (convert string to array)
let allowedIPs = [];
if (tenant.ip_whitelist) {
if (Array.isArray(tenant.ip_whitelist)) {
allowedIPs = tenant.ip_whitelist;
} else if (typeof tenant.ip_whitelist === 'string') {
allowedIPs = tenant.ip_whitelist.split(',').map(ip => ip.trim()).filter(ip => ip);
}
}
// Check if IP is allowed
const isAllowed = this.isIPAllowed(clientIP, allowedIPs);
console.log('🔍 IP Restriction - Is IP allowed:', isAllowed, 'Allowed IPs:', allowedIPs);
if (!isAllowed) {
console.log(`🚫 IP Access Denied: ${clientIP} attempted to access tenant "${tenantId}"`);
// Log the access attempt for security auditing
console.log(`[SECURITY AUDIT] ${new Date().toISOString()} - IP ${clientIP} denied access to tenant ${tenantId} - User-Agent: ${req.headers['user-agent']}`);
return res.status(403).json({
success: false,
message: tenant.ip_restriction_message || 'Access denied. Your IP address is not authorized to access this service.',
code: 'IP_RESTRICTED',
timestamp: new Date().toISOString()
});
}
// IP is allowed, continue
console.log(`✅ IP Access Allowed: ${clientIP} accessing tenant "${tenantId}"`);
next();
} catch (error) {
console.error('Error in IP restriction middleware:', error);
// In case of error, allow access but log the issue
next();
}
}
}
module.exports = IPRestrictionMiddleware;