404 lines
16 KiB
Lua
404 lines
16 KiB
Lua
local active = redis.call('GET', KEYS[1])
|
|
local source_ip = ARGV[1] or ''
|
|
local caller = ARGV[2] or ''
|
|
local raw_callee = ARGV[3] or ''
|
|
local call_id = ARGV[4] or ''
|
|
|
|
local prefix = active and active ~= '' and ('cfg:v:' .. active) or ''
|
|
local number_info = nil
|
|
|
|
local function field(json, name)
|
|
if not json then
|
|
return nil
|
|
end
|
|
return string.match(json, '"' .. name .. '":"([^"]*)"')
|
|
end
|
|
|
|
local function number_field(json, name)
|
|
if not json then
|
|
return nil
|
|
end
|
|
return string.match(json, '"' .. name .. '":([0-9]+)')
|
|
end
|
|
|
|
local function starts_with(value, prefix_value)
|
|
return prefix_value ~= '' and string.sub(value, 1, string.len(prefix_value)) == prefix_value
|
|
end
|
|
|
|
local function normalize_callee(value)
|
|
local digits = string.gsub(value or '', '[^0-9]', '')
|
|
if string.sub(digits, 1, 4) == '0086' then
|
|
digits = string.sub(digits, 5)
|
|
elseif string.sub(digits, 1, 2) == '86' and string.len(digits) == 13 then
|
|
digits = string.sub(digits, 3)
|
|
end
|
|
return digits
|
|
end
|
|
|
|
local function unknown_number(callee_digits)
|
|
return {
|
|
city_code = 'UNKNOWN',
|
|
city_name = 'UNKNOWN',
|
|
province_code = 'UNKNOWN',
|
|
province_name = 'UNKNOWN',
|
|
carrier = 'UNKNOWN',
|
|
number_type = 'UNKNOWN',
|
|
normalized = callee_digits or ''
|
|
}
|
|
end
|
|
|
|
local function with_geo_city(info)
|
|
if info.city_code == 'UNKNOWN' or info.province_code ~= 'UNKNOWN' then
|
|
return info
|
|
end
|
|
local city_json = redis.call('GET', prefix .. ':geo_city:' .. info.city_code)
|
|
if city_json then
|
|
info.province_code = field(city_json, 'provinceCode') or info.province_code
|
|
info.province_name = field(city_json, 'provinceName') or info.province_name
|
|
info.city_name = field(city_json, 'cityName') or info.city_name
|
|
end
|
|
return info
|
|
end
|
|
|
|
local function resolve_carrier(digits, fallback)
|
|
if string.len(digits) >= 4 then
|
|
local prefix4_json = redis.call('GET', prefix .. ':carrier_prefix:' .. string.sub(digits, 1, 4))
|
|
if prefix4_json then
|
|
return field(prefix4_json, 'carrier') or fallback
|
|
end
|
|
end
|
|
if string.len(digits) >= 3 then
|
|
local prefix3_json = redis.call('GET', prefix .. ':carrier_prefix:' .. string.sub(digits, 1, 3))
|
|
if prefix3_json then
|
|
return field(prefix3_json, 'carrier') or fallback
|
|
end
|
|
end
|
|
return fallback
|
|
end
|
|
|
|
local function resolve_number(value)
|
|
local digits = normalize_callee(value)
|
|
local info = unknown_number(digits)
|
|
|
|
if string.match(digits, '^1%d%d%d%d%d%d%d%d%d%d$') then
|
|
info.number_type = 'MOBILE'
|
|
local segment_json = redis.call('GET', prefix .. ':phone_segment:' .. string.sub(digits, 1, 7))
|
|
if segment_json then
|
|
info.city_code = field(segment_json, 'cityCode') or info.city_code
|
|
info.city_name = field(segment_json, 'cityName') or info.city_name
|
|
info.province_code = field(segment_json, 'provinceCode') or info.province_code
|
|
info.province_name = field(segment_json, 'provinceName') or info.province_name
|
|
info.carrier = field(segment_json, 'carrier') or info.carrier
|
|
end
|
|
info.carrier = resolve_carrier(digits, info.carrier)
|
|
return with_geo_city(info)
|
|
end
|
|
|
|
if string.sub(digits, 1, 1) == '0' and string.len(digits) >= 3 then
|
|
info.number_type = 'LANDLINE'
|
|
local area_json = nil
|
|
if string.len(digits) >= 4 then
|
|
area_json = redis.call('GET', prefix .. ':area_code:' .. string.sub(digits, 1, 4))
|
|
end
|
|
if not area_json and string.len(digits) >= 3 then
|
|
area_json = redis.call('GET', prefix .. ':area_code:' .. string.sub(digits, 1, 3))
|
|
end
|
|
if area_json then
|
|
info.city_code = field(area_json, 'cityCode') or info.city_code
|
|
info.city_name = field(area_json, 'cityName') or info.city_name
|
|
info.province_code = field(area_json, 'provinceCode') or info.province_code
|
|
info.province_name = field(area_json, 'provinceName') or info.province_name
|
|
end
|
|
return with_geo_city(info)
|
|
end
|
|
|
|
info.carrier = resolve_carrier(digits, info.carrier)
|
|
return info
|
|
end
|
|
|
|
local function ensure_number_info(callee_value)
|
|
if not number_info then
|
|
number_info = resolve_number(callee_value)
|
|
end
|
|
return number_info
|
|
end
|
|
|
|
local function hash_value(value)
|
|
local hash = 0
|
|
for index = 1, string.len(value or '') do
|
|
hash = (hash * 31 + string.byte(value, index)) % 2147483647
|
|
end
|
|
return hash
|
|
end
|
|
|
|
local function choose_landing_caller(vendor_gateway_id)
|
|
local pool = redis.call('LRANGE', prefix .. ':vendor_gateway:' .. vendor_gateway_id .. ':caller_rewrite_pool', 0, -1)
|
|
local total_weight = 0
|
|
local choices = {}
|
|
for _, item_json in ipairs(pool) do
|
|
if string.find(item_json, '"status":"ENABLED"', 1, true) or not string.find(item_json, '"status"', 1, true) then
|
|
local rewrite_caller = field(item_json, 'caller')
|
|
local weight = tonumber(number_field(item_json, 'weight') or '0') or 0
|
|
if rewrite_caller and rewrite_caller ~= '' and weight > 0 then
|
|
total_weight = total_weight + weight
|
|
choices[#choices + 1] = {rewrite_caller, total_weight}
|
|
end
|
|
end
|
|
end
|
|
if total_weight <= 0 then
|
|
return caller
|
|
end
|
|
local slot = (hash_value(call_id .. ':' .. vendor_gateway_id) % total_weight) + 1
|
|
for _, choice in ipairs(choices) do
|
|
if slot <= choice[2] then
|
|
return choice[1]
|
|
end
|
|
end
|
|
return choices[#choices][1]
|
|
end
|
|
|
|
local function result(decision, reason, gateway_id, version, line_group_id, policy_id, customer_id, vendor_id, vendor_gateway_id, host, port, real_callee, business_prefix_id, business_prefix, landing_caller, landing_callee)
|
|
local info = ensure_number_info(real_callee or raw_callee)
|
|
return {
|
|
decision,
|
|
reason,
|
|
gateway_id,
|
|
version,
|
|
line_group_id,
|
|
policy_id,
|
|
customer_id,
|
|
vendor_id,
|
|
vendor_gateway_id,
|
|
host,
|
|
port,
|
|
info.city_code,
|
|
info.city_name,
|
|
info.province_name,
|
|
info.carrier,
|
|
info.number_type,
|
|
info.province_code,
|
|
real_callee or raw_callee,
|
|
raw_callee,
|
|
business_prefix_id or 'none',
|
|
business_prefix or 'none',
|
|
landing_caller or caller,
|
|
landing_callee or info.normalized,
|
|
info.normalized ~= '' and info.normalized or 'none'
|
|
}
|
|
end
|
|
|
|
if not active or active == '' then
|
|
return result('reject', 'CONFIG_MISSING', 'none', 'no_active_version', 'none', 'none', 'no_active_version', 'none', 'none', 'none', 'none', raw_callee, 'none', 'none', caller, raw_callee)
|
|
end
|
|
|
|
local function customer_ok(gateway_id, gateway_json)
|
|
local customer_id = field(gateway_json, 'customerId')
|
|
if not customer_id then
|
|
return nil, 'CUSTOMER_MISSING', 'customer_id_missing'
|
|
end
|
|
local customer_json = redis.call('GET', prefix .. ':customer:' .. customer_id)
|
|
if not customer_json then
|
|
return nil, 'CUSTOMER_MISSING', 'customer_missing'
|
|
end
|
|
if not string.find(customer_json, '"status":"ENABLED"', 1, true) then
|
|
return nil, 'CUSTOMER_DISABLED', 'customer_disabled'
|
|
end
|
|
return customer_id, nil, nil
|
|
end
|
|
|
|
local function match_caller(gateway_id, gateway_json)
|
|
local mode = field(gateway_json, 'callerMatchMode') or 'ANY'
|
|
if mode == 'ANY' then
|
|
return true
|
|
end
|
|
if mode == 'PREFIXES' then
|
|
local prefixes = redis.call('LRANGE', prefix .. ':customer_gateway:' .. gateway_id .. ':caller_prefixes', 0, -1)
|
|
for _, caller_prefix in ipairs(prefixes) do
|
|
if starts_with(caller, caller_prefix) then
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function match_callee(gateway_id, gateway_json)
|
|
local mode = field(gateway_json, 'calleeMatchMode') or 'ANY'
|
|
if mode == 'ANY' then
|
|
return raw_callee, 'none', 'none'
|
|
end
|
|
if mode == 'BUSINESS_PREFIXES' then
|
|
local prefixes = redis.call('LRANGE', prefix .. ':customer_gateway:' .. gateway_id .. ':business_prefixes', 0, -1)
|
|
local best_prefix = ''
|
|
local best_prefix_id = 'none'
|
|
for _, item_json in ipairs(prefixes) do
|
|
local item_prefix = field(item_json, 'prefix') or ''
|
|
if starts_with(raw_callee, item_prefix) and string.len(item_prefix) > string.len(best_prefix) then
|
|
best_prefix = item_prefix
|
|
best_prefix_id = field(item_json, 'id') or 'none'
|
|
end
|
|
end
|
|
if best_prefix ~= '' then
|
|
return string.sub(raw_callee, string.len(best_prefix) + 1), best_prefix_id, best_prefix
|
|
end
|
|
end
|
|
return nil, 'none', 'none'
|
|
end
|
|
|
|
local function region_blocked(vendor_gateway_id)
|
|
local info = ensure_number_info(raw_callee)
|
|
if info.city_code ~= 'UNKNOWN' then
|
|
local city_blocked = redis.call('SISMEMBER', prefix .. ':vendor_gateway:' .. vendor_gateway_id .. ':blocked_city_codes', info.city_code)
|
|
if city_blocked == 1 then
|
|
return true
|
|
end
|
|
end
|
|
if info.province_code ~= 'UNKNOWN' then
|
|
local province_blocked = redis.call('SISMEMBER', prefix .. ':vendor_gateway:' .. vendor_gateway_id .. ':blocked_province_codes', info.province_code)
|
|
if province_blocked == 1 then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function first_vendor_route(line_group_id, real_callee)
|
|
local line_group_json = redis.call('GET', prefix .. ':line_group:' .. line_group_id)
|
|
if not line_group_json then
|
|
return nil, 'LINE_GROUP_MISSING'
|
|
end
|
|
if not string.find(line_group_json, '"status":"ENABLED"', 1, true) then
|
|
return nil, 'LINE_GROUP_DISABLED'
|
|
end
|
|
|
|
number_info = resolve_number(real_callee)
|
|
local normalized_callee = number_info.normalized
|
|
local route_callee = normalized_callee ~= '' and normalized_callee or real_callee
|
|
local items = redis.call('LRANGE', prefix .. ':line_group:' .. line_group_id .. ':items', 0, -1)
|
|
local skipped_region = false
|
|
for _, item_json in ipairs(items) do
|
|
if string.find(item_json, '"status":"ENABLED"', 1, true) then
|
|
local vendor_gateway_id = field(item_json, 'vendorGatewayId')
|
|
if vendor_gateway_id then
|
|
local vendor_gateway_json = redis.call('GET', prefix .. ':vendor_gateway:' .. vendor_gateway_id)
|
|
if vendor_gateway_json and string.find(vendor_gateway_json, '"status":"ENABLED"', 1, true) then
|
|
if region_blocked(vendor_gateway_id) then
|
|
skipped_region = true
|
|
else
|
|
local vendor_id = field(vendor_gateway_json, 'vendorId') or 'none'
|
|
local host = field(vendor_gateway_json, 'host') or 'none'
|
|
local port = number_field(vendor_gateway_json, 'port') or '5060'
|
|
local landing_prefix = field(vendor_gateway_json, 'landingCalleePrefix') or ''
|
|
local landing_callee = landing_prefix .. route_callee
|
|
local rewrite_caller = choose_landing_caller(vendor_gateway_id)
|
|
return {vendor_id, vendor_gateway_id, host, port, rewrite_caller, landing_callee}, 'OK'
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if skipped_region then
|
|
return nil, 'NO_VENDOR_ROUTE_REGION_BLOCKED'
|
|
end
|
|
return nil, 'NO_VENDOR_ROUTE'
|
|
end
|
|
|
|
local function legacy_policy_route(gateway_id, customer_id)
|
|
local policies = redis.call('LRANGE', prefix .. ':customer_gateway:' .. gateway_id .. ':policies', 0, -1)
|
|
for _, policy_json in ipairs(policies) do
|
|
if string.find(policy_json, '"status":"ENABLED"', 1, true) then
|
|
local caller_mode = field(policy_json, 'callerMode') or 'ANY'
|
|
local caller_value = field(policy_json, 'callerValue') or ''
|
|
local callee_mode = field(policy_json, 'calleeMode') or 'ANY'
|
|
local callee_value = field(policy_json, 'calleeValue') or ''
|
|
|
|
local caller_ok = caller_mode == 'ANY'
|
|
or (caller_mode == 'EQUALS' and caller == caller_value)
|
|
or (caller_mode == 'PREFIX' and starts_with(caller, caller_value))
|
|
local callee_ok = callee_mode == 'ANY'
|
|
or (callee_mode == 'EQUALS' and raw_callee == callee_value)
|
|
or (callee_mode == 'PREFIX' and starts_with(raw_callee, callee_value))
|
|
|
|
if caller_ok and callee_ok then
|
|
local policy_id = field(policy_json, 'id') or 'none'
|
|
local line_group_id = field(policy_json, 'lineGroupId') or 'none'
|
|
local vendor_route, route_reason = first_vendor_route(line_group_id, raw_callee)
|
|
if vendor_route then
|
|
return result('allow', 'OK', gateway_id, active, line_group_id, policy_id, customer_id, vendor_route[1], vendor_route[2], vendor_route[3], vendor_route[4], raw_callee, 'none', 'none', vendor_route[5], vendor_route[6])
|
|
end
|
|
return result('reject', route_reason, gateway_id, active, line_group_id, policy_id, customer_id, 'none', 'none', 'none', 'none', raw_callee, 'none', 'none', caller, raw_callee)
|
|
end
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local candidates = redis.call('LRANGE', prefix .. ':auth:ip:' .. source_ip .. ':gateways', 0, -1)
|
|
if #candidates == 0 then
|
|
local legacy_gateway_id = redis.call('GET', prefix .. ':auth:ip:' .. source_ip)
|
|
if legacy_gateway_id then
|
|
candidates = {legacy_gateway_id}
|
|
end
|
|
end
|
|
|
|
if #candidates == 0 then
|
|
return result('reject', 'AUTH_MISSING', 'none', active, 'none', 'none', 'no_auth_ip', 'none', 'none', 'none', 'none', raw_callee, 'none', 'none', caller, raw_callee)
|
|
end
|
|
|
|
local saw_gateway = false
|
|
local saw_customer = false
|
|
local saw_caller = false
|
|
local saw_business_prefix_required = false
|
|
|
|
for _, gateway_id in ipairs(candidates) do
|
|
local gateway_json = redis.call('GET', prefix .. ':customer_gateway:' .. gateway_id)
|
|
if gateway_json and string.find(gateway_json, '"status":"ENABLED"', 1, true) then
|
|
saw_gateway = true
|
|
local customer_id, customer_reason, customer_marker = customer_ok(gateway_id, gateway_json)
|
|
if customer_id then
|
|
saw_customer = true
|
|
if match_caller(gateway_id, gateway_json) then
|
|
saw_caller = true
|
|
if (field(gateway_json, 'calleeMatchMode') or 'ANY') == 'BUSINESS_PREFIXES' then
|
|
saw_business_prefix_required = true
|
|
end
|
|
local real_callee, business_prefix_id, business_prefix = match_callee(gateway_id, gateway_json)
|
|
if real_callee then
|
|
local line_group_id = field(gateway_json, 'lineGroupId')
|
|
if line_group_id and line_group_id ~= '' then
|
|
local vendor_route, route_reason = first_vendor_route(line_group_id, real_callee)
|
|
if vendor_route then
|
|
return result('allow', 'OK', gateway_id, active, line_group_id, 'single_gateway', customer_id, vendor_route[1], vendor_route[2], vendor_route[3], vendor_route[4], real_callee, business_prefix_id, business_prefix, vendor_route[5], vendor_route[6])
|
|
end
|
|
return result('reject', route_reason, gateway_id, active, line_group_id, 'single_gateway', customer_id, 'none', 'none', 'none', 'none', real_callee, business_prefix_id, business_prefix, caller, real_callee)
|
|
end
|
|
|
|
local legacy = legacy_policy_route(gateway_id, customer_id)
|
|
if legacy then
|
|
return legacy
|
|
end
|
|
return result('reject', 'LINE_GROUP_MISSING', gateway_id, active, 'none', 'single_gateway', customer_id, 'none', 'none', 'none', 'none', real_callee, business_prefix_id, business_prefix, caller, real_callee)
|
|
end
|
|
end
|
|
else
|
|
return result('reject', customer_reason, gateway_id, active, 'none', 'none', customer_marker, 'none', 'none', 'none', 'none', raw_callee, 'none', 'none', caller, raw_callee)
|
|
end
|
|
end
|
|
end
|
|
|
|
if not saw_gateway then
|
|
return result('reject', 'GATEWAY_MISSING', candidates[1] or 'none', active, 'none', 'none', 'gateway_missing', 'none', 'none', 'none', 'none', raw_callee, 'none', 'none', caller, raw_callee)
|
|
end
|
|
if not saw_customer then
|
|
return result('reject', 'CUSTOMER_MISSING', candidates[1] or 'none', active, 'none', 'none', 'customer_missing', 'none', 'none', 'none', 'none', raw_callee, 'none', 'none', caller, raw_callee)
|
|
end
|
|
if not saw_caller then
|
|
return result('reject', 'CALLER_PREFIX_NOT_MATCHED', candidates[1] or 'none', active, 'none', 'none', 'caller_prefix_not_matched', 'none', 'none', 'none', 'none', raw_callee, 'none', 'none', caller, raw_callee)
|
|
end
|
|
if saw_business_prefix_required then
|
|
return result('reject', 'BUSINESS_PREFIX_NOT_MATCHED', candidates[1] or 'none', active, 'none', 'none', 'business_prefix_not_matched', 'none', 'none', 'none', 'none', raw_callee, 'none', 'none', caller, raw_callee)
|
|
end
|
|
|
|
return result('reject', 'NO_POLICY', candidates[1] or 'none', active, 'none', 'none', 'no_gateway_match', 'none', 'none', 'none', 'none', raw_callee, 'none', 'none', caller, raw_callee)
|