First commit merge

This commit is contained in:
kbe
2025-10-09 16:37:02 +02:00
parent 915ef14fc5
commit 5aae456ab3
20 changed files with 2584 additions and 2 deletions

View File

@@ -0,0 +1,142 @@
const logger = require('../utils/logger');
const RequestValidator = require('./validation');
const UserAgentFilter = require('./userAgentFilter');
/**
* Request Processing Pipeline - Equivalent to Kamailio's main request_route
* This is the primary entry point for all SIP requests received by Drachtio
*/
class RequestProcessor {
constructor(srf) {
this.srf = srf;
this.validator = new RequestValidator();
this.uaFilter = new UserAgentFilter();
}
/**
* Handle OPTIONS requests (equivalent to kamailio.cfg:297-301)
*/
handleOptions(req, res) {
logger.info('[OPTIONS] Options request from %s:%s | Call-ID: %s',
req.source_address, req.source_port, req.get('Call-ID'));
res.send(200, 'OK');
}
/**
* Handle CANCEL requests (equivalent to kamailio.cfg:196-201)
*/
handleCancel(req, res) {
const callId = req.get('Call-ID');
logger.info('[CANCEL] CANCEL: Call-ID: %s', callId);
// Check if transaction exists
if (this.srf.hasUacTransaction(callId)) {
// Relay the CANCEL to the appropriate destination
this.srf.createUacRequest('CANCEL', req.uri, {
headers: {
'Call-ID': callId,
'CSeq': req.get('CSeq'),
'From': req.get('From'),
'To': req.get('To'),
'Via': req.get('Via'),
'Route': req.get('Route'),
'Max-Forwards': req.get('Max-Forwards')
}
})
.then(() => {
res.send(200, 'OK');
})
.catch((err) => {
logger.error('[CANCEL] Failed to relay CANCEL: %s', err.message);
res.send(500, 'Server Error');
});
} else {
res.send(481, 'Transaction Does Not Exist');
}
}
/**
* Handle in-dialog requests (equivalent to kamailio.cfg:239-241)
*/
handleInDialog(req, res, next) {
if (req.has('To') && req.getParsedHeader('to').params.tag && req.method !== 'INVITE') {
logger.info('[WITHINDLG] %s request | Call-ID: %s', req.method, req.get('Call-ID'));
// Pass to in-dialog handler
next();
} else {
next();
}
}
/**
* Handle presence-related requests (equivalent to kamailio.cfg:304)
*/
handlePresence(req, res) {
if (req.method === 'PUBLISH' || req.method === 'SUBSCRIBE') {
logger.info('[PRESENCE] %s request from %s:%s | Call-ID: %s',
req.method, req.source_address, req.source_port, req.get('Call-ID'));
res.send(404, 'Not here');
return true;
}
return false;
}
/**
* Handle requests that don't match specific routes (equivalent to kamailio.cfg:312-315)
*/
handleUnmatchedRequest(req, res) {
logger.warn('[MAIN_ROUTE] No specific route found for method: %s to %s | Call-ID: %s',
req.method, req.uri.user, req.get('Call-ID'));
res.send(501, 'Not Implemented - No action found for destination or method');
}
/**
* Main request processing pipeline
*/
processRequest() {
return (req, res, next) => {
logger.debug('[REQUEST_PROCESSOR] Processing %s request from %s:%s',
req.method, req.source_address, req.source_port);
// Apply validation middleware
this.validator.middleware()(req, res, () => {
// Apply User-Agent filtering
this.uaFilter.middleware()(req, res, () => {
// Handle specific method types
switch (req.method) {
case 'OPTIONS':
this.handleOptions(req, res);
break;
case 'CANCEL':
this.handleCancel(req, res);
break;
case 'REGISTER':
// Pass to registration handler
next();
break;
case 'INVITE':
// Pass to INVITE handler
next();
break;
default:
// Handle in-dialog requests
this.handleInDialog(req, res, () => {
// Check for presence requests
if (!this.handlePresence(req, res)) {
// If no specific handler, return error
this.handleUnmatchedRequest(req, res);
}
});
break;
}
});
});
};
}
}
module.exports = RequestProcessor;

View File

@@ -0,0 +1,187 @@
const config = require('config');
const logger = require('../utils/logger');
/**
* User-Agent Filtering Middleware - Equivalent to Kamailio UA_FILTER route
* Provides security protection by filtering SIP requests based on User-Agent header
*/
class UserAgentFilter {
constructor() {
this.blockedPatterns = config.get('security.blockedUserAgents');
this.allowedPatterns = config.get('security.allowedUserAgents');
this.enabled = config.get('security.enableUAFilter');
}
/**
* Extract User-Agent header from SIP request
*/
getUserAgent(req) {
return req.get('User-Agent') || '';
}
/**
* Block requests with no User-Agent header (equivalent to kamailio.cfg:425-429)
*/
checkEmptyUserAgent(userAgent, req, res) {
if (!userAgent || userAgent.trim() === '') {
logger.warn('[UA_FILTER] Blocked request with empty User-Agent from %s:%s | Method: %s | Call-ID: %s',
req.source_address, req.source_port, req.method, req.get('Call-ID'));
res.send(403, 'Forbidden - User-Agent header required');
return true;
}
return false;
}
/**
* Block known malicious scanners (equivalent to kamailio.cfg:438-442)
*/
checkBlockedPatterns(userAgent, req, res) {
const userAgentLower = userAgent.toLowerCase();
for (const pattern of this.blockedPatterns) {
if (userAgentLower.includes(pattern.toLowerCase())) {
logger.warn('[UA_FILTER] Blocked malicious scanner User-Agent: \'%s\' from %s:%s | Method: %s | Call-ID: %s',
userAgent, req.source_address, req.source_port, req.method, req.get('Call-ID'));
res.send(403, 'Forbidden - Access denied');
return true;
}
}
return false;
}
/**
* Block User-Agents containing attack tool keywords (equivalent to kamailio.cfg:446-450)
*/
checkAttackToolPatterns(userAgent, req, res) {
const attackPatterns = [
'scanner', 'vicious', 'warvox', 'sipdic', 'sip-scan',
'brute', 'attack', 'exploit', 'flood', 'dos'
];
const userAgentLower = userAgent.toLowerCase();
for (const pattern of attackPatterns) {
if (userAgentLower.includes(pattern)) {
logger.warn('[UA_FILTER] Blocked attack tool User-Agent: \'%s\' from %s:%s | Method: %s | Call-ID: %s',
userAgent, req.source_address, req.source_port, req.method, req.get('Call-ID'));
res.send(403, 'Forbidden - Access denied');
return true;
}
}
return false;
}
/**
* Block User-Agents with only numbers/special characters (equivalent to kamailio.cfg:461-465)
*/
checkNumericOnlyUserAgent(userAgent, req, res) {
if (userAgent.length > 1 && /^[0-9\-\._\s]+$/.test(userAgent)) {
logger.warn('[UA_FILTER] Blocked numeric/special-char-only User-Agent: \'%s\' from %s:%s | Method: %s | Call-ID: %s',
userAgent, req.source_address, req.source_port, req.method, req.get('Call-ID'));
res.send(403, 'Forbidden - Invalid User-Agent');
return true;
}
return false;
}
/**
* Check allowlist for legitimate SIP clients (equivalent to kamailio.cfg:469-472)
*/
checkAllowlist(userAgent, req, res) {
const userAgentLower = userAgent.toLowerCase();
for (const pattern of this.allowedPatterns) {
if (userAgentLower.includes(pattern.toLowerCase())) {
logger.info('[UA_FILTER] Allowed legitimate SIP client: \'%s\' from %s:%s | Method: %s | Call-ID: %s',
userAgent, req.source_address, req.source_port, req.method, req.get('Call-ID'));
return true;
}
}
return false;
}
/**
* Check for carrier/provider infrastructure (equivalent to kamailio.cfg:477-480)
*/
checkCarrierPatterns(userAgent, req, res) {
const carrierPatterns = [
'carrier', 'provider', 'operator', 'core', 'gateway',
'trunk', 'pbx', 'sbc', 'session-border', 'voip'
];
const userAgentLower = userAgent.toLowerCase();
for (const pattern of carrierPatterns) {
if (userAgentLower.includes(pattern)) {
logger.info('[UA_FILTER] Allowed carrier/provider system: \'%s\' from %s:%s | Method: %s | Call-ID: %s',
userAgent, req.source_address, req.source_port, req.method, req.get('Call-ID'));
return true;
}
}
return false;
}
/**
* Log unknown but allowed User-Agents for monitoring (equivalent to kamailio.cfg:485)
*/
logUnknownUserAgent(userAgent, req) {
logger.info('[UA_FILTER] Unknown User-Agent allowed (for monitoring): \'%s\' from %s:%s | Method: %s | Call-ID: %s',
userAgent, req.source_address, req.source_port, req.method, req.get('Call-ID'));
}
/**
* Main filtering function that processes User-Agent checks
*/
filter(req, res, next) {
if (!this.enabled) {
return next();
}
const userAgent = this.getUserAgent(req);
// Check for empty User-Agent
if (this.checkEmptyUserAgent(userAgent, req, res)) {
return;
}
// Check blocked patterns
if (this.checkBlockedPatterns(userAgent, req, res)) {
return;
}
// Check attack tool patterns
if (this.checkAttackToolPatterns(userAgent, req, res)) {
return;
}
// Check numeric-only User-Agents
if (this.checkNumericOnlyUserAgent(userAgent, req, res)) {
return;
}
// Check allowlist
if (this.checkAllowlist(userAgent, req, res)) {
return next();
}
// Check carrier patterns
if (this.checkCarrierPatterns(userAgent, req, res)) {
return next();
}
// Unknown User-Agent - allow but log for monitoring
this.logUnknownUserAgent(userAgent, req);
next();
}
/**
* Middleware function
*/
middleware() {
return (req, res, next) => {
this.filter(req, res, next);
};
}
}
module.exports = UserAgentFilter;

128
middleware/validation.js Normal file
View File

@@ -0,0 +1,128 @@
const config = require('config');
const logger = require('../utils/logger');
/**
* Request Validation Middleware - Equivalent to Kamailio REQINIT route
* Performs initial security and validation checks on ALL incoming requests
*/
class RequestValidator {
constructor() {
this.maxForwards = config.get('security.maxForwards');
}
/**
* Validate Max-Forwards header (equivalent to kamailio.cfg:370-373)
* Prevents infinite routing loops and excessive hops
*/
validateMaxForwards(req, res, next) {
const maxForwards = req.get('Max-Forwards');
if (maxForwards === undefined || parseInt(maxForwards) <= 0) {
logger.warn('[REQINIT] Invalid Max-Forwards header from %s:%s', req.source_address, req.source_port);
res.send(483, 'Too Many Hops');
return;
}
// Decrement Max-Forwards (equivalent to mf_process_maxfwd_header)
req.set('Max-Forwards', parseInt(maxForwards) - 1);
next();
}
/**
* Basic SIP message validation (equivalent to kamailio.cfg:379-382)
* Validates required SIP headers and message structure
*/
validateSipMessage(req, res, next) {
// Check required headers
const requiredHeaders = ['From', 'To', 'Call-ID', 'CSeq', 'Via'];
for (const header of requiredHeaders) {
if (!req.get(header)) {
logger.warn('[REQINIT] Missing required header: %s from %s:%s', header, req.source_address, req.source_port);
res.send(400, 'Bad Request - Missing ' + header);
return;
}
}
// Validate Request-URI for non-REGISTER requests
if (req.method !== 'REGISTER' && (!req.uri || !req.uri.user)) {
logger.warn('[REQINIT] Invalid R-URI user part from %s:%s', req.source_address, req.source_port);
res.send(484, 'Address Incomplete');
return;
}
next();
}
/**
* Enhanced logging for incoming requests (equivalent to kamailio.cfg:157-169)
* Provides visibility into all SIP traffic
*/
logIncomingRequest(req, res, next) {
const method = req.method;
const sourceIp = req.source_address;
const sourcePort = req.source_port;
const callId = req.get('Call-ID');
const fromUser = req.getParsedHeader('from').uri.user;
const toUser = req.getParsedHeader('to').uri.user;
let logMessage;
switch (method) {
case 'REGISTER':
logMessage = `[REGISTER] Registration request from ${sourceIp}:${sourcePort} | User: ${fromUser} | Call-ID: ${callId}`;
logger.info(logMessage);
break;
case 'INVITE':
logMessage = `[INVITE] INVITE: ${fromUser} -> ${toUser} | Call-ID: ${callId}`;
logger.info(logMessage);
break;
case 'BYE':
logMessage = `[BYE] BYE: Call-ID: ${callId}`;
logger.info(logMessage);
break;
case 'ACK':
logMessage = `[ACK] ACK: Call-ID: ${callId}`;
logger.info(logMessage);
break;
case 'CANCEL':
logMessage = `[CANCEL] CANCEL: Call-ID: ${callId}`;
logger.info(logMessage);
break;
default:
logMessage = `[DEBUG] Other method: ${method} from ${sourceIp}:${sourcePort}`;
logger.debug(logMessage);
}
next();
}
/**
* Detect and handle retransmissions (equivalent to kamailio.cfg:219-231)
*/
handleRetransmissions(req, res, next) {
// This will be handled by Drachtio's built-in transaction management
// We just add logging for visibility
const callId = req.get('Call-ID');
const cseq = req.get('CSeq');
logger.debug('[RETRANS] Processing request: %s | Call-ID: %s | CSeq: %s', req.method, callId, cseq);
next();
}
/**
* Main middleware function that chains all validation steps
*/
middleware() {
return (req, res, next) => {
this.logIncomingRequest(req, res, () => {
this.handleRetransmissions(req, res, () => {
this.validateMaxForwards(req, res, () => {
this.validateSipMessage(req, res, next);
});
});
});
};
}
}
module.exports = RequestValidator;