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)