Files

1876 lines
68 KiB
Lua

#!/usr/bin/lua
-- Probe status:
-- 0 = reset / not tested
-- 1 = reachable
-- 2 = unlocked & region matches
-- 3 = unlocked but region regex mismatch
-- 4 = unlocked but region differs from cached old_region but not unlocked
local SYS = require "luci.sys"
local HTTP = require "luci.http"
local FS = require "luci.openclash"
local JSON = require "luci.jsonc"
local UA = FS.user_agent
local UA_SEC_CH_UA = FS.user_agent_ch_ua
local class_type = type
local type = arg[1]
local all_test = arg[2] == "all"
local check_only = arg[2] == "check"
local result_output = arg[2] == "result"
math.randomseed(os.time())
local status, ip, port, passwd, group_match_name
local tested_set = {}
local proxy_index = {}
local group_set = {}
local config_cache = {}
local unlock_cache_file = "/etc/openclash/history/streaming_unlock_cache"
local unlock_cache_info = {}
-- MSG holds every fixed log fragment. Fragments outside the 【】 markers are
-- translated through exact msgid matches by luci-app-openclash trans_line, so
-- wording and spacing must stay byte-identical with the po files.
local MSG = {
info = " [Info] ",
error = " [Error] ",
err_no_type = "Streaming Unlock Has No Parameter of Type, Exiting...",
err_router_self_proxy = "Streaming Unlock Could not Work Because of Router-Self Proxy Disabled, Exiting...",
err_multiple_scripts = "Multiple Scripts Running, Exiting...",
err_network = "Network Anomaly, Suspend Unlock Detection...",
no_group_find = "failed to search based on keywords and automatically obtain the group, please confirm the validity of the regex!",
full_support = "full support, area:",
full_support_no_area = "full support",
area = ", area:",
only_original = "only support homemade!",
no_unlock = "not support unlock!",
test_failed = "unlock test failed!",
select_success = "unlock node auto selected successfully, the current selected is",
select_failed = "unlock node auto selected failed, no node available, rolled back to the",
select_failed_other_region = "unlock node auto selected failed, no node match the regex, rolled back to other full support node",
select_success_no_old_region = "unlock node auto selected successfully, no node match the old region, rolled back to other full support node",
select_all_full_support = "unlock node test finished, rolled back to the full support node",
select_all_other_region = "unlock node test finished, no node match the regex, rolled back to other full support node",
select_all_no_old_region = "unlock node test finished, no node match the old region, rolled back to other full support node",
select_all_failed = "unlock node test finished, no node available, rolled back to the",
original_no_select = "only support homemade! the type of group is not select, auto select could not work!",
no_unlock_no_select = "not support unlock! the type of group is not select, auto select could not work!",
failed_no_select = "unlock test failed! the type of group is not select, auto select could not work!",
original_test_start = "only support homemade! start auto select unlock proxy...",
no_unlock_test_start = "not support unlock! start auto select unlock proxy...",
failed_test_start = "unlock test failed! start auto select unlock proxy...",
other_region_unlock_test = ", but not match the regex!",
other_region_unlock_test_start = ", but not match the regex! start auto select unlock proxy...",
other_region_unlock_no_select = ", but not match the regex! the type of group is not select, auto select could not work!",
no_old_region_unlock_test = "full support but not match the old region!",
no_old_region_unlock_old_region = ", but not match the old region:",
no_old_region_unlock_old_region_no_select = ", the type of group is not select, auto select could not work!",
no_old_region_unlock_no_select = "but not match the old region! the type of group is not select, auto select could not work!",
no_nodes_filter = "no nodes name match the regex!",
cached_nodes = "Cached Compliant Nodes Number:",
cached_nodes_excluded = ", Cached Non-compliant Nodes Number:",
cached_nodes_priority = ", Prioritize Testing With Cached Compliant Nodes...",
}
local function log_prefix()
return os.date("%Y-%m-%d %H:%M:%S")..MSG.info
end
local function log_info(text)
print(log_prefix()..text)
end
local function log_error(text)
print(os.date("%Y-%m-%d %H:%M:%S")..MSG.error..text)
end
local function get_config(key)
if key == nil then
return nil
end
local value = config_cache[key]
if value == nil then
value = FS.uci_get_config("config", key)
if value == nil then
value = false
end
config_cache[key] = value
end
if value == false then
return nil
end
return value
end
local result_st, result_region, result_node
local function probe_status(stat, region)
local st = 0
if stat == 2 or stat == 3 or stat == 4 then
st = 2
elseif stat == 1 then
st = 1
end
if st == 2 and (region == nil or region == "") then
st = 0
end
return st
end
local function print_result(st, region)
print(JSON.stringify({ service = type or "", st = st, region = region or "", node = result_node or "" }))
end
local function record_result(stat, region)
result_st = probe_status(stat, region)
result_region = region or ""
end
local unlock_cache = FS.readfile(unlock_cache_file)
if unlock_cache then
unlock_cache_info = JSON.parse(unlock_cache) or {}
end
local function bail(message)
if check_only then
print_result(0, "")
else
log_error(message)
end
os.exit(0)
end
if not type then
bail(MSG.err_no_type)
end
MSG.group = "【"..type.."】Group:"
local self_status = SYS.exec(string.format('ps -w |grep -v grep |grep "openclash_streaming_unlock.lua %s" |grep -vc " check"', type))
local select_logic = get_config("stream_auto_select_logic") or "urltest"
if (tonumber(get_config("router_self_proxy")) or 1) == 0 then
bail(MSG.err_router_self_proxy)
elseif tonumber(self_status) > 1 and not check_only then
bail(MSG.err_multiple_scripts)
end
-- One row per streaming service: setting keys, connection sniff site and host.
local streams = {
["Netflix"] = { group_key = "stream_auto_select_group_key_netflix", group_default = "netflix|奈飞", region_key = "stream_auto_select_region_key_netflix", node_key = "stream_auto_select_node_key_netflix", site = "https://www.netflix.com", host = "www%.netflix%.com" },
["Disney Plus"] = { group_key = "stream_auto_select_group_key_disney", group_default = "disney|迪士尼", region_key = "stream_auto_select_region_key_disney", node_key = "stream_auto_select_node_key_disney", site = "https://www.disneyplus.com", host = "www%.disneyplus%.com" },
["HBO Max"] = { group_key = "stream_auto_select_group_key_hbo_max", group_default = "hbo|hbomax|hbo max", region_key = "stream_auto_select_region_key_hbo_max", node_key = "stream_auto_select_node_key_hbo_max", site = "https://www.max.com", host = "www%.max%.com" },
["YouTube Premium"] = { group_key = "stream_auto_select_group_key_ytb", group_default = "youtube|油管", region_key = "stream_auto_select_region_key_ytb", node_key = "stream_auto_select_node_key_ytb", site = "https://m.youtube.com/premium", host = "m%.youtube%.com" },
["TVB Anywhere+"] = { group_key = "stream_auto_select_group_key_tvb_anywhere", group_default = "tvb", region_key = "stream_auto_select_region_key_tvb_anywhere", node_key = "stream_auto_select_node_key_tvb_anywhere", site = "https://www.tvbanywhere.com/img/tvb/vip_purchase.png", host = "www%.tvbanywhere%.com" },
["Amazon Prime Video"] = { group_key = "stream_auto_select_group_key_prime_video", group_default = "prime video|amazon", region_key = "stream_auto_select_region_key_prime_video", node_key = "stream_auto_select_node_key_prime_video", site = "https://www.primevideo.com", host = "www%.primevideo%.com" },
["DAZN"] = { group_key = "stream_auto_select_group_key_dazn", group_default = "dazn", region_key = "stream_auto_select_region_key_dazn", node_key = "stream_auto_select_node_key_dazn", site = "https://www.dazn.com", host = "www%.dazn%.com" },
["Paramount Plus"] = { group_key = "stream_auto_select_group_key_paramount_plus", group_default = "paramount", region_key = "stream_auto_select_region_key_paramount_plus", node_key = "stream_auto_select_node_key_paramount_plus", site = "https://www.paramountplus.com/", host = "www%.paramountplus%.com" },
["Discovery Plus"] = { group_key = "stream_auto_select_group_key_discovery_plus", group_default = "discovery", region_key = "stream_auto_select_region_key_discovery_plus", node_key = "stream_auto_select_node_key_discovery_plus", site = "https://www.discoveryplus.com/", host = "www%.discoveryplus%.com" },
["Bilibili"] = { group_key = "stream_auto_select_group_key_bilibili", group_default = "bilibili", region_key = "stream_auto_select_region_key_bilibili", node_key = "stream_auto_select_node_key_bilibili", site = "https://www.bilibili.com/", host = "www%.bilibili%.com" },
["Google"] = { group_key = "stream_auto_select_group_key_google_not_cn", group_default = "google|谷歌", node_key = "stream_auto_select_node_key_google_not_cn", site = "https://timeline.google.com", host = "timeline%.google%.com" },
["OpenAI"] = { group_key = "stream_auto_select_group_key_openai", group_default = "OpenAI|ChatGPT", region_key = "stream_auto_select_region_key_openai", node_key = "stream_auto_select_node_key_openai", site = "https://chatgpt.com/", host = "chatgpt%.com" },
["Claude"] = { group_key = "stream_auto_select_group_key_claude", group_default = "Claude", region_key = "stream_auto_select_region_key_claude", node_key = "stream_auto_select_node_key_claude", site = "https://claude.ai/", host = "claude%.ai" },
["Gemini"] = { group_key = "stream_auto_select_group_key_gemini", group_default = "Gemini", region_key = "stream_auto_select_region_key_gemini", node_key = "stream_auto_select_node_key_gemini", site = "https://gemini.google.com/", host = "gemini%.google%.com" },
["Bahamut Anime"] = { group_key = "stream_auto_select_group_key_bahamut", group_default = "bahamut|巴哈", region_key = "stream_auto_select_region_key_bahamut", node_key = "stream_auto_select_node_key_bahamut", site = "https://ani.gamer.com.tw/", host = "ani%.gamer%.com%.tw" },
["Spotify"] = { group_key = "stream_auto_select_group_key_spotify", group_default = "spotify", region_key = "stream_auto_select_region_key_spotify", node_key = "stream_auto_select_node_key_spotify", site = "https://www.spotify.com", host = "www%.spotify%.com" },
["Steam"] = { group_key = "stream_auto_select_group_key_steam", group_default = "steam", region_key = "stream_auto_select_region_key_steam", node_key = "stream_auto_select_node_key_steam", site = "https://store.steampowered.com/app/761830", host = "store%.steampowered%.com" },
}
local function stream_config(field)
local stream = streams[type]
return stream and stream[field]
end
local function get_region_regex()
return get_config(stream_config("region_key"))
end
local function get_node_regex()
return get_config(stream_config("node_key")) or ""
end
-- Pattern fast path: ASCII-only case folding matches ruby /i exactly, and CJK/symbol
-- code points (>= U+3000) are case-invariant, so those patterns can be matched literally.
local function has_case_mapping(text)
local i = 1
while i <= #text do
local b = text:byte(i)
local code
if b < 0x80 then
code = b
i = i + 1
elseif b >= 0xF0 then
local b1, b2, b3 = text:byte(i + 1), text:byte(i + 2), text:byte(i + 3)
if not (b1 and b2 and b3) then
return true
end
code = (b % 0x08) * 0x40000 + (b1 % 0x40) * 0x1000 + (b2 % 0x40) * 0x40 + b3 % 0x40
i = i + 4
elseif b >= 0xE0 then
local b1, b2 = text:byte(i + 1), text:byte(i + 2)
if not (b1 and b2) then
return true
end
code = (b % 0x10) * 0x1000 + (b1 % 0x40) * 0x40 + b2 % 0x40
i = i + 3
elseif b >= 0xC0 then
local b1 = text:byte(i + 1)
if not b1 then
return true
end
code = (b % 0x20) * 0x40 + b1 % 0x40
i = i + 2
else
return true
end
if code >= 0x80 and (code < 0x3000 or (code >= 0xFF00 and code <= 0xFFEF)) then
return true
end
end
return false
end
local regex_meta = { ".", "^", "$", "*", "+", "?", "(", ")", "[", "]", "{", "}", "\\" }
local function build_plain_pattern(regex)
local first = regex:sub(1, 1)
local last = regex:sub(-1)
if first == "^" and last == "$" and #regex > 2 then
local inner = regex:sub(2, -2)
if inner ~= "" and not inner:find("|", 1, true) then
local ok = true
for _, meta in ipairs(regex_meta) do
if inner:find(meta, 1, true) then
ok = false
break
end
end
if ok and not has_case_mapping(inner) then
return { exact = string.lower(inner) }
end
end
return nil
end
if first == "|" or last == "|" or regex:find("||", 1, true) then
return nil
end
for _, meta in ipairs(regex_meta) do
if regex:find(meta, 1, true) then
return nil
end
end
if has_case_mapping(regex) then
return nil
end
local parts = {}
for part in regex:gmatch("[^|]+") do
parts[#parts + 1] = string.lower(part)
end
return { parts = parts }
end
local pattern_cache = {}
local function shell_quote(value)
return "'" .. tostring(value):gsub("'", "'\\''") .. "'"
end
local function datamatch(data, regex)
if not data or not regex then
return false
end
if regex == "" then
return true
end
local pattern = pattern_cache[regex]
if pattern == nil then
pattern = build_plain_pattern(regex) or false
pattern_cache[regex] = pattern
end
if pattern then
if pattern.exact then
for line in (string.lower(data) .. "\n"):gmatch("([^\n]*)\n") do
if line == pattern.exact then
return true
end
end
return false
end
local target = string.lower(data)
for i = 1, #pattern.parts do
if target:find(pattern.parts[i], 1, true) then
return true
end
end
return false
end
local result = SYS.exec(string.format("OC_MATCH_TARGET=%s OC_MATCH_PATTERN=%s ruby -E UTF-8 -e 'print((ENV[\"OC_MATCH_TARGET\"] =~ Regexp.new(ENV[\"OC_MATCH_PATTERN\"], Regexp::IGNORECASE)) ? \"true\" : \"false\")'", shell_quote(data), shell_quote(regex)))
return result == "true"
end
local function get_cache_value(key, stream_type)
local entry = unlock_cache_info[stream_type or type]
local value = entry and entry[key]
if value == nil or value == false then
return ""
end
return value
end
local function get_old_region(stream_type)
return get_cache_value("old_region", stream_type)
end
local function get_old_regex(stream_type)
return get_cache_value("old_regex", stream_type)
end
local function probe_context()
return get_region_regex() or "", get_old_region(), get_old_regex()
end
local function write_cache(stream_type, node, value)
if not value then
value = ""
end
if not unlock_cache_info[stream_type] then
unlock_cache_info[stream_type] = {}
end
unlock_cache_info[stream_type][node] = value
end
local function delete_cache(stream_type, node)
if unlock_cache_info[stream_type] and unlock_cache_info[stream_type][node] then
unlock_cache_info[stream_type][node] = nil
end
end
local function is_group(name)
return name ~= nil and group_set[name] == true
end
local function is_tested(name)
return name ~= nil and tested_set[name] == true
end
local function mark_tested(name)
if name ~= nil then
tested_set[name] = true
end
end
local function build_proxy_index(info)
proxy_index = {}
group_set = {}
for _, value in pairs(info.proxies) do
proxy_index[value.name] = value
if value.all then
group_set[value.name] = true
end
end
end
local function get_group_now(group)
local now
local name = group
local visited = {}
while group_set[name] and not visited[name] do
visited[name] = true
local value = proxy_index[name]
if not value then
break
end
now = value.now
name = value.now
end
return now or group
end
local function get_proxy(group, name)
--group maybe a proxy
local ctx = { show = "", name = "", now = nil, type = nil, nodes = {} }
local expand_group = tonumber(get_config("stream_auto_select_expand_group")) or 0
if expand_group == 1 then
if group_set[group] then
local visited = {}
while group_set[group] and not visited[group] do
visited[group] = true
local value = proxy_index[group]
if not value then
break
end
if ctx.show ~= "" then
ctx.show = ctx.show .. " ➟ " .. group
else
if name == group then
ctx.show = group
else
ctx.show = name .. " ➟ " .. group
end
end
ctx.name = group
group = value.now
ctx.now = value.now or ctx.name
ctx.nodes = value.all
ctx.type = value.type
end
if ctx.type ~= "Selector" then
local value = proxy_index[name]
if value then
ctx.name = name
ctx.nodes = {}
table.insert(ctx.nodes, group)
end
end
else
local value = proxy_index[name]
if value then
ctx.show = name
ctx.name = name
ctx.now = value.now or name
table.insert(ctx.nodes, group)
ctx.type = value.type
end
end
else
if group_set[group] then
local value = proxy_index[name]
if value then
ctx.name = name
table.insert(ctx.nodes, group)
ctx.type = value.type
end
local visited = {}
while group_set[group] and not visited[group] do
visited[group] = true
local target = proxy_index[group]
if not target then
break
end
if ctx.show ~= "" then
ctx.show = ctx.show .. " ➟ " .. group
else
if name == group then
ctx.show = group
else
ctx.show = name .. " ➟ " .. group
end
end
ctx.now = target.now or group
group = target.now
end
else
local value = proxy_index[name]
if value then
table.insert(ctx.nodes, group)
ctx.now = value.now or name
ctx.show = name
ctx.name = name
ctx.type = value.type
end
end
end
return ctx
end
local function nodes_filter(t)
if t == nil then
return
end
local tab = {}
local regex = get_node_regex()
if class_type(t) == "table" then
if regex == "" then
return t
end
for n = 1, #t do
if is_group(t[n]) then
if datamatch(get_group_now(t[n]), regex) then
table.insert(tab, t[n])
end
elseif datamatch(t[n], regex) then
table.insert(tab, t[n])
end
end
else
if regex == "" then
table.insert(tab, t)
return tab
end
if is_group(t) then
if datamatch(get_group_now(t), regex) then
table.insert(tab, t)
end
elseif datamatch(t, regex) then
table.insert(tab, t)
end
end
return tab
end
local function urlencode(data)
if not data then
return
end
return HTTP.urlencode(data) or data
end
local function table_rand(t, d)
if t == nil then
return
end
local tab = {}
while #t ~= 0 do
local n = math.random(0, #t)
if t[n] ~= nil then
if type == "YouTube Premium" and get_old_region("Google") == t[n] then
table.insert(tab, 1, t[n])
elseif d ~= nil and is_group(d) and d == t[n] then
table.insert(tab, 1, t[n])
else
table.insert(tab, t[n])
end
table.remove(t, n)
end
end
return tab
end
local function table_sort_by_urltest(t, d)
local tab = {}
local result = {}
if t == nil then
return
end
local providers = SYS.exec(string.format('curl -sL -m 5 --retry 2 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -XGET http://%s:%s/providers/proxies', passwd, ip, port))
if providers then
providers = JSON.parse(providers)
if not providers or not providers.providers then return t end
end
-- Provider nodes are not listed by /proxies, their delay history comes from /providers/proxies,
-- while inline nodes and groups carry their own history in the /proxies snapshot.
local delay_map = {}
for name, proxy in pairs(proxy_index) do
if proxy.history and #(proxy.history) ~= 0 and proxy.history[#(proxy.history)].delay ~= 0 then
delay_map[name] = proxy.history[#(proxy.history)].delay
end
end
for _, provider in pairs(providers.providers) do
if provider.proxies and provider.name ~= "default" then
for _, proxy in pairs(provider.proxies) do
if delay_map[proxy.name] == nil and proxy.history and #(proxy.history) ~= 0 and proxy.history[#(proxy.history)].delay ~= 0 then
delay_map[proxy.name] = proxy.history[#(proxy.history)].delay
end
end
end
end
local delay_url
local urltest_address_mod = get_config("urltest_address_mod")
if urltest_address_mod and urltest_address_mod ~= "0" then
delay_url = urltest_address_mod
else
delay_url = "http://www.gstatic.com/generate_204"
end
for n = 1, #(t) do
local delay = delay_map[t[n]]
if not delay and (is_group(t[n]) or t[n] == "DIRECT") then
local group_delay = SYS.exec(string.format('curl -sL -m 60 --retry 2 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -XGET "http://%s:%s/proxies/%s/delay?timeout=5000&url=%s"', passwd, ip, port, urlencode(t[n]), urlencode(delay_url)))
if group_delay then
group_delay = JSON.parse(group_delay)
end
if group_delay and group_delay.delay and group_delay.delay ~= 0 then
delay = group_delay.delay
end
end
table.insert(tab, {t[n], delay or 123456})
end
table.sort(tab, function(a, b)
return a[2] < b[2]
end)
for _, value in ipairs(tab) do
if type == "YouTube Premium" and get_old_region("Google") == value[1] then
table.insert(result, 1, value[1])
elseif d ~= nil and is_group(d) and d == value[1] then
table.insert(result, 1, value[1])
else
table.insert(result, value[1])
end
end
return result
end
local function table_sort_by_cache(t)
local tab = {}
local tab_b = {}
local old_region = get_old_region()
for n = 1, #(t) do
if unlock_cache_info and unlock_cache_info[type] and unlock_cache_info[type][t[n]] and unlock_cache_info[type][t[n]] == old_region then
table.insert(tab, t[n])
else
table.insert(tab_b, t[n])
end
end
if #tab > 0 then
print(log_prefix().."【"..group_match_name.."】"..MSG.cached_nodes.."【"..#(tab).."】"..MSG.cached_nodes_excluded.."【"..#(tab_b).."】"..MSG.cached_nodes_priority)
end
for _, v in ipairs(tab_b) do table.insert(tab, v) end
return tab
end
local function set_selected(group, node)
result_node = node or ""
SYS.exec(string.format("curl -sL -m 5 --retry 2 -w %%{http_code} -o /dev/null -H 'Authorization: Bearer %s' -H 'Content-Type:application/json' -X PUT -d '{\"name\":\"%s\"}' http://%s:%s/proxies/%s", passwd, node, ip, port, urlencode(group)))
end
local function get_auth_info()
port = get_config("cn_port")
passwd = get_config("dashboard_password") or ""
ip = FS.lanip(true)
if not ip or not port then
os.exit(0)
end
end
local function close_connections()
local enable = tonumber(get_config("stream_auto_select_close_con")) or 1
if enable == 0 then return end
local con = SYS.exec(string.format('curl -sL -m 5 --retry 2 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -XGET http://%s:%s/connections', passwd, ip, port))
if con then
con = JSON.parse(con)
end
if not con or not con.connections then
return
end
local group_cons_id = {}
for i = 1, #(con.connections) do
local chains = con.connections[i].chains
if chains and #chains ~= 0 and chains[#chains] == group_match_name then
table.insert(group_cons_id, (con.connections[i].id))
end
end
--close connections
for i = 1, #(group_cons_id) do
SYS.exec(string.format('curl -sL -m 5 --retry 2 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -X DELETE http://%s:%s/connections/%s >/dev/null 2>&1 &', passwd, ip, port, group_cons_id[i]))
end
end
local function finish_region_test(region, regex, old_region, old_regex)
if not datamatch(region, regex) then
status = 3
elseif old_regex ~= regex and not all_test then
status = 2
elseif old_region ~= "" and region ~= old_region and not all_test then
status = 4
end
if status == 2 and not all_test and region ~= old_region then
write_cache(type, "old_region", region)
end
if status == 2 and not all_test and regex ~= old_regex then
write_cache(type, "old_regex", regex)
end
end
-- The service probes are defined at the end of the file, the entries below resolve them at call time
local netflix_unlock_test, disney_unlock_test, hbo_max_unlock_test, ytb_unlock_test,
tvb_anywhere_unlock_test, prime_video_unlock_test, dazn_unlock_test, paramount_plus_unlock_test,
discovery_plus_unlock_test, bilibili_unlock_test, google_not_cn_test, openai_unlock_test,
claude_unlock_test, gemini_unlock_test, bahamut_unlock_test, spotify_unlock_test,
steam_unlock_test
local unlock_tests = {
["Netflix"] = function(tested) return netflix_unlock_test(tested) end,
["Disney Plus"] = function(tested) return disney_unlock_test(tested) end,
["HBO Max"] = function(tested) return hbo_max_unlock_test(tested) end,
["YouTube Premium"] = function(tested) return ytb_unlock_test(tested) end,
["TVB Anywhere+"] = function(tested) return tvb_anywhere_unlock_test(tested) end,
["Amazon Prime Video"] = function(tested) return prime_video_unlock_test(tested) end,
["DAZN"] = function(tested) return dazn_unlock_test(tested) end,
["Paramount Plus"] = function(tested) return paramount_plus_unlock_test(tested) end,
["Discovery Plus"] = function(tested) return discovery_plus_unlock_test(tested) end,
["Bilibili"] = function(tested) return bilibili_unlock_test(tested) end,
["Google"] = function(tested) return google_not_cn_test(tested) end,
["OpenAI"] = function(tested) return openai_unlock_test(tested) end,
["Claude"] = function(tested) return claude_unlock_test(tested) end,
["Gemini"] = function(tested) return gemini_unlock_test(tested) end,
["Bahamut Anime"] = function(tested) return bahamut_unlock_test(tested) end,
["Spotify"] = function(tested) return spotify_unlock_test(tested) end,
["Steam"] = function(tested) return steam_unlock_test(tested) end,
}
local function proxy_unlock_test(tested)
local unlock_test = unlock_tests[type]
if not unlock_test then
return ""
end
return unlock_test(tested)
end
local function get_sniffed_chains(host)
local con = SYS.exec(string.format('curl -sL -m 5 --retry 2 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -XGET http://%s:%s/connections', passwd, ip, port))
if con then
con = JSON.parse(con)
end
if not con or not con.connections then
return nil
end
for i = 1, #(con.connections) do
local metadata = con.connections[i].metadata
if metadata and metadata.host and string.match(metadata.host, host) then
return con.connections[i].chains
end
end
end
-- The probes resolve through the OpenClash DNS: the router's own resolver may bypass
-- dnsmasq, and the core only attaches the domain when the query went through it.
local function resolve_via_openclash_dns(domain)
local ip
local out = SYS.exec(string.format("nslookup %s 127.0.0.1 2>/dev/null", domain)) or ""
for v in out:gmatch("Address: (%d+%.%d+%.%d+%.%d+)") do
if v ~= "127.0.0.1" then
ip = v
end
end
return ip
end
local function sniff_service_chains()
local site = stream_config("site")
local host = stream_config("host")
if not site or not host then
return nil
end
local domain = string.match(site, "^https?://([^/:]+)")
local domain_port = string.match(site, "^https://") and 443 or 80
local probe_target = domain
if domain then
probe_target = resolve_via_openclash_dns(domain) or domain
SYS.call(string.format('( (sleep 4) | nc %s %s ) >/dev/null 2>&1 &', probe_target, domain_port))
SYS.call(string.format('( (sleep 4) | openssl s_client -connect %s:%s -servername %s -quiet -no_ign_eof ) >/dev/null 2>&1 &', probe_target, domain_port, domain))
end
local resolve_arg = ""
if probe_target ~= domain then
resolve_arg = string.format("--resolve %s:%s:%s ", domain, domain_port, probe_target)
end
SYS.call(string.format('curl -sL -m 5 --limit-rate 1k -o /dev/null %s%s &', resolve_arg, site))
local deadline = os.time() + 4
local polls = 0
while os.time() < deadline and polls < 4 do
polls = polls + 1
local chains = get_sniffed_chains(host)
if chains and #chains > 0 then
return chains
end
if polls < 4 then
SYS.call("sleep 1")
end
end
return nil
end
local function auto_get_policy_group()
local chains = sniff_service_chains()
return chains and chains[#chains]
end
local function sniff_service_node()
local chains = sniff_service_chains()
return chains and chains[1] or ""
end
local function load_proxies()
local info = SYS.exec(string.format('curl -sL -m 5 --retry 2 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -XGET http://%s:%s/proxies', passwd, ip, port))
if info then
info = JSON.parse(info)
if not info or not info.proxies then os.exit(0) end
end
--save group name
build_proxy_index(info)
return info
end
--try to get group instead of matching the key
local function build_key_group(auto_get_group)
if auto_get_group then
return "^" .. auto_get_group .. "$"
end
local key_group = get_config(stream_config("group_key")) or stream_config("group_default")
if not key_group then
key_group = type
end
return key_group
end
local function current_service_node()
port = get_config("cn_port")
passwd = get_config("dashboard_password") or ""
ip = FS.lanip(true)
if not ip or not port then
return ""
end
local node = sniff_service_node()
if node ~= "" then
return node
end
local info = SYS.exec(string.format('curl -sL -m 5 --retry 2 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -XGET http://%s:%s/proxies', passwd, ip, port))
if not info or info == "" then
return ""
end
info = JSON.parse(info)
if not info or not info.proxies then
return ""
end
build_proxy_index(info)
local key_group = build_key_group(nil)
for _, value in pairs(info.proxies) do
if datamatch(value.name, key_group) then
return get_group_now(value.name)
end
end
return ""
end
local function test_matched_group(value, job)
--get groups info
group_match_name = value.name
local ctx = get_proxy(value.name, value.name)
mark_tested(ctx.now)
--test now proxy
local region = proxy_unlock_test(ctx.now)
if is_group(ctx.now) then
job.now = log_prefix()..MSG.group.."【"..ctx.show.."】"
else
job.now = log_prefix()..MSG.group.."【"..ctx.show.." ➟ "..ctx.now.."】"
end
if status ~= 2 and status ~= 4 then
region = proxy_unlock_test(ctx.now)
end
record_result(status, region)
local group_node = get_group_now(value.now)
if status == 2 or status == 4 then
if region ~= "" then
table.insert(job.full_support, {value.now, value.now, group_node, region})
print(job.now..MSG.full_support.."【"..region.."】")
write_cache(type, group_node, region)
else
table.insert(job.full_support, {value.now, value.now, group_node})
print(job.now..MSG.full_support_no_area)
write_cache(type, group_node)
end
if all_test or #nodes_filter(ctx.now) == 0 then
status = 0
return false
end
if status == 4 then
status = 2
if region ~= "" then
write_cache(type, "old_region", region)
end
end
if status == 2 and type == "Google" then
write_cache(type, "old_region", ctx.now)
end
return true
elseif status == 3 then
if region ~= "" then
table.insert(job.other_region, {value.now, value.now, group_node, region})
write_cache(type, group_node, region)
else
table.insert(job.other_region, {value.now, value.now, group_node})
write_cache(type, group_node)
end
if not all_test then
if region ~= "" then
print(job.now..MSG.full_support.."【"..region.."】"..MSG.other_region_unlock_test_start)
else
print(job.now..MSG.full_support_no_area..MSG.other_region_unlock_test_start)
end
else
if region ~= "" then
print(job.now..MSG.full_support.."【"..region.."】"..MSG.other_region_unlock_test)
else
print(job.now..MSG.full_support_no_area..MSG.other_region_unlock_test)
end
end
return false
elseif status == 1 then
table.insert(job.original, {value.now, value.now, group_node})
if not all_test then
if type == "Netflix" then
print(job.now..MSG.original_test_start)
else
print(job.now..MSG.no_unlock_test_start)
end
else
if type == "Netflix" then
print(job.now..MSG.only_original)
else
print(job.now..MSG.no_unlock)
end
end
delete_cache(type, group_node)
return false
else
if not all_test then
print(job.now..MSG.failed_test_start)
else
print(job.now..MSG.test_failed)
end
return false
end
end
local function test_candidate(value, job, nctx, candidate, pnode)
--skip tested proxy
local pnode_now = get_group_now(pnode)
if is_tested(pnode_now) then
return
end
mark_tested(pnode)
if pnode == "REJECT" or pnode == "REJECT-DROP" or pnode == "PASS" or pnode_now == "REJECT" or pnode_now == "REJECT-DROP" or pnode_now == "PASS" then
return
end
local now
if is_group(pnode) then
now = log_prefix()..MSG.group.."【"..nctx.show.." ➟ "..pnode_now.."】"
else
now = log_prefix()..MSG.group.."【"..nctx.show.." ➟ "..pnode.."】"
end
set_selected(nctx.name, pnode)
local region = proxy_unlock_test(pnode_now)
if status == 2 then
record_result(status, region)
if region ~= "" then
table.insert(job.full_support, {candidate, nctx.name, pnode, region})
if not all_test then
print(now..MSG.full_support.."【"..region.."】")
print(log_prefix()..MSG.group.."【"..value.name.."】"..MSG.select_success.."【"..pnode.."】"..MSG.area.."【"..region.."】")
else
print(now..MSG.full_support.."【"..region.."】")
end
write_cache(type, pnode, region)
else
table.insert(job.full_support, {candidate, nctx.name, pnode})
if not all_test then
print(now..MSG.full_support_no_area)
print(log_prefix()..MSG.group.."【"..value.name.."】"..MSG.select_success.."【"..pnode.."】")
else
print(now..MSG.full_support_no_area)
end
write_cache(type, pnode)
end
elseif status == 3 then
if region ~= "" then
table.insert(job.other_region, {candidate, nctx.name, pnode, region})
print(now..MSG.full_support.."【"..region.."】"..MSG.other_region_unlock_test)
write_cache(type, pnode, region)
else
table.insert(job.other_region, {candidate, nctx.name, pnode})
print(now..MSG.full_support_no_area..MSG.other_region_unlock_test)
write_cache(type, pnode)
end
elseif status == 4 then
if region ~= "" then
table.insert(job.no_old_region, {candidate, nctx.name, pnode, region})
print(now..MSG.full_support.."【"..region.."】"..MSG.no_old_region_unlock_old_region.."【"..job.old_region.."】")
write_cache(type, pnode, region)
else
table.insert(job.no_old_region, {candidate, nctx.name, pnode})
print(now..MSG.no_old_region_unlock_test)
write_cache(type, pnode)
end
elseif status == 1 then
table.insert(job.original, {candidate, nctx.name, pnode})
if type == "Netflix" then
print(now..MSG.only_original)
else
print(now..MSG.no_unlock)
end
delete_cache(type, pnode)
else
print(now..MSG.test_failed)
end
end
local function test_selector_nodes(value, job, nctx, candidate)
for p = 1, #(nctx.nodes) do
local pnode = nctx.nodes[p]
test_candidate(value, job, nctx, candidate, pnode)
if status == 2 and not all_test then
break
elseif p == #(nctx.nodes) and #(nctx.nodes) ~= 1 then
set_selected(nctx.name, nctx.now)
end
end
end
local function test_expand_candidate(value, job, nctx, candidate)
--only group expand
set_selected(nctx.name, candidate)
if is_tested(nctx.now) or #nodes_filter(nctx.now) == 0 then
return
end
mark_tested(nctx.now)
local expand_now = get_group_now(nctx.now)
local region = proxy_unlock_test(expand_now)
local now
if is_group(nctx.now) then
now = log_prefix()..MSG.group.."【"..nctx.show.."】"
else
now = log_prefix()..MSG.group.."【"..nctx.show.." ➟ "..nctx.now.."】"
end
if status == 2 then
record_result(status, region)
if region ~= "" then
table.insert(job.full_support, {candidate, nctx.name, candidate, region})
if not all_test then
print(now..MSG.full_support.."【"..region.."】")
print(log_prefix()..MSG.group.."【"..value.name.."】"..MSG.select_success.."【"..expand_now.."】"..MSG.area.."【"..region.."】")
else
print(now..MSG.full_support.."【"..region.."】")
end
write_cache(type, candidate, region)
else
table.insert(job.full_support, {candidate, nctx.name, candidate})
if not all_test then
print(now..MSG.full_support_no_area)
print(log_prefix()..MSG.group.."【"..value.name.."】"..MSG.select_success.."【"..expand_now.."】")
else
print(now..MSG.full_support_no_area)
end
write_cache(type, candidate)
end
elseif status == 3 then
if region ~= "" then
table.insert(job.other_region, {candidate, nctx.name, candidate, region})
print(now..MSG.full_support.."【"..region.."】"..MSG.other_region_unlock_no_select)
write_cache(type, candidate, region)
else
table.insert(job.other_region, {candidate, nctx.name, candidate})
print(now..MSG.full_support_no_area..MSG.other_region_unlock_no_select)
write_cache(type, candidate)
end
elseif status == 4 then
if region ~= "" then
table.insert(job.no_old_region, {candidate, nctx.name, candidate, region})
print(now..MSG.full_support.."【"..region.."】"..MSG.no_old_region_unlock_old_region.."【"..job.old_region.."】"..MSG.no_old_region_unlock_old_region_no_select)
write_cache(type, candidate, region)
else
table.insert(job.no_old_region, {candidate, nctx.name, candidate})
print(now..MSG.full_support_no_area..MSG.no_old_region_unlock_no_select)
write_cache(type, candidate)
end
elseif status == 1 then
table.insert(job.original, {candidate, nctx.name, candidate})
if type == "Netflix" then
print(now..MSG.original_no_select)
else
print(now..MSG.no_unlock_no_select)
end
delete_cache(type, candidate)
else
print(now..MSG.failed_no_select)
end
end
local function apply_fallback_select(value, job)
local fallback_select
if #job.full_support > 0 then
fallback_select = job.full_support
elseif #job.no_old_region > 0 then
fallback_select = job.no_old_region
elseif #job.other_region > 0 then
fallback_select = job.other_region
else
fallback_select = job.original
end
for _, v in ipairs(fallback_select) do
if #nodes_filter(v[3]) ~= 0 then
if v[4] then
table.insert(fallback_select, 1, {v[1], v[2], v[3], v[4]})
write_cache(type, "old_region", v[4])
else
table.insert(fallback_select, 1, {v[1], v[2], v[3]})
end
break
end
end
for _, v in ipairs(fallback_select) do
if #job.full_support > 0 then
record_result(2, v[4])
elseif #job.no_old_region > 0 then
record_result(4, v[4])
elseif #job.other_region > 0 then
record_result(3, v[4])
else
record_result(1, v[4])
end
set_selected(value.name, v[1])
set_selected(v[2], v[3])
local group_now
if is_group(v[3]) then
group_now = "【".. v[3] .. " ➟ " .. get_group_now(v[3]) .. "】"
else
group_now = "【".. v[3] .. "】"
end
if v[4] then
group_now = group_now .. MSG.area.."【"..v[4].."】"
end
if #job.full_support > 0 then
print(log_prefix()..MSG.group.."【"..value.name.."】"..MSG.select_all_full_support..group_now)
elseif #job.no_old_region > 0 then
if not all_test then
print(log_prefix()..MSG.group.."【"..value.name.."】"..MSG.select_success_no_old_region..group_now)
else
print(log_prefix()..MSG.group.."【"..value.name.."】"..MSG.select_all_no_old_region..group_now)
end
elseif #job.other_region > 0 then
if not all_test then
print(log_prefix()..MSG.group.."【"..value.name.."】"..MSG.select_failed_other_region..group_now)
else
print(log_prefix()..MSG.group.."【"..value.name.."】"..MSG.select_all_other_region..group_now)
end
else
if not all_test then
print(log_prefix()..MSG.group.."【"..value.name.."】"..MSG.select_failed..group_now)
else
print(log_prefix()..MSG.group.."【"..value.name.."】"..MSG.select_all_failed..group_now)
end
end
close_connections()
break
end
end
local function find_new_unlock(value, job)
if value.type ~= "Selector" then
local node_now = get_group_now(value.name)
if #nodes_filter(node_now) == 0 then
return
end
local region = proxy_unlock_test(node_now)
record_result(status, region)
if status == 2 then
if region ~= "" then
if not all_test then
print(job.now..MSG.full_support.."【"..region.."】")
end
print(log_prefix()..MSG.group.."【"..value.name.."】"..MSG.select_success.."【"..node_now.."】"..MSG.area.."【"..region.."】")
else
if not all_test then
print(job.now..MSG.full_support_no_area)
end
print(log_prefix()..MSG.group.."【"..value.name.."】"..MSG.select_success.."【"..node_now.."】")
end
elseif status == 3 then
if region ~= "" then
print(job.now..MSG.full_support.."【"..region.."】"..MSG.other_region_unlock_no_select)
else
print(job.now..MSG.full_support_no_area..MSG.other_region_unlock_no_select)
end
elseif status == 4 then
if region ~= "" then
print(job.now..MSG.full_support.."【"..region.."】"..MSG.no_old_region_unlock_old_region.."【"..job.old_region.."】"..MSG.no_old_region_unlock_old_region_no_select)
else
print(job.now..MSG.full_support_no_area..MSG.no_old_region_unlock_no_select)
end
elseif status == 1 then
if type == "Netflix" then
print(job.now..MSG.original_no_select)
else
print(job.now..MSG.no_unlock_no_select)
end
else
print(job.now..MSG.failed_no_select)
end
return
end
--save group current selected
local proxy_default = value.now
if not all_test then
--filter nodes
value.all = nodes_filter(value.all)
if #value.all > 1 then
if select_logic == "random" then
--sort by random
value.all = table_rand(value.all, proxy_default)
else
--sort by urltest
value.all = table_sort_by_urltest(value.all, proxy_default)
end
--sort by cache
value.all = table_sort_by_cache(value.all)
end
end
if #(value.all) == 0 then
log_info(MSG.group.."【"..value.name.."】"..MSG.no_nodes_filter)
return
end
--loop proxy test
for i = 1, #(value.all) do
local candidate = value.all[i]
if candidate ~= "REJECT" and candidate ~= "REJECT-DROP" and candidate ~= "PASS" then
local nctx = get_proxy(candidate, value.name)
if nctx.type == "Selector" then
if nctx.name == candidate then
set_selected(value.name, nctx.name)
end
if not all_test then
--filter nodes
nctx.nodes = nodes_filter(nctx.nodes)
if #nctx.nodes > 1 then
if select_logic == "random" then
--sort by random
nctx.nodes = table_rand(nctx.nodes)
else
--sort by urltest
nctx.nodes = table_sort_by_urltest(nctx.nodes)
end
--sort by cache
nctx.nodes = table_sort_by_cache(nctx.nodes)
end
end
if #(nctx.nodes) == 0 then
log_info(MSG.group.."【"..nctx.show.."】"..MSG.no_nodes_filter)
else
test_selector_nodes(value, job, nctx, candidate)
end
else
test_expand_candidate(value, job, nctx, candidate)
end
end
if status == 2 and not all_test then
close_connections()
break
elseif i == #(value.all) and (#job.original > 0 or #job.other_region > 0 or #job.no_old_region > 0 or #job.full_support > 0) then
apply_fallback_select(value, job)
elseif i == #(value.all) then
set_selected(value.name, proxy_default)
local group_now
if is_group(proxy_default) then
group_now = value.name.." ➟ "..proxy_default.." ➟ "..get_group_now(proxy_default)
else
group_now = value.name.." ➟ "..proxy_default
end
print(log_prefix()..MSG.group.."【"..value.name.."】"..MSG.select_failed.."【"..group_now.."】")
end
end
end
local function unlock_auto_select()
--Get ip port and password
get_auth_info()
local info = load_proxies()
--try to get group instead of matching the key
local auto_get_group = auto_get_policy_group()
local key_group = build_key_group(auto_get_group)
local job = {
now = nil,
old_region = get_old_region(),
full_support = {},
other_region = {},
no_old_region = {},
original = {},
}
local group_match = false
for _, value in pairs(info.proxies) do
--match only once
if datamatch(value.name, key_group) then
group_match = true
if not test_matched_group(value, job) then
--find new unlock
find_new_unlock(value, job)
end
end
if auto_get_group and group_match then break end
if status == 2 and not all_test then break end
end
if not group_match and not auto_get_group then
log_info(MSG.group.."【"..key_group.."】"..MSG.no_group_find)
end
--write cache
FS.writefile(unlock_cache_file, JSON.stringify(unlock_cache_info))
if result_st and result_output then
if not result_node or result_node == "" then
result_node = current_service_node() or ""
end
print_result(result_st, result_region)
end
end
local function response_code(data)
local tag = string.match(data or "", "_TAG_%d+_TAG_")
return tag and tonumber(string.sub(tag, 6, 8))
end
local function trace_region(data)
local loc = string.match(data or "", "loc=%a+")
if not loc then
return nil
end
return string.upper(string.sub(loc, 5, -1))
end
local function fetch_trace_region(url)
local host = string.match(url, "^https?://([^/:]+)")
local port = string.match(url, "^https://") and 443 or 80
local ip = host and resolve_via_openclash_dns(host)
local resolve_arg = ip and string.format("--resolve %s:%s:%s ", host, port, ip) or ""
for _ = 1, 2 do
local data = SYS.exec(string.format("curl -sL --connect-timeout 5 -m 5 --speed-time 5 --speed-limit 1 --retry 2 %s-H 'Accept-Language: en' -H 'Content-Type: application/json' -H 'User-Agent: %s' '%s'", resolve_arg, UA, url))
local loc = trace_region(data)
if loc then
return loc
end
end
return nil
end
-- Thanks https://github.com/lmc999/RegionRestrictionCheck --
function netflix_unlock_test()
status = 0
local filmId = 70143836
local url = "https://www.netflix.com/title/"..filmId
local headers = "User-Agent: "..UA
local info = SYS.exec(string.format('curl -sLI --connect-timeout 5 -m 5 --speed-time 5 --speed-limit 1 --retry 2 -o /dev/null -w %%{json} -H "Content-Type: application/json" -H "host: www.netflix.com" -H "accept-language: en-US,en;q=0.9" -H "sec-ch-ua: Google Chrome;v=125, Chromium;v=125, Not.A/Brand;v=24" -H "sec-ch-ua-mobile: ?0" -H "sec-ch-ua-platform: Windows" -H "sec-fetch-site: none" -H "sec-fetch-mode: navigate" -H "sec-fetch-user: ?1" -H "sec-fetch-dest: document" -H "%s" -XGET %s', headers, url))
local result = {}
local region = ""
local regex, old_region, old_regex = probe_context()
if info then
info = JSON.parse(info)
end
if info then
if info.http_code == 200 then
status = 2
string.gsub(info.url_effective, '[^/]+', function(w) table.insert(result, w) end)
if string.match(result[3], "^%a+") then
region = string.upper(string.match(result[3], "^%a+"))
if region == "TITLE" then
region = "US"
end
finish_region_test(region, regex, old_region, old_regex)
end
elseif info.http_code == 404 or info.http_code == 403 then
status = 1
end
end
return region
end
function disney_unlock_test()
status = 0
local url = "https://disney.api.edge.bamgrid.com/devices"
local url2 = "https://disney.api.edge.bamgrid.com/token"
local url3 = "https://disney.api.edge.bamgrid.com/graph/v1/device/graphql"
local headers = '-H "Accept-Language: en" -H "Content-Type: application/json" -H "authorization: ZGlzbmV5JmJyb3dzZXImMS4wLjA.Cu56AgSfBTDag5NiRA81oLHkDZfu5L3CKadnefEAY84"'
local auth = '-H "authorization: Bearer ZGlzbmV5JmJyb3dzZXImMS4wLjA.Cu56AgSfBTDag5NiRA81oLHkDZfu5L3CKadnefEAY84"'
local body = '{"query":"mutation registerDevice($input: RegisterDeviceInput!) { registerDevice(registerDevice: $input) { grant { grantType assertion } } }","variables":{"input":{"deviceFamily":"browser","applicationRuntime":"chrome","deviceProfile":"windows","deviceLanguage":"en","attributes":{"osDeviceIds":[],"manufacturer":"microsoft","model":null,"operatingSystem":"windows","operatingSystemVersion":"10.0","browserName":"chrome","browserVersion":"96.0.4606"}}}}'
local region = ""
local assertion, disneycookie
local regex, old_region, old_regex = probe_context()
local preassertion = SYS.exec(string.format("curl -sL --connect-timeout 5 -m 5 --speed-time 5 --speed-limit 1 --retry 2 %s -H 'User-Agent: %s' -H 'content-type: application/json; charset=UTF-8' -d '{\"deviceFamily\":\"browser\",\"applicationRuntime\":\"chrome\",\"deviceProfile\":\"windows\",\"attributes\":{}}' -XPOST %s", auth, UA, url))
if preassertion then
local preassertion_info = JSON.parse(preassertion)
if preassertion_info then
assertion = preassertion_info.assertion
end
end
if not assertion then return region end
disneycookie = "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&latitude=0&longitude=0&platform=browser&subject_token="..assertion.."&subject_token_type=urn%3Abamtech%3Aparams%3Aoauth%3Atoken-type%3Adevice"
local tokencontent = SYS.exec(string.format("curl -sL --connect-timeout 5 -m 5 --speed-time 5 --speed-limit 1 --retry 2 %s -H 'User-Agent: %s' -d '%s' -XPOST %s", auth, UA, disneycookie, url2))
if tokencontent then
local token_info = JSON.parse(tokencontent)
if token_info and token_info.error_description then
status = 1
return region
end
end
local data = SYS.exec(string.format("curl -sL --connect-timeout 5 -m 5 --speed-time 5 --speed-limit 1 --retry 2 %s -H 'User-Agent: %s' -d '%s' -XPOST %s", headers, UA, body, url3))
if data then
data = JSON.parse(data)
if data then
status = 1
end
end
if data and data.extensions and data.extensions.sdk and data.extensions.sdk.session and data.extensions.sdk.session.location and data.extensions.sdk.session.location.countryCode then
region = data.extensions.sdk.session.location.countryCode
local inSupportedLocation = data.extensions.sdk.session.inSupportedLocation or ""
if region == "JP" then
status = 2
if not datamatch(region, regex) then
status = 3
elseif old_regex ~= regex and not all_test then
status = 2
elseif old_region ~= "" and not datamatch(region, old_region) and not all_test then
status = 4
end
if status == 2 and not all_test and region ~= "" and region ~= old_region then
write_cache(type, "old_region", region)
end
if status == 2 and not all_test and regex ~= old_regex then
write_cache(type, "old_regex", regex)
end
return region
end
if region ~= "" and inSupportedLocation then
status = 2
if not datamatch(region, regex) then
status = 3
elseif old_region ~= "" and not datamatch(region, old_region) and not all_test then
status = 4
end
if status == 2 and not all_test and region ~= old_region then
write_cache(type, "old_region", region)
end
if status == 2 and not all_test and regex ~= old_regex then
write_cache(type, "old_regex", regex)
end
return region
end
end
return region
end
function hbo_max_unlock_test()
status = 0
local url = "https://www.max.com/"
local data = SYS.exec(string.format("curl -sL --connect-timeout 5 -m 5 --speed-time 5 --speed-limit 1 --retry 2 -w '_TAG_%%{http_code}_TAG_' -H 'Content-Type: application/json' -H 'User-Agent: %s' %s", UA, url))
local region = ""
local outofregion
local regex, old_region, old_regex = probe_context()
if response_code(data) == 200 then
status = 1
if string.match(data, "\"isUserOutOfRegion\":%a+") then
outofregion = string.lower(string.sub(string.match(data, "\"isUserOutOfRegion\":%a+"), 21, -1))
end
if string.match(data, "\"userCountry\":\"%a+\"") then
region = string.upper(string.sub(string.match(data, "\"userCountry\":\"%a+\""), 16, -2))
end
if region ~= "" and outofregion == "false" then
status = 2
finish_region_test(region, regex, old_region, old_regex)
end
end
return region
end
function ytb_unlock_test()
status = 0
local url = "https://m.youtube.com/premium"
local region = ""
local data, he_data
local regex, old_region, old_regex = probe_context()
data = SYS.exec(string.format("curl -sL --connect-timeout 5 -m 5 --speed-time 5 --speed-limit 1 --retry 2 -w '_TAG_%%{http_code}_TAG_' -H 'Accept-Language: en' -H 'Content-Type: application/json' -H 'User-Agent: %s' -b 'YSC=BiCUU3-5Gdk; CONSENT=YES+cb.20220301-11-p0.en+FX+700; GPS=1; VISITOR_INFO1_LIVE=4VwPMkB7W5A; PREF=tz=Asia.Shanghai; _gcl_au=1.1.1809531354.1646633279' %s", UA, url))
if response_code(data) == 200 then
status = 1
if string.find(data,"www%.google%.cn") or string.find(data, "is not available in your country") then
return region
end
if string.match(data, "\"GL\":\"%a+\"") then
region = string.sub(string.match(data, "\"GL\":\"%a+\""), 7, -2)
status = 2
else
he_data = SYS.exec(string.format("curl -sIL --connect-timeout 5 -m 5 --speed-time 5 --speed-limit 1 --retry 2 -H 'Accept-Language: en' -H 'Content-Type: application/json' -H 'User-Agent: %s' %s", UA, url))
if string.match(he_data, "gl=%a+") then
region = string.sub(string.match(he_data, "gl=%a+"), 4, -1)
status = 2
else
region = "US"
end
end
finish_region_test(region, regex, old_region, old_regex)
end
return region
end
function tvb_anywhere_unlock_test()
status = 0
local url = "https://uapisfm.tvbanywhere.com.sg/geoip/check/platform/android"
local region = ""
local regex, old_region, old_regex = probe_context()
local data = SYS.exec(string.format("curl -sL --connect-timeout 5 -m 5 --speed-time 5 --speed-limit 1 --retry 2 -w '_TAG_%%{http_code}_TAG_' -H 'Accept-Language: en' -H 'Content-Type: application/json' -H 'User-Agent: %s' %s", UA, url))
if response_code(data) == 200 then
status = 1
data = JSON.parse(data)
if data and data.allow_in_this_country then
status = 2
if data.country then
region = string.upper(data.country)
end
if region ~= "" then
finish_region_test(region, regex, old_region, old_regex)
end
end
end
return region
end
function prime_video_unlock_test()
status = 0
local url = "https://www.primevideo.com"
local region = ""
local regex, old_region, old_regex = probe_context()
local data = SYS.exec(string.format("curl -sL --connect-timeout 5 -m 5 --speed-time 5 --speed-limit 1 --retry 2 -w '_TAG_%%{http_code}_TAG_' -H 'Accept-Language: en' -H 'Content-Type: application/json' -H 'User-Agent: %s' %s", UA, url))
if response_code(data) == 200 then
status = 1
if string.match(data, "\"currentTerritory\":\"%a+\"") then
region = string.sub(string.match(data, "\"currentTerritory\":\"%a+\""), 21, -2)
status = 2
finish_region_test(region, regex, old_region, old_regex)
end
end
return region
end
function dazn_unlock_test()
status = 0
local url = "https://startup.core.indazn.com/misl/v5/Startup"
local region = ""
local regex, old_region, old_regex = probe_context()
local data = SYS.exec(string.format("curl -sL --connect-timeout 5 -m 5 --speed-time 5 --speed-limit 1 --retry 2 -w '_TAG_%%{http_code}_TAG_' -H 'Accept-Language: en' -H 'Content-Type: application/json' -H 'User-Agent: %s' -X POST -d '{\"LandingPageKey\":\"generic\",\"Languages\":\"zh-CN,zh,en\",\"Platform\":\"web\",\"PlatformAttributes\":{},\"Manufacturer\":\"\",\"PromoCode\":\"\",\"Version\":\"2\"}' %s", UA, url))
if response_code(data) == 200 then
status = 1
data = JSON.parse(data)
if data and data.Region and data.Region.isAllowed then
status = 2
if data.Region.GeolocatedCountry then
region = string.upper(data.Region.GeolocatedCountry)
end
if region ~= "" then
finish_region_test(region, regex, old_region, old_regex)
end
end
end
return region
end
function paramount_plus_unlock_test()
status = 0
local url = "https://www.paramountplus.com/"
local region = ""
local regex, old_region, old_regex = probe_context()
local data = SYS.exec(string.format("curl -sL --connect-timeout 5 -m 5 --speed-time 5 --speed-limit 1 --retry 2 -w '_TAG_%%{http_code}_TAG_%%{url_effective}_TAGS_' -H 'Accept-Language: en' -H 'Content-Type: application/json' -H 'User-Agent: %s' %s", UA, url))
if response_code(data) == 200 then
status = 1
if string.match(data, "_TAG_[^\n]+_TAGS_") then
if not string.find(string.match(data, "_TAG_[^\n]+_TAGS_"), "intl") then
status = 2
local edition = string.match(data, "\"siteEdition\":\"%a+|%a+\"")
local region_start = 19
if not edition then
edition = string.match(data, "property: '%a+'")
region_start = 12
end
if edition then
region = string.upper(string.sub(edition, region_start, -2))
finish_region_test(region, regex, old_region, old_regex)
end
end
end
end
return region
end
function discovery_plus_unlock_test()
status = 0
local url = "https://us1-prod-direct.discoveryplus.com/token?deviceId=d1a4a5d25212400d1e6985984604d740&realm=go&shortlived=true"
local url1 = "https://us1-prod-direct.discoveryplus.com/users/me"
local region = ""
local regex, old_region, old_regex = probe_context()
local token_resp = SYS.exec(string.format("curl -sL --connect-timeout 5 -m 5 --speed-time 5 --speed-limit 1 --retry 2 -H 'Accept-Language: en' -H 'Content-Type: application/json' -H 'User-Agent: %s' '%s'", UA, url))
local token_info = token_resp and JSON.parse(token_resp)
if token_info and token_info.data and token_info.data.attributes then
status = 1
local token = token_info.data.attributes.token
local cookie = string.format("-b \"_gcl_au=1.1.858579665.1632206782; _rdt_uuid=1632206782474.6a9ad4f2-8ef7-4a49-9d60-e071bce45e88; _scid=d154b864-8b7e-4f46-90e0-8b56cff67d05; _pin_unauth=dWlkPU1qWTRNR1ZoTlRBdE1tSXdNaTAwTW1Nd0xUbGxORFV0WWpZMU0yVXdPV1l6WldFeQ; _sctr=1|1632153600000; aam_fw=aam%%3D9354365%%3Baam%%3D9040990; aam_uuid=24382050115125439381416006538140778858; st=%s; gi_ls=0; _uetvid=a25161a01aa711ec92d47775379d5e4d; AMCV_BC501253513148ED0A490D45%%40AdobeOrg=-1124106680%%7CMCIDTS%%7C18894%%7CMCMID%%7C24223296309793747161435877577673078228%%7CMCAAMLH-1633011393%%7C9%%7CMCAAMB-1633011393%%7CRKhpRz8krg2tLO6pguXWp5olkAcUniQYPHaMWWgdJ3xzPWQmdj0y%%7CMCOPTOUT-1632413793s%%7CNONE%%7CvVersion%%7C5.2.0; ass=19ef15da-95d6-4b1d-8fa2-e9e099c9cc38.1632408400.1632406594\"", token)
local data = SYS.exec(string.format("curl -sL --connect-timeout 5 -m 5 --speed-time 5 --speed-limit 1 --retry 2 -H 'Accept-Language: en' -H 'Content-Type: application/json' -H 'User-Agent: %s' %s %s", UA, cookie, url1))
local body = data and JSON.parse(data)
if body and body.data and body.data.attributes and (body.data.attributes.currentLocationSovereignTerritory or body.data.attributes.currentLocationTerritory) then
region = body.data.attributes.currentLocationTerritory or body.data.attributes.currentLocationSovereignTerritory
if region ~= "" then
region = string.upper(region)
status = 2
finish_region_test(region, regex, old_region, old_regex)
end
end
end
return region
end
function bilibili_unlock_test()
status = 0
local randsession = SYS.exec("cat /dev/urandom 2>/dev/null | head -n 32 | md5sum | head -c 32")
local region = ""
local data, url
local regex = get_region_regex() or "CN"
local old_region = get_old_region()
local old_regex = get_old_regex()
if regex == "HK/MO/TW" then
url = string.format("https://api.bilibili.com/pgc/player/web/playurl?avid=18281381&cid=29892777&qn=0&type=&otype=json&ep_id=183799&fourk=1&fnver=0&fnval=16&session=%s&module=bangumi", randsession)
region = "HK/MO/TW"
elseif regex == "TW" then
url = string.format("https://api.bilibili.com/pgc/player/web/playurl?avid=50762638&cid=100279344&qn=0&type=&otype=json&ep_id=268176&fourk=1&fnver=0&fnval=16&session=%s&module=bangumi", randsession)
region = "TW"
else
url = string.format("https://api.bilibili.com/pgc/player/web/playurl?avid=82846771&qn=0&type=&otype=json&ep_id=307247&fourk=1&fnver=0&fnval=16&session=%s&module=bangumi", randsession)
region = "CN"
end
data = SYS.exec(string.format("curl -sL --connect-timeout 5 -m 5 --speed-time 5 --speed-limit 1 --retry 2 -w '_TAG_%%{http_code}_TAG_' -H 'Accept-Language: en' -H 'Content-Type: application/json' -H 'User-Agent: %s' '%s'", UA, url))
if response_code(data) == 200 then
data = JSON.parse(data)
status = 1
if data and data.code and data.code == 0 then
status = 2
if old_region ~= "" and region ~= old_region and not all_test then
status = 4
end
if status == 2 and not all_test and region ~= old_region then
write_cache(type, "old_region", region)
end
if status == 2 and not all_test and regex ~= old_regex then
write_cache(type, "old_regex", regex)
end
end
end
return region
end
function google_not_cn_test(tested)
status = 0
local url = "https://timeline.google.com"
local region = ""
local httpcode = SYS.exec(string.format("curl -sL --connect-timeout 5 -m 5 --speed-time 5 --speed-limit 1 --retry 2 -o /dev/null -w %%{http_code} -H 'Accept-Language: en' -H 'Content-Type: application/json' -H 'User-Agent: %s' '%s'", UA, url))
if httpcode then
if tonumber(httpcode) == 200 then
status = 2
region = "NOT CN"
local data = SYS.exec(string.format("curl -sL --connect-timeout 5 -m 8 --speed-time 5 --speed-limit 1 --max-filesize 3145728 --retry 2 -H 'Accept-Language: en' -H 'User-Agent: %s' 'https://play.google.com/'", UA))
local loc = data and string.match(data, '<div class="yVZQTb">([^<(]+)')
if loc then
loc = string.match(loc, "^%s*(.-)%s*$")
if loc ~= "" then
region = loc
end
end
local lc = string.lower(region)
if lc == "china" or lc == "cn" then
region = "CN"
status = 1
end
if status == 2 and not all_test and tested then
write_cache(type, "old_region", tested)
end
else
region = "CN"
status = 1
end
end
return region
end
function openai_unlock_test()
status = 0
local url = "https://api.openai.com/compliance/cookie_requirements"
local url2 = "https://ios.chat.openai.com/"
local region_url = "https://chat.openai.com/cdn-cgi/trace"
local regex, old_region, old_regex = probe_context()
local region = ""
local data = SYS.exec(string.format("curl -sL --connect-timeout 5 -m 5 --speed-time 5 --speed-limit 1 --retry 2 -H 'authority: api.openai.com' -H 'accept: */*' -H 'accept-language: en-US,en;q=0.9' -H 'authorization: Bearer null' -H 'content-type: application/json' -H 'origin: https://platform.openai.com' -H 'referer: https://platform.openai.com/' -H 'sec-ch-ua: %s' -H 'sec-ch-ua-mobile: ?0' -H 'sec-ch-ua-platform: \"Windows\"' -H 'sec-fetch-dest: empty' -H 'sec-fetch-mode: cors' -H 'sec-fetch-site: same-site' -H 'User-Agent: %s' '%s'", UA_SEC_CH_UA, UA, url))
local datas = SYS.exec(string.format("curl -sL --connect-timeout 5 -m 5 --speed-time 5 --speed-limit 1 --retry 2 -H 'authority: ios.chat.openai.com' -H 'accept: */*;q=0.8,application/signed-exchange;v=b3;q=0.7' -H 'accept-language: en-US,en;q=0.9' -H 'sec-ch-ua: %s' -H 'sec-ch-ua-mobile: ?0' -H 'sec-ch-ua-platform: \"Windows\"' -H 'sec-fetch-dest: document' -H 'sec-fetch-mode: navigate' -H 'sec-fetch-site: none' -H 'sec-fetch-user: ?1' -H 'upgrade-insecure-requests: 1' -H 'User-Agent: %s' '%s'", UA_SEC_CH_UA, UA, url2))
if data and datas then
if string.find(data, "unsupported_country") or string.find(string.lower(datas), "vpn") then
status = 1
else
status = 2
local loc = fetch_trace_region(region_url)
if loc then
region = loc
finish_region_test(region, regex, old_region, old_regex)
end
end
end
return region
end
function claude_unlock_test()
status = 0
local url = "https://claude.ai/"
local region_url = "https://claude.ai/cdn-cgi/trace"
local regex, old_region, old_regex = probe_context()
local region = ""
local data = SYS.exec(string.format("curl -sL --connect-timeout 5 -m 5 --speed-time 5 --speed-limit 1 --retry 2 -o /dev/null -w %%{url_effective} -H 'Accept-Language: en' -H 'Content-Type: application/json' -H 'User-Agent: %s' '%s'", UA, url))
if data and data ~= "" then
if string.find(data, "app-unavailable-in-region") then
status = 1
elseif data == url then
status = 2
local loc = fetch_trace_region(region_url)
if loc then
region = loc
finish_region_test(region, regex, old_region, old_regex)
end
else
status = 1
end
end
return region
end
function gemini_unlock_test()
status = 0
local url = "https://gemini.google.com/"
local regex, old_region, old_regex = probe_context()
local region = ""
local data = SYS.exec(string.format("curl -sL --connect-timeout 5 -m 5 --speed-time 5 --speed-limit 1 --retry 2 -H 'Accept-Language: en' -H 'Content-Type: application/json' -H 'User-Agent: %s' '%s'", UA, url))
if not data or string.sub(data, 1, 4) == "curl" then
status = 1
return region
end
if string.find(data, "45631641,null,true", 1, true) then
local cc = string.match(data, ',2,1,200,"([A-Z][A-Z][A-Z])"')
if cc and cc ~= "" then
region = cc
end
status = 2
if region ~= "" then
finish_region_test(region, regex, old_region, old_regex)
end
else
status = 1
end
return region
end
function bahamut_unlock_test()
status = 0
local pid = SYS.exec("echo $PPID") or ""
pid = pid:gsub("%s+$", "")
local jar = "/tmp/oc_bahamut_"..pid..".txt"
local regex, old_region, old_regex = probe_context()
local region = ""
for _ = 1, 2 do
local device = SYS.exec(string.format("curl -sL --connect-timeout 5 -m 5 --speed-time 5 --speed-limit 1 --retry 2 -c %s -H 'User-Agent: %s' 'https://ani.gamer.com.tw/ajax/getdeviceid.php'", jar, UA))
local deviceid = device and string.match(device, '"deviceid"%s*:%s*"([^"]+)"')
if deviceid then
local token = SYS.exec(string.format("curl -sL --connect-timeout 5 -m 5 --speed-time 5 --speed-limit 1 --retry 2 -b %s -H 'User-Agent: %s' 'https://ani.gamer.com.tw/ajax/token.php?adID=89422&sn=37783&device=%s'", jar, UA, deviceid))
local home = SYS.exec(string.format("curl -sL --connect-timeout 5 -m 5 --speed-time 5 --speed-limit 1 --retry 2 -b %s -H 'accept: */*;q=0.8,application/signed-exchange;v=b3;q=0.7' -H 'accept-language: zh-CN,zh;q=0.9' -H 'User-Agent: %s' 'https://ani.gamer.com.tw/'", jar, UA))
if token and home then
if string.find(token, "animeSn", 1, true) then
status = 2
local geo = string.match(home, 'data%-geo="([^"]+)"')
if geo then
region = string.upper(geo)
finish_region_test(region, regex, old_region, old_regex)
end
else
status = 1
end
break
end
end
end
os.remove(jar)
return region
end
function spotify_unlock_test()
status = 0
local url = "https://spclient.wg.spotify.com/signup/public/v1/account"
local body = "birth_day=11&birth_month=11&birth_year=2000&collect_personal_info=undefined&creation_flow=&creation_point=https%%3A%%2F%%2Fwww.spotify.com%%2Fhk-en%%2F&displayname=Test%%20User&gender=male&iagree=1&key=a1e486e2729f46d6bb368d6b2bcda326&platform=www&referrer=&send-email=0&thirdpartyemail=0&identifier_token=AgE6YTvEzkReHNfJpO114514"
local regex, old_region, old_regex = probe_context()
local region = ""
local data = SYS.exec(string.format("curl -sL --connect-timeout 5 -m 5 --speed-time 5 --speed-limit 1 --retry 2 -H 'Accept-Language: en' -H 'User-Agent: %s' -X POST -d '%s' '%s'", UA, body, url))
local info = data and JSON.parse(data)
if info then
local code = tostring(info.status or "")
if code == "320" or code == "120" or info.is_country_launched == false then
status = 1
elseif code == "311" then
status = 2
if info.country and info.country ~= "" then
region = string.upper(info.country)
finish_region_test(region, regex, old_region, old_regex)
end
end
end
return region
end
function steam_unlock_test()
status = 0
local url = "https://store.steampowered.com/app/761830"
local regex, old_region, old_regex = probe_context()
local region = ""
local data = SYS.exec(string.format("curl -sL --connect-timeout 5 -m 5 --speed-time 5 --speed-limit 1 --retry 2 -H 'User-Agent: %s' '%s'", UA, url))
local currency = data and (string.match(data, '"priceCurrency" content="(%u%u%u)"') or string.match(data, '"priceCurrency"%s*:%s*"(%u%u%u)"'))
if currency then
status = 2
region = currency
finish_region_test(region, regex, old_region, old_regex)
end
return region
end
local function network_test()
local test_url1 = "https://www.gstatic.com/generate_204"
local test_url2 = "https://cp.cloudflare.com/generate_204"
local httpcode1 = SYS.exec(string.format("curl -sL --connect-timeout 10 -m 15 --speed-time 5 --speed-limit 1 --retry 2 -o /dev/null -w %%{http_code} -H 'Accept-Language: en' -H 'Content-Type: application/json' -H 'User-Agent: %s' '%s'", UA, test_url1))
local httpcode2 = SYS.exec(string.format("curl -sL --connect-timeout 10 -m 15 --speed-time 5 --speed-limit 1 --retry 2 -o /dev/null -w %%{http_code} -H 'Accept-Language: en' -H 'Content-Type: application/json' -H 'User-Agent: %s' '%s'", UA, test_url2))
if httpcode1 or httpcode2 then
if tonumber(httpcode1) == 204 or tonumber(httpcode2) == 204 then
unlock_auto_select()
return
end
end
log_error(MSG.err_network)
end
if check_only then
local ok = pcall(function()
local region = proxy_unlock_test() or ""
local st = probe_status(status, region)
pcall(function()
result_node = current_service_node() or ""
end)
print_result(st, region)
end)
if not ok then
print_result(0, "")
end
os.exit(0)
end
network_test()