mirror of
https://github.com/jambonz/sbc-inbound.git
synced 2025-12-19 04:37:43 +00:00
Fixed issue when wild cards or regex is used in phone number for multiple carriers (#202)
* Fixed issue when wild cards or regex is used in phone number for multiple carriers https://github.com/jambonz/sbc-inbound/issues/201 * Implemanted the regex/pattern based logic when url is a sip_realm * Refactored getApplicationForDidAndCarrier as it is failing to parse other regex if first regex is invalid In gateway is defined at "all accounts" using the phone number's account_sid * Performance improvements * Exported getApplicationForDidAndCarriers method * Added additional unit test cases
This commit is contained in:
127
lib/db-utils.js
127
lib/db-utils.js
@@ -107,6 +107,22 @@ module.exports = (srf, logger) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the application for a given DID (phone number) and carrier combination.
|
||||
* First attempts an exact DID match, then falls back to pattern/wildcard matching
|
||||
* if no exact match is found. Pattern matching is done in order of pattern length
|
||||
* (longest/most specific first) and handles both regex and wildcard (*) patterns.
|
||||
*
|
||||
* @param {Object} req - The request object containing the called number
|
||||
* @param {string} voip_carrier_sid - The SID of the VoIP carrier
|
||||
* @returns {Promise<(string|Object|null)>} Returns either:
|
||||
* - application_sid (string) if appObject is false
|
||||
* - full application object if appObject is true
|
||||
* - null if no matching application is found
|
||||
* @throws {Error} Database errors or other unexpected errors
|
||||
*/
|
||||
// Refactored to handle invalid regular expressions gracefully as part of
|
||||
// https://github.com/jambonz/sbc-inbound/issues/201
|
||||
const getApplicationForDidAndCarrier = async(req, voip_carrier_sid) => {
|
||||
const did = normalizeDID(req.calledNumber) || 'anonymous';
|
||||
|
||||
@@ -117,17 +133,75 @@ module.exports = (srf, logger) => {
|
||||
|
||||
/* wildcard / regex match */
|
||||
const [r2] = await pp.query(sqlQueryAllDidsForCarrier, [voip_carrier_sid]);
|
||||
const match = r2
|
||||
const patterns = r2
|
||||
.filter((o) => o.number.match(/\D/)) // look at anything with non-digit characters
|
||||
.sort((a, b) => b.number.length - a.number.length) // prefer longest match
|
||||
.find((o) => did.match(new RegExp(o.number.endsWith('*') ? `${o.number.slice(0, -1)}\\d*` : o.number)));
|
||||
if (match) return match.application_sid;
|
||||
.sort((a, b) => b.number.length - a.number.length);
|
||||
for (const pattern of patterns) {
|
||||
try {
|
||||
const regexPattern = pattern.number.endsWith('*') ?
|
||||
`${pattern.number.slice(0, -1)}\\d*` :
|
||||
pattern.number;
|
||||
if (did.match(new RegExp(regexPattern))) {
|
||||
logger.debug({voip_carrier_sid, 'pattern':pattern.number, 'did':did}, 'Found a matching pattern');
|
||||
return pattern.application_sid;
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn({err, voip_carrier_sid, pattern: pattern.number}, 'Invalid regex pattern encountered - skipping');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
logger.debug({voip_carrier_sid, did}, 'No matching pattern found');
|
||||
return null;
|
||||
} catch (err) {
|
||||
logger.error({err}, 'getApplicationForDidAndCarrier');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Searches for a matching application across multiple carriers using pattern/wildcard matching.
|
||||
* This function is similar to getApplicationForDidAndCarrier but operates on multiple carriers
|
||||
* simultaneously for better performance. It searches through all patterns from the specified
|
||||
* carriers, ordered by pattern length (longest/most specific first).
|
||||
*
|
||||
* @param {Object} req - The request object containing the called number
|
||||
* @param {string} voip_carrier_sids - Comma-separated string of carrier SIDs, formatted for SQL IN clause
|
||||
* (e.g., "'carrier1','carrier2'")
|
||||
* @returns {Promise<Object|null>} Returns either:
|
||||
* - The matching phone_numbers record containing application_sid, voip_carrier_sid, and account_sid
|
||||
* - null if no matching pattern is found
|
||||
* @throws {Error} Database errors or other unexpected errors
|
||||
*/
|
||||
const getApplicationForDidAndCarriers = async(req, voip_carrier_sids) => {
|
||||
const did = normalizeDID(req.calledNumber) || 'anonymous';
|
||||
const sql = `SELECT * FROM phone_numbers WHERE voip_carrier_sid IN (${voip_carrier_sids})`;
|
||||
|
||||
try {
|
||||
/* wildcard / regex match */
|
||||
const [r2] = await pp.query(sql);
|
||||
const patterns = r2
|
||||
.filter((o) => o.number.match(/\D/)) // look at anything with non-digit characters
|
||||
.sort((a, b) => b.number.length - a.number.length);
|
||||
for (const pattern of patterns) {
|
||||
try {
|
||||
const regexPattern = pattern.number.endsWith('*') ?
|
||||
`${pattern.number.slice(0, -1)}\\d*` :
|
||||
pattern.number;
|
||||
if (did.match(new RegExp(regexPattern))) {
|
||||
logger.debug({'pattern':pattern.number, 'did':did}, 'Found a matching pattern');
|
||||
return pattern;
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn({err, pattern: pattern.number}, 'Invalid regex pattern encountered - skipping');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
logger.debug('No matching pattern found');
|
||||
return null;
|
||||
} catch (err) {
|
||||
logger.error({err}, 'getApplicationForDidAndCarriers');
|
||||
}
|
||||
};
|
||||
|
||||
const wasOriginatedFromCarrier = async(req) => {
|
||||
const failure = {fromCarrier: false};
|
||||
const uri = parseUri(req.uri);
|
||||
@@ -187,6 +261,28 @@ module.exports = (srf, logger) => {
|
||||
logger.debug({voip_carriers, sql, did}, 'looking up DID');
|
||||
|
||||
const [r] = await pp.query(sql);
|
||||
if (r.length === 0) {
|
||||
// if no matching phone number is found, find whether there are any applications having phone numbers
|
||||
// matching the regex pattern or wild card
|
||||
logger.debug({isDotDecimal},
|
||||
'Did not find a matching phone number, checking for applications with Regex or wildcard');
|
||||
const vc_sids = voip_carriers.map((m) => `'${m.voip_carrier_sid}'`).join(',');
|
||||
const application = await getApplicationForDidAndCarriers(req, vc_sids);
|
||||
if (application) {
|
||||
logger.debug({application}, 'sip_realm looking up DID: found application with Regex or wildcard ');
|
||||
const gateway = gateways.find((m) => m.voip_carrier_sid === application.voip_carrier_sid);
|
||||
// the gateway may belong to "all accounts", so we need to use the account_sid of the application
|
||||
gateway.account_sid = gateway.account_sid ? gateway.account_sid : application.account_sid;
|
||||
return {
|
||||
fromCarrier: true,
|
||||
gateway: gateway,
|
||||
service_provider_sid: a[0].service_provider_sid,
|
||||
account_sid: gateway.account_sid,
|
||||
application_sid: application.application_sid,
|
||||
account: a[0]
|
||||
};
|
||||
}
|
||||
}
|
||||
if (r.length > 1) {
|
||||
logger.info({r},
|
||||
'multiple carriers with the same gateway have the same number provisioned for the same account'
|
||||
@@ -289,9 +385,29 @@ module.exports = (srf, logger) => {
|
||||
const vc_sids = matches.map((m) => `'${m.voip_carrier_sid}'`).join(',');
|
||||
const sql = `SELECT * FROM phone_numbers WHERE number = '${did}' AND voip_carrier_sid IN (${vc_sids})`;
|
||||
logger.debug({matches, sql, did, vc_sids}, 'looking up DID');
|
||||
|
||||
const [r] = await pp.query(sql);
|
||||
if (0 === r.length) {
|
||||
// if no matching phone number is found, find whether there are any applications having phone numbers
|
||||
// matching the regex pattern or wild card
|
||||
logger.debug({isDotDecimal},
|
||||
'Did not find a matching phone number, checking for applications with Regex or wildcard');
|
||||
const vc_sids = matches.map((m) => `'${m.voip_carrier_sid}'`).join(',');
|
||||
const application = await getApplicationForDidAndCarriers(req, vc_sids);
|
||||
if (application) {
|
||||
logger.debug({application}, 'looking up DID: found application with Regex or wildcard');
|
||||
const gateway = matches.find((m) => m.voip_carrier_sid === application.voip_carrier_sid);
|
||||
// the gateway may belong to "all accounts", so we need to use the account_sid of the application
|
||||
const [a] = await pp.query(sqlAccountBySid, [application.account_sid]);
|
||||
return {
|
||||
fromCarrier: true,
|
||||
gateway: gateway,
|
||||
service_provider_sid: gateway.service_provider_sid,
|
||||
account_sid: application.account_sid,
|
||||
application_sid: application.application_sid,
|
||||
account: a[0],
|
||||
};
|
||||
}
|
||||
|
||||
/* came from a provisioned carrier, but the dialed number is not provisioned.
|
||||
check if we have an account with default routing of that carrier to an application
|
||||
*/
|
||||
@@ -364,6 +480,7 @@ module.exports = (srf, logger) => {
|
||||
return {
|
||||
wasOriginatedFromCarrier,
|
||||
getApplicationForDidAndCarrier,
|
||||
getApplicationForDidAndCarriers,
|
||||
getOutboundGatewayForRefer,
|
||||
getSPForAccount,
|
||||
getApplicationBySid
|
||||
|
||||
Reference in New Issue
Block a user