update 2026-05-27 23:25:57

This commit is contained in:
action
2026-05-27 23:25:57 +08:00
parent 84f707b52b
commit d0159b49a5
759 changed files with 362365 additions and 0 deletions
+218
View File
@@ -0,0 +1,218 @@
include $(TOPDIR)/rules.mk
LUCI_TITLE:=luci-app-ssr-plus
LUCI_PKGARCH:=all
PKG_NAME:=luci-app-ssr-plus
PKG_VERSION:=193
PKG_RELEASE:=23
PKG_CONFIG_DEPENDS:= \
CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \
CONFIG_PACKAGE_$(PKG_NAME)_Nftables_Transparent_Proxy \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_NONE_V2RAY \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Xray \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_ChinaDNS_NG \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_DNS2TCP \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_MosDNS \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Http_Proxy \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Mihomo \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadow_TLS \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Kcptun \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_NaiveProxy \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_NONE_Client \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_NONE_Server \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Server \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Simple_Obfs \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_V2ray_Plugin \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Client \
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Server
LUCI_TITLE:=SS/SSR/V2Ray/Trojan/NaiveProxy/Tuic/ShadowTLS/Hysteria/Socks5/Clash LuCI interface
LUCI_PKGARCH:=all
LUCI_DEPENDS:= \
+libuci-lua +lua +luci-compat +coreutils +coreutils-base64 +dns2tcp +dnsmasq-full \
+jq +ip-full +lua-neturl +libuci-lua +microsocks +ipt2socks +lyaml \
+resolveip +curl +nping +unzip +xz-utils \
+PACKAGE_$(PKG_NAME)_INCLUDE_ChinaDNS_NG:chinadns-ng \
+PACKAGE_$(PKG_NAME)_INCLUDE_MosDNS:mosdns \
+PACKAGE_$(PKG_NAME)_INCLUDE_Shadow_TLS:shadow-tls \
+PACKAGE_$(PKG_NAME)_INCLUDE_Kcptun:kcptun-client \
+PACKAGE_$(PKG_NAME)_INCLUDE_NaiveProxy:naiveproxy \
+PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Client:shadowsocks-rust-sslocal \
+PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Server:shadowsocks-rust-ssserver \
+PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Simple_Obfs:simple-obfs-client \
+PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_V2ray_Plugin:v2ray-plugin \
+PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Client:shadowsocksr-libev-ssr-local \
+PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Client:shadowsocksr-libev-ssr-redir \
+PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Server:shadowsocksr-libev-ssr-server
define Package/$(PKG_NAME)/config
choice
prompt "Transparent Proxy Backend"
default PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy if ! PACKAGE_firewall4
default PACKAGE_$(PKG_NAME)_Nftables_Transparent_Proxy if PACKAGE_firewall4
config PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy
bool "Iptables Transparent Proxy"
select PACKAGE_dnsmasq_full_ipset
select PACKAGE_ipset
select PACKAGE_iptables
select PACKAGE_iptables-zz-legacy
select PACKAGE_iptables-mod-conntrack-extra
select PACKAGE_iptables-mod-iprange
select PACKAGE_iptables-mod-socket
select PACKAGE_iptables-mod-tproxy
select PACKAGE_kmod-ipt-nat
config PACKAGE_$(PKG_NAME)_Nftables_Transparent_Proxy
bool "Nftables Transparent Proxy"
select PACKAGE_dnsmasq_full_nftset
select PACKAGE_nftables
select PACKAGE_kmod-nft-socket
select PACKAGE_kmod-nft-tproxy
select PACKAGE_kmod-nft-nat
endchoice
choice
prompt "Shadowsocks Client Selection"
default PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_NONE_Client
config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_NONE_Client
bool "None"
config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Client
bool "Shadowsocks Rust"
endchoice
choice
prompt "Shadowsocks Server Selection"
default PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_NONE_Server
config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_NONE_Server
bool "None"
config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Server
bool "Shadowsocks Rust"
endchoice
choice
prompt "V2ray-core Selection"
default PACKAGE_$(PKG_NAME)_INCLUDE_Xray
default PACKAGE_$(PKG_NAME)_INCLUDE_NONE_V2RAY
config PACKAGE_$(PKG_NAME)_INCLUDE_NONE_V2RAY
bool "None"
config PACKAGE_$(PKG_NAME)_INCLUDE_Xray
bool "Xray-core"
select PACKAGE_xray-core
endchoice
config PACKAGE_$(PKG_NAME)_INCLUDE_ChinaDNS_NG
bool "Include ChinaDNS-NG"
default y if aarch64||arm||i386||x86_64
config PACKAGE_$(PKG_NAME)_INCLUDE_MosDNS
bool "Include MosDNS"
default n
config PACKAGE_$(PKG_NAME)_INCLUDE_Http_Proxy
bool "Include HTTP(S) Proxy Server"
select PACKAGE_3proxy
default y if (aarch64||arm||i386||loongarch64||riscv64||x86_64)
config PACKAGE_$(PKG_NAME)_INCLUDE_Mihomo
bool "Include Mihomo (Clash Support)"
select PACKAGE_mihomo
depends on aarch64||arm||i386||loongarch64||riscv64||x86_64
default y if aarch64||arm||i386||loongarch64||riscv64||x86_64
config PACKAGE_$(PKG_NAME)_INCLUDE_GeoData
bool "Include GeoData (GeoIP and GeoSite)"
select PACKAGE_v2ray-geoip
select PACKAGE_v2ray-geosite
default y if i386||x86_64
config PACKAGE_$(PKG_NAME)_INCLUDE_Shadow_TLS
bool "Include Shadow-TLS"
select PACKAGE_$(PKG_NAME)_INCLUDE_ChinaDNS_NG
select PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Client
depends on aarch64||arm||x86_64
depends on !(TARGET_x86_geode||TARGET_x86_legacy)
default n
config PACKAGE_$(PKG_NAME)_INCLUDE_Kcptun
bool "Include Kcptun"
default n
config PACKAGE_$(PKG_NAME)_INCLUDE_NaiveProxy
bool "Include NaiveProxy"
depends on !(arc||armeb||mips||mips64||powerpc||TARGET_gemini)
default n
config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Simple_Obfs
bool "Include Shadowsocks Simple Obfs Plugin"
default n
config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_V2ray_Plugin
bool "Include Shadowsocks V2ray Plugin"
default n
config PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Client
bool "Include ShadowsocksR Libev Client"
default y
config PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Server
bool "Include ShadowsocksR Libev Server"
default n
endef
define Package/$(PKG_NAME)/conffiles
/etc/config/shadowsocksr
/etc/ssrplus/
endef
include $(TOPDIR)/feeds/luci/luci.mk
define Package/$(PKG_NAME)/install
ifneq ($(wildcard ${CURDIR}/luasrc),)
$(INSTALL_DIR) $(1)$(LUCI_LIBRARYDIR)
cp -pR $(PKG_BUILD_DIR)/luasrc/* $(1)$(LUCI_LIBRARYDIR)/
$(FIND) $(1)$(LUCI_LIBRARYDIR)/ -type f -name '*.luadoc' | $(XARGS) rm
$(if $(CONFIG_LUCI_SRCDIET),$(call SrcDiet,$(1)$(LUCI_LIBRARYDIR)/),true)
$(call SubstituteVersion,$(1)$(LUCI_LIBRARYDIR)/)
endif
ifneq ($(wildcard ${CURDIR}/ucode),)
$(INSTALL_DIR) $(1)$(UCODE_LIBRARYDIR)
cp -pR $(PKG_BUILD_DIR)/ucode/* $(1)$(UCODE_LIBRARYDIR)/
$(call SubstituteVersion,$(1)$(UCODE_LIBRARYDIR)/)
endif
ifneq ($(wildcard ${CURDIR}/htdocs),)
$(INSTALL_DIR) $(1)$(HTDOCS)
cp -pR $(PKG_BUILD_DIR)/htdocs/* $(1)$(HTDOCS)/
$(if $(CONFIG_LUCI_JSMIN),$(call JsMin,$(1)$(HTDOCS)/),true)
$(if $(CONFIG_LUCI_CSSTIDY),$(call CssTidy,$(1)$(HTDOCS)/),true)
endif
ifneq ($(wildcard ${CURDIR}/root),)
$(INSTALL_DIR) $(1)/
cp -pR $(PKG_BUILD_DIR)/root/* $(1)/
endif
ifneq ($(wildcard ${CURDIR}/src),)
$(call Build/Install/Default)
$(CP) $(PKG_INSTALL_DIR)/* $(1)/
endif
if [ -d "$(1)/usr/bin" ]; then \
chmod 0755 "$(1)/usr/bin/ssr-monitor" 2>/dev/null || true; \
chmod 0755 "$(1)/usr/bin/ssr-rules" 2>/dev/null || true; \
chmod 0755 "$(1)/usr/bin/ssr-switch" 2>/dev/null || true; \
fi
if [ -f "$(1)/etc/hotplug.d/iface/99-ssrplus-pppoe" ]; then \
chmod 0755 "$(1)/etc/hotplug.d/iface/99-ssrplus-pppoe" 2>/dev/null || true; \
fi
endef
# call BuildPackage - OpenWrt buildroot signature
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,65 @@
--- luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua
+++ luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua
@@ -26,6 +26,62 @@ function index()
entry({"admin", "services", "shadowsocksr", "reset"}, call("act_reset"))
entry({"admin", "services", "shadowsocksr", "restart"}, call("act_restart"))
entry({"admin", "services", "shadowsocksr", "delete"}, call("act_delete"))
+ entry({'admin', 'services', "shadowsocksr", 'ip'}, call('check_ip')) -- 获取ip情况
+end
+
+function check_site(host, port)
+ local nixio = require "nixio"
+ local socket = nixio.socket("inet", "stream")
+ socket:setopt("socket", "rcvtimeo", 2)
+ socket:setopt("socket", "sndtimeo", 2)
+ local ret = socket:connect(host, port)
+ socket:close()
+ return ret
+end
+
+function get_ip_geo_info()
+ local result = luci.sys.exec('curl --retry 3 -m 10 -LfsA "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36" http://ip-api.com/json/')
+ local json = require "luci.jsonc"
+ local info = json.parse(result)
+
+ return {
+ flag = string.lower(info.countryCode) or "un",
+ country = get_country_name(info.countryCode) or "Unknown",
+ ip = info.query,
+ isp = info.isp
+ }
+end
+
+function get_country_name(countryCode)
+ local country_names = {
+ US = "美国", CN = "中国", JP = "日本", GB = "英国", DE = "德国",
+ FR = "法国", BR = "巴西", IT = "意大利", RU = "俄罗斯", CA = "加拿大",
+ KR = "韩国", ES = "西班牙", AU = "澳大利亚", MX = "墨西哥", ID = "印度尼西亚",
+ NL = "荷兰", TR = "土耳其", CH = "瑞士", SA = "沙特阿拉伯", SE = "瑞典",
+ PL = "波兰", BE = "比利时", AR = "阿根廷", NO = "挪威", AT = "奥地利",
+ TW = "台湾", ZA = "南非", TH = "泰国", DK = "丹麦", MY = "马来西亚",
+ PH = "菲律宾", SG = "新加坡", IE = "爱尔兰", HK = "香港", FI = "芬兰",
+ CL = "智利", PT = "葡萄牙", GR = "希腊", IL = "以色列", NZ = "新西兰",
+ CZ = "捷克", RO = "罗马尼亚", VN = "越南", UA = "乌克兰", HU = "匈牙利",
+ AE = "阿联酋", CO = "哥伦比亚", IN = "印度", EG = "埃及", PE = "秘鲁", TW = "台湾"
+ }
+ return country_names[countryCode]
+end
+
+function check_ip()
+ local e = {}
+ local port = 80
+ local geo_info = get_ip_geo_info(ip)
+ e.ip = geo_info.ip
+ e.flag = geo_info.flag
+ e.country = geo_info.country
+ e.isp = geo_info.isp
+ e.baidu = check_site('www.baidu.com', port)
+ e.taobao = check_site('www.taobao.com', port)
+ e.google = check_site('www.google.com', port)
+ e.youtube = check_site('www.youtube.com', port)
+ luci.http.prepare_content('application/json')
+ luci.http.write_json(e)
end
function subscribe()
@@ -0,0 +1,309 @@
local m, s, o
local cbi = require "luci.cbi"
local uci = require "luci.model.uci".cursor()
local URL = require "url"
-- 获取 LAN IP 地址
function lanip()
local lan_ip
-- 尝试从 UCI 直接读取
lan_ip = luci.sys.exec("uci -q get network.lan.ipaddr 2>/dev/null | awk -F'/' '{print $1}' | tr -d '\\n'")
-- 尝试从 LAN 接口信息中读取(优先 ifname,再 fallback 到 device
if not lan_ip or lan_ip == "" then
lan_ip = luci.sys.exec([[
ip -4 addr show $(uci -q -p /tmp/state get network.lan.ifname || uci -q -p /tmp/state get network.lan.device) 2>/dev/null \
| grep -w 'inet' | awk '{print $2}' | cut -d'/' -f1 | grep -v '^127\.' | head -n1 | tr -d '\n']])
end
-- 取任意一个 global IPv4 地址
if not lan_ip or lan_ip == "" then
lan_ip = luci.sys.exec([[
ip -4 addr show scope global 2>/dev/null \
| grep -w 'inet' | awk '{print $2}' | cut -d'/' -f1 | grep -v '^127\.' | head -n1 | tr -d '\n']])
end
return lan_ip
end
local lan_ip = lanip()
local server_table = {}
local type_table = {}
local function is_finded(e)
return luci.sys.exec(string.format('type -t -p "%s" -p "/usr/libexec/%s" 2>/dev/null', e, e)) ~= ""
end
local function clash_display_name(s)
if s.type ~= "clash" or not s.clash_url or s.clash_url == "" then
return nil
end
local ok, parsed = pcall(URL.parse, s.clash_url)
if ok and parsed and parsed.host then
return "[CLASH]:" .. parsed.host
end
return "[CLASH]"
end
uci:foreach("shadowsocksr", "servers", function(s)
if s.type ~= "tun" and s.alias then
server_table[s[".name"]] = "[%s]:%s" % {string.upper(s.v2ray_protocol or s.type), s.alias}
elseif s.type ~= "tun" and s.server and s.server_port then
server_table[s[".name"]] = "[%s]:%s:%s" % {string.upper(s.v2ray_protocol or s.type), s.server, s.server_port}
elseif s.type ~= "tun" then
local display_name = clash_display_name(s)
if display_name then
server_table[s[".name"]] = display_name
end
end
if s.type and s.type ~= "tun" then
type_table[s[".name"]] = s.type
end
end)
local key_table = {}
for key, _ in pairs(server_table) do
table.insert(key_table, key)
end
table.sort(key_table)
m = Map("shadowsocksr")
-- [[ global ]]--
s = m:section(TypedSection, "global", translate("Server failsafe auto swith and custom update settings"))
s.anonymous = true
-- o = s:option(Flag, "monitor_enable", translate("Enable Process Deamon"))
-- o.rmempty = false
-- o.default = "1"
o = s:option(Flag, "enable_switch", translate("Enable Auto Switch"))
o.rmempty = false
o.default = "1"
o = s:option(Value, "switch_time", translate("Switch check cycly(second)"))
o.datatype = "uinteger"
o:depends("enable_switch", "1")
o.default = 667
o = s:option(Value, "switch_timeout", translate("Check timout(second)"))
o.datatype = "uinteger"
o:depends("enable_switch", "1")
o.default = 5
o = s:option(Value, "switch_try_count", translate("Check Try Count"))
o.datatype = "uinteger"
o:depends("enable_switch", "1")
o.default = 3
s:append(cbi.Template("shadowsocksr/advanced_switch_compact"))
o = s:option(Value, "default_node_local_port", translate("Default Node Local Port"))
o.datatype = "port"
o.default = 1234
o.rmempty = false
o = s:option(Value, "gfwlist_url", translate("gfwlist Update url"))
o:value("https://fastly.jsdelivr.net/gh/YW5vbnltb3Vz/domain-list-community@release/gfwlist.txt", translate("v2fly/domain-list-community"))
o:value("https://fastly.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/gfw.txt", translate("Loyalsoldier/v2ray-rules-dat"))
o:value("https://fastly.jsdelivr.net/gh/Loukky/gfwlist-by-loukky/gfwlist.txt", translate("Loukky/gfwlist-by-loukky"))
o:value("https://fastly.jsdelivr.net/gh/gfwlist/gfwlist/gfwlist.txt", translate("gfwlist/gfwlist"))
o.default = "https://fastly.jsdelivr.net/gh/YW5vbnltb3Vz/domain-list-community@release/gfwlist.txt"
o = s:option(Value, "chnroute_url", translate("Chnroute Update url"))
o:value("https://ispip.clang.cn/all_cn.txt", translate("Clang.CN"))
o:value("https://ispip.clang.cn/all_cn_cidr.txt", translate("Clang.CN.CIDR"))
o:value("https://fastly.jsdelivr.net/gh/gaoyifan/china-operator-ip@ip-lists/china.txt", translate("china-operator-ip"))
o.default = "https://ispip.clang.cn/all_cn.txt"
o = s:option(Flag, "apple_optimization", translate("Apple domains optimization"), translate("For Apple domains equipped with Chinese mainland CDN, always responsive to Chinese CDN IP addresses"))
o.rmempty = false
o.default = "1"
o = s:option(Value, "apple_url", translate("Apple Domains Update url"))
o:value("https://fastly.jsdelivr.net/gh/felixonmars/dnsmasq-china-list/apple.china.conf", translate("felixonmars/dnsmasq-china-list"))
o.default = "https://fastly.jsdelivr.net/gh/felixonmars/dnsmasq-china-list/apple.china.conf"
o:depends("apple_optimization", "1")
o = s:option(Value, "apple_dns", translate("Apple Domains DNS"), translate("If empty, Not change Apple domains parsing DNS (Default is empty)"))
o.rmempty = true
o.default = ""
o.datatype = "ip4addr"
o:depends("apple_optimization", "1")
o = s:option(Flag, "adblock", translate("Enable adblock"))
o.rmempty = false
o = s:option(Value, "adblock_url", translate("adblock_url"))
o:value("https://raw.githubusercontent.com/neodevpro/neodevhost/master/dnsmasq.conf", translate("NEO DEV HOST"))
o:value("https://anti-ad.net/anti-ad-for-dnsmasq.conf", translate("anti-AD"))
o.default = "https://raw.githubusercontent.com/neodevpro/neodevhost/master/dnsmasq.conf"
o:depends("adblock", "1")
o.description = translate("Support AdGuardHome and DNSMASQ format list")
o = s:option(Button, "Reset", translate("Reset to defaults"))
o.inputstyle = "reload"
o.write = function()
luci.sys.call("/etc/init.d/shadowsocksr reset")
luci.http.redirect(luci.dispatcher.build_url("admin", "services", "shadowsocksr", "servers"))
end
-- [[ SOCKS5 Proxy ]]--
s = m:section(TypedSection, "socks5_proxy", translate("Global SOCKS5 Proxy Server"))
s.anonymous = true
-- s.description = translate("Only Same as Global Server is supported here. If the current main program supports a built-in SOCKS5 listener, it will be enabled in-process; otherwise SSR Plus will fall back to microsocks.")
-- Enable/Disable Option
o = s:option(Flag, "enabled", translate("Enable"))
o.default = 0
o.rmempty = false
-- Socks Auth
if is_finded("xray") then
o = s:option(ListValue, "socks5_auth", translate("Socks5 Auth Mode"), translate("Socks protocol auth methods, default:noauth."))
o.default = "noauth"
o:value("noauth", "NOAUTH")
o:value("password", "PASSWORD")
o.rmempty = true
-- Socks User
o = s:option(Value, "socks5_user", translate("Socks5 User"), translate("Only when Socks5 Auth Mode is password valid, Mandatory."))
o.rmempty = true
o:depends("socks5_auth", "password")
-- Socks Password
o = s:option(Value, "socks5_pass", translate("Socks5 Password"), translate("Only when Socks5 Auth Mode is password valid, Not mandatory."))
o.password = true
o.rmempty = true
o:depends("socks5_auth", "password")
-- Socks Mixed
o = s:option(Flag, "socks5_mixed", translate("Enabled Mixed"), translate("Mixed as an alias of socks, default:Enabled."))
o.default = "1"
o.rmempty = false
end
-- Local Port
o = s:option(Value, "local_port", translate("Local Port"))
o.datatype = "port"
o.default = 1080
o.rmempty = false
if is_finded("3proxy") then
-- [[ HTTP/HTTPS Proxy ]]--
s = m:section(TypedSection, "http_proxy", translate("Global HTTP/HTTPS Proxy Server"))
s.anonymous = true
-- s.description = translate("Only Same as Global Server is supported here. HTTP/HTTPS proxy service is provided through 3proxy.")
o = s:option(Flag, "enabled", translate("Enable"))
o.default = 0
o.rmempty = false
o = s:option(ListValue, "http_auth", translate("HTTP Auth Mode"), translate("HTTP proxy auth method, default:none."))
o.default = "none"
o:value("none", "NONE")
o:value("password", "PASSWORD")
o.rmempty = false
o = s:option(Value, "http_user", translate("HTTP User"), translate("Only when HTTP Auth Mode is password valid, Mandatory."))
o.rmempty = true
o:depends("http_auth", "password")
o = s:option(Value, "http_pass", translate("HTTP Password"), translate("Only when HTTP Auth Mode is password valid, Not mandatory."))
o.password = true
o.rmempty = true
o:depends("http_auth", "password")
o = s:option(Value, "local_port", translate("Local Port"))
o.datatype = "port"
o.default = 3128
o.rmempty = false
end
-- [[ fragmen Settings ]]--
if is_finded("xray") then
s = m:section(TypedSection, "global_xray_fragment", translate("Xray Fragment Settings"))
s.anonymous = true
o = s:option(Flag, "fragment", translate("Fragment"), translate("TCP fragments, which can deceive the censorship system in some cases, such as bypassing SNI blacklists."))
o.default = 0
o = s:option(ListValue, "fragment_packets", translate("Fragment Packets"), translate("\"1-3\" is for segmentation at TCP layer, applying to the beginning 1 to 3 data writes by the client. \"tlshello\" is for TLS client hello packet fragmentation."))
o.default = "tlshello"
o:value("tlshello", "tlshello")
o:value("1-1", "1-1")
o:value("1-2", "1-2")
o:value("1-3", "1-3")
o:value("1-5", "1-5")
o:depends("fragment", true)
o = s:option(Value, "fragment_length", translate("Fragment Length"), translate("Fragmented packet length (byte)"))
o.datatype = "or(uinteger,portrange)"
o.default = "100-200"
o:depends("fragment", true)
o = s:option(Value, "fragment_delay", translate("Fragment Delay"), translate("Fragmentation interval (ms)"))
o.datatype = "or(uinteger,portrange)"
o.default = "10-20"
o:depends("fragment", true)
o = s:option(Value, "fragment_maxSplit", translate("Max Split"), translate("Limit the maximum number of splits."))
o.datatype = "or(uinteger,portrange)"
o.default = "100-200"
o:depends("fragment", true)
o = s:option(Flag, "noise", translate("Noise"), translate("UDP noise, Under some circumstances it can bypass some UDP based protocol restrictions."))
o.default = 0
s = m:section(TypedSection, "xray_noise_packets", translate("Xray Noise Packets"))
s.description = translate(
"<font style='color:red'>" .. translate("To send noise packets, select \"Noise\" in Xray Settings.") .. "</font>" ..
"<br/><font><b>" .. translate("Packet or Rand length as a string, e.g., 10-20.") .. "</b></font>" ..
"<br/><font><b>" .. translate("For specific usage, see:") .. "</b></font>" ..
"<a href='https://xtls.github.io/config/outbounds/freedom.html' target='_blank'>" ..
"<font style='color:green'><b>" .. translate("Click to the page") .. "</b></font></a>")
s.template = "cbi/tblsection"
s.sortable = true
s.anonymous = true
s.addremove = true
s.remove = function(self, section)
for k, v in pairs(self.children) do
v.rmempty = true
v.validate = nil
end
TypedSection.remove(self, section)
end
o = s:option(Flag, "enabled", translate("Enable"))
o.default = 1
o.rmempty = false
o = s:option(ListValue, "type", translate("Type"))
o.default = "base64"
o:value("rand", "rand")
o:value("str", "str")
o:value("hex", "hex")
o:value("base64", "base64")
o = s:option(Value, "domainStrategy", translate("Domain Strategy"))
o.default = "AsIs"
o:value("AsIs", "AsIs")
o:value("UseIP", "UseIP")
o:value("UseIPv4", "UseIPv4")
o:value("ForceIP", "ForceIP")
o:value("ForceIPv4", "ForceIPv4")
o.rmempty = false
o = s:option(Value, "packet", translate("Packet | Rand Length"))
o.datatype = "minlength(1)"
o.rmempty = false
o = s:option(Value, "delay", translate("Delay (ms)"))
o.datatype = "or(uinteger,portrange)"
o.rmempty = false
s:append(cbi.Template("shadowsocksr/optimize_cbi_ui"))
end
return m
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,234 @@
-- Copyright (C) 2017 yushi studio <ywb94@qq.com> github.com/ywb94
-- Copyright (C) 2018 lean <coolsnowwolf@gmail.com> github.com/coolsnowwolf
-- Licensed to the public under the GNU General Public License v3.
local m, s, sec, o
local uci = require "luci.model.uci".cursor()
local URL = require "url"
-- 获取 LAN IP 地址
function lanip()
local lan_ip
-- 尝试从 UCI 直接读取
lan_ip = luci.sys.exec("uci -q get network.lan.ipaddr 2>/dev/null | awk -F'/' '{print $1}' | tr -d '\\n'")
-- 尝试从 LAN 接口信息中读取(优先 ifname,再 fallback 到 device
if not lan_ip or lan_ip == "" then
lan_ip = luci.sys.exec([[
ip -4 addr show $(uci -q -p /tmp/state get network.lan.ifname || uci -q -p /tmp/state get network.lan.device) 2>/dev/null \
| grep -w 'inet' | awk '{print $2}' | cut -d'/' -f1 | grep -v '^127\.' | head -n1 | tr -d '\n']])
end
-- 取任意一个 global IPv4 地址
if not lan_ip or lan_ip == "" then
lan_ip = luci.sys.exec([[
ip -4 addr show scope global 2>/dev/null \
| grep -w 'inet' | awk '{print $2}' | cut -d'/' -f1 | grep -v '^127\.' | head -n1 | tr -d '\n']])
end
return lan_ip
end
local lan_ip = lanip()
local validation = require "luci.cbi.datatypes"
local clash_nodes = {}
local function is_finded(e)
return luci.sys.exec(string.format('type -t -p "%s" -p "/usr/libexec/%s" 2>/dev/null', e, e)) ~= ""
end
local function clash_display_name(s)
if s.type ~= "clash" or not s.clash_url or s.clash_url == "" then
return nil
end
local ok, parsed = pcall(URL.parse, s.clash_url)
if ok and parsed and parsed.host then
return "[CLASH]:" .. parsed.host
end
return "[CLASH]"
end
m = Map("shadowsocksr", translate("ShadowSocksR Plus+ Settings"), translate("<h3>Support SS/SSR/V2RAY/XRAY/TROJAN/TUIC/HYSTERIA2/NAIVEPROXY/SOCKS5/CLASH etc.</h3>"))
m:section(SimpleSection).template = "shadowsocksr/status"
local server_table = {}
local server_order = {}
uci:foreach("shadowsocksr", "servers", function(s)
if s.type == "clash" then
clash_nodes[s[".name"]] = true
end
if s.type ~= "tun" and s.alias then
server_table[s[".name"]] = "[%s]:%s" % {string.upper(s.v2ray_protocol or s.type), s.alias}
elseif s.type ~= "tun" and s.server and s.server_port then
server_table[s[".name"]] = "[%s]:%s:%s" % {string.upper(s.v2ray_protocol or s.type), s.server, s.server_port}
elseif s.type ~= "tun" then
local display_name = clash_display_name(s)
if display_name then
server_table[s[".name"]] = display_name
end
end
if s.type ~= "tun" and server_table[s[".name"]] then
table.insert(server_order, s[".name"])
end
end)
-- [[ Global Setting ]]--
s = m:section(TypedSection, "global")
s.anonymous = true
o = s:option(ListValue, "global_server", translate("Main Server"))
o:value("nil", translate("Disable"))
for _, key in ipairs(server_order) do
o:value(key, server_table[key])
end
o.default = "nil"
o.rmempty = false
o = s:option(DummyValue, "_clash_panel", translate("Clash Panel"))
o.template = "shadowsocksr/clash_main_panel"
o.clash_nodes = clash_nodes
o = s:option(ListValue, "threads", translate("Multi Threads Option"))
o:value("0", translate("Auto Threads"))
o:value("1", translate("1 Thread"))
o:value("2", translate("2 Threads"))
o:value("4", translate("4 Threads"))
o:value("8", translate("8 Threads"))
o:value("16", translate("16 Threads"))
o:value("32", translate("32 Threads"))
o:value("64", translate("64 Threads"))
o:value("128", translate("128 Threads"))
o.default = "0"
o.rmempty = false
o = s:option(ListValue, "run_mode", translate("Running Mode"))
o:value("gfw", translate("GFW List Mode"))
o:value("router", translate("IP Route Mode"))
o:value("all", translate("Global Mode"))
o.default = gfw
o = s:option(ListValue, "dports", translate("Proxy Ports"))
o:value("1", translate("All Ports"))
o:value("2", translate("Only Common Ports"))
o:value("3", translate("Custom Ports"))
cp = s:option(Value, "custom_ports", translate("Enter Custom Ports"))
cp:depends("dports", "3") -- 仅当用户选择“Custom Ports”时显示
cp.placeholder = "e.g., 80,443,8080"
o.default = 1
o = s:option(ListValue, "pdnsd_enable", translate("Resolve Dns Mode"))
if is_finded("dns2tcp") then
o:value("1", translate("Use DNS2TCP query"))
end
if is_finded("mosdns") then
o:value("4", translate("Use MosDNS query"))
end
if is_finded("chinadns-ng") then
o:value("6", translate("Use ChinaDNS-NG query and cache"))
end
o:value("7", translate("Prefer module built-in DNS"))
o:value("0", translate("Use Local DNS Service listen port 5335"))
o.default = 1
o = s:option(Value, "tunnel_forward", translate("Anti-pollution DNS Server"))
o:value("8.8.4.4:53", translate("Google Public DNS (8.8.4.4)"))
o:value("8.8.8.8:53", translate("Google Public DNS (8.8.8.8)"))
o:value("208.67.222.222:53", translate("OpenDNS (208.67.222.222)"))
o:value("208.67.220.220:53", translate("OpenDNS (208.67.220.220)"))
o:value("209.244.0.3:53", translate("Level 3 Public DNS (209.244.0.3)"))
o:value("209.244.0.4:53", translate("Level 3 Public DNS (209.244.0.4)"))
o:value("4.2.2.1:53", translate("Level 3 Public DNS (4.2.2.1)"))
o:value("4.2.2.2:53", translate("Level 3 Public DNS (4.2.2.2)"))
o:value("4.2.2.3:53", translate("Level 3 Public DNS (4.2.2.3)"))
o:value("4.2.2.4:53", translate("Level 3 Public DNS (4.2.2.4)"))
o:value("1.1.1.1:53", translate("Cloudflare DNS (1.1.1.1)"))
o:depends("pdnsd_enable", "1")
o:depends("pdnsd_enable", "7")
o.description = translate("Custom DNS Server format as IP:PORT (default: 8.8.4.4:53)")
o.datatype = "ip4addrport"
o.default = "8.8.4.4:53"
o = s:option(Value, "tunnel_forward_mosdns", translate("Anti-pollution DNS Server"))
o:value("tcp://8.8.4.4:53,tcp://8.8.8.8:53", translate("Google Public DNS"))
o:value("tcp://208.67.222.222:53,tcp://208.67.220.220:53", translate("OpenDNS"))
o:value("tcp://209.244.0.3:53,tcp://209.244.0.4:53", translate("Level 3 Public DNS-1 (209.244.0.3-4)"))
o:value("tcp://4.2.2.1:53,tcp://4.2.2.2:53", translate("Level 3 Public DNS-2 (4.2.2.1-2)"))
o:value("tcp://4.2.2.3:53,tcp://4.2.2.4:53", translate("Level 3 Public DNS-3 (4.2.2.3-4)"))
o:value("tcp://1.1.1.1:53,tcp://1.0.0.1:53", translate("Cloudflare DNS"))
o:depends("pdnsd_enable", "4")
o.description = translate("Custom DNS Server format as tcp://IP:PORT or tls://DOMAIN:PORT (tcp://8.8.8.8 or tls://dns.google:853)")
o.default = "tcp://8.8.4.4:53,tcp://8.8.8.8:53"
o = s:option(Flag, "filter_aaaa", translate("Disable IPv6 for Overseas FQDN"))
o:depends("pdnsd_enable", "1")
o:depends("pdnsd_enable", "4")
o:depends("pdnsd_enable", "7")
o.rmempty = false
o.default = "1"
if is_finded("chinadns-ng") then
o = s:option(Value, "chinadns_ng_tunnel_forward", translate("Anti-pollution DNS Server"))
o:value("8.8.4.4:53", translate("Google Public DNS (8.8.4.4)"))
o:value("8.8.8.8:53", translate("Google Public DNS (8.8.8.8)"))
o:value("208.67.222.222:53", translate("OpenDNS (208.67.222.222)"))
o:value("208.67.220.220:53", translate("OpenDNS (208.67.220.220)"))
o:value("209.244.0.3:53", translate("Level 3 Public DNS (209.244.0.3)"))
o:value("209.244.0.4:53", translate("Level 3 Public DNS (209.244.0.4)"))
o:value("4.2.2.1:53", translate("Level 3 Public DNS (4.2.2.1)"))
o:value("4.2.2.2:53", translate("Level 3 Public DNS (4.2.2.2)"))
o:value("4.2.2.3:53", translate("Level 3 Public DNS (4.2.2.3)"))
o:value("4.2.2.4:53", translate("Level 3 Public DNS (4.2.2.4)"))
o:value("1.1.1.1:53", translate("Cloudflare DNS (1.1.1.1)"))
o:depends("pdnsd_enable", "6")
o.default = "8.8.4.4:53"
o.description = translate(
"<ul>" ..
"<li>" .. translate("Custom DNS Server format as IP:PORT (default: 8.8.4.4:53)") .. "</li>" ..
"<li>" .. translate("Muitiple DNS server can saperate with ','") .. "</li>" ..
"</ul>"
)
o = s:option(ListValue, "chinadns_ng_proto", translate("ChinaDNS-NG query protocol"))
o:value("none", translate("UDP/TCP upstream"))
o:value("tcp", translate("TCP upstream"))
o:value("udp", translate("UDP upstream"))
o:value("tls", translate("DoT upstream (Need use wolfssl version)"))
o:depends("pdnsd_enable", "6")
o = s:option(Value, "chinadns_forward", translate("Domestic DNS Server"))
o:value("", translate("Disable ChinaDNS-NG"))
o:value("wan", translate("Use DNS from WAN"))
o:value("wan_114", translate("Use DNS from WAN and 114DNS"))
o:value("114.114.114.114:53", translate("Nanjing Xinfeng 114DNS (114.114.114.114)"))
o:value("119.29.29.29:53", translate("DNSPod Public DNS (119.29.29.29)"))
o:value("223.5.5.5:53", translate("AliYun Public DNS (223.5.5.5)"))
o:value("180.76.76.76:53", translate("Baidu Public DNS (180.76.76.76)"))
o:value("101.226.4.6:53", translate("360 Security DNS (China Telecom) (101.226.4.6)"))
o:value("123.125.81.6:53", translate("360 Security DNS (China Unicom) (123.125.81.6)"))
o:value("1.2.4.8:53", translate("CNNIC SDNS (1.2.4.8)"))
o:depends({pdnsd_enable = "6", run_mode = "router"})
o.default = "wan"
o.description = translate("Custom DNS Server format as IP:PORT (default: disabled)")
o.validate = function(self, value, section)
if (section and value) then
if value == "wan" or value == "wan_114" then
return value
end
if validation.ip4addrport(value) then
return value
end
return nil, translate("Expecting: %s"):format(translate("valid address:port"))
end
return value
end
end
local dns_defaults_section = m:section(SimpleSection)
dns_defaults_section.template = "shadowsocksr/client_dns_defaults"
m:section(SimpleSection).template = "shadowsocksr/status_bottom"
return m
@@ -0,0 +1,30 @@
local m, s, o
m = SimpleForm(
"component_update",
translate("Component Update"),
translate("Check installed component versions and upgrade them online from the upstream release page.")
)
m.reset = false
m.submit = false
s = m:section(SimpleSection)
o = m:field(ListValue, "component_mirror", translate("Mirror URL"))
o:value("direct", translate("GitHub Direct"))
o:value("ghproxy", "mirror.ghproxy.com")
o:value("ghproxy_cc", "ghproxy.cc")
o:value("ghfast", "ghfast.top")
o:value("jsdelivr", "cdn.jsdelivr.net")
o.rmempty = false
o.cfgvalue = function(self)
return m.uci:get_first("shadowsocksr", "global", "component_mirror") or "direct"
end
o.write = function(self, section, value)
m.uci:set("shadowsocksr", "@global[0]", "component_mirror", value)
m.uci:commit("shadowsocksr")
end
s.template = "shadowsocksr/component"
return m
@@ -0,0 +1,137 @@
require "luci.ip"
require "nixio.fs"
require "luci.sys"
local m, s, o
m = Map("shadowsocksr")
s = m:section(TypedSection, "access_control")
s.anonymous = true
-- Interface control
s:tab("Interface", translate("Interface control"))
o = s:taboption("Interface", DynamicList, "Interface", translate("Interface"))
o.template = "cbi/network_netlist"
o.widget = "checkbox"
o.nocreate = true
o.unspecified = true
o.description = translate("Listen only on the given interface or, if unspecified, on all")
-- Part of WAN
s:tab("wan_ac", translate("WAN IP AC"))
o = s:taboption("wan_ac", DynamicList, "wan_bp_ips", translate("WAN White List IP"))
o.datatype = "ip4addr"
o = s:taboption("wan_ac", DynamicList, "wan_fw_ips", translate("WAN Force Proxy IP"))
o.datatype = "ip4addr"
-- Part of LAN
s:tab("lan_ac", translate("LAN IP AC"))
o = s:taboption("lan_ac", ListValue, "lan_ac_mode", translate("LAN Access Control"))
o:value("0", translate("Disable"))
o:value("w", translate("Allow listed only"))
o:value("b", translate("Allow all except listed"))
o.rmempty = false
o = s:taboption("lan_ac", DynamicList, "lan_ac_ips", translate("LAN Host List"))
o.datatype = "ipaddr"
luci.ip.neighbors({family = 4}, function(entry)
if entry.reachable then
o:value(entry.dest:string())
end
end)
o:depends("lan_ac_mode", "w")
o:depends("lan_ac_mode", "b")
o = s:taboption("lan_ac", DynamicList, "lan_bp_ips", translate("LAN Bypassed Host List"))
o.datatype = "ipaddr"
luci.ip.neighbors({family = 4}, function(entry)
if entry.reachable then
o:value(entry.dest:string())
end
end)
o = s:taboption("lan_ac", DynamicList, "lan_fp_ips", translate("LAN Force Proxy Host List"))
o.datatype = "ipaddr"
luci.ip.neighbors({family = 4}, function(entry)
if entry.reachable then
o:value(entry.dest:string())
end
end)
o = s:taboption("lan_ac", DynamicList, "lan_gm_ips", translate("Game Mode Host List"))
o.datatype = "ipaddr"
luci.ip.neighbors({family = 4}, function(entry)
if entry.reachable then
o:value(entry.dest:string())
end
end)
-- Part of Self
-- s:tab("self_ac", translate("Router Self AC"))
-- o = s:taboption("self_ac",ListValue, "router_proxy", translate("Router Self Proxy"))
-- o:value("1", translatef("Normal Proxy"))
-- o:value("0", translatef("Bypassed Proxy"))
-- o:value("2", translatef("Forwarded Proxy"))
-- o.rmempty = false
s:tab("esc", translate("Bypass Domain List"))
local escconf = "/etc/ssrplus/white.list"
o = s:taboption("esc", TextValue, "escconf")
o.rows = 13
o.wrap = "off"
o.rmempty = true
o.cfgvalue = function(self, section)
return nixio.fs.readfile(escconf) or ""
end
o.write = function(self, section, value)
nixio.fs.writefile(escconf, value:gsub("\r\n", "\n"))
end
o.remove = function(self, section, value)
nixio.fs.writefile(escconf, "")
end
s:tab("block", translate("Black Domain List"))
local blockconf = "/etc/ssrplus/black.list"
o = s:taboption("block", TextValue, "blockconf")
o.rows = 13
o.wrap = "off"
o.rmempty = true
o.cfgvalue = function(self, section)
return nixio.fs.readfile(blockconf) or " "
end
o.write = function(self, section, value)
nixio.fs.writefile(blockconf, value:gsub("\r\n", "\n"))
end
o.remove = function(self, section, value)
nixio.fs.writefile(blockconf, "")
end
s:tab("denydomain", translate("Deny Domain List"))
local denydomainconf = "/etc/ssrplus/deny.list"
o = s:taboption("denydomain", TextValue, "denydomainconf")
o.rows = 13
o.wrap = "off"
o.rmempty = true
o.cfgvalue = function(self, section)
return nixio.fs.readfile(denydomainconf) or " "
end
o.write = function(self, section, value)
nixio.fs.writefile(denydomainconf, value:gsub("\r\n", "\n"))
end
o.remove = function(self, section, value)
nixio.fs.writefile(denydomainconf, "")
end
if luci.sys.call('[ -f "/www/luci-static/resources/uci.js" ]') == 0 then
m.apply_on_parse = true
function m.on_apply(self)
luci.sys.call("/etc/init.d/shadowsocksr reload > /dev/null 2>&1 &")
end
end
m:append(Template("shadowsocksr/control_layout"))
return m
@@ -0,0 +1,104 @@
require "luci.util"
require "nixio.fs"
require "luci.sys"
require "luci.http"
f = SimpleForm("logview")
f.reset = false
f.submit = false
f:append(Template("shadowsocksr/log"))
-- 自定义 log 函数
function log(...)
local result = os.date("%Y-%m-%d %H:%M:%S: ") .. table.concat({...}, " ")
local f, err = io.open("/var/log/ssrplus.log", "a")
if f and err == nil then
f:write(result .. "\n")
f:close()
end
end
-- 创建备份与恢复表单
fb = SimpleForm('backup-restore')
fb.reset = false
fb.submit = false
s = fb:section(SimpleSection, translate("Backup and Restore"), translate("Backup or Restore Client and Server Configurations.") ..
"<br><font style='color:red'><b>" ..
translate("Note: Restoring configurations across different versions may cause compatibility issues.") ..
"</b></font>")
s.anonymous = true
s:append(Template("shadowsocksr/backup_restore"))
-- 定义备份目标文件和目录
local backup_targets = {
files = {
"/etc/config/shadowsocksr"
},
dirs = {
"/etc/ssrplus"
}
}
local file_path = '/tmp/shadowsocksr_upload.tar.gz'
local temp_dir = '/tmp/shadowsocksr_bak'
local fd
-- 处理文件上传
luci.http.setfilehandler(function(meta, chunk, eof)
if not fd and meta and meta.name == "ulfile" and chunk then
-- 初始化上传处理
luci.sys.call("rm -rf " .. temp_dir)
nixio.fs.remove(file_path)
fd = nixio.open(file_path, "w")
luci.sys.call("ls /tmp/sub_md5_* >/dev/null 2>&1 && rm -f /tmp/sub_md5_*")
luci.sys.call("echo '' > /var/log/ssrplus.log")
end
if fd and chunk then
fd:write(chunk)
end
if eof and fd then
fd:close()
fd = nil
if nixio.fs.access(file_path) then
log(" * shadowsocksr 配置文件上传成功…") -- 使用自定义的 log 函数
luci.sys.call("mkdir -p " .. temp_dir)
if luci.sys.call("tar -xzf " .. file_path .. " -C " .. temp_dir) == 0 then
-- 处理文件还原
for _, target in ipairs(backup_targets.files) do
local temp_file = temp_dir .. target
if nixio.fs.access(temp_file) then
luci.sys.call(string.format("cp -f '%s' '%s'", temp_file, target))
log(" * 文件 " .. target .. " 还原成功…") -- 使用自定义的 log 函数
end
end
-- 处理目录还原
for _, target in ipairs(backup_targets.dirs) do
local temp_dir_path = temp_dir .. target
if nixio.fs.access(temp_dir_path) then
luci.sys.call(string.format("cp -rf '%s'/* '%s/'", temp_dir_path, target))
log(" * 目录 " .. target .. " 还原成功…") -- 使用自定义的 log 函数
end
end
log(" * shadowsocksr 配置还原成功…") -- 使用自定义的 log 函数
log(" * 重启 shadowsocksr 服务中…\n") -- 使用自定义的 log 函数
luci.sys.call('/etc/init.d/shadowsocksr restart > /dev/null 2>&1 &')
else
log(" * shadowsocksr 配置文件解压失败,请重试!") -- 使用自定义的 log 函数
end
else
log(" * shadowsocksr 配置文件上传失败,请重试!") -- 使用自定义的 log 函数
end
-- 清理临时文件
luci.sys.call("rm -rf " .. temp_dir)
nixio.fs.remove(file_path)
luci.sys.call("ls /tmp/sub_md5_* >/dev/null 2>&1 && rm -f /tmp/sub_md5_*")
end
end)
return f, fb
@@ -0,0 +1,396 @@
-- Copyright (C) 2017 yushi studio <ywb94@qq.com>
-- Licensed to the public under the GNU General Public License v3.
require "luci.http"
require "luci.dispatcher"
require "nixio.fs"
local m, s, o
local sid = arg[1]
-- 加密方式(SS/SSR用)
local encrypt_methods = {
"rc4-md5", "rc4-md5-6", "rc4", "table",
"aes-128-cfb", "aes-192-cfb", "aes-256-cfb",
"aes-128-ctr", "aes-192-ctr", "aes-256-ctr",
"bf-cfb", "camellia-128-cfb", "camellia-192-cfb", "camellia-256-cfb",
"cast5-cfb", "des-cfb", "idea-cfb", "rc2-cfb", "seed-cfb",
"salsa20", "chacha20", "chacha20-ietf"
}
local encrypt_methods_ss = {
"aes-128-gcm", "aes-192-gcm", "aes-256-gcm",
"chacha20-ietf-poly1305", "xchacha20-ietf-poly1305",
"2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305"
}
-- Shadowsocks 加密方法(Xray 版)
local v_ss_encrypt_method_list = {
"aes-128-cfb", "aes-256-cfb", "aes-128-gcm", "aes-256-gcm",
"chacha20", "chacha20-ietf", "chacha20-poly1305", "chacha20-ietf-poly1305"
}
-- 伪装类型(用于 mKCP/QUIC
local header_type_list = {
"none", "srtp", "utp", "wechat-video", "dtls", "wireguard"
}
local protocol = {"origin"}
obfs = {"plain", "http_simple", "http_post"}
m = Map("shadowsocksr", translate("Edit ShadowSocksR Server"))
m.redirect = luci.dispatcher.build_url("admin/services/shadowsocksr/server")
if m.uci:get("shadowsocksr", sid) ~= "server_config" then
luci.http.redirect(m.redirect)
return
end
-- [[ Server Setting ]]--
s = m:section(NamedSection, sid, "server_config")
s.anonymous = true
s.addremove = false
-- ========== 基本设置 ==========
o = s:option(Flag, "enable", translate("Enable"))
o.default = 1
o.rmempty = false
o = s:option(ListValue, "type", translate("Server Type"))
o:value("socks5", translate("Socks5"))
if nixio.fs.access("/usr/bin/mihomo") or nixio.fs.access("/usr/libexec/mihomo") or nixio.fs.access("/usr/bin/ssserver") or nixio.fs.access("/usr/libexec/ssserver") then
o:value("ss", translate("ShadowSocks"))
end
if nixio.fs.access("/usr/bin/ssr-server") then
o:value("ssr", translate("ShadowsocksR"))
end
-- Xray 协议支持
if nixio.fs.access("/usr/bin/xray") or nixio.fs.access("/usr/libexec/xray") then
o:value("vmess", "VMess (Xray)")
o:value("vless", "VLESS (Xray)")
o:value("trojan", "Trojan (Xray)")
o:value("shadowsocks", "Shadowsocks (Xray)")
end
o.default = "socks5"
o = s:option(Value, "server_port", translate("Server Port"))
o.datatype = "port"
math.randomseed(tostring(os.time()):reverse():sub(1, 7))
o.default = math.random(10240, 20480)
o.rmempty = false
o.description = translate("warning! Please do not reuse the port!")
o = s:option(Value, "timeout", translate("Connection Timeout"))
o.datatype = "uinteger"
o.default = 60
o.rmempty = false
o:depends("type", "ss")
o:depends("type", "ssr")
o = s:option(Value, "username", translate("Username"))
o.rmempty = false
o:depends("type", "socks5")
o = s:option(Value, "password", translate("Password"))
o.password = true
o.rmempty = false
o:depends("type", "socks5")
o:depends("type", "ss")
o:depends("type", "ssr")
o:depends("type", "trojan")
o:depends("type", "shadowsocks")
-- ========== SS/SSR 协议特有 ==========
o = s:option(ListValue, "encrypt_method", translate("Encrypt Method"))
for _, v in ipairs(encrypt_methods) do o:value(v) end
o.rmempty = false
o:depends("type", "ssr")
o = s:option(ListValue, "encrypt_method_ss", translate("Encrypt Method"))
for _, v in ipairs(encrypt_methods_ss) do o:value(v) end
o.rmempty = false
o:depends("type", "ss")
o = s:option(ListValue, "protocol", translate("Protocol"))
for _, v in ipairs(protocol) do o:value(v) end
o.rmempty = false
o:depends("type", "ssr")
o = s:option(ListValue, "obfs", translate("Obfs"))
for _, v in ipairs(obfs) do o:value(v) end
o.rmempty = false
o:depends("type", "ssr")
o = s:option(Value, "obfs_param", translate("Obfs param (optional)"))
o:depends("type", "ssr")
o = s:option(Flag, "fast_open", translate("TCP Fast Open"))
o.rmempty = false
o:depends("type", "ss")
o:depends("type", "ssr")
-- ========== Xray 协议通用字段 ==========
-- 用户备注
o = s:option(Value, "remarks", translate("Remarks"))
o.default = translate("Remarks")
o.rmempty = true
o:depends("type", "vmess")
o:depends("type", "vless")
o:depends("type", "trojan")
o:depends("type", "shadowsocks")
-- 用户等级
o = s:option(Value, "level", translate("User Level"))
o.datatype = "uinteger"
o.default = 1
o.rmempty = true
o:depends("type", "vmess")
o:depends("type", "vless")
o:depends("type", "shadowsocks")
o:depends("type", "trojan")
-- ========== Xray 协议认证字段 ==========
-- UUID (用于 vmess/vless)
o = s:option(Value, "uuid", translate("UUID"))
o.description = translate("Required for VMess/VLESS. Generate with: uuidgen")
o.rmempty = true
o:depends("type", "vmess")
o:depends("type", "vless")
-- Trojan 密码
o = s:option(Value, "trojan_password", translate("Trojan Password"))
o.password = true
o.rmempty = true
o:depends("type", "trojan")
-- Shadowsocks 密码和加密(Xray 内置)
o = s:option(Value, "ss_password", translate("Shadowsocks Password"))
o.password = true
o.rmempty = true
o:depends("type", "shadowsocks")
o = s:option(ListValue, "ss_method", translate("Encrypt Method"))
for _, v in ipairs(v_ss_encrypt_method_list) do o:value(v) end
o.default = "chacha20-ietf-poly1305"
o.rmempty = true
o:depends("type", "shadowsocks")
o = s:option(ListValue, "ss_network", translate("Transport"))
o:value("tcp", "TCP")
o:value("udp", "UDP")
o:value("tcp,udp", "TCP,UDP")
o.default = "tcp,udp"
o.rmempty = true
o:depends("type", "shadowsocks")
-- VMess alterId
o = s:option(Value, "alter_id", translate("Alter ID"))
o.datatype = "uinteger"
o.default = 0
o.rmempty = true
o:depends("type", "vmess")
-- VLESS decryption
o = s:option(Value, "decryption", translate("Decryption"))
o.default = "none"
o.rmempty = true
o:depends("type", "vless")
-- ========== TLS / XTLS 设置 ==========
o = s:option(Flag, "tls", translate("TLS"))
o.default = 0
o.rmempty = true
o:depends("type", "vmess")
o:depends("type", "vless")
o:depends("type", "trojan")
o:depends("type", "shadowsocks")
o = s:option(Flag, "xtls", translate("XTLS"))
o.default = 0
o.rmempty = true
o:depends({ type = "vless", tls = "1" })
o = s:option(ListValue, "flow", translate("Flow"))
o:value("xtls-rprx-origin", "xtls-rprx-origin")
o:value("xtls-rprx-origin-udp443", "xtls-rprx-origin-udp443")
o:value("xtls-rprx-direct", "xtls-rprx-direct")
o:value("xtls-rprx-direct-udp443", "xtls-rprx-direct-udp443")
o:value("xtls-rprx-splice", "xtls-rprx-splice")
o:value("xtls-rprx-splice-udp443", "xtls-rprx-splice-udp443")
o.default = "xtls-rprx-direct"
o.rmempty = true
o:depends("xtls", "1")
o = s:option(Value, "tls_serverName", translate("Server Name (SNI)"))
o.rmempty = true
o:depends("tls", "1")
o = s:option(Value, "tls_certificateFile", translate("Certificate File Path"))
o.description = translate("e.g.: /etc/ssl/fullchain.pem")
o.rmempty = true
o:depends("tls", "1")
o = s:option(Value, "tls_keyFile", translate("Private Key File Path"))
o.description = translate("e.g.: /etc/ssl/private.key")
o.rmempty = true
o:depends("tls", "1")
-- ========== 传输层设置 (Transport) ==========
o = s:option(ListValue, "transport", translate("Transport Protocol"))
o:value("tcp", "TCP")
o:value("mkcp", "mKCP")
o:value("ws", "WebSocket")
o:value("h2", "HTTP/2")
o:value("ds", "DomainSocket")
o:value("quic", "QUIC")
o.default = "tcp"
o.rmempty = true
o:depends("type", "vmess")
o:depends("type", "vless")
o:depends("type", "trojan")
-- ----- WebSocket 设置 -----
o = s:option(Value, "ws_host", translate("WebSocket Host"))
o.rmempty = true
o:depends("transport", "ws")
o:depends("type", "vmess")
o:depends("type", "vless")
o:depends("type", "trojan")
o = s:option(Value, "ws_path", translate("WebSocket Path"))
o.default = "/"
o.rmempty = true
o:depends("transport", "ws")
o:depends("type", "vmess")
o:depends("type", "vless")
o:depends("type", "trojan")
-- ----- HTTP/2 设置 -----
o = s:option(Value, "h2_host", translate("HTTP/2 Host"))
o.rmempty = true
o:depends("transport", "h2")
o:depends("type", "vmess")
o:depends("type", "vless")
o = s:option(Value, "h2_path", translate("HTTP/2 Path"))
o.default = "/"
o.rmempty = true
o:depends("transport", "h2")
o:depends("type", "vmess")
o:depends("type", "vless")
-- ----- TCP 伪装设置 -----
o = s:option(ListValue, "tcp_guise", translate("TCP Camouflage Type"))
o:value("none", "none")
o:value("http", "http")
o.default = "none"
o.rmempty = true
o:depends("transport", "tcp")
o:depends("type", "vmess")
o:depends("type", "vless")
o = s:option(DynamicList, "tcp_guise_http_host", translate("HTTP Host"))
o.rmempty = true
o:depends("tcp_guise", "http")
o = s:option(DynamicList, "tcp_guise_http_path", translate("HTTP Path"))
o.rmempty = true
o:depends("tcp_guise", "http")
-- ----- mKCP 设置 -----
o = s:option(ListValue, "mkcp_guise", translate("mKCP Camouflage Type"))
for _, v in ipairs(header_type_list) do o:value(v) end
o.default = "none"
o.rmempty = true
o:depends("transport", "mkcp")
o = s:option(Value, "mkcp_mtu", translate("KCP MTU"))
o.datatype = "uinteger"
o.default = 1350
o.rmempty = true
o:depends("transport", "mkcp")
o = s:option(Value, "mkcp_tti", translate("KCP TTI"))
o.datatype = "uinteger"
o.default = 20
o.rmempty = true
o:depends("transport", "mkcp")
o = s:option(Value, "mkcp_uplinkCapacity", translate("KCP Uplink Capacity"))
o.datatype = "uinteger"
o.default = 5
o.rmempty = true
o:depends("transport", "mkcp")
o = s:option(Value, "mkcp_downlinkCapacity", translate("KCP Downlink Capacity"))
o.datatype = "uinteger"
o.default = 20
o.rmempty = true
o:depends("transport", "mkcp")
o = s:option(Flag, "mkcp_congestion", translate("KCP Congestion Control"))
o.default = 0
o.rmempty = true
o:depends("transport", "mkcp")
o = s:option(Value, "mkcp_readBufferSize", translate("KCP Read Buffer Size"))
o.datatype = "uinteger"
o.default = 1
o.rmempty = true
o:depends("transport", "mkcp")
o = s:option(Value, "mkcp_writeBufferSize", translate("KCP Write Buffer Size"))
o.datatype = "uinteger"
o.default = 1
o.rmempty = true
o:depends("transport", "mkcp")
o = s:option(Value, "mkcp_seed", translate("KCP Seed"))
o.datatype = "uinteger"
o.rmempty = true
o:depends("transport", "mkcp")
-- ----- DomainSocket 设置 -----
o = s:option(Value, "ds_path", translate("DomainSocket Path"))
o.description = translate("A legal file path. This file must not exist before running.")
o.rmempty = true
o:depends("transport", "ds")
-- ----- QUIC 设置 -----
o = s:option(ListValue, "quic_security", translate("QUIC Security"))
o:value("none", "none")
o:value("aes-128-gcm", "aes-128-gcm")
o:value("chacha20-poly1305", "chacha20-poly1305")
o.default = "none"
o.rmempty = true
o:depends("transport", "quic")
o = s:option(Value, "quic_key", translate("QUIC Key"))
o.rmempty = true
o:depends("transport", "quic")
o = s:option(ListValue, "quic_guise", translate("QUIC Camouflage Type"))
for _, v in ipairs(header_type_list) do o:value(v) end
o.default = "none"
o.rmempty = true
o:depends("transport", "quic")
-- ========== 访问控制 ==========
o = s:option(Flag, "bind_local", translate("Bind Local Only"))
o.description = translate("When selected, it can only be accessed locally. Recommended when using reverse proxies.")
o.default = 0
o.rmempty = true
o:depends("type", "vmess")
o:depends("type", "vless")
o:depends("type", "trojan")
o:depends("type", "shadowsocks")
o = s:option(Flag, "accept_lan", translate("Accept LAN Access"))
o.description = translate("When selected, it can be accessed from LAN. This may not be safe!")
o.default = 0
o.rmempty = true
o:depends("type", "vmess")
o:depends("type", "vless")
o:depends("type", "trojan")
o:depends("type", "shadowsocks")
return m
@@ -0,0 +1,143 @@
-- Copyright (C) 2017 yushi studio <ywb94@qq.com>
-- Licensed to the public under the GNU General Public License v3.
require "luci.http"
require "luci.dispatcher"
local m, sec, o
local encrypt_methods = {
"table",
"rc4",
"rc4-md5",
"rc4-md5-6",
"aes-128-cfb",
"aes-192-cfb",
"aes-256-cfb",
"aes-128-ctr",
"aes-192-ctr",
"aes-256-ctr",
"bf-cfb",
"camellia-128-cfb",
"camellia-192-cfb",
"camellia-256-cfb",
"cast5-cfb",
"des-cfb",
"idea-cfb",
"rc2-cfb",
"seed-cfb",
"salsa20",
"chacha20",
"chacha20-ietf"
}
local encrypt_methods_ss = {
-- aead
"aes-128-gcm",
"aes-192-gcm",
"aes-256-gcm",
"chacha20-ietf-poly1305",
"xchacha20-ietf-poly1305",
-- aead 2022
"2022-blake3-aes-128-gcm",
"2022-blake3-aes-256-gcm",
"2022-blake3-chacha20-poly1305"
--[[ stream
"table",
"rc4",
"rc4-md5",
"aes-128-cfb",
"aes-192-cfb",
"aes-256-cfb",
"aes-128-ctr",
"aes-192-ctr",
"aes-256-ctr",
"bf-cfb",
"camellia-128-cfb",
"camellia-192-cfb",
"camellia-256-cfb",
"salsa20",
"chacha20",
"chacha20-ietf" ]]
}
local protocol = {
"origin",
"verify_deflate",
"auth_sha1_v4",
"auth_aes128_sha1",
"auth_aes128_md5",
"auth_chain_a"
}
obfs = {
"plain",
"http_simple",
"http_post",
"random_head",
"tls1.2_ticket_auth",
"tls1.2_ticket_fastauth"
}
m = Map("shadowsocksr")
-- [[ Global Setting ]]--
sec = m:section(TypedSection, "server_global", translate("Global Setting"))
sec.anonymous = true
o = sec:option(Flag, "enable_server", translate("Enable Server"))
o.rmempty = false
-- [[ Server Setting ]]--
sec = m:section(TypedSection, "server_config", translate("Server Setting"))
sec.anonymous = true
sec.addremove = true
sec.template = "cbi/tblsection"
sec.extedit = luci.dispatcher.build_url("admin/services/shadowsocksr/server/%s")
function sec.create(...)
local sid = TypedSection.create(...)
if sid then
luci.http.redirect(sec.extedit % sid)
return
end
end
o = sec:option(Flag, "enable", translate("Enable"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or translate("0")
end
o.rmempty = false
o = sec:option(DummyValue, "type", translate("Server Type"))
function o.cfgvalue(self, section)
local val = Value.cfgvalue(self, section) or "ss"
if val == "vmess" then return "VMess (Xray)"
elseif val == "vless" then return "VLESS (Xray)"
elseif val == "trojan" then return "Trojan (Xray)"
elseif val == "shadowsocks" then return "Shadowsocks (Xray)"
else return val end
end
o = sec:option(DummyValue, "server_port", translate("Server Port"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or "-"
end
o = sec:option(DummyValue, "username", translate("Username"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or "-"
end
o = sec:option(DummyValue, "encrypt_method", translate("Encrypt Method"))
function o.cfgvalue(self, section)
local method = self.map:get(section, "encrypt_method") or self.map:get(section, "encrypt_method_ss")
return method and method:upper() or "-"
end
o = sec:option(DummyValue, "protocol", translate("Protocol"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or "-"
end
o = sec:option(DummyValue, "obfs", translate("Obfs"))
function o.cfgvalue(...)
return Value.cfgvalue(...) or "-"
end
return m
@@ -0,0 +1,776 @@
-- Licensed to the public under the GNU General Public License v3.
require "luci.http"
require "luci.sys"
require "luci.util"
require "nixio.fs"
require "luci.dispatcher"
require "luci.model.uci"
local nixio = require "nixio"
local json = require "luci.jsonc"
local cbi = require "luci.cbi"
local uci = require "luci.model.uci".cursor()
local URL = require "url"
local m, s, o, node
local server_count = 0
local server_cache = {}
local server_sections = {}
local detect_cache = {}
local CLASH_YAML_DIR = "/etc/ssrplus/clash"
local CLASH_YAML_HELPER = "/usr/share/shadowsocksr/clash_yaml.lua"
local DEFAULT_SERVER_PAGE_SIZE = 50
local SERVER_PAGE_SIZE_OPTIONS = {25, 50, 100, 200, 0}
local function is_finded(e)
return luci.sys.exec(string.format('type -t -p "%s" -p "/usr/libexec/%s" 2>/dev/null', e, e)) ~= ""
end
local function is_js_luci()
return luci.sys.call('[ -f "/www/luci-static/resources/uci.js" ]') == 0
end
-- 保存并应用行为
local function apply_redirect(m)
local tmp_uci_file = "/etc/config/" .. "shadowsocksr" .. "_redirect"
if m.redirect and m.redirect ~= "" then
if nixio.fs.access(tmp_uci_file) then
local redirect
for line in io.lines(tmp_uci_file) do
redirect = line:match("option%s+url%s+['\"]([^'\"]+)['\"]")
if redirect and redirect ~= "" then break end
end
if redirect and redirect ~= "" then
luci.sys.call("/bin/rm -f " .. tmp_uci_file)
luci.http.redirect(redirect)
end
else
nixio.fs.writefile(tmp_uci_file, "config redirect\n")
end
m.on_after_save = function(self)
local redirect = self.redirect
if redirect and redirect ~= "" then
m.uci:set("shadowsocksr" .. "_redirect", "@redirect[0]", "url", redirect)
end
end
else
luci.sys.call("/bin/rm -f " .. tmp_uci_file)
end
end
local function set_apply_on_parse(map)
if not map then return end
if is_js_luci() then
apply_redirect(map)
local old = map.on_after_save
map.on_after_save = function(self)
if old then old(self) end
-- map:set("@global[0]", "timestamp", os.time())
end
end
end
local function trim(text)
if not text or text == "" then
return ""
end
return (text:gsub("^%s*(.-)%s*$", "%1"))
end
local function parse_nonnegative_int(value)
local number = tonumber(value)
if not number then
return nil
end
number = math.floor(number)
if number < 0 then
return nil
end
return number
end
local function in_list(list, target)
for _, item in ipairs(list) do
if item == target then
return true
end
end
return false
end
local function upload_alias(filename)
local stem = tostring(filename or ""):gsub("\\", "/"):match("([^/]+)$") or "custom"
stem = stem:gsub("%.%w+$", "")
stem = trim(stem):gsub("[%c\r\n]+", " "):gsub("%s+", " ")
if stem == "" then
stem = "custom"
end
return "Clash_" .. stem
end
local function hash_file(path)
local cmd = "md5sum " .. luci.util.shellquote(path) .. " 2>/dev/null | awk '{print $1}'"
return trim(luci.sys.exec(cmd))
end
local function preprocess_clash_yaml(input_path, output_path)
local cmd = string.format(
"/usr/bin/lua %s prepare %s %s >/dev/null 2>&1",
luci.util.shellquote(CLASH_YAML_HELPER),
luci.util.shellquote(input_path),
luci.util.shellquote(output_path)
)
return luci.sys.call(cmd) == 0
end
local function clash_path_in_use(path, exclude_sid)
local in_use = false
uci:foreach("shadowsocksr", "servers", function(section)
if section[".name"] ~= exclude_sid and section.clash_path == path then
in_use = true
return false
end
end)
return in_use
end
local function cleanup_old_clash_path(old_path, new_path, sid)
if not old_path or old_path == "" or old_path == new_path then
return
end
if old_path:sub(1, #CLASH_YAML_DIR + 1) ~= CLASH_YAML_DIR .. "/" then
return
end
if clash_path_in_use(old_path, sid) then
return
end
nixio.fs.remove(old_path)
end
local function find_uploaded_clash_section(upload_name, final_path)
local sid
uci:foreach("shadowsocksr", "servers", function(section)
if section.type ~= "clash" or section.yaml_upload ~= "1" then
return
end
if upload_name ~= "" and section.yaml_upload_name == upload_name then
sid = section[".name"]
return false
end
if final_path ~= "" and section.clash_path == final_path then
sid = section[".name"]
return false
end
end)
return sid
end
local function save_uploaded_clash_node(upload_name, final_path)
local sid = find_uploaded_clash_section(upload_name, final_path)
local old_path
local alias
if not sid then
sid = uci:add("shadowsocksr", "servers")
end
if not sid then
return nil
end
old_path = uci:get("shadowsocksr", sid, "clash_path")
alias = uci:get("shadowsocksr", sid, "alias")
if not alias or alias == "" then
alias = upload_alias(upload_name)
end
uci:set("shadowsocksr", sid, "type", "clash")
uci:set("shadowsocksr", sid, "alias", alias)
uci:set("shadowsocksr", sid, "server", "127.0.0.1")
uci:set("shadowsocksr", sid, "server_port", "0")
uci:delete("shadowsocksr", sid, "clash_url")
uci:set("shadowsocksr", sid, "clash_path", final_path)
uci:set("shadowsocksr", sid, "clash_user_agent", uci:get("shadowsocksr", sid, "clash_user_agent") or "clash")
if not uci:get("shadowsocksr", sid, "switch_enable") then
uci:set("shadowsocksr", sid, "switch_enable", uci:get_first("shadowsocksr", "server_subscribe", "switch", "1") or "1")
end
uci:set("shadowsocksr", sid, "yaml_upload", "1")
uci:set("shadowsocksr", sid, "yaml_upload_name", upload_name)
uci:save("shadowsocksr")
uci:commit("shadowsocksr")
cleanup_old_clash_path(old_path, final_path, sid)
luci.sys.call(string.format("/etc/init.d/shadowsocksr clash_cache %s >/dev/null 2>&1 &", luci.util.shellquote(sid)))
return sid, alias
end
local has_mihomo = is_finded("mihomo")
local upload_fd
local upload_tmp_path
local upload_filename
local upload_message
local upload_errmessage
if has_mihomo then
luci.http.setfilehandler(function(meta, chunk, eof)
if not meta or meta.name ~= "clash_yaml_file" then
return
end
if not upload_fd then
if not meta.file or meta.file == "" then
return
end
upload_filename = tostring(meta.file):gsub("[\r\n]", "")
upload_tmp_path = string.format("/tmp/ssrplus-clash-upload-%d-%d.yaml", nixio.getpid(), os.time())
upload_fd = nixio.open(upload_tmp_path, "w")
if not upload_fd then
upload_errmessage = translate("Failed to create temporary YAML upload file.")
upload_tmp_path = nil
upload_filename = nil
return
end
end
if chunk and upload_fd then
upload_fd:write(chunk)
end
if eof and upload_fd then
upload_fd:close()
upload_fd = nil
end
end)
if luci.http.formvalue("upload_clash_yaml") then
if not upload_tmp_path or not upload_filename or not nixio.fs.access(upload_tmp_path) then
upload_errmessage = upload_errmessage or translate("No custom YAML file was selected.")
else
local hash
local final_path
local tmp_output = string.format("%s/.upload-%d-%d.yaml", CLASH_YAML_DIR, nixio.getpid(), os.time())
local sid
local alias
nixio.fs.mkdirr(CLASH_YAML_DIR)
nixio.fs.remove(tmp_output)
if preprocess_clash_yaml(upload_tmp_path, tmp_output) then
hash = hash_file(tmp_output)
if hash == "" then
nixio.fs.remove(tmp_output)
upload_errmessage = translate("Uploaded YAML validation or preprocessing failed.")
else
final_path = string.format("%s/%s.yaml", CLASH_YAML_DIR, hash)
luci.sys.call(string.format("mv -f %s %s", luci.util.shellquote(tmp_output), luci.util.shellquote(final_path)))
sid, alias = save_uploaded_clash_node(upload_filename, final_path)
if sid then
upload_message = string.format(translate("Custom YAML imported successfully: %s"), alias or sid)
else
upload_errmessage = translate("Uploaded YAML validation or preprocessing failed.")
end
end
else
nixio.fs.remove(tmp_output)
upload_errmessage = translate("Uploaded YAML validation or preprocessing failed.")
end
end
if upload_tmp_path then
nixio.fs.remove(upload_tmp_path)
end
end
end
local function preserve_when_hidden(opt, controller, enabled_value)
local original_parse = opt.parse
opt.parse = function(self, section, novld)
local current = self.map:get(section, controller)
if current == nil then
current = self.map:formvalue("cbid." .. self.map.config .. "." .. section .. "." .. controller)
end
if tostring(current or "") ~= tostring(enabled_value) then
return
end
return original_parse(self, section, novld)
end
end
local function migrate_legacy_subscribe_urls()
local subscribe_sid = uci:get_first("shadowsocksr", "server_subscribe")
if not subscribe_sid then
return
end
local legacy_urls = uci:get_list("shadowsocksr", subscribe_sid, "subscribe_url") or {}
if #legacy_urls == 0 then
return
end
local has_items = false
uci:foreach("shadowsocksr", "server_subscribe_item", function()
has_items = true
return false
end)
if has_items then
return
end
for index, url in ipairs(legacy_urls) do
local trimmed = trim(url)
if trimmed ~= "" then
local sid_output = luci.sys.exec("uci add shadowsocksr server_subscribe_item")
local sid = sid_output:match("%S+")
if sid and sid ~= "" then
local escaped_sid = luci.util.shellquote(sid)
local alias = string.format("Subscribe %d", index)
local escaped_alias = luci.util.shellquote(alias)
local escaped_url = luci.util.shellquote(trimmed)
luci.sys.call("uci set shadowsocksr." .. escaped_sid .. ".enabled=1")
luci.sys.call("uci set shadowsocksr." .. escaped_sid .. ".alias=" .. escaped_alias)
luci.sys.call("uci set shadowsocksr." .. escaped_sid .. ".url=" .. escaped_url)
end
end
end
luci.sys.call("uci delete shadowsocksr." .. subscribe_sid .. ".subscribe_url")
luci.sys.call("uci commit shadowsocksr")
end
local function clash_host_port(clash_url)
if not clash_url or clash_url == "" then
return nil, nil
end
local ok, parsed = pcall(URL.parse, clash_url)
if not ok or not parsed then
return nil, nil
end
local host = parsed.host
local port = parsed.port
if not port or port == "" then
port = (parsed.scheme == "http") and "80" or "443"
end
return host, port
end
migrate_legacy_subscribe_urls()
uci:foreach("shadowsocksr", "servers", function(s)
server_count = server_count + 1
server_sections[#server_sections + 1] = s[".name"]
server_cache[s[".name"]] = {
type = s.type,
v2ray_protocol = s.v2ray_protocol,
alias = s.alias,
server_port = s.server_port,
server = s.server,
transport = s.transport,
ws_path = s.ws_path,
ws_host = s.ws_host,
tls_host = s.tls_host,
tls = s.tls,
reality = s.reality,
clash_url = s.clash_url
}
end)
local function get_server(section)
return server_cache[section] or {}
end
do
local raw = nixio.fs.readfile("/tmp/ssrplus_server_detect.json")
if raw and raw ~= "" then
local parsed = json.parse(raw)
if type(parsed) == "table" then
detect_cache = parsed
end
end
end
m = Map("shadowsocksr", translate("Servers subscription and manage"))
if upload_errmessage then
m.errmessage = upload_errmessage
elseif upload_message then
m.message = upload_message
end
local style_section = m:section(SimpleSection)
style_section.template = "shadowsocksr/servers_subscribe_url_style"
-- Server Subscribe
s = m:section(TypedSection, "server_subscribe")
s.anonymous = true
o = s:option(Flag, "auto_update", translate("Auto Update"))
o.rmempty = false
o.description = translate("Auto Update Server subscription, GFW list and CHN route")
o = s:option(ListValue, "config_auto_update_mode", translate("Update Mode"))
o:value("0", translate("Appointment Mode"))
o:value("1", translate("Loop Mode"))
o.default = "0"
o.rmempty = true
o:depends("auto_update", "1")
o = s:option(ListValue, "auto_update_week_time", translate("Update cycle (Day/Week)"))
o:value('*', translate("Every Day"))
o:value("1", translate("Every Monday"))
o:value("2", translate("Every Tuesday"))
o:value("3", translate("Every Wednesday"))
o:value("4", translate("Every Thursday"))
o:value("5", translate("Every Friday"))
o:value("6", translate("Every Saturday"))
o:value("0", translate("Every Sunday"))
o.default = "*"
o.rmempty = true
o:depends({auto_update = "1", config_auto_update_mode = "0"})
o = s:option(ListValue, "auto_update_day_time", translate("Regular update (Hour)"))
for t = 0, 23 do
o:value(t, t .. ":00")
end
o.default = 2
o.rmempty = true
o:depends({auto_update = "1", config_auto_update_mode = "0"})
o = s:option(ListValue, "auto_update_min_time", translate("Regular update (Min)"))
for i = 0, 59 do
o:value(i, i .. ":00")
end
o.default = 30
o.rmempty = true
o:depends({auto_update = "1", config_auto_update_mode = "0"})
o = s:option(Value, "config_update_interval", translate("Update Interval(min)"))
o.default = "60"
o.datatype = "uinteger"
o.rmempty = true
o:depends({auto_update = "1", config_auto_update_mode = "1"})
o = s:option(Flag, "_subscribe_advanced_toggle", translate("Subscribe Advanced Settings"))
o.rmempty = false
o.default = "0"
o.write = function() end
o.remove = function() end
o = s:option(Value, "filter_words", translate("Subscribe Filter Words"))
o.rmempty = true
o.description = translate("Filter Words splited by /")
o:depends("_subscribe_advanced_toggle", "1")
preserve_when_hidden(o, "_subscribe_advanced_toggle", "1")
o = s:option(Value, "save_words", translate("Subscribe Save Words"))
o.rmempty = true
o.description = translate("Save Words splited by /")
o:depends("_subscribe_advanced_toggle", "1")
preserve_when_hidden(o, "_subscribe_advanced_toggle", "1")
o = s:option(Flag, "allow_insecure", translate("Allow subscribe Insecure nodes By default"))
o.rmempty = false
o.description = translate("Subscribe nodes allows insecure connection as TLS client (insecure)")
o.default = "0"
o:depends("_subscribe_advanced_toggle", "1")
preserve_when_hidden(o, "_subscribe_advanced_toggle", "1")
o = s:option(Flag, "switch", translate("Subscribe Default Auto-Switch"))
o.rmempty = false
o.description = translate("Subscribe new add server default Auto-Switch on")
o.default = "1"
o:depends("_subscribe_advanced_toggle", "1")
preserve_when_hidden(o, "_subscribe_advanced_toggle", "1")
o = s:option(Flag, "proxy", translate("Through proxy update"))
o.rmempty = false
o.description = translate("Through proxy update list, Not Recommended ")
o.default = "1"
o:depends("_subscribe_advanced_toggle", "1")
preserve_when_hidden(o, "_subscribe_advanced_toggle", "1")
o = s:option(Button, "_save_subscribe_settings", translate("Save Subscribe Settings"))
o.inputstyle = "save"
o.description = translate("Save current subscribe settings")
o.write = function() end
o = s:option(Button, "subscribe", translate("Update All Subscribe Servers"))
o.rawhtml = true
o.template = "shadowsocksr/subscribe"
o.write = function(self, section)
self.map.ssr_subscribe_requested = true
end
o = s:option(Button, "delete", translate("Delete All Subscribe Servers"))
o.inputstyle = "reset"
o.description = string.format(translate("Server Count") .. ": %d", server_count)
o.write = function()
uci:delete_all("shadowsocksr", "servers", function(s)
if s.hashkey or s.isSubscribe then
return true
else
return false
end
end)
uci:save("shadowsocksr")
uci:commit("shadowsocksr")
for file in nixio.fs.glob("/tmp/sub_md5_*") do
nixio.fs.remove(file)
end
luci.http.redirect(luci.dispatcher.build_url("admin", "services", "shadowsocksr", "delete"))
return
end
o = s:option(Value, "url_test_url", translate("URL Test Address"))
o:value("https://cp.cloudflare.com/", "Cloudflare")
o:value("https://www.gstatic.com/generate_204", "Gstatic")
o:value("https://www.google.com/generate_204", "Google")
o:value("https://www.youtube.com/generate_204", "YouTube")
o:value("https://connect.rom.miui.com/generate_204", "MIUI (CN)")
o:value("https://connectivitycheck.platform.hicloud.com/generate_204", "HiCloud (CN)")
o.default = o.keylist[3]
o = s:option(Value, "user_agent", translate("User-Agent"))
o.default = "v2rayN/9.99"
o:value("curl", "Curl")
o:value("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0", "Edge for Linux")
o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0", "Edge for Windows")
o:value("v2rayN/9.99", "v2rayN")
o:depends("_subscribe_advanced_toggle", "1")
preserve_when_hidden(o, "_subscribe_advanced_toggle", "1")
if has_mihomo then
o = s:option(DummyValue, "_upload_clash_yaml", translate("Upload Custom YAML File"))
o.template = "shadowsocksr/clash_yaml_upload"
o.description = translate("Upload a custom Clash/Mihomo YAML file. The file will be preprocessed and saved as a local Clash node.")
end
s:append(cbi.Template("shadowsocksr/subscribe_schedule_compact"))
s = m:section(TypedSection, "server_subscribe_item", translate("Subscribe URL"))
s.anonymous = true
s.addremove = true
s.sortable = true
s.template = "shadowsocksr/subscribe_actions_footer"
--s.template = "cbi/tblsection"
--s.template_addremove = "shadowsocksr/subscribe_actions_footer"
s.description = translate("Manage multiple subscribe URLs, including Clash subscriptions. Only enabled entries are included when updating all subscriptions.")
o = s:option(Flag, "enabled", translate("Enable"))
o.rmempty = false
o.default = "1"
o.width = "1%"
function o.cfgvalue(...)
return Flag.cfgvalue(...) or "1"
end
o = s:option(Value, "alias", translate("Alias"))
o.rmempty = true
o.width = "7.5rem"
function o.cfgvalue(self, section)
return Value.cfgvalue(self, section) or string.format("Subscribe %s", section:sub(-4))
end
o = s:option(Value, "url", translate("Subscribe URL"))
o.rmempty = false
-- [[ Servers Manage ]]--
s = m:section(TypedSection, "servers")
s.anonymous = true
s.addremove = true
s.description = translate("Node order can be dragged with the mouse and takes effect immediately. The automatic switch order of server nodes is consistent with the node order in the table.")
s.template = "shadowsocksr/server_table"
set_apply_on_parse(m)
s:append(cbi.Template("shadowsocksr/optimize_cbi_ui"))
s.extedit = luci.dispatcher.build_url("admin", "services", "shadowsocksr", "servers", "%s")
local server_page_size = parse_nonnegative_int(luci.http.formvalue("server_page_size"))
if not server_page_size or not in_list(SERVER_PAGE_SIZE_OPTIONS, server_page_size) then
server_page_size = DEFAULT_SERVER_PAGE_SIZE
end
local server_page_count = 1
if server_page_size > 0 and server_count > 0 then
server_page_count = math.ceil(server_count / server_page_size)
end
local server_page = parse_nonnegative_int(luci.http.formvalue("server_page")) or 1
if server_page < 1 then
server_page = 1
end
if server_page > server_page_count then
server_page = server_page_count
end
local visible_server_sections = server_sections
local server_first_index = server_count > 0 and 1 or 0
local server_last_index = server_count
if server_page_size > 0 then
local first = ((server_page - 1) * server_page_size) + 1
local last = math.min(first + server_page_size - 1, server_count)
visible_server_sections = {}
server_first_index = server_count > 0 and first or 0
server_last_index = server_count > 0 and last or 0
for index = first, last do
visible_server_sections[#visible_server_sections + 1] = server_sections[index]
end
end
s.sortable = true
function s.cfgsections(self)
return visible_server_sections
end
s.server_page = server_page
s.server_page_size = server_page_size
s.server_page_count = server_page_count
s.server_page_sizes = SERVER_PAGE_SIZE_OPTIONS
s.server_total = server_count
s.server_first_index = server_first_index
s.server_last_index = server_last_index
s.server_base_url = luci.dispatcher.build_url("admin", "services", "shadowsocksr", "servers")
function s.create(self, ...)
local used_sid = {}
local next_sid = 1
self.map.uci:foreach(self.config, self.sectiontype, function(s)
local num = s[".name"]:match("^cfg(%x%x)")
if num then
local n = tonumber(num, 16)
if n then
used_sid[n] = true
end
end
end)
local function get_next_sid()
while used_sid[next_sid] do
next_sid = next_sid + 1
end
used_sid[next_sid] = true
return next_sid
end
local sid = TypedSection.create(self, ...)
if sid then
local suffix = sid:sub(-4)
self.map.uci:delete(self.config, sid)
local id = get_next_sid()
local newsid = string.format("cfg%02x%s", id, suffix)
local success = self.map.uci:section(self.config, self.sectiontype, newsid)
if success then
--self.map.uci:save(self.config)
luci.http.redirect(self.extedit % newsid)
return
end
end
end
o = s:option(DummyValue, "type", translate("Type"))
function o.cfgvalue(self, section)
local cfg = get_server(section)
return cfg.v2ray_protocol or cfg.type or translate("None")
end
o = s:option(DummyValue, "alias", translate("Alias"))
function o.cfgvalue(self, section)
return get_server(section).alias or translate("None")
end
o = s:option(DummyValue, "server_port", translate("Socket Connected"))
o.template = "shadowsocksr/socket"
o.width = "10%"
function o.cfgvalue(self, section)
self.detect_cache = detect_cache[section]
local cfg = get_server(section)
local stype = cfg.type
if stype == "clash" then
return "N/A"
end
return cfg.server_port
end
o.render = function(self, section, scope)
local cfg = get_server(section)
local stype = cfg.type
self.type = stype or ""
self.proto = cfg.v2ray_protocol or ""
self.reality = cfg.reality or ""
if stype == "clash" then
self.transport = ""
self.ws_path = ""
self.ws_host = ""
self.tls_host = ""
self.tls = ""
self.reality = ""
else
self.transport = cfg.transport or ""
self.ws_host = cfg.ws_host or ""
self.tls_host = cfg.tls_host or ""
if self.transport == 'ws' then
self.ws_path = cfg.ws_path or ""
self.tls = cfg.tls or ""
else
self.ws_path = ""
self.tls = ""
end
end
DummyValue.render(self, section, scope)
end
o = s:option(DummyValue, "server", translate("Ping Latency"))
o.template = "shadowsocksr/ping"
o.width = "10%"
function o.cfgvalue(self, section)
self.detect_cache = detect_cache[section]
local cfg = get_server(section)
self.type = cfg.type or ""
if cfg.type == "clash" then
return "N/A"
end
return cfg.server or "N/A"
end
local global_server = uci:get_first('shadowsocksr', 'global', 'global_server')
node = s:option(Button, "apply_node", translate("Apply"))
node.inputstyle = "apply"
node.render = function(self, section, scope)
if section == global_server then
self.title = translate("Reapply")
else
self.title = translate("Apply")
end
Button.render(self, section, scope)
end
node.write = function(self, section)
local safe_section = luci.util.shellquote(section)
local cmd = string.format("uci set shadowsocksr.@global[0].global_server=%s && uci commit shadowsocksr", safe_section)
luci.sys.call(cmd)
luci.http.redirect(luci.dispatcher.build_url("admin", "services", "shadowsocksr", "restart"))
end
o = s:option(Flag, "switch_enable", translate("Auto Switch"))
o.rmempty = false
function o.cfgvalue(...)
return Value.cfgvalue(...) or 1
end
m:append(cbi.Template("shadowsocksr/server_list"))
m.commit_handler = function(self)
if not self.ssr_subscribe_requested then
return
end
for _, config in ipairs(self.parsechain or {}) do
self.uci:commit(config)
end
self.ssr_subscribe_autostart = true
end
return m
@@ -0,0 +1,284 @@
-- Copyright (C) 2017 yushi studio <ywb94@qq.com>
-- Licensed to the public under the GNU General Public License v3.
require "nixio.fs"
require "luci.sys"
require "luci.model.uci"
local m, s, o
local redir_run = 0
local reudp_run = 0
local sock5_run = 0
local http_run = 0
local server_run = 0
local kcptun_run = 0
local tunnel_run = 0
local gfw_count = 0
local ad_count = 0
local ip_count = 0
local Process_list = luci.sys.exec("busybox ps -w 2>/dev/null || busybox ps")
local uci = require "luci.model.uci".cursor()
local global_server = uci:get_first("shadowsocksr", "global", "global_server", "nil")
local global_type = global_server ~= "nil" and (uci:get("shadowsocksr", global_server, "type") or "") or ""
local global_socks_enabled = uci:get_first("shadowsocksr", "socks5_proxy", "enabled", "0") == "1"
local global_socks_server = uci:get_first("shadowsocksr", "socks5_proxy", "server", "nil")
local global_http_enabled = uci:get_first("shadowsocksr", "http_proxy", "enabled", "0") == "1"
local has_3proxy = nixio.fs.access("/usr/bin/3proxy") or nixio.fs.access("/usr/libexec/3proxy") or nixio.fs.access("/bin/3proxy")
local pdnsd_mode = uci:get_first("shadowsocksr", 'global', 'pdnsd_enable', '0')
-- html constants
font_blue = [[<b style=color:green>]]
style_blue = [[<b style=color:red>]]
font_off = [[</b>]]
bold_on = [[<strong>]]
bold_off = [[</strong>]]
local kcptun_version = translate("Unknown")
local kcp_file = "/usr/bin/kcptun-client"
if not nixio.fs.access(kcp_file) then
kcptun_version = translate("Not exist")
else
if not nixio.fs.access(kcp_file, "rwx", "rx", "rx") then
nixio.fs.chmod(kcp_file, 755)
end
kcptun_version = "<b>" ..luci.sys.exec(kcp_file .. " -v | awk '{printf $3}'") .. "</b>"
if not kcptun_version or kcptun_version == "" then
kcptun_version = translate("Unknown")
end
end
if nixio.fs.access("/etc/ssrplus/gfw_list.conf") then
gfw_count = tonumber(luci.sys.exec("cat /etc/ssrplus/gfw_list.conf | wc -l")) / 2
end
if nixio.fs.access("/etc/ssrplus/ad.conf") then
ad_count = tonumber(luci.sys.exec("cat /etc/ssrplus/ad.conf | wc -l"))
end
if nixio.fs.access("/etc/ssrplus/china_ssr.txt") then
ip_count = tonumber(luci.sys.exec("cat /etc/ssrplus/china_ssr.txt | wc -l"))
end
if nixio.fs.access("/etc/ssrplus/applechina.conf") then
apple_count = tonumber(luci.sys.exec("cat /etc/ssrplus/applechina.conf | wc -l"))
end
if Process_list:find("udp.only.ssr.reudp") then
reudp_run = 1
end
--[[
if Process_list:find("tcp.udp.dual.ssr.retcp") then
redir_run = 1
end
]]--
if Process_list:find("tcp.only.ssr.retcp") then
redir_run = 1
end
if Process_list:find("tcp.udp.ssr.local") then
sock5_run = 1
end
if has_3proxy and Process_list:find("3proxy%-ssr%-http%.cfg") then
http_run = 1
end
if Process_list:find("tcp.udp.ssr.retcp") then
redir_run = 1
reudp_run = 1
end
--[[
if Process_list:find("nft.ssr.retcp") then
redir_run = 1
end
]]--
if Process_list:find("local.ssr.retcp") then
redir_run = 1
sock5_run = 1
end
--[[
if Process_list:find("local.nft.ssr.retcp") then
redir_run = 1
sock5_run = 1
end
]]--
if Process_list:find("local.udp.ssr.retcp") then
reudp_run = 1
redir_run = 1
sock5_run = 1
end
if global_type == "socks5" and Process_list:find("ipt2socks") then
if Process_list:find("%-T") or Process_list:find("%-%-tcp%-only") then
redir_run = 1
end
if Process_list:find("%-U") or Process_list:find("%-%-udp%-only") then
reudp_run = 1
end
if global_socks_enabled and (global_socks_server == "same" and global_socks_server == global_server) then
sock5_run = 1
end
end
if (global_type == "clash" or global_type == "tuic" or global_type == "ss") and Process_list:find("ssr%-retcp") then
redir_run = 1
reudp_run = 1
if global_socks_enabled and (global_socks_server == "same" and global_socks_server == global_server) then
sock5_run = 1
end
end
if (global_type == "clash" or global_type == "tuic" or global_type == "ss") and Process_list:find("mihomo") and (Process_list:find("/clash%-") or Process_list:find("/tuic%-") or Process_list:find("/ss%-")) then
redir_run = 1
reudp_run = 1
if global_socks_enabled and (global_socks_server == "same" and global_socks_server == global_server) then
sock5_run = 1
end
end
if has_3proxy and global_http_enabled and http_run == 0 and Process_list:find("3proxy%-ssr%-http%.cfg") then
http_run = 1
end
if Process_list:find("kcptun.client") then
kcptun_run = 1
end
if Process_list:find("ssr.server") then
server_run = 1
end
if Process_list:find("mihomo") and Process_list:find("/ss%-server%-") then
server_run = 1
end
if Process_list:find("ssrplus/bin/dns2tcp") or
Process_list:find("ssrplus/bin/mosdns") or
Process_list:find("chinadns.*127.0.0.1.*5335") then
pdnsd_run = 1
end
if pdnsd_mode == "7" and (global_type == "clash" or global_type == "tuic" or global_type == "ss") and Process_list:find("ssr%-retcp") then
pdnsd_run = 1
end
if pdnsd_mode == "7" and global_type == "v2ray" and Process_list:find("ssr%-retcp%.json") then
pdnsd_run = 1
end
m = SimpleForm("Version")
m.reset = false
m.submit = false
s = m:field(DummyValue, "redir_run", translate("Global Client"))
s.rawhtml = true
if redir_run == 1 then
s.value = font_blue .. bold_on .. translate("Running") .. bold_off .. font_off
else
s.value = style_blue .. bold_on .. translate("Not Running") .. bold_off .. font_off
end
s = m:field(DummyValue, "reudp_run", translate("Game Mode UDP Relay"))
s.rawhtml = true
if reudp_run == 1 then
s.value = font_blue .. bold_on .. translate("Running") .. bold_off .. font_off
else
s.value = style_blue .. bold_on .. translate("Not Running") .. bold_off .. font_off
end
if uci:get_first("shadowsocksr", 'global', 'pdnsd_enable', '0') ~= '0' then
s = m:field(DummyValue, "pdnsd_run", translate("DNS Anti-pollution"))
s.rawhtml = true
if pdnsd_run == 1 then
s.value = font_blue .. bold_on .. translate("Running") .. bold_off .. font_off
else
s.value = style_blue .. bold_on .. translate("Not Running") .. bold_off .. font_off
end
end
s = m:field(DummyValue, "sock5_run", translate("Global SOCKS5 Proxy Server"))
s.rawhtml = true
if sock5_run == 1 then
s.value = font_blue .. bold_on .. translate("Running") .. bold_off .. font_off
else
s.value = style_blue .. bold_on .. translate("Not Running") .. bold_off .. font_off
end
if has_3proxy then
s = m:field(DummyValue, "http_run", translate("Global HTTP/HTTPS Proxy Server"))
s.rawhtml = true
if http_run == 1 then
s.value = font_blue .. bold_on .. translate("Running") .. bold_off .. font_off
else
s.value = style_blue .. bold_on .. translate("Not Running") .. bold_off .. font_off
end
end
s = m:field(DummyValue, "server_run", translate("Local Servers"))
s.rawhtml = true
if server_run == 1 then
s.value = font_blue .. bold_on .. translate("Running") .. bold_off .. font_off
else
s.value = style_blue .. bold_on .. translate("Not Running") .. bold_off .. font_off
end
if nixio.fs.access("/usr/bin/kcptun-client") then
s = m:field(DummyValue, "kcp_version", translate("KcpTun Version"))
s.rawhtml = true
s.value = kcptun_version
s = m:field(DummyValue, "kcptun_run", translate("KcpTun"))
s.rawhtml = true
if kcptun_run == 1 then
s.value = font_blue .. bold_on .. translate("Running") .. bold_off .. font_off
else
s.value = style_blue .. bold_on .. translate("Not Running") .. bold_off .. font_off
end
end
s = m:field(Button, "Restart", translate("Restart ShadowSocksR Plus+"))
s.inputtitle = translate("Restart Service")
s.inputstyle = "reload"
s.write = function()
luci.sys.call("/etc/init.d/shadowsocksr restart >/dev/null 2>&1 &")
luci.http.redirect(luci.dispatcher.build_url("admin", "services", "shadowsocksr", "client"))
end
s = m:field(DummyValue, "google", translate("Google Connectivity"))
s.value = translate("No Check")
s.template = "shadowsocksr/check"
s = m:field(DummyValue, "baidu", translate("Baidu Connectivity"))
s.value = translate("No Check")
s.template = "shadowsocksr/check"
s = m:field(DummyValue, "gfw_data", translate("GFW List Data"))
s.rawhtml = true
s.template = "shadowsocksr/refresh"
s.value = gfw_count .. " " .. translate("Records")
s = m:field(DummyValue, "ip_data", translate("China IP Data"))
s.rawhtml = true
s.template = "shadowsocksr/refresh"
s.value = ip_count .. " " .. translate("Records")
if uci:get_first("shadowsocksr", 'global', 'apple_optimization', '0') ~= '0' then
s = m:field(DummyValue, "apple_data", translate("Apple Domains Data"))
s.rawhtml = true
s.template = "shadowsocksr/refresh"
s.value = apple_count .. " " .. translate("Records")
end
if uci:get_first("shadowsocksr", 'global', 'adblock', '0') == '1' then
s = m:field(DummyValue, "ad_data", translate("Advertising Data"))
s.rawhtml = true
s.template = "shadowsocksr/refresh"
s.value = ad_count .. " " .. translate("Records")
end
s = m:field(DummyValue, "check_port", translate("Check Server Port"))
s.template = "shadowsocksr/checkport"
s.value = translate("No Check")
return m
@@ -0,0 +1,143 @@
<style type="text/css">
.ssr-advanced-switch-inline {
display: flex;
gap: 2%;
align-items: flex-start;
flex-wrap: nowrap;
}
.ssr-advanced-switch-item {
width: 30%;
min-width: 0;
}
.ssr-advanced-switch-item input {
width: 100%;
box-sizing: border-box;
}
.ssr-advanced-switch-subtitle {
font-size: 12px;
color: #666;
margin-bottom: 4px;
line-height: 1.2;
}
.ssr-advanced-switch-hidden {
display: none !important;
}
</style>
<script type="text/javascript">
//<![CDATA[
function ssrCompactAdvancedSwitch() {
function getInputByName(suffix) {
return document.querySelector('input[name$=".' + suffix + '"]');
}
function getRowByName(suffix) {
var el = getInputByName(suffix);
return el ? el.closest('.cbi-value') : null;
}
function compactSwitchRow() {
var timeRow = getRowByName('switch_time');
var timeoutRow = getRowByName('switch_timeout');
var tryRow = getRowByName('switch_try_count');
if (!timeRow || !timeoutRow || !tryRow) return;
var timeField = timeRow.querySelector('.cbi-value-field');
var timeoutField = timeoutRow.querySelector('.cbi-value-field');
var tryField = tryRow.querySelector('.cbi-value-field');
if (!timeField || !timeoutField || !tryField) return;
if (timeField.querySelector('.ssr-advanced-switch-inline')) {
return;
}
var timeInput = timeField.querySelector('input');
var timeoutInput = timeoutField.querySelector('input');
var tryInput = tryField.querySelector('input');
if (!timeInput || !timeoutInput || !tryInput) return;
var wrap = document.createElement('div');
wrap.className = 'ssr-advanced-switch-inline';
function makeItem(title, input) {
var item = document.createElement('div');
item.className = 'ssr-advanced-switch-item';
var subtitle = document.createElement('div');
subtitle.className = 'ssr-advanced-switch-subtitle';
subtitle.textContent = title;
item.appendChild(subtitle);
item.appendChild(input);
return item;
}
wrap.appendChild(makeItem('<%:Second%>', timeInput));
wrap.appendChild(makeItem('<%:Timeout%>', timeoutInput));
wrap.appendChild(makeItem('<%:Try Count%>', tryInput));
timeField.innerHTML = '';
timeField.appendChild(wrap);
timeoutRow.classList.add('ssr-advanced-switch-hidden');
tryRow.classList.add('ssr-advanced-switch-hidden');
}
compactSwitchRow();
}
(function() {
var retryCount = 0;
var maxRetries = 30;
var retryTimer = null;
var observer = null;
function hasCompactLayout() {
return !!document.querySelector('.ssr-advanced-switch-inline');
}
function tryCompact() {
ssrCompactAdvancedSwitch();
if (hasCompactLayout()) {
if (retryTimer) {
window.clearTimeout(retryTimer);
retryTimer = null;
}
if (observer) {
observer.disconnect();
observer = null;
}
return;
}
if (retryCount >= maxRetries) {
return;
}
retryCount = retryCount + 1;
retryTimer = window.setTimeout(tryCompact, 200);
}
function startWatcher() {
tryCompact();
if (window.MutationObserver) {
observer = new MutationObserver(function() {
if (!hasCompactLayout()) {
tryCompact();
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', startWatcher);
window.addEventListener('load', tryCompact);
} else {
startWatcher();
}
})();
//]]>
</script>
@@ -0,0 +1,152 @@
<div class="cbi-value" id="_backup_div">
<label class="cbi-value-title"><%:Create Backup File%></label>
<div class="cbi-value-field">
<input class="btn cbi-button cbi-button-save" type="button" onclick="dl_backup()" value="<%:DL Backup%>" />
</div>
</div>
<div class="cbi-value" id="_upload_div">
<label class="cbi-value-title"><%:Restore Backup File%></label>
<div class="cbi-value-field">
<input class="btn cbi-button cbi-button-apply" type="button" id="upload-btn" value="<%:RST Backup%>" />
</div>
</div>
<div class="cbi-value" id="_reset_div">
<label class="cbi-value-title"><%:Restore to default configuration%></label>
<div class="cbi-value-field">
<input class="btn cbi-button cbi-button-remove" type="button" onclick="do_reset()" value="<%:Do Reset%>" />
</div>
</div>
<div id="upload-modal" class="up-modal" style="display:none;">
<div class="up-modal-content">
<h3><%:Restore Backup File%></h3>
<div class="cbi-value" id="_upload_div">
<div class="up-cbi-value-field">
<input class="cbi-input-file" type="file" id="ulfile" name="ulfile" accept=".tar.gz" required />
<br />
<div class="up-button-container">
<input class="btn cbi-button cbi-button-apply" type="submit" value="<%:UL Restore%>" />
<input class="btn cbi-button cbi-button-remove" type="button" id="upload-close" value="<%:CLOSE WIN%>" />
</div>
</div>
</div>
</div>
</div>
<style>
.up-modal {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
border: 2px solid #ccc;
box-shadow: 0 0 10px rgba(0,0,0,0.5);
z-index: 1000;
}
.up-modal-content {
width: 100%;
max-width: 400px;
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.up-button-container {
display: flex;
justify-content: space-between;
width: 100%;
max-width: 250px;
}
.up-cbi-value-field {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
</style>
<script>
// JavaScript 版本的 url 函数
function url(...args) {
let url = "/cgi-bin/luci/admin/services/shadowsocksr";
for (let i = 0; i < args.length; i++) {
if (args[i] !== "") {
url += "/" + args[i];
}
}
return url;
}
// 上传按钮点击事件
document.getElementById("upload-btn").addEventListener("click", function() {
document.getElementById("upload-modal").style.display = "block";
});
// 关闭上传模态框
document.getElementById("upload-close").addEventListener("click", function() {
document.getElementById("upload-modal").style.display = "none";
});
// 备份下载函数
function dl_backup(btn) {
fetch(url("backup"), { // 使用 JavaScript 版本的 url 函数
method: 'POST',
credentials: 'same-origin'
})
.then(response => {
if (!response.ok) {
throw new Error("备份失败!");
}
const filename = response.headers.get("X-Backup-Filename");
if (!filename) {
return;
}
return response.blob().then(blob => ({ blob, filename }));
})
.then(result => {
if (!result) return;
const { blob, filename } = result;
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
})
.catch(error => alert(error.message));
}
// 恢复出厂设置
function do_reset(btn) {
if (confirm("<%: Do you want to restore the client to default settings?%>")) {
setTimeout(function () {
if (confirm("<%: Are you sure you want to restore the client to default settings?%>")) {
// 清理日志
var xhr1 = new XMLHttpRequest();
xhr1.open("GET", url("clear_log"), true); // 使用 JavaScript 版本的 url 函数
xhr1.send();
// 恢复出厂
var xhr2 = new XMLHttpRequest();
xhr2.open("GET", url("reset"), true); // 使用 JavaScript 版本的 url 函数
xhr2.send();
// 处理响应
xhr2.onload = function() {
if (xhr2.status === 200) {
window.location.href = url("reset");
}
};
}
}, 1000);
}
}
</script>
@@ -0,0 +1,4 @@
<%+cbi/valueheader%>
<input class="cbi-input-file" style="width: 400px" type="file" id="ulfile" name="ulfile" />
<input type="submit" class="cbi-button cbi-input-apply" name="upload" value="<%:Upload%>" />
<%+cbi/valuefooter%>
@@ -0,0 +1,29 @@
<%+cbi/valueheader%>
<script type="text/javascript">//<![CDATA[
function check_connect(btn,urlname)
{
btn.disabled = true;
btn.value = '<%:Check...%>';
murl=urlname;
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr","check")%>',
{ set:murl },
function(x,rv)
{
var s = document.getElementById(urlname+'-status');
if (s)
{
if (rv.ret=="0")
s.innerHTML ="<font style=\'color:green\'>"+"<%:Connect OK%>"+"</font>";
else
s.innerHTML ="<font style=\'color:red\'>"+"<%:Connect Error%>"+"</font>";
}
btn.disabled = false;
btn.value = '<%:Check Connect%>';
}
);
return false;
}
//]]></script>
<input type="button" class="btn cbi-button cbi-button-apply" value="<%:Check Connect%>" onclick="return check_connect(this,'<%=self.option%>')" />
<span id="<%=self.option%>-status"><em><%=self.value%></em></span>
<%+cbi/valuefooter%>
@@ -0,0 +1,25 @@
<%+cbi/valueheader%>
<script type="text/javascript">//<![CDATA[
function check_port(btn)
{
btn.disabled = true;
btn.value = '<%:Check...%>';
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr","checkport")%>',
null,
function(x,rv)
{
var s = document.getElementById('<%=self.option%>-status');
if (s)
{
s.innerHTML =rv.ret;
}
btn.disabled = false;
btn.value = '<%:Check Server%>';
}
);
return false;
}
//]]></script>
<input type="button" class="btn cbi-button cbi-button-apply" value="<%:Check Server%>" onclick="return check_port(this)" />
<span id="<%=self.option%>-status"><em><%=self.value%></em></span>
<%+cbi/valuefooter%>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,104 @@
<%+header%>
<h2><%:Clash Panel%> - <%=alias%></h2>
<div class="cbi-map-descr">
<%:This panel is available only when the current Clash total node is active.%>
</div>
<div class="cbi-section-actions" style="margin: 1em 0;">
<input type="button" id="clash-panel-refresh" class="btn cbi-button cbi-button-apply" value="<%:Refresh%>" />
</div>
<div id="clash-panel-status" style="margin: 1em 0; color: #666;"></div>
<div id="clash-panel-groups"></div>
<script type="text/javascript">
//<![CDATA[
(function() {
const sid = <%=string.format("%q", sid)%>;
const statusEl = document.getElementById('clash-panel-status');
const groupsEl = document.getElementById('clash-panel-groups');
const refreshBtn = document.getElementById('clash-panel-refresh');
function setStatus(text, color) {
statusEl.style.color = color || '#666';
statusEl.textContent = text;
}
function renderGroups(groups) {
groupsEl.innerHTML = '';
if (!groups || !groups.length) {
groupsEl.innerHTML = '<p><%:No switchable proxy groups found.%></p>';
return;
}
groups.forEach(function(group) {
const wrapper = document.createElement('div');
wrapper.className = 'cbi-value';
wrapper.style.marginBottom = '1em';
const label = document.createElement('label');
label.className = 'cbi-value-title';
label.textContent = group.name + (group.type ? ' (' + group.type + ')' : '');
const field = document.createElement('div');
field.className = 'cbi-value-field';
const select = document.createElement('select');
select.className = 'cbi-input-select';
group.all.forEach(function(name) {
const option = document.createElement('option');
option.value = name;
option.textContent = name;
if (name === group.now) {
option.selected = true;
}
select.appendChild(option);
});
select.onchange = function() {
setStatus('<%:Switching...%>', '#0072c3');
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "clash_switch")%>', {
sid: sid,
group: group.name,
name: select.value
}, function(x, result) {
if (result && result.success) {
setStatus('<%:Switched successfully.%>', '#249400');
loadGroups();
} else {
setStatus('<%:Switch failed.%>', '#ff0000');
}
});
};
field.appendChild(select);
wrapper.appendChild(label);
wrapper.appendChild(field);
groupsEl.appendChild(wrapper);
});
}
function loadGroups() {
setStatus('<%:Loading...%>', '#666');
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "clash_groups")%>', {
sid: sid
}, function(x, result) {
if (!result || !result.active) {
setStatus('<%:This Clash total node is not active. Apply this node first.%>', '#ff7700');
groupsEl.innerHTML = '';
return;
}
setStatus('<%:Ready.%>', '#249400');
renderGroups(result.groups || []);
});
}
refreshBtn.onclick = loadGroups;
loadGroups();
})();
//]]>
</script>
<%+footer%>
@@ -0,0 +1,74 @@
<%+cbi/valueheader%>
<%
local stype = self.map:get(section, "type")
-%>
<script type="text/javascript">
if (typeof(window.ssrClashRefreshSubscription) !== "function") {
window.ssrClashRefreshSubscription = function(btn, sid) {
var original = btn.value;
btn.disabled = true;
btn.value = '<%:Reloading YAML...%>';
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "clash_refresh")%>', {
sid: sid
}, function(x, result) {
btn.disabled = false;
btn.value = original;
if (result && result.success) {
btn.value = result.reapplied ? '<%:Reapply%>' : '<%:YAML Reloaded!%>';
setTimeout(function() { btn.value = original; }, 1200);
} else {
btn.value = '<%:YAML Reload Failed!%>';
setTimeout(function() { btn.value = original; }, 1500);
}
});
return false;
}
}
if (typeof(window.ssrClashResetDefaults) !== "function") {
window.ssrClashResetDefaults = function(btn, sid) {
var original = btn.value;
if (!window.confirm('<%:Reset saved Mihomo proxy-group selections and restore YAML defaults?%>')) {
return false;
}
btn.disabled = true;
btn.value = '<%:Resetting Defaults...%>';
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "clash_reset_defaults")%>', {
sid: sid
}, function(x, result) {
btn.disabled = false;
btn.value = original;
if (result && result.success) {
btn.value = result.reapplied ? '<%:Defaults Restored!%>' : '<%:Defaults Cleared!%>';
setTimeout(function() { btn.value = original; }, 1500);
} else {
btn.value = '<%:Reset Defaults Failed!%>';
setTimeout(function() { btn.value = original; }, 1600);
}
});
return false;
}
}
</script>
<% if stype == "clash" then %>
<input
type="button"
class="btn cbi-button"
value="<%:Reload YAML%>"
title="<%:Reload YAML%>"
onclick="return ssrClashRefreshSubscription(this, '<%=section%>');" />
<input
type="button"
class="btn cbi-button"
value="<%:Reset Default Proxies Rules%>"
title="<%:Reset Default Proxies Rules%>"
onclick="return ssrClashResetDefaults(this, '<%=section%>');" />
<input
type="button"
class="btn cbi-button cbi-button-apply"
value="<%:Panel%>"
title="<%:Panel%>"
onclick="window.open('<%=luci.dispatcher.build_url('admin', 'services', 'shadowsocksr', 'clash_panel')%>?sid=<%=section%>', '_blank');" />
<% else %>
-
<% end %>
<%+cbi/valuefooter%>
@@ -0,0 +1,4 @@
<%+cbi/valueheader%>
<input class="cbi-input-file" style="width: 400px" type="file" id="clash_yaml_file" name="clash_yaml_file" accept=".yaml,.yml" />
<input type="submit" class="cbi-button cbi-input-apply" name="upload_clash_yaml" value="<%:Upload%>" />
<%+cbi/valuefooter%>
@@ -0,0 +1,78 @@
<script type="text/javascript">
//<![CDATA[
(function() {
function getField(name) {
return document.querySelector('select[name$=".' + name + '"], input[name$=".' + name + '"]');
}
function ensureDefaultValue(modeValue, fieldName) {
var modeEl = getField('pdnsd_enable');
var dnsEl = getField(fieldName);
if (!modeEl || !dnsEl || modeEl.value !== modeValue) {
return;
}
var current = (dnsEl.value || '').trim();
var placeholder = (dnsEl.options && dnsEl.selectedIndex >= 0 && dnsEl.options[dnsEl.selectedIndex])
? (dnsEl.options[dnsEl.selectedIndex].text || '').trim()
: '';
if (current !== '' && current !== '-- 请选择 --' && current !== '-- Please choose --' &&
placeholder !== '-- 请选择 --' && placeholder !== '-- Please choose --') {
return;
}
if (dnsEl.tagName === 'SELECT') {
var firstValue = '';
for (var i = 0; i < dnsEl.options.length; i++) {
var option = dnsEl.options[i];
if (option && option.value) {
firstValue = option.value;
break;
}
}
if (firstValue) {
dnsEl.value = firstValue;
}
} else if (dnsEl.dataset && dnsEl.dataset.default) {
dnsEl.value = dnsEl.dataset.default;
}
}
function ensureMosdnsDefault() {
ensureDefaultValue('4', 'tunnel_forward_mosdns');
}
function ensureChinaDnsNgDefault() {
ensureDefaultValue('6', 'chinadns_ng_tunnel_forward');
}
function bindModeChange() {
var modeEl = getField('pdnsd_enable');
if (!modeEl || modeEl.dataset.ssrDnsDefaultBound) {
return;
}
modeEl.dataset.ssrDnsDefaultBound = '1';
modeEl.addEventListener('change', function() {
window.setTimeout(function() {
ensureMosdnsDefault();
ensureChinaDnsNgDefault();
}, 0);
});
}
function initMosdnsDefault() {
bindModeChange();
ensureMosdnsDefault();
ensureChinaDnsNgDefault();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initMosdnsDefault);
window.addEventListener('load', initMosdnsDefault);
} else {
initMosdnsDefault();
}
})();
//]]>
</script>
@@ -0,0 +1,446 @@
<script type="text/javascript">
//<![CDATA[
var componentMeta = {
xray: {
label: '<%:Xray-core%>',
help: '<%:The online upgrade downloads the matching Xray-core linux archive for the current ARCH from the official GitHub release page.%>'
},
mihomo: {
label: '<%:Mihomo%>',
help: '<%:The online upgrade only tracks the latest stable Mihomo release and downloads the matching linux archive for the current ARCH from the official GitHub release page.%>'
},
naiveproxy: {
label: '<%:NaiveProxy%>',
help: '<%:The online upgrade tracks the latest NaiveProxy release and prefers OpenWrt static archives matching the current ARCH before falling back to other release assets.%>'
}
};
var geoMeta = {
country_mmdb: {
label: '<%:Country MMDB%>',
help: '<%:Update the Country.mmdb database used by Mihomo/Clash runtime when available.%>'
},
geosite: {
label: '<%:GeoSite Database%>',
help: '<%:Update the GeoSite.dat database used by Mihomo/Clash runtime when available.%>'
},
v2ray_geo: {
label: '<%:V2Ray GEO Databases%>',
help: '<%:Update geoip.dat and geosite.dat used by Xray/V2Ray runtime when available.%>'
}
};
var componentMessageMap = {
'Already up to date': '<%:Already up to date%>',
'Upgrade completed': '<%:Upgrade completed%>',
'Unsupported ARCH': '<%:Unsupported ARCH%>',
'Failed to fetch release metadata': '<%:Failed to fetch release metadata%>',
'Matching release asset not found': '<%:Matching release asset not found%>',
'Missing unzip support': '<%:Missing unzip support%>',
'Missing gzip support': '<%:Missing gzip support%>',
'Missing xz support': '<%:Missing xz support%>',
'Failed to create temp directory': '<%:Failed to create temp directory%>',
'Download failed': '<%:Download failed%>',
'Extract failed': '<%:Extract failed%>',
'xray binary not found in archive': '<%:xray binary not found in archive%>',
'mihomo binary not found in archive': '<%:mihomo binary not found in archive%>',
'naive binary not found in archive': '<%:naive binary not found in archive%>',
'Install failed': '<%:Install failed%>',
'Installed binary failed to run': '<%:Installed binary failed to run%>'
};
function selectedComponent() {
var selector = document.getElementById('component-selector');
return selector ? selector.value : 'xray';
}
function selectedMirror() {
var selector = document.getElementById('component-mirror');
return selector ? selector.value : 'direct';
}
function componentHelp(component) {
return componentMeta[component] ? componentMeta[component].help : '';
}
function displayMessage(message) {
return componentMessageMap[message] || message || '';
}
function displayError(error) {
if (!error) {
return '';
}
var map = {
unsupported_arch: '<%:Unsupported ARCH%>',
fetch_failed: '<%:Failed to fetch release metadata%>',
asset_not_found: '<%:Matching release asset not found%>',
unsupported_component: '<%:Unsupported component%>',
unknown_error: '<%:Unknown error%>'
};
return map[error] || error;
}
function renderComponentInfo(data, checkedRemote) {
var panel = document.getElementById('component-status');
var upgradeButton = document.getElementById('component-upgrade');
var action = document.getElementById('component-action');
var help = document.getElementById('component-help');
var html = [];
var component = data && data.component ? data.component : selectedComponent();
if (!panel) {
return;
}
if (!data) {
panel.innerHTML = '<span style="color:red"><%:Failed to query component information.%></span>';
if (upgradeButton) upgradeButton.disabled = true;
return;
}
if (help) {
help.innerHTML = componentHelp(component);
}
html.push('<div><strong><%:Installed Version%>:</strong> ' + (data.current_version || '<%:Not installed%>') + '</div>');
html.push('<div><strong><%:Latest Version%>:</strong> ' + (data.latest_version || '<%:Unknown%>') + '</div>');
html.push('<div><strong><%:Current ARCH%>:</strong> ' + (data.arch || '<%:Unknown%>') + '</div>');
html.push('<div><strong><%:Release Asset%>:</strong> ' + (data.asset || '<%:Unknown%>') + '</div>');
if (data.error) {
html.push('<div style="color:red"><%:Status%>: ' + displayError(data.error) + '</div>');
} else if (!checkedRemote) {
html.push('<div style="color:#666"><%:Status%>: <%:No Check%></div>');
} else if (data.can_upgrade) {
html.push('<div style="color:#d48806"><%:Status%>: <%:Upgrade available%></div>');
} else {
html.push('<div style="color:green"><%:Status%>: <%:Already up to date%></div>');
}
panel.innerHTML = html.join('');
if (upgradeButton) {
upgradeButton.disabled = !!data.error || !checkedRemote || !data.latest_version || !data.can_upgrade;
}
if (action) {
action.innerHTML = data.message
? '<span style="color:' + (data.success ? 'green' : 'red') + '">' + displayMessage(data.message) + '</span>'
: '';
}
}
function queryComponentInfo(button, doUpgrade) {
var component = selectedComponent();
var mirror = selectedMirror();
var action = document.getElementById('component-action');
var url = doUpgrade
? '<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "component_upgrade")%>'
: '<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "component_status")%>';
if (button) {
button.disabled = true;
button.value = doUpgrade ? '<%:Upgrading...%>' : '<%:Checking...%>';
}
if (action) {
action.innerHTML = doUpgrade ? '<%:Processing...%>' : '';
}
XHR.get(url, { component: component, mirror: mirror }, function(x, rv) {
renderComponentInfo(rv, true);
if (button) {
button.disabled = false;
button.value = doUpgrade ? '<%:Upgrade%>' : '<%:Check Update%>';
}
});
}
function queryLocalComponentInfo() {
var action = document.getElementById('component-action');
if (action) {
action.innerHTML = '';
}
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "component_local_status")%>', {
component: selectedComponent(),
mirror: selectedMirror()
}, function(x, rv) {
renderComponentInfo(rv, false);
});
}
function onComponentChange() {
queryLocalComponentInfo();
}
function onMirrorChange() {
var mirror = selectedMirror();
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "component_set_mirror")%>', {
mirror: mirror
}, function() {
queryLocalComponentInfo();
});
}
function selectedGeoComponent() {
var selector = document.getElementById('geo-selector');
return selector ? selector.value : 'country_mmdb';
}
function geoHelp(component) {
return geoMeta[component] ? geoMeta[component].help : '';
}
function renderGeoInfo(data) {
var panel = document.getElementById('geo-status');
var upgradeButton = document.getElementById('geo-upgrade');
var action = document.getElementById('geo-action');
var help = document.getElementById('geo-help');
var html = [];
var component = data && data.component ? data.component : selectedGeoComponent();
if (!panel) {
return;
}
if (!data) {
panel.innerHTML = '<span style="color:red"><%:Failed to query geo database information.%></span>';
if (upgradeButton) upgradeButton.disabled = true;
return;
}
if (help) {
help.innerHTML = geoHelp(component);
}
html.push('<div><strong><%:Current Version%>:</strong> ' + (data.current_version || '<%:File Not Exist%>') + '</div>');
if (data.component !== 'v2ray_geo' && data.current_version_extra) {
html.push('<div><strong><%:Additional Version%>:</strong> ' + data.current_version_extra + '</div>');
}
if (data.error) {
html.push('<div style="color:red"><%:Status%>: ' + displayError(data.error) + '</div>');
} else if (data.message) {
html.push('<div style="color:green"><%:Status%>: ' + (data.success ? displayMessage(data.message) : (data.message || '<%:Unknown%>')) + '</div>');
} else {
html.push('<div style="color:#666"><%:Status%>: <%:Ready.%></div>');
}
panel.innerHTML = html.join('');
if (upgradeButton) {
upgradeButton.disabled = false;
}
if (action) {
action.innerHTML = data.message
? '<span style="color:' + (data.success ? 'green' : 'red') + '">' + displayMessage(data.message) + '</span>'
: '';
}
}
function queryGeoInfo(button) {
var component = selectedGeoComponent();
var mirror = selectedMirror();
var action = document.getElementById('geo-action');
var url = '<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "geo_upgrade")%>';
if (button) {
button.disabled = true;
button.value = '<%:Checking...%>';
}
if (action) {
action.innerHTML = '<%:Processing...%>';
}
XHR.get(url, { component: component, mirror: mirror }, function(x, rv) {
renderGeoInfo(rv);
if (button) {
button.disabled = false;
button.value = '<%:CHECK AND UPDATE%>';
}
});
}
function queryLocalGeoInfo() {
var action = document.getElementById('geo-action');
if (action) {
action.innerHTML = '';
}
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "geo_local_status")%>', {
component: selectedGeoComponent(),
mirror: selectedMirror()
}, function(x, rv) {
renderGeoInfo(rv);
});
}
function onGeoChange() {
queryLocalGeoInfo();
}
window.setTimeout(function() {
onComponentChange();
onGeoChange();
}, 0);
//]]>
</script>
<style>
.component-upgrade-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-top: 10px;
}
.component-upgrade-card {
line-height: 1.8;
}
.component-upgrade-help {
margin-top: 6px;
color: #666;
}
.component-upgrade-result {
margin-top: 8px;
}
.component-selector {
min-width: 180px;
width: 100%;
}
.component-layout {
display: flex;
gap: 24px;
align-items: flex-start;
}
.component-left {
width: 320px;
min-width: 260px;
}
.component-right {
flex: 1;
min-width: 0;
}
.component-select-stack {
display: flex;
flex-direction: column;
gap: 12px;
}
.component-select-item {
display: flex;
flex-direction: column;
gap: 6px;
}
.component-select-label {
font-size: 12px;
line-height: 1.3;
color: #666;
}
.component-select-label strong {
display: block;
font-size: 13px;
color: #444;
}
@media (max-width: 900px) {
.component-layout {
flex-direction: column;
}
.component-left {
width: 100%;
min-width: 0;
}
}
</style>
<fieldset class="cbi-section">
<legend><%:Core Components%></legend>
<div class="cbi-value">
<div class="cbi-value-field component-upgrade-card">
<div class="component-layout">
<div class="component-left">
<div class="component-select-stack">
<div class="component-select-item">
<div class="component-select-label">
<strong><%:Download Source%></strong>
<span><%:Address%></span>
</div>
<select id="component-mirror" class="cbi-input-select component-selector" onchange="onMirrorChange()">
<option value="direct" selected="selected"><%:GitHub Direct%></option>
<option value="ghproxy">mirror.ghproxy.com</option>
<option value="ghproxy_cc">ghproxy.cc</option>
<option value="ghfast">ghfast.top</option>
<option value="jsdelivr">cdn.jsdelivr.net</option>
</select>
</div>
<div class="component-select-item">
<div class="component-select-label">
<strong><%:Component%></strong>
<span><%:Package Name%></span>
</div>
<select id="component-selector" class="cbi-input-select component-selector" onchange="onComponentChange()">
<option value="xray"><%:Xray-core%></option>
<option value="mihomo"><%:Mihomo%></option>
<option value="naiveproxy"><%:NaiveProxy%></option>
</select>
</div>
</div>
</div>
<div class="component-right">
<div id="component-status"><em><%:Collecting data...%></em></div>
<div class="component-upgrade-help" id="component-help"></div>
<div class="component-upgrade-actions">
<input type="button" class="btn cbi-button cbi-button-reload" value="<%:Check Update%>" onclick="queryComponentInfo(this, false)" />
<input type="button" class="btn cbi-button cbi-button-apply" id="component-upgrade" value="<%:Upgrade%>" onclick="queryComponentInfo(this, true)" />
</div>
<div class="component-upgrade-result" id="component-action"></div>
</div>
</div>
</div>
</div>
</fieldset>
<fieldset class="cbi-section">
<legend><%:Geo Database Update%></legend>
<div class="cbi-value">
<div class="cbi-value-field component-upgrade-card">
<div class="component-layout">
<div class="component-left">
<div class="component-select-stack">
<div class="component-select-item">
<div class="component-select-label">
<strong><%:Database%></strong>
<span><%:Geo Resource%></span>
</div>
<select id="geo-selector" class="cbi-input-select component-selector" onchange="onGeoChange()">
<option value="country_mmdb"><%:Country MMDB%></option>
<option value="geosite"><%:GeoSite Database%></option>
<option value="v2ray_geo"><%:V2Ray GEO Databases%></option>
</select>
</div>
</div>
</div>
<div class="component-right">
<div id="geo-status"><em><%:Collecting data...%></em></div>
<div class="component-upgrade-help" id="geo-help"></div>
<div class="component-upgrade-actions">
<input type="button" class="btn cbi-button cbi-button-apply" id="geo-upgrade" value="<%:CHECK AND UPDATE%>" onclick="queryGeoInfo(this)" />
</div>
<div class="component-upgrade-result" id="geo-action"></div>
</div>
</div>
</div>
</div>
</fieldset>
@@ -0,0 +1,9 @@
<style type="text/css">
[data-page="admin-services-shadowsocksr-control"] .cbi-section-node-tabbed {
min-height: 20rem;
}
[data-page="admin-services-shadowsocksr-control"] .cbi-tabcontainer[data-tab-active="true"] {
min-height: 20rem;
}
</style>
@@ -0,0 +1,37 @@
<%
local dsp = require "luci.dispatcher"
-%>
<script type="text/javascript">
//<![CDATA[
function clearlog(btn) {
XHR.get('<%=dsp.build_url("admin/services/shadowsocksr/clear_log")%>', null,
function(x, data) {
if (x && x.status == 200) {
var log_textarea = document.getElementById('log_textarea');
log_textarea.innerHTML = "";
log_textarea.scrollTop = log_textarea.scrollHeight;
}
}
);
}
XHR.poll(5, '<%=dsp.build_url("admin/services/shadowsocksr/get_log")%>', null,
function(x, data) {
if (x && x.status == 200) {
var log_textarea = document.getElementById('log_textarea');
// 将日志分行处理,移除最后一行空行但保留中间空行
var logs = x.responseText.split("\n");
if (logs[logs.length - 1].trim() === "") {
logs.pop(); // 删除最后的空行
}
logs = logs.reverse().join("\n"); // 倒序排列
log_textarea.innerHTML = logs;
}
}
);
//]]>
</script>
<fieldset class="cbi-section" id="_log_fieldset">
<input class="btn cbi-button cbi-button-remove" type="button" onclick="clearlog()" value="<%:Clear logs%>" />
<textarea id="log_textarea" class="cbi-input-textarea" style="width: 100%;margin-top: 10px;" data-update="change" rows="20" wrap="off" readonly="readonly"></textarea>
</fieldset>
@@ -0,0 +1,24 @@
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", function () {
setTimeout(updateElements, 200);
function updateElements() {
//修正上移、下移按钮名称
document.querySelectorAll("input.btn.cbi-button.cbi-button-up").forEach(function(btn) {
btn.value = "<%:Move up%>";
});
document.querySelectorAll("input.btn.cbi-button.cbi-button-down").forEach(function(btn) {
btn.value = "<%:Move down%>";
});
//删除控件和说明之间的多余换行
document.querySelectorAll("div.cbi-value-description").forEach(function(descDiv) {
var prev = descDiv.previousSibling;
while (prev && prev.nodeType === Node.TEXT_NODE && prev.textContent.trim() === "") {
prev = prev.previousSibling;
}
if (prev && prev.nodeType === Node.ELEMENT_NODE && prev.tagName === "BR") {
prev.remove();
}
});
}
});
</script>
@@ -0,0 +1,17 @@
<%+cbi/valueheader%>
<%
local hint = self:cfgvalue(section)
local cache = self.detect_cache
local is_clash = self.type == "clash"
local text = is_clash and "N/A" or "--- ms"
local color = is_clash and "#999999" or "#ff0000"
if not is_clash and cache and tonumber(cache.ping or 0) > 0 then
local ping = tonumber(cache.ping)
text = tostring(ping) .. " ms"
if ping < 300 then color = "#ff3300" end
if ping < 200 then color = "#ff7700" end
if ping < 100 then color = "#249400" end
end
%>
<span class="pingtime" hint="<%=hint%>" data-sid="<%=section%>"><font style="color:<%=color%>"><%=text%></font></span>
<%+cbi/valuefooter%>
@@ -0,0 +1,37 @@
<%+cbi/valueheader%>
<script type="text/javascript">//<![CDATA[
function refresh_data(btn,dataname)
{
btn.disabled = true;
btn.value = '<%:Refresh...%> ';
murl=dataname;
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr","refresh")%>',
{ set:murl },
function(x,rv)
{
var s = document.getElementById(dataname+'-status');
if (s)
{
switch (rv.ret)
{
case 0:
s.innerHTML ="<font style=\'color:green\'>"+"<%:Refresh OK!%> "+"<%:Total Records:%>"+rv.retcount+"</font>";
break;
case 1:
s.innerHTML ="<font style=\'color:green\'>"+"<%:No new data!%> "+"</font>";
break;
default:
s.innerHTML ="<font style=\'color:red\'>"+"<%:Refresh Error!%> "+"</font>";
break;
}
}
btn.disabled = false;
btn.value = '<%:Refresh Data %>';
}
);
return false;
}
//]]></script>
<input type="button" class="btn cbi-button cbi-button-reload" value="<%:Refresh Data%> " onclick="return refresh_data(this,'<%=self.option%>')" />
<span id="<%=self.option%>-status"><em><%=self.value%></em></span>
<%+cbi/valuefooter%>
@@ -0,0 +1,25 @@
<%+cbi/valueheader%>
<script type="text/javascript">//<![CDATA[
function reset(btn,dataname) {
var s = document.getElementById(dataname + '-status');
var reset = prompt('<%:Really reset all changes?%><%:Please fill in reset%>',"")
if (reset == null || reset == "") {
return false;
}
if (reset != "reset") {
s.innerHTML = "<font style=\'color:red\'><%:The content entered is incorrect!%></font>";
return false;
}
btn.disabled = true;
btn.value = '<%:Perform reset%>';
murl=dataname;
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr","reset")%>', { set:murl }, function(x,rv) {
btn.value = '<%:Reset complete%>';
s.innerHTML = "<font style=\'color:green\'><%:Reset complete%></font>";
});
return false;
}
//]]></script>
<input type="button" class="btn cbi-button cbi-button-reset" value="<%:Perform reset%> " onclick="return reset(this,'<%=self.option%>')" />
<span id="<%=self.option%>-status"></span>
<%+cbi/valuefooter%>
@@ -0,0 +1,418 @@
<%#
Copyright 2018-2019 Lienol <lawlienol@gmail.com>
Licensed to the public under the Apache License 2.0.
-%>
<%
require "luci.sys"
function is_js_luci()
return luci.sys.call('[ -f "/www/luci-static/resources/uci.js" ]') == 0
end
-%>
<% if is_js_luci() then -%>
<script type="text/javascript">
var cbi_t = [];
function cbi_t_add(section, tab) {
var t = document.getElementById('tab.' + section + '.' + tab);
var c = document.getElementById('container.' + section + '.' + tab);
if( t && c ) {
cbi_t[section] = (cbi_t[section] || [ ]);
cbi_t[section][tab] = { 'tab': t, 'container': c, 'cid': c.id };
}
}
function cbi_t_switch(section, tab) {
if( cbi_t[section] && cbi_t[section][tab] ) {
//在切换选项卡之前,先取消当前激活选项卡的全选状态
dechecked_all_node();
var o = cbi_t[section][tab];
var h = document.getElementById('tab.' + section);
for( var tid in cbi_t[section] ) {
var o2 = cbi_t[section][tid];
if( o.tab.id != o2.tab.id ) {
o2.tab.className = o2.tab.className.replace(/(^| )cbi-tab( |$)/, " cbi-tab-disabled ");
o2.container.style.display = 'none';
}
else {
if(h) h.value = tab;
o2.tab.className = o2.tab.className.replace(/(^| )cbi-tab-disabled( |$)/, " cbi-tab ");
o2.container.style.display = 'block';
}
}
}
return false
}
</script>
<%- else %>
<script type="text/javascript">
(function() {
if (typeof(cbi_t_switch) === "function") {
var old_switch = cbi_t_switch;
cbi_t_switch = function(section, tab) {
dechecked_all_node();
return old_switch(section, tab);
};
}
})();
</script>
<%- end %>
<script type="text/javascript">
//<![CDATA[
window.addEventListener('load', function () {
const doms = document.getElementsByClassName('pingtime');
const ports = document.getElementsByClassName("socket-connected");
const transports = document.getElementsByClassName("transport");
const wsPaths = document.getElementsByClassName("wsPath");
const wsHosts = document.getElementsByClassName("wsHost");
const tlsHosts = document.getElementsByClassName("tlsHost");
const tlss = document.getElementsByClassName("tls");
const types = document.getElementsByClassName("type");
const protos = document.getElementsByClassName("proto");
const realities = document.getElementsByClassName("reality");
const startText = '<%:Start Detection%>';
const stopText = '<%:Stop Detection%>';
const maxConcurrent = 10;
let activeRunId = 0;
let probing = false;
let queueIndex = 0;
let toggleBtn = null;
const normalizeHeaderLayout = () => {
const headerRow = document.querySelector("#cbi-shadowsocksr-servers table tr");
if (!headerRow) {
return null;
}
const headerCells = headerRow.querySelectorAll("th");
if (headerCells.length > 1) {
const first = headerCells[0];
if (first && !first.textContent.trim()) {
first.parentNode.removeChild(first);
}
}
return headerRow;
};
const ensureProbeButton = () => {
if (toggleBtn) {
return toggleBtn;
}
const headerRow = normalizeHeaderLayout();
if (!headerRow) {
return null;
}
const headerCells = headerRow.querySelectorAll("th");
const targetHeader = headerCells.length ? headerCells[headerCells.length - 1] : null;
if (!targetHeader) {
return null;
}
targetHeader.style.width = "1%";
targetHeader.style.whiteSpace = "nowrap";
targetHeader.style.textAlign = "left";
toggleBtn = document.createElement("input");
toggleBtn.type = "button";
toggleBtn.id = "ssr-probe-toggle";
toggleBtn.className = "btn cbi-button cbi-button-apply";
toggleBtn.style.marginLeft = "8px";
toggleBtn.style.minWidth = "150px";
toggleBtn.value = startText;
toggleBtn.title = startText;
targetHeader.appendChild(toggleBtn);
return toggleBtn;
};
const setProbeState = (running) => {
probing = running;
const btn = ensureProbeButton();
if (btn) {
toggleBtn = btn;
toggleBtn.value = running ? stopText : startText;
toggleBtn.title = toggleBtn.value;
}
};
const resetProbeDisplay = () => {
for (let i = 0; i < doms.length; i++) {
if (doms[i]) {
doms[i].innerHTML = 'N/A';
}
if (ports[i]) {
ports[i].innerHTML = 'N/A';
}
}
};
const xhr = (index, runId) => {
return new Promise((res) => {
const dom = doms[index];
const port = ports[index];
const transport = transports[index];
const wsPath = wsPaths[index];
const wsHost = wsHosts[index];
const tlsHost = tlsHosts[index];
const tls = tlss[index];
const type = types[index];
const proto = protos[index];
const reality = realities[index];
if (!dom || !port || runId !== activeRunId) {
res();
return;
}
if (type && type.getAttribute("hint") === "clash") {
dom.innerHTML = '<font style="color:#999999">N/A</font>';
port.innerHTML = '<font style="color:#999999">N/A</font>';
res();
return;
}
port.innerHTML = '<font style=\"color:#0072c3\">connect</font>';
XHR.get('<%=luci.dispatcher.build_url("admin/services/shadowsocksr/ping")%>', {
index,
sid: dom.getAttribute("data-sid"),
domain: dom.getAttribute("hint"),
port: port.getAttribute("hint"),
transport: transport.getAttribute("hint"),
wsPath: wsPath.getAttribute("hint"),
host: wsHost ? wsHost.getAttribute("hint") : "",
tlsHost: tlsHost ? tlsHost.getAttribute("hint") : "",
tls: tls.getAttribute("hint"),
type: type.getAttribute("hint"),
proto: proto.getAttribute("hint"),
reality: reality ? reality.getAttribute("hint") : ""
},
(x, result) => {
if (runId !== activeRunId) {
res();
return;
}
result = result || {};
let col = '#ff0000';
if (result.ping) {
if (result.ping < 300) col = '#ff3300';
if (result.ping < 200) col = '#ff7700';
if (result.ping < 100) col = '#249400';
}
dom.innerHTML = `<font style=\"color:${col}\">${(result.ping ? result.ping : "--") + " ms"}</font>`;
if (result.socket) {
port.innerHTML = '<font style=\"color:#249400\">ok</font>';
} else {
port.innerHTML = '<font style=\"color:#ff0000\">fail</font>';
}
res();
});
});
};
const launchNext = (runId) => {
if (runId !== activeRunId || queueIndex >= doms.length) {
return Promise.resolve();
}
const index = queueIndex++;
return xhr(index, runId).then(() => launchNext(runId));
};
const startProbe = () => {
activeRunId++;
const runId = activeRunId;
queueIndex = 0;
resetProbeDisplay();
setProbeState(true);
const workers = [];
for (let i = 0; i < Math.min(maxConcurrent, doms.length); i++) {
workers.push(launchNext(runId));
}
Promise.all(workers).then(() => {
if (runId === activeRunId) {
setProbeState(false);
}
});
};
const stopProbe = () => {
activeRunId++;
setProbeState(false);
};
setProbeState(false);
ensureProbeButton();
if (toggleBtn) {
toggleBtn.onclick = function () {
if (probing) {
stopProbe();
} else {
startProbe();
}
};
}
});
function cbi_row_drop(fromId, toId, store, isToBottom) {
var fromNode = document.getElementById(fromId);
var toNode = document.getElementById(toId);
if (!fromNode || !toNode) return false;
var table = fromNode.parentNode;
while (table && table.nodeName.toLowerCase() != "table")
table = table.parentNode;
if (!table) return false;
var ids = [];
if (isToBottom) {
toNode.parentNode.appendChild(fromNode);
} else {
fromNode.parentNode.insertBefore(fromNode, toNode);
}
for (var idx = 1; idx < table.rows.length; idx++) {
table.rows[idx].className = table.rows[idx].className.replace(
/cbi-rowstyle-[12]/,
"cbi-rowstyle-" + (1 + (idx % 2))
);
if (table.rows[idx].id && table.rows[idx].id.match(/-([^\-]+)$/))
ids.push(RegExp.$1);
}
var input = document.getElementById(store);
var order = ids.join(" ");
var pageElem = document.getElementById('cbi-server-page');
var pageSizeElem = document.getElementById('cbi-server-page-size');
var server_page = pageElem ? pageElem.value : '<%=self.server_page or 1%>';
var server_page_size = pageSizeElem ? pageSizeElem.value : '<%=self.server_page_size or 0%>';
if (input) input.value = order;
XHR.post('<%=luci.dispatcher.build_url("admin/services/shadowsocksr/save_order")%>', {
order: order,
page: server_page,
page_size: server_page_size
}, function(x, result) {
result = result || {};
if (result.ret !== 1 && result.ret !== "1") {
window.location.reload();
return;
}
if (input) {
input.value = "";
}
});
return false;
}
// set tr draggable
function enableDragForTable(table_selector, store) {
var storeInput = document.getElementById(store);
if (!storeInput) {
return;
}
// 添加 CSS 样式
const style = document.createElement("style");
style.textContent = `
tr[draggable="true"] {
cursor: move;
user-select: none;
}
.cbi-button-up,
.cbi-button-down,
.cbi-button-top,
.cbi-button-bottom {
display: none !important;
}
`;
document.head.appendChild(style);
var trs = document.querySelectorAll(table_selector + " tr");
if (!trs || trs.length < 2) {
return;
}
function getDataRows() {
return Array.prototype.filter.call(
document.querySelectorAll(table_selector + " tr"),
function(row) {
return row.id && row.id.indexOf("cbi-shadowsocksr-") === 0;
}
);
}
function ondragstart(ev) {
ev.dataTransfer.setData("Text", ev.target.id);
}
function ondrop(ev) {
var from = ev.dataTransfer.getData("Text");
cbi_row_drop(from, this.id, store);
}
function ondragover(ev) {
ev.preventDefault();
ev.dataTransfer.dropEffect = "move";
}
// 上移一行
function moveUp(id) {
var trList = getDataRows();
var currentIndex = -1;
for (var i = 0; i < trList.length; i++) {
if (trList[i].id === id) {
currentIndex = i;
break;
}
}
if (currentIndex > 0) {
var prevRow = trList[currentIndex - 1];
cbi_row_drop(id, prevRow.id, store);
}
}
// 下移一行
function moveDown(id) {
var trList = getDataRows();
var currentIndex = -1;
for (var i = 0; i < trList.length; i++) {
if (trList[i].id === id) {
currentIndex = i;
break;
}
}
if (currentIndex < trList.length - 1) {
var nextRow = trList[currentIndex + 1];
var nextNextRow = trList[currentIndex + 2];
if (nextNextRow) {
cbi_row_drop(id, nextNextRow.id, store, false);
} else {
cbi_row_drop(id, nextRow.id, store, true);
}
}
}
function moveToTop(id) {
var trList = getDataRows();
if (trList.length > 0) {
cbi_row_drop(id, trList[0].id, store);
}
}
function moveToBottom(id) {
var trList = getDataRows();
if (trList.length > 0) {
var bottom = trList[trList.length - 1];
cbi_row_drop(id, bottom.id, store, true);
}
}
var dataRows = getDataRows();
for (let index = 0; index < dataRows.length; index++) {
const el = dataRows[index];
el.setAttribute("draggable", true);
el.ondragstart = ondragstart;
el.ondrop = ondrop;
el.ondragover = ondragover;
}
}
// enable
enableDragForTable(
"#cbi-shadowsocksr-servers table",
"cbi.sts.shadowsocksr.servers"
);
//]]>
</script>
@@ -0,0 +1,304 @@
<%-
local rowcnt = 0
function rowstyle()
rowcnt = rowcnt + 1
if rowcnt % 2 == 0 then
return " cbi-rowstyle-1"
else
return " cbi-rowstyle-2"
end
end
function width(o)
if o.width then
if type(o.width) == "number" then
return ' style="width:%dpx"' % o.width
end
return ' style="width:%s"' % o.width
end
return ""
end
local has_titles = false
local has_descriptions = false
local anonclass = (not self.anonymous or self.sectiontitle) and "named" or "anonymous"
local titlename = ifattr(not self.anonymous or self.sectiontitle, "data-title", translate("Name"))
local i, k
for i, k in pairs(self.children) do
if not k.typename then
k.typename = k.template and k.template:gsub("^.+/", "") or ""
end
if not has_titles and k.title and #k.title > 0 then
has_titles = true
end
if not has_descriptions and k.description and #k.description > 0 then
has_descriptions = true
end
end
local total = tonumber(self.server_total) or 0
local page = tonumber(self.server_page) or 1
local page_size = tonumber(self.server_page_size) or 0
local page_count = tonumber(self.server_page_count) or 1
local first_index = tonumber(self.server_first_index) or 0
local last_index = tonumber(self.server_last_index) or 0
local page_sizes = self.server_page_sizes or {}
local base_url = self.server_base_url or ""
local function page_url(target_page, target_size)
return string.format(
"%s?server_page=%d&server_page_size=%d",
base_url,
target_page,
target_size
)
end
function render_titles()
if not has_titles then
return
end
%><tr class="tr cbi-section-table-titles <%=anonclass%>"<%=titlename%>><%
local i, k
for i, k in ipairs(self.children) do
if not k.optional then
%><th class="th cbi-section-table-cell"<%=
width(k) .. attr("data-widget", k.typename) %>><%
if k.titleref then
%><a title="<%=self.titledesc or translate("Go to relevant configuration page")%>" class="cbi-title-ref" href="<%=k.titleref%>"><%
end
write(k.title)
if k.titleref then
%></a><%
end
%></th><%
end
end
if self.sortable or self.extedit or self.addremove then
%><th class="th cbi-section-table-cell cbi-section-actions"></th><%
end
%></tr><%
rowcnt = rowcnt + 1
end
function render_descriptions()
if not has_descriptions then
return
end
%><tr class="tr cbi-section-table-descr <%=anonclass%>"><%
local i, k
for i, k in ipairs(self.children) do
if not k.optional then
%><th class="th cbi-section-table-cell"<%=
width(k) .. attr("data-widget", k.typename) %>><%
write(k.description)
%></th><%
end
end
if self.sortable or self.extedit or self.addremove then
%><th class="th cbi-section-table-cell cbi-section-actions"></th><%
end
%></tr><%
rowcnt = rowcnt + 1
end
function render_pager()
if total == 0 then
return
end
%>
<div class="ssr-server-pager">
<span class="ssr-server-pager-summary">
<%=string.format(translate("Showing %d-%d of %d nodes"), first_index, last_index, total)%>
</span>
<span class="ssr-server-pager-pages">
<% if page_count > 1 then %>
<% if page > 1 then %>
<a class="cbi-button cbi-button-link" href="<%=page_url(page - 1, page_size)%>"><%:Prev%></a>
<% end %>
<% for current = 1, page_count do %>
<% if current == page then %>
<strong><%=current%></strong>
<% else %>
<a class="cbi-button cbi-button-link" href="<%=page_url(current, page_size)%>"><%=current%></a>
<% end %>
<% end %>
<% if page < page_count then %>
<a class="cbi-button cbi-button-link" href="<%=page_url(page + 1, page_size)%>"><%:Next%></a>
<% end %>
<% end %>
</span>
<span class="ssr-server-pager-sizes">
<%:Per page%>:
<% for _, size in ipairs(page_sizes) do %>
<%
local label = size == 0 and translate("All") or tostring(size)
local selected = size == page_size
%>
<% if selected then %>
<strong><%=label%></strong>
<% else %>
<a class="cbi-button cbi-button-link" href="<%=page_url(1, size)%>"><%=label%></a>
<% end %>
<% end %>
</span>
</div>
<input type="hidden" id="cbi-server-page" value="<%=page%>" />
<input type="hidden" id="cbi-server-page-size" value="<%=page_size%>" />
<%
end
-%>
<style type="text/css">
.ssr-server-pager {
display: flex;
flex-wrap: wrap;
gap: 0.5rem 0.75rem;
align-items: center;
margin: 0.5rem 0 0.75rem;
}
.ssr-server-pager-pages,
.ssr-server-pager-sizes {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
gap: 0.35rem;
}
.ssr-server-pager .cbi-button-link {
padding: 0 0.45rem;
min-width: 1.75rem;
text-align: center;
}
</style>
<!-- tblsection -->
<div class="cbi-section cbi-tblsection" id="cbi-<%=self.config%>-<%=self.sectiontype%>">
<% if self.title and #self.title > 0 then -%>
<h3><%=self.title%></h3>
<%- end %>
<%- if self.sortable then -%>
<input type="hidden" id="cbi.sts.<%=self.config%>.<%=self.sectiontype%>" name="cbi.sts.<%=self.config%>.<%=self.sectiontype%>" value="" />
<%- end -%>
<div class="cbi-section-descr"><%=self.description%></div>
<% render_pager() %>
<table class="table cbi-section-table">
<%-
render_titles()
render_descriptions()
local isempty, section, i, k = true, nil, nil
for i, k in ipairs(self:cfgsections()) do
isempty = false
section = k
local sectionname = striptags((type(self.sectiontitle) == "function") and self:sectiontitle(section) or k)
local sectiontitle = ifattr(sectionname and (not self.anonymous or self.sectiontitle), "data-title", sectionname, true)
local colorclass = (self.extedit or self.rowcolors) and rowstyle() or ""
local scope = {
valueheader = "cbi/cell_valueheader",
valuefooter = "cbi/cell_valuefooter"
}
-%>
<tr class="tr cbi-section-table-row<%=colorclass%>" id="cbi-<%=self.config%>-<%=section%>"<%=sectiontitle%>>
<%-
local node
for k, node in ipairs(self.children) do
if not node.optional then
node:render(section, scope or {})
end
end
-%>
<%- if self.sortable or self.extedit or self.addremove then -%>
<td class="td cbi-section-table-cell nowrap cbi-section-actions">
<div>
<% if self.extedit then -%>
<input class="btn cbi-button cbi-button-edit" type="button" value="<%:Edit%>"
<%- if type(self.extedit) == "string" then
%> onclick="location.href='<%=self.extedit:format(section)%>'"
<%- elseif type(self.extedit) == "function" then
%> onclick="location.href='<%=self:extedit(section)%>'"
<%- end
%> alt="<%:Edit%>" title="<%:Edit%>" />
<% end; if self.addremove then %>
<input class="btn cbi-button cbi-button-remove" type="button" value="<%:Delete%>"
onclick="
var sid = '<%=k%>';
XHR.post('<%=luci.dispatcher.build_url("admin/services/shadowsocksr/delete_node")%>', {
sid: sid
}, function(x, result) {
result = result || {};
if (result.ret == 1) {
window.location.reload();
return;
}
});
return false;
"
/>
<%- end -%>
</div>
</td>
<%- end -%>
</tr>
<%- end -%>
<%- if isempty then -%>
<tr class="tr cbi-section-table-row placeholder">
<td class="td"><em><%:This section contains no values yet%></em></td>
</tr>
<%- end -%>
</table>
<% render_pager() %>
<% if self.error then %>
<div class="cbi-section-error">
<ul><% for _, c in pairs(self.error) do for _, e in ipairs(c) do -%>
<li><%=pcdata(e):gsub("\n", "<br />")%></li>
<%- end end %></ul>
</div>
<% end %>
<%- if self.addremove then -%>
<% if self.template_addremove then include(self.template_addremove) else -%>
<div class="cbi-section-create cbi-tblsection-create">
<% if self.anonymous then %>
<input class="btn cbi-button cbi-button-add" type="submit" value="<%:Add%>" name="cbi.cts.<%=self.config%>.<%=self.sectiontype%>.<%=section%>" title="<%:Add%>" />
<% else %>
<% if self.invalid_cts then -%>
<div class="cbi-section-error"><%:Invalid%></div>
<%- end %>
<div>
<input type="text" class="cbi-section-create-name" id="cbi.cts.<%=self.config%>.<%=self.sectiontype%>.<%=section%>" name="cbi.cts.<%=self.config%>.<%=self.sectiontype%>.<%=section%>" data-type="uciname" data-optional="true" onkeyup="cbi_validate_named_section_add(this)" />
</div>
<input class="btn cbi-button cbi-button-add" type="submit" onclick="this.form.cbi_state='add-section'; return true" value="<%:Add%>" title="<%:Add%>" disabled="" />
<% end %>
</div>
<%- end %>
<%- end -%>
</div>
<!-- /tblsection -->
@@ -0,0 +1,166 @@
<style type="text/css">
#cbi-shadowsocksr-server_subscribe_item > h3 {
font-weight: 800 !important;
margin-bottom: 0.45rem !important;
}
#cbi-shadowsocksr-server_subscribe_item > .cbi-section-descr {
margin-bottom: 0.7rem !important;
}
#cbi-shadowsocksr-server_subscribe_item > .table {
table-layout: fixed !important;
width: 100% !important;
}
#cbi-shadowsocksr-server_subscribe_item .th,
#cbi-shadowsocksr-server_subscribe_item .td {
vertical-align: middle !important;
line-height: 1 !important;
}
#cbi-shadowsocksr-server_subscribe_item .table.cbi-section-table .th,
#cbi-shadowsocksr-server_subscribe_item .table.cbi-section-table .td {
padding: 4px 6px !important;
}
#cbi-shadowsocksr-server_subscribe_item .cbi-section-table-row {
text-align: left !important;
height: auto !important;
min-height: 0 !important;
}
#cbi-shadowsocksr-server_subscribe_item .cbi-section-table-row .td {
text-align: left !important;
}
#cbi-shadowsocksr-server_subscribe_item .cbi-section-table-row.placeholder {
display: none !important;
}
#cbi-shadowsocksr-server_subscribe_item .cbi-value {
margin-bottom: 0;
min-height: 0 !important;
padding: 0 !important;
}
#cbi-shadowsocksr-server_subscribe_item .td.cbi-value-field {
width: auto !important;
}
#cbi-shadowsocksr-server_subscribe_item input[type="text"] {
width: 100%;
height: 2.1rem !important;
line-height: 1.15 !important;
font-size: 1.08rem !important;
padding-top: 0.22rem !important;
padding-bottom: 0.22rem !important;
padding-left: 0.5rem !important;
padding-right: 0.5rem !important;
min-width: 0;
box-sizing: border-box;
}
#cbi-shadowsocksr-server_subscribe_item .cbi-value-field {
margin-left: 0;
}
#cbi-shadowsocksr-server_subscribe_item .cbi-value label.cbi-value-title {
display: none;
}
#cbi-shadowsocksr-server_subscribe_item .td.cbi-value-field[data-name="enabled"] > div {
display: flex;
justify-content: center !important;
align-items: center !important;
min-height: 1.7rem;
width: 100% !important;
}
#cbi-shadowsocksr-server_subscribe_item .td.cbi-value-field[data-name="enabled"] [data-ui-widget*="Checkbox"] {
display: flex !important;
justify-content: center !important;
align-items: center !important;
width: 100% !important;
}
#cbi-shadowsocksr-server_subscribe_item .td.cbi-value-field[data-name="enabled"] input[type="checkbox"] {
margin-left: 0 !important;
margin-right: 0 !important;
margin-top: 0 !important;
}
#cbi-shadowsocksr-server_subscribe_item .th:nth-child(1),
#cbi-shadowsocksr-server_subscribe_item .td.cbi-value-field[data-name="enabled"] {
width: 4.2rem !important;
min-width: 4.2rem !important;
max-width: 4.2rem !important;
text-align: center !important;
white-space: nowrap;
padding-left: 4px !important;
padding-right: 2px !important;
}
#cbi-shadowsocksr-server_subscribe_item .th:nth-child(2),
#cbi-shadowsocksr-server_subscribe_item .td.cbi-value-field[data-name="alias"] {
width: 10.2rem !important;
min-width: 10.2rem !important;
max-width: 10.2rem !important;
}
#cbi-shadowsocksr-server_subscribe_item .th:nth-child(3),
#cbi-shadowsocksr-server_subscribe_item .td.cbi-value-field[data-name="url"] {
width: auto !important;
min-width: 0 !important;
}
#cbi-shadowsocksr-server_subscribe_item .th.cbi-section-actions,
#cbi-shadowsocksr-server_subscribe_item .td.cbi-section-actions {
width: 7.4rem !important;
min-width: 7.4rem !important;
max-width: 7.4rem !important;
text-align: right !important;
white-space: nowrap;
padding-left: 0 !important;
padding-right: 0 !important;
}
#cbi-shadowsocksr-server_subscribe_item .td.cbi-section-actions > div {
display: flex;
justify-content: flex-end;
}
#cbi-shadowsocksr-server_subscribe_item .td.cbi-section-actions .btn {
height: 2.1rem !important;
line-height: 1.1 !important;
padding-top: 0.22rem !important;
padding-bottom: 0.22rem !important;
padding-left: 0.9rem !important;
padding-right: 0.9rem !important;
margin: 0 !important;
}
#cbi-shadowsocksr-server_subscribe_item .td.cbi-value-field[data-name="url"] input[type="text"] {
font-family: "Menlo", "Monaco", "Consolas", monospace;
}
#cbi-shadowsocksr-server_subscribe_item .td.cbi-value-field[data-name="alias"] .cbi-input-text,
#cbi-shadowsocksr-server_subscribe_item .td.cbi-value-field[data-name="url"] .cbi-input-text {
width: 100% !important;
max-width: none !important;
min-width: 0 !important;
display: block !important;
margin: 0 !important;
}
#cbi-shadowsocksr-server_subscribe_item .td.cbi-value-field[data-name="alias"] .cbi-input-text {
font-size: 0.96rem !important;
}
@media (max-width: 900px) {
#cbi-shadowsocksr-server_subscribe_item > .table {
display: block;
overflow-x: auto;
}
}
</style>
@@ -0,0 +1,26 @@
<%+cbi/valueheader%>
<%
local hint = self:cfgvalue(section)
local cache = self.detect_cache
local text = "N/A"
local color = "#999999"
if cache then
if cache.socket then
text = "ok"
color = "#249400"
else
text = "fail"
color = "#ff0000"
end
end
%>
<span class="socket-connected" hint="<%=hint%>"><font style="color:<%=color%>"><%=text%></font></span>
<span class="transport" hint="<%=self.transport%>"></span>
<span class="wsPath" hint="<%=self.ws_path%>"></span>
<span class="wsHost" hint="<%=self.ws_host%>"></span>
<span class="tlsHost" hint="<%=self.tls_host%>"></span>
<span class="tls" hint="<%=self.tls%>"></span>
<span class="type" hint="<%=self.type%>"></span>
<span class="proto" hint="<%=self.proto%>"></span>
<span class="reality" hint="<%=self.reality%>"></span>
<%+cbi/valuefooter%>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,23 @@
<script type="text/javascript">//<![CDATA[
XHR.poll(3, '<%=url([[admin]], [[services]], [[shadowsocksr]], [[run]])%>', null,
function(x, data) {
var tb = document.getElementById('shadowsocksr_status');
if (data && tb) {
if (data.running) {
var text = data.status_text || '<%:RUNNING%>';
var links = '<em><b style=color:green>ShadowsocksR Plus+ ' + text + '</b></em>';
tb.innerHTML = links;
} else {
tb.innerHTML = '<em><b style=color:red>ShadowsocksR Plus+ <%:NOT RUNNING%></b></em>';
}
}
}
);
//]]>
</script>
<style>.mar-10 {margin-left: 50px; margin-right: 10px;}</style>
<fieldset class="cbi-section">
<p id="shadowsocksr_status">
<em><%:Collecting data...%></em>
</p>
</fieldset>
@@ -0,0 +1,128 @@
<style>
.pure-img {
max-height: 100%;
width: auto;
}
.flag .pure-img {
max-height: none;
margin-top: -0.34rem;
}
.status-bar {
position: fixed;
bottom: 0;
right: 0;
box-shadow: 0 0 2rem 0 rgba(136, 152, 170, .3);
color: #525f7f;
background: #fff;
z-index: 5;
box-sizing: border-box;
}
.status-bar .inner {
margin: 0.5em;
}
.status-bar .inner .flag {
height: 2.6em;
display: block;
float: left;
margin-right: 1em;
}
.status-bar .inner .status-info {
font-weight: bold;
}
.status-bar .icon-con {
height: 2.6em;
text-align: right;
}
#cbi-passwall+.cbi-page-actions.control-group.fixed {
bottom: 3.3rem;
}
footer{
display:block !important;
}
@media screen and (max-width: 700px) {
.status-bar .icon-con {
height: 2.5em;
}
}
</style>
<div class="status-bar">
<div class="inner">
<div class="pure-g">
<div class="pure-u-1-2">
<span class="flag"><img src="/luci-static/passwall/flags/loading.svg" class="pure-img"></span> <span
class="status-info">获取中...</span>
</div>
<div class="pure-u-1-2">
<div class="icon-con">
<img src="/luci-static/passwall/img/site_icon1_01.png" class="pure-img i1">
<img src="/luci-static/passwall/img/site_icon1_02.png" class="pure-img i2">
<img src="/luci-static/passwall/img/site_icon1_03.png" class="pure-img i3">
<img src="/luci-static/passwall/img/site_icon1_04.png" class="pure-img i4">
</div>
</div>
</div>
</div>
</div>
<script>
const _ASSETS = '/luci-static/passwall/';
const CHECK_IP_URL = '<%=url([[admin]], [[services]], [[passwall]], [[ip]])%>';
let wW = window.innerWidth;
function resize() {
wW = window.innerWidth;
let lw = document.querySelector(".main-left").offsetWidth;
let statusBar = document.querySelector(".status-bar");
statusBar.style.width = (wW - lw) + 'px';
let flagElement = statusBar.querySelector(".flag");
flagElement.style.width = (flagElement.offsetHeight / 3 * 4) + 'px';
document.querySelectorAll(".flag-icon").forEach(function(el) {
if (el.offsetHeight < 60) {
el.parentElement.style.height = '60px';
el.style.width = '60px';
} else {
el.style.width = el.offsetHeight + 'px';
}
});
}
function write_status(data) {
document.querySelector(".flag img").src = _ASSETS + "flags/" + data.flag + ".svg";
document.querySelector(".status-info").innerHTML = data.ip + "<br>" + data.country + "&nbsp;" + data.isp;
document.querySelector(".i1").src = data.baidu ? _ASSETS + "img/site_icon_01.png" : _ASSETS + "img/site_icon1_01.png";
document.querySelector(".i2").src = data.taobao ? _ASSETS + "img/site_icon_02.png" : _ASSETS + "img/site_icon1_02.png";
document.querySelector(".i3").src = data.google ? _ASSETS + "img/site_icon_03.png" : _ASSETS + "img/site_icon1_03.png";
document.querySelector(".i4").src = data.youtube ? _ASSETS + "img/site_icon_04.png" : _ASSETS + "img/site_icon1_04.png";
setTimeout(function() {
let event = new Event('iploaded');
document.body.dispatchEvent(event);
}, 200);
}
XHR.poll(5, CHECK_IP_URL, null,
function (x, data) {
write_status(data);
}
);
document.addEventListener('DOMContentLoaded', function() {
setTimeout("resize()",100)
fetch(CHECK_IP_URL)
.then(response => response.json())
.then(data => {
write_status(data);
})
.catch(error => console.error('Error:', error));
});
window.addEventListener('resize', resize);
</script>
@@ -0,0 +1,257 @@
<%+cbi/valueheader%>
<%
local dsp = require "luci.dispatcher"
local subscribe_url = dsp.build_url("admin", "services", "shadowsocksr", "subscribe")
local get_log_url = dsp.build_url("admin", "services", "shadowsocksr", "get_log")
local servers_url = dsp.build_url("admin", "services", "shadowsocksr", "servers")
local autostart = self.map and self.map.ssr_subscribe_autostart
-%>
<div id="ssr-subscribe-modal-wrap">
<style type="text/css">
#ssr-subscribe-modal-mask {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.35);
z-index: 2000;
}
#ssr-subscribe-modal {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: min(900px, calc(100vw - 32px));
max-height: min(720px, calc(100vh - 80px));
overflow: auto;
background: #fff;
border-radius: 10px;
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.18);
z-index: 2001;
padding: 12px 16px 14px;
}
#ssr-subscribe-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 8px;
}
#ssr-subscribe-modal-title {
font-size: 17px;
font-weight: 600;
color: #333;
}
#ssr-subscribe-modal-status {
margin: 0 0 8px;
color: #666;
}
#ssr-subscribe-modal-status.ssr-subscribe-highlight {
font-size: 18px;
font-weight: 700;
line-height: 1.5;
}
#ssr-subscribe-log {
margin: 0;
padding: 14px 16px;
min-height: 420px;
max-height: min(520px, calc(100vh - 220px));
overflow: auto;
background: #111827;
color: #e5e7eb;
border-radius: 8px;
box-sizing: border-box;
white-space: pre-wrap;
word-break: break-word;
font: 13px/1.5 Menlo, Monaco, Consolas, monospace;
}
@media (max-width: 700px) {
#ssr-subscribe-modal {
width: calc(100vw - 16px);
max-height: calc(100vh - 24px);
padding: 10px 12px 12px;
}
#ssr-subscribe-modal-header {
flex-direction: column;
align-items: flex-start;
}
#ssr-subscribe-log {
max-height: calc(100vh - 260px);
min-height: 280px;
}
}
</style>
<script type="text/javascript">//<![CDATA[
(function() {
var pollTimer = null;
var running = false;
var autoStart = <%=autostart and "true" or "false"%>;
function getEl(id) {
return document.getElementById(id);
}
function setStatus(text, color) {
var status = getEl('ssr-subscribe-modal-status');
if (!status) {
return;
}
status.className = 'ssr-subscribe-highlight';
status.style.color = color || '#666';
status.textContent = text;
}
function updateLog(text) {
var log = getEl('ssr-subscribe-log');
if (!log) {
return;
}
log.textContent = text || '';
log.scrollTop = log.scrollHeight;
}
function appendLog(text) {
var log = getEl('ssr-subscribe-log');
if (!log || !text) {
return;
}
log.textContent = (log.textContent ? log.textContent.replace(/\s*$/, '') + '\n' : '') + text;
log.scrollTop = log.scrollHeight;
}
function clearPoll() {
if (pollTimer) {
window.clearInterval(pollTimer);
pollTimer = null;
}
}
function fetchLog() {
XHR.get('<%=get_log_url%>', null, function(x) {
if (x && x.status === 200) {
updateLog(x.responseText || '');
}
});
}
function openModal() {
var mask = getEl('ssr-subscribe-modal-mask');
var modal = getEl('ssr-subscribe-modal');
if (mask) {
mask.style.display = 'block';
}
if (modal) {
modal.style.display = 'block';
}
}
function closeModal(force) {
if (running && !force) {
return false;
}
clearPoll();
var mask = getEl('ssr-subscribe-modal-mask');
var modal = getEl('ssr-subscribe-modal');
if (mask) {
mask.style.display = 'none';
}
if (modal) {
modal.style.display = 'none';
}
return false;
}
function finishSubscribe(success) {
running = false;
clearPoll();
fetchLog();
if (success) {
var remain = 5;
appendLog('订阅已结束,将在倒数5秒后返回页面');
setStatus('订阅已结束,将在倒数' + remain + '秒后返回页面', '#249400');
var countdown = window.setInterval(function() {
remain = remain - 1;
if (remain <= 0) {
window.clearInterval(countdown);
closeModal(true);
window.location.href = '<%=servers_url%>';
return;
}
setStatus('订阅已结束,将在倒数' + remain + '秒后返回页面', '#249400');
}, 1000);
return;
}
setStatus('<%:Subscribe failed, refreshing page...%>', '#ff0000');
window.setTimeout(function() {
closeModal(true);
window.location.href = '<%=servers_url%>';
}, 1200);
}
function startSubscribe(options, done) {
options = options || {};
running = true;
updateLog('');
setStatus(options.title || '<%:Starting subscription update...%>', '#0072c3');
openModal();
clearPoll();
fetchLog();
pollTimer = window.setInterval(fetchLog, 1000);
XHR.get('<%=subscribe_url%>', options.sid ? { sid: options.sid } : null, function(x, rv) {
var ok = !!(rv && typeof rv.ret !== 'undefined' && Number(rv.ret) === 0);
if (typeof done === 'function') {
done(ok);
}
finishSubscribe(ok);
});
}
window.ssrSubscribeRun = startSubscribe;
window.setTimeout(function() {
var closeBtn = getEl('ssr-subscribe-modal-close');
var mask = getEl('ssr-subscribe-modal-mask');
if (closeBtn) {
closeBtn.onclick = function() {
return closeModal(false);
};
}
if (mask) {
mask.onclick = function() {
return closeModal(false);
};
}
if (autoStart) {
startSubscribe();
}
}, 0);
})();
//]]></script>
<input class="btn cbi-button cbi-button-apply" type="submit"<%= attr("name", cbid) .. attr("id", cbid) .. attr("value", self.inputtitle or self.title)%> />
<div id="ssr-subscribe-modal-mask"></div>
<div id="ssr-subscribe-modal" role="dialog" aria-modal="true" aria-labelledby="ssr-subscribe-modal-title">
<div id="ssr-subscribe-modal-header">
<div id="ssr-subscribe-modal-title"><%:Subscribe Log%></div>
<input type="button" id="ssr-subscribe-modal-close" class="btn cbi-button" value="<%:Close%>" />
</div>
<div id="ssr-subscribe-modal-status"><%:Ready.%></div>
<pre id="ssr-subscribe-log"></pre>
</div>
</div>
<%+cbi/valuefooter%>
@@ -0,0 +1,490 @@
<%-
local rowcnt = 0
local function rowstyle()
rowcnt = rowcnt + 1
if rowcnt % 2 == 0 then
return " cbi-rowstyle-1"
end
return " cbi-rowstyle-2"
end
local function width(o)
if o.width then
if type(o.width) == "number" then
return ' style="width:%dpx"' % o.width
end
return ' style="width:%s"' % o.width
end
return ""
end
-%>
<script type="text/javascript">//<![CDATA[
window.ssrUpdateSubscribeItem = window.ssrUpdateSubscribeItem || function(btn) {
if (!btn) return false;
var sid = btn.getAttribute('data-sid') || '';
var title = btn.getAttribute('data-title') || '';
var original = btn.getAttribute('data-original-value') || btn.value || 'Update';
btn.disabled = true;
btn.value = '<%:Updating...%>';
window.ssrSubscribeRun = window.ssrSubscribeRun || function(_, done) {
if (done) done();
};
window.ssrSubscribeRun({
sid: sid,
title: title
}, function() {
btn.disabled = false;
btn.value = original;
});
return false;
};
function saveAllSubscribeItems(btn) {
// var btn = document.querySelector('#ssr-subscribe-toolbar .cbi-button-save');
if (!btn) return;
var originalText = btn.value;
btn.disabled = true;
btn.value = '<%:Saving...%>';
var fieldsMap = {};
var allInputs = document.querySelectorAll('[name^="cbid.shadowsocksr."]');
allInputs.forEach(function(input) {
var match = input.name.match(/^cbid\.shadowsocksr\.([^.]+)\.(url|alias|enabled)$/);
if (!match) return;
var sid = match[1];
var field = match[2];
if (!fieldsMap[sid]) fieldsMap[sid] = {};
if (field === 'enabled') {
fieldsMap[sid][field] = input.checked ? '1' : '0';
} else {
fieldsMap[sid][field] = input.value;
}
});
var promises = [];
for (var sid in fieldsMap) {
var fields = fieldsMap[sid];
for (var field in fields) {
var value = fields[field];
var promise = new Promise(function(resolve, reject) {
XHR.post(
'<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "toggle_subscribe_item_enabled")%>',
{ sid: sid, field: field, value: value },
function(x, rv) {
if (rv && Number(rv.ret) === 1) {
resolve();
} else {
reject(new Error('<%:Save failed:%> ' + sid + '.' + field + ': ' + (rv && rv.error ? rv.error : '<%:Unknown error%>')));
}
}
);
});
promises.push(promise);
}
}
if (promises.length === 0) {
btn.disabled = false;
btn.value = originalText;
alert('<%:No subscription items found%>');
return;
}
Promise.all(promises).then(function() {
btn.disabled = false;
btn.value = originalText;
alert('<%:All settings saved successfully.%>');
}).catch(function(err) {
btn.disabled = false;
btn.value = originalText;
alert('<%:Save failed:%>' + err.message);
});
}
function deleteSubscribeItem(btn) {
var sid = btn.getAttribute('data-sid');
if (!sid) return;
if (!confirm('<%:Are you sure you want to delete subscribe link: %s ?%>'.replace('%s', sid))) return;
btn.disabled = true;
XHR.post('<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "delete_subscribe_item")%>',
{ sid: sid },
function(x, rv) {
btn.disabled = false;
if (rv && rv.ret === 1) {
var row = btn.closest('tr');
if (row) row.remove();
var table = document.querySelector('#cbi-shadowsocksr-server_subscribe_item .cbi-section-table');
if (table.querySelectorAll('.cbi-section-table-row:not(.cbi-section-table-titles)').length === 0) {
var placeholder = document.createElement('tr');
placeholder.className = 'tr cbi-section-table-row placeholder';
placeholder.innerHTML = '<td class="td" colspan="4"><em><%:This section contains no values yet%></em></td>';
table.appendChild(placeholder);
}
} else {
alert('<%:Delete failed:%> ' + (rv ? rv.error : '<%:Unknown error%>'));
}
}
);
}
function createEmptyRow(sid) {
var templateRow = document.querySelector('#cbi-shadowsocksr-server_subscribe_item .cbi-section-table-row:not(.cbi-section-table-titles):not(.placeholder)');
if (!templateRow) {
console.error('No template row found');
return null;
}
var newRow = templateRow.cloneNode(true);
newRow.id = 'cbi-shadowsocksr-server_subscribe_item-' + sid;
var inputs = newRow.querySelectorAll('input');
inputs.forEach(function(input) {
if (input.name) {
input.name = input.name.replace(/cbid\.shadowsocksr\.[^.]+\./, 'cbid.shadowsocksr.' + sid + '.');
}
if (input.id) {
input.id = input.id.replace(/cfg[^.]+/, sid);
}
if (input.type === 'checkbox') {
input.checked = false;
input.defaultChecked = false;
} else if (input.type === 'text' || input.type === 'url') {
input.value = '';
input.defaultValue = '';
}
input.disabled = false;
});
var delBtn = newRow.querySelector('.cbi-button-remove');
if (delBtn) {
delBtn.setAttribute('data-sid', sid);
delBtn.removeAttribute('onclick');
delBtn.onclick = function(e) { deleteSubscribeItem(this); return false; };
}
var updateBtn = newRow.querySelector('.cbi-button-apply');
if (updateBtn) {
updateBtn.setAttribute('data-sid', sid);
}
return newRow;
}
function addSubscribeItem() {
var btn = document.getElementById('ssr-add-btn');
if (!btn) return;
btn.disabled = true;
btn.value = '<%:Adding...%>';
XHR.post(
'<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "add_subscribe_item")%>',
{},
function(x, rv) {
btn.disabled = false;
btn.value = '<%:Add%>';
if (rv && rv.ret === 1 && rv.sid) {
var newRow = createEmptyRow(rv.sid);
if (newRow) {
var aliasInput = newRow.querySelector('input[name$=".alias"]');
if (aliasInput) {
aliasInput.value = rv.alias || '';
aliasInput.defaultValue = rv.alias || '';
}
var urlInput = newRow.querySelector('input[name$=".url"]');
if (urlInput) {
urlInput.value = rv.url || '';
urlInput.defaultValue = rv.url || '';
}
var enabledInput = newRow.querySelector('input[type="checkbox"]');
if (enabledInput) {
var checked = (rv.enabled === '1');
enabledInput.checked = checked;
enabledInput.defaultChecked = checked;
}
var table = document.querySelector('#cbi-shadowsocksr-server_subscribe_item .cbi-section-table');
var placeholder = table.querySelector('.placeholder');
if (placeholder) placeholder.remove();
table.appendChild(newRow);
}
} else {
alert('<%:Add failed:%> ' + (rv && rv.error ? rv.error : '<%:Unknown error%>'));
}
}
);
}
//]]></script>
<div class="cbi-section cbi-tblsection" id="cbi-<%=self.config%>-<%=self.sectiontype%>">
<% if self.title and #self.title > 0 then -%>
<h3><%=self.title%></h3>
<%- end %>
<%- if self.sortable then -%>
<input type="hidden" id="cbi.sts.<%=self.config%>.<%=self.sectiontype%>" name="cbi.sts.<%=self.config%>.<%=self.sectiontype%>" value="" />
<%- end -%>
<div class="cbi-section-descr"><%=self.description%></div>
<table class="table cbi-section-table">
<tr class="tr cbi-section-table-titles anonymous">
<% for _, node in ipairs(self.children) do if not node.optional and node.option ~= "_update" then -%>
<th class="th cbi-section-table-cell"<%=width(node) .. attr("data-widget", node.typename or "")%>><%=node.title%></th>
<% end end -%>
<th class="th cbi-section-table-cell cbi-section-actions" style="width:1%"><%:Actions%></th>
</tr>
<%
local isempty = true
for i, section in ipairs(self:cfgsections()) do
isempty = false
local colorclass = rowstyle()
local scope = {
valueheader = "cbi/cell_valueheader",
valuefooter = "cbi/cell_valuefooter"
}
-%>
<tr class="tr cbi-section-table-row<%=colorclass%>" id="cbi-<%=self.config%>-<%=section%>">
<%
for _, node in ipairs(self.children) do
if not node.optional and node.option ~= "_update" then
node:render(section, scope)
end
end
local alias = self.map:get(section, "alias") or string.format("Subscribe %s", section:sub(-4))
local update_title = "Subscribe: " .. tostring(alias)
-%>
<td class="td cbi-section-table-cell nowrap cbi-section-actions">
<div class="ssr-subscribe-actions">
<input class="btn cbi-button cbi-button-save" type="button" value="<%:Save%>" title="<%:Save%>" onclick="saveAllSubscribeItems(this)" />
<input class="btn cbi-button cbi-button-remove" type="button" value="<%:Delete%>" data-sid="<%=section%>" onclick="deleteSubscribeItem(this); return false;" />
<input
type="button"
class="btn cbi-button cbi-button-apply"
value="<%:Update%>"
data-original-value="<%:Update%>"
<%=attr("data-sid", section) .. attr("data-title", update_title)%>
onclick="return window.ssrUpdateSubscribeItem(this)"
/>
</div>
</td>
</tr>
<%- end -%>
<%- if isempty then -%>
<tr class="tr cbi-section-table-row placeholder">
<td class="td" colspan="4"><em><%:This section contains no values yet%></em></td>
</tr>
<%- end -%>
</table>
</div>
<div class="cbi-section-create cbi-tblsection-create ssr-subscribe-toolbar" id="ssr-subscribe-toolbar">
<input class="btn cbi-button cbi-button-add" type="button" value="<%:Add%>" id="ssr-add-btn" title="<%:Add%>" />
<input class="btn cbi-button cbi-button-save" type="button" value="<%:Save%>" title="<%:Save%>" onclick="saveAllSubscribeItems(this)" />
<!-- <input class="btn cbi-button cbi-button-save ssr-subscribe-save-settings" type="submit" value="<%:Save%>" title="<%:Save%>" name="cbi.apply" /> -->
<div class="ssr-subscribe-toolbar-slot ssr-subscribe-toolbar-slot-delete"></div>
<div class="ssr-subscribe-toolbar-slot ssr-subscribe-toolbar-slot-update"></div>
</div>
<script type="text/javascript">//<![CDATA[
(function() {
function styleActionButton(btn, extraClass) {
if (!btn) return;
btn.classList.remove('cbi-button-apply', 'cbi-button-reset');
btn.classList.add('cbi-button-add');
if (extraClass) btn.classList.add(extraClass);
}
function mountSubscribeActions() {
var toolbar = document.getElementById('ssr-subscribe-toolbar');
var subscribeSection = document.getElementById('cbi-shadowsocksr-server_subscribe');
if (!toolbar || !subscribeSection) return;
if (toolbar.getAttribute('data-ssr-subscribe-actions-ready') === '1') return;
var deleteSlot = toolbar.querySelector('.ssr-subscribe-toolbar-slot-delete');
var updateSlot = toolbar.querySelector('.ssr-subscribe-toolbar-slot-update');
var updateRow = subscribeSection.querySelector('.cbi-value[id$="-subscribe"]');
var deleteRow = subscribeSection.querySelector('.cbi-value[id$="-delete"]');
if (!deleteSlot || !updateSlot || !updateRow || !deleteRow) return;
var modalWrap = updateRow.querySelector('#ssr-subscribe-modal-wrap');
if (modalWrap) {
var updateBtn = modalWrap.querySelector('input[type="submit"]');
styleActionButton(updateBtn, 'ssr-subscribe-update-all');
updateSlot.appendChild(modalWrap);
}
var deleteBtn = deleteRow.querySelector('input[type="submit"]');
if (deleteBtn) {
styleActionButton(deleteBtn, 'ssr-subscribe-delete-all');
deleteSlot.appendChild(deleteBtn);
}
updateRow.style.display = 'none';
deleteRow.style.display = 'none';
toolbar.setAttribute('data-ssr-subscribe-actions-ready', '1');
}
function bindAddButton() {
var addBtn = document.getElementById('ssr-add-btn');
if (addBtn && !addBtn.getAttribute('data-bound')) {
addBtn.addEventListener('click', addSubscribeItem);
addBtn.setAttribute('data-bound', '1');
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
mountSubscribeActions();
bindAddButton();
});
window.addEventListener('load', function() {
mountSubscribeActions();
bindAddButton();
});
} else {
window.setTimeout(mountSubscribeActions, 0);
window.setTimeout(bindAddButton, 0);
}
})();
//]]></script>
<style type="text/css">
#ssr-subscribe-toolbar {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
}
#ssr-subscribe-toolbar > .btn.cbi-button-add,
#ssr-subscribe-toolbar > .btn.cbi-button-save {
margin: 0;
}
#ssr-subscribe-toolbar .ssr-subscribe-toolbar-slot-delete,
#ssr-subscribe-toolbar .ssr-subscribe-toolbar-slot-update {
display: flex;
align-items: center;
}
#ssr-subscribe-toolbar .ssr-subscribe-toolbar-slot-delete {
margin-left: auto;
}
#ssr-subscribe-toolbar #ssr-subscribe-modal-wrap,
#ssr-subscribe-toolbar .ssr-subscribe-toolbar-slot input.btn {
display: flex;
align-items: center;
}
#ssr-subscribe-toolbar #ssr-subscribe-modal-wrap {
margin: 0 0 0 12px;
}
#ssr-subscribe-toolbar .ssr-subscribe-delete-all {
margin-right: 0 !important;
}
#ssr-subscribe-toolbar > .btn.cbi-button-add,
#ssr-subscribe-toolbar > .btn.cbi-button-save,
#ssr-subscribe-toolbar .ssr-subscribe-toolbar-slot .btn {
min-height: 2.1rem;
padding-top: 0.18rem;
padding-bottom: 0.18rem;
}
#ssr-subscribe-toolbar .ssr-subscribe-toolbar-slot .btn.cbi-button-add {
margin: 0;
width: auto;
max-width: 100%;
justify-content: center;
text-align: center;
}
#ssr-subscribe-toolbar .ssr-subscribe-delete-all {
background: #5f6874 !important;
border-color: #5f6874 !important;
color: #fff !important;
}
/* 电脑端按钮间距 */
#cbi-shadowsocksr-server_subscribe_item .ssr-subscribe-actions {
display: flex;
flex-direction: row;
gap: 2px; /* 按钮之间增加 8px 间距 */
align-items: center;
white-space: nowrap;
}
/* 移除电脑端默认外边距,统一由 gap 控制间距 */
#cbi-shadowsocksr-server_subscribe_item .ssr-subscribe-actions .btn {
margin: 0 !important;
}
/* 清除手机端伪元素问题(出现断层显示)*/
@media screen and (max-width: 1152px) {
td[data-title]::after {
content: none !important;
display: none !important;
background: none !important;
}
}
@media (max-width: 900px) {
#ssr-subscribe-toolbar {
flex-wrap: wrap;
}
#ssr-subscribe-toolbar > .btn.cbi-button-add,
#ssr-subscribe-toolbar > .btn.cbi-button-save,
#ssr-subscribe-toolbar .ssr-subscribe-toolbar-slot-delete,
#ssr-subscribe-toolbar .ssr-subscribe-toolbar-slot-update {
margin-left: 0;
}
/* 订阅列表 table 卡片化 */
#cbi-shadowsocksr-server_subscribe_item .cbi-section-table,
#cbi-shadowsocksr-server_subscribe_item .cbi-section-table tbody,
#cbi-shadowsocksr-server_subscribe_item .cbi-section-table tr,
#cbi-shadowsocksr-server_subscribe_item .cbi-section-table td {
display: block !important;
width: 100% !important;
box-sizing: border-box;
}
/* 隐藏表头 */
#cbi-shadowsocksr-server_subscribe_item .cbi-section-table-titles {
display: none !important;
}
/* 操作列,将 td 容器调整为 100% */
#cbi-shadowsocksr-server_subscribe_item .cbi-section-table td.cbi-section-actions {
width: 100% !important;
max-width: 100% !important;
display: block !important;
box-sizing: border-box !important;
}
/* 按钮区域 */
#cbi-shadowsocksr-server_subscribe_item .ssr-subscribe-actions {
display: flex !important;
gap: 8px;
width: 100%;
}
/* 保存、删除、更新 三个按钮均分 */
#cbi-shadowsocksr-server_subscribe_item .ssr-subscribe-actions .btn {
flex: 1;
margin: 0 !important;
white-space: nowrap;
}
/* 修复手机端卡片化布局中 placeholder 行的显示问题 */
#cbi-shadowsocksr-server_subscribe_item .placeholder td {
display: table-cell !important;
}
}
</style>
@@ -0,0 +1,47 @@
<script type="text/javascript">//<![CDATA[
(function() {
function bindSubscribeItemEnabledAutoSave() {
var root = document.getElementById('cbi-shadowsocksr-server_subscribe_item');
if (!root || root.getAttribute('data-enabled-autosave-bound') === '1') {
return;
}
root.addEventListener('change', function(ev) {
var target = ev.target;
if (!target || target.type !== 'checkbox' || !target.name) {
return;
}
var matched = target.name.match(/^cbid\.shadowsocksr\.([^.]+)\.enabled$/);
if (!matched) {
return;
}
var sid = matched[1];
var original = target.checked;
target.disabled = true;
XHR.post(
'<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "toggle_subscribe_item_enabled")%>',
{ sid: sid, enabled: original ? '1' : '0' },
function(x, rv) {
target.disabled = false;
if (!(rv && Number(rv.ret) === 1)) {
target.checked = !original;
alert('Failed to save subscription enable state.');
}
}
);
});
root.setAttribute('data-enabled-autosave-bound', '1');
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', bindSubscribeItemEnabledAutoSave);
window.addEventListener('load', bindSubscribeItemEnabledAutoSave);
} else {
window.setTimeout(bindSubscribeItemEnabledAutoSave, 0);
}
})();
//]]></script>
@@ -0,0 +1,182 @@
<style type="text/css">
.ssr-schedule-inline {
display: flex;
gap: 2%;
align-items: flex-start;
flex-wrap: nowrap;
}
.ssr-schedule-item {
width: 30%;
min-width: 0;
}
.ssr-schedule-item select {
width: 100%;
}
.ssr-schedule-subtitle {
font-size: 12px;
color: #666;
margin-bottom: 4px;
line-height: 1.2;
}
.ssr-schedule-hidden {
display: none !important;
}
</style>
<script type="text/javascript">
//<![CDATA[
function ssrCompactSubscribeSchedule() {
function getSelectByName(suffix) {
return document.querySelector('select[name$=".' + suffix + '"], input[name$=".' + suffix + '"]');
}
function shortLabel(row, fallback) {
if (!row) return fallback;
var label = row.querySelector('.cbi-value-title');
if (!label) return fallback;
var text = (label.textContent || '').trim();
var matched = text.match(/\(([^)]+)\)/);
return matched ? matched[1] : fallback;
}
function getRowByName(suffix) {
var el = getSelectByName(suffix);
return el ? el.closest('.cbi-value') : null;
}
function compactScheduleRow() {
var modeRow = getRowByName('config_auto_update_mode');
var weekRow = getRowByName('auto_update_week_time');
var hourRow = getRowByName('auto_update_day_time');
var minRow = getRowByName('auto_update_min_time');
var intervalRow = getRowByName('config_update_interval');
if (!weekRow || !hourRow || !minRow) return;
var weekField = weekRow.querySelector('.cbi-value-field');
var hourField = hourRow.querySelector('.cbi-value-field');
var minField = minRow.querySelector('.cbi-value-field');
if (!weekField || !hourField || !minField) return;
if (weekField.querySelector('.ssr-schedule-inline')) {
return;
}
var weekSelect = weekField.querySelector('select');
var hourSelect = hourField.querySelector('select');
var minSelect = minField.querySelector('select');
if (!weekSelect || !hourSelect || !minSelect) return;
var wrap = document.createElement('div');
wrap.className = 'ssr-schedule-inline';
function makeItem(title, select) {
var item = document.createElement('div');
item.className = 'ssr-schedule-item';
var subtitle = document.createElement('div');
subtitle.className = 'ssr-schedule-subtitle';
subtitle.textContent = title;
item.appendChild(subtitle);
item.appendChild(select);
return item;
}
wrap.appendChild(makeItem(shortLabel(weekRow, '<%:Day/Week%>'), weekSelect));
wrap.appendChild(makeItem(shortLabel(hourRow, '<%:Hour%>'), hourSelect));
wrap.appendChild(makeItem(shortLabel(minRow, '<%:Min%>'), minSelect));
weekField.innerHTML = '';
weekField.appendChild(wrap);
hourRow.classList.add('ssr-schedule-hidden');
minRow.classList.add('ssr-schedule-hidden');
function applyModeVisibility() {
var modeEl = getSelectByName('config_auto_update_mode');
var loopMode = modeEl && modeEl.value === '1';
if (weekRow) {
weekRow.classList.toggle('ssr-schedule-hidden', loopMode);
}
if (hourRow) {
hourRow.classList.add('ssr-schedule-hidden');
}
if (minRow) {
minRow.classList.add('ssr-schedule-hidden');
}
if (intervalRow) {
intervalRow.classList.toggle('ssr-schedule-hidden', !loopMode);
}
}
if (modeRow) {
var modeSelect = modeRow.querySelector('select');
if (modeSelect && !modeSelect.dataset.ssrLoopBound) {
modeSelect.dataset.ssrLoopBound = '1';
modeSelect.addEventListener('change', applyModeVisibility);
}
}
applyModeVisibility();
}
compactScheduleRow();
}
(function() {
var retryCount = 0;
var maxRetries = 30;
var retryTimer = null;
var observer = null;
function hasCompactLayout() {
return !!document.querySelector('.ssr-schedule-inline');
}
function tryCompactSchedule() {
ssrCompactSubscribeSchedule();
if (hasCompactLayout()) {
if (retryTimer) {
window.clearTimeout(retryTimer);
retryTimer = null;
}
if (observer) {
observer.disconnect();
observer = null;
}
return;
}
if (retryCount >= maxRetries) {
return;
}
retryCount = retryCount + 1;
retryTimer = window.setTimeout(tryCompactSchedule, 200);
}
function startCompactWatcher() {
tryCompactSchedule();
if (window.MutationObserver) {
observer = new MutationObserver(function() {
if (!hasCompactLayout()) {
tryCompactSchedule();
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', startCompactWatcher);
window.addEventListener('load', tryCompactSchedule);
} else {
startCompactWatcher();
}
})();
//]]>
</script>
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
zh_Hans
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,22 @@
#!/bin/sh
[ "$ACTION" = "ifup" ] || exit 0
[ "$INTERFACE" = "wan" ] || exit 0
WAN_PROTO="$(uci -q get network.wan.proto 2>/dev/null)"
[ "$WAN_PROTO" = "pppoe" ] || exit 0
LOCK_FILE="/var/run/ssrplus-pppoe-hotplug.lock"
(
if ! mkdir "$LOCK_FILE" 2>/dev/null; then
exit 0
fi
trap 'rmdir "$LOCK_FILE" 2>/dev/null' EXIT INT TERM
sleep 10
/etc/init.d/shadowsocksr restart >/dev/null 2>&1
) >/dev/null 2>&1 &
exit 0
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,173 @@
server=/a1.mzstatic.com/114.114.114.114
server=/a2.mzstatic.com/114.114.114.114
server=/a3.mzstatic.com/114.114.114.114
server=/a4.mzstatic.com/114.114.114.114
server=/a5.mzstatic.com/114.114.114.114
server=/adcdownload.apple.com.akadns.net/114.114.114.114
server=/adcdownload.apple.com/114.114.114.114
server=/amp-api-updates.apps.apple.com/114.114.114.114
server=/amp-api.media.apple.com/114.114.114.114
server=/api-p-ap-c.smoot.apple.com/114.114.114.114
server=/api-p-ap-d.smoot.apple.com/114.114.114.114
server=/api-p-ap-e.smoot.apple.com/114.114.114.114
server=/app-site-association.cdn-apple.com/114.114.114.114
server=/appldnld.apple.com/114.114.114.114
server=/appldnld.g.aaplimg.com/114.114.114.114
server=/appleid.cdn-apple.com/114.114.114.114
server=/apps.apple.com/114.114.114.114
server=/apps.mzstatic.com/114.114.114.114
server=/bag-cdn.itunes-apple.com.akadns.net/114.114.114.114
server=/cdn-cn1.apple-mapkit.com/114.114.114.114
server=/cdn-cn2.apple-mapkit.com/114.114.114.114
server=/cdn-cn3.apple-mapkit.com/114.114.114.114
server=/cdn-cn4.apple-mapkit.com/114.114.114.114
server=/cdn.apple-mapkit.com/114.114.114.114
server=/cdn1.apple-mapkit.com/114.114.114.114
server=/cdn2.apple-mapkit.com/114.114.114.114
server=/cdn3.apple-mapkit.com/114.114.114.114
server=/cdn4.apple-mapkit.com/114.114.114.114
server=/cds-cdn.v.aaplimg.com/114.114.114.114
server=/cds.apple.com.akadns.net/114.114.114.114
server=/cds.apple.com/114.114.114.114
server=/cdsassets.apple.com/114.114.114.114
server=/cl1-cdn.origin-apple.com.akadns.net/114.114.114.114
server=/cl1.apple.com/114.114.114.114
server=/cl2-cn.apple.com/114.114.114.114
server=/cl2.apple.com/114.114.114.114
server=/cl3-cdn.origin-apple.com.akadns.net/114.114.114.114
server=/cl3.apple.com/114.114.114.114
server=/cl4-cdn.origin-apple.com.akadns.net/114.114.114.114
server=/cl4-cn.apple.com/114.114.114.114
server=/cl4.apple.com/114.114.114.114
server=/cl5-cdn.origin-apple.com.akadns.net/114.114.114.114
server=/cl5.apple.com/114.114.114.114
server=/clientflow.apple.com.akadns.net/114.114.114.114
server=/clientflow.apple.com/114.114.114.114
server=/cn-smp-paymentservices.apple.com/114.114.114.114
server=/configuration.apple.com.akadns.net/114.114.114.114
server=/configuration.apple.com/114.114.114.114
server=/crl.apple.com/114.114.114.114
server=/cstat.apple.com/114.114.114.114
server=/cstat.cdn-apple.com/114.114.114.114
server=/dd-cdn.origin-apple.com.akadns.net/114.114.114.114
server=/dejavu.apple.com/114.114.114.114
server=/devstreaming-cdn.apple.com/114.114.114.114
server=/download.developer.apple.com/114.114.114.114
server=/experiments.apple.com/114.114.114.114
server=/gs-loc-cn.apple.com/114.114.114.114
server=/gs-loc.apple.com/114.114.114.114
server=/gsp10-ssl-cn.ls.apple.com/114.114.114.114
server=/gsp12-cn.ls.apple.com/114.114.114.114
server=/gsp13-cn.ls.apple.com/114.114.114.114
server=/gsp4-cn.ls.apple.com.edgekey.net.globalredir.akadns.net/114.114.114.114
server=/gsp4-cn.ls.apple.com.edgekey.net/114.114.114.114
server=/gsp4-cn.ls.apple.com/114.114.114.114
server=/gsp5-cn.ls.apple.com/114.114.114.114
server=/gsp85-cn-ssl.ls.apple.com/114.114.114.114
server=/gspe19-2-cn-ssl.ls-apple.com.akadns.net/114.114.114.114
server=/gspe19-2-cn-ssl.ls.apple.com/114.114.114.114
server=/gspe19-cn-ssl.ls.apple.com/114.114.114.114
server=/gspe19-cn.ls-apple.com.akadns.net/114.114.114.114
server=/gspe19-cn.ls.apple.com/114.114.114.114
server=/gspe21-ssl.ls.apple.com/114.114.114.114
server=/gspe21.ls.apple.com/114.114.114.114
server=/gspe35-ssl.ls.apple.com/114.114.114.114
server=/gspe79-cn-ssl.ls.apple.com/114.114.114.114
server=/guzzoni-apple-com.v.aaplimg.com/114.114.114.114
server=/guzzoni.apple.com/114.114.114.114
server=/guzzoni.smoot.apple.com/114.114.114.114
server=/iadsdk.apple.com/114.114.114.114
server=/icloud-cdn.icloud.com.akadns.net/114.114.114.114
server=/icloud.cdn-apple.com/114.114.114.114
server=/images.apple.com.akadns.net/114.114.114.114
server=/images.apple.com.edgekey.net.globalredir.akadns.net/114.114.114.114
server=/images.apple.com/114.114.114.114
server=/init-kt.apple.com/114.114.114.114
server=/init-p01md-lb.push-apple.com.akadns.net/114.114.114.114
server=/init-p01md.apple.com/114.114.114.114
server=/init-p01st-lb.push-apple.com.akadns.net/114.114.114.114
server=/init-p01st.push.apple.com/114.114.114.114
server=/init-s01st-lb.push-apple.com.akadns.net/114.114.114.114
server=/init-s01st.push.apple.com/114.114.114.114
server=/init.ess.apple.com/114.114.114.114
server=/iosapps.itunes.g.aaplimg.com/114.114.114.114
server=/ipcdn.apple.com/114.114.114.114
server=/iphone-ld.apple.com/114.114.114.114
server=/iphone-ld.origin-apple.com.akadns.net/114.114.114.114
server=/is-ssl.mzstatic.com-cn-lb.itunes-apple.com.akadns.net/114.114.114.114
server=/is1-ssl.mzstatic.com/114.114.114.114
server=/is1.mzstatic.com/114.114.114.114
server=/is2-ssl.mzstatic.com/114.114.114.114
server=/is2.mzstatic.com/114.114.114.114
server=/is3-ssl.mzstatic.com/114.114.114.114
server=/is3.mzstatic.com/114.114.114.114
server=/is4-ssl.mzstatic.com/114.114.114.114
server=/is4.mzstatic.com/114.114.114.114
server=/is5-ssl.mzstatic.com/114.114.114.114
server=/is5.mzstatic.com/114.114.114.114
server=/itunes-apple.com.akadns.net/114.114.114.114
server=/itunes.apple.com/114.114.114.114
server=/itunesconnect.apple.com/114.114.114.114
server=/mesu-cdn.apple.com.akadns.net/114.114.114.114
server=/mesu-china.apple.com.akadns.net/114.114.114.114
server=/mesu.apple.com/114.114.114.114
server=/ml.cdn-apple.com/114.114.114.114
server=/music.apple.com/114.114.114.114
server=/ocsp-lb.apple.com.akadns.net/114.114.114.114
server=/ocsp.apple.com/114.114.114.114
server=/ocsp2-lb.apple.com.akadns.net/114.114.114.114
server=/ocsp2.apple.com/114.114.114.114
server=/oscdn.apple.com/114.114.114.114
server=/oscdn.origin-apple.com.akadns.net/114.114.114.114
server=/osxapps.itunes.g.aaplimg.com/114.114.114.114
server=/pancake.apple.com/114.114.114.114
server=/pancake.cdn-apple.com.akadns.net/114.114.114.114
server=/pba0.apple.com/114.114.114.114
server=/probe.siri.apple.com/114.114.114.114
server=/prod-support.apple-support.akadns.net/114.114.114.114
server=/publicassets.cdn-apple.com/114.114.114.114
server=/reserve-prime.apple.com/114.114.114.114
server=/s.mzstatic.com/114.114.114.114
server=/seed-sequoia.siri.apple.com/114.114.114.114
server=/seed-swallow.siri.apple.com/114.114.114.114
server=/seed.siri.apple.com/114.114.114.114
server=/sequoia.apple.com/114.114.114.114
server=/sh-pod2-smp-device.apple.com/114.114.114.114
server=/shazam-insights.cdn-apple.com/114.114.114.114
server=/smp-device-content.apple.com/114.114.114.114
server=/static.gc.apple.com/114.114.114.114
server=/stocks-sparkline-lb.apple.com.akadns.net/114.114.114.114
server=/stocks-sparkline.apple.com/114.114.114.114
server=/store.apple.com.edgekey.net.globalredir.akadns.net/114.114.114.114
server=/store.apple.com.edgekey.net/114.114.114.114
server=/store.apple.com/114.114.114.114
server=/store.storeimages.apple.com.akadns.net/114.114.114.114
server=/store.storeimages.cdn-apple.com/114.114.114.114
server=/support-china.apple-support.akadns.net/114.114.114.114
server=/support.apple.com/114.114.114.114
server=/swallow-apple-com.v.aaplimg.com/114.114.114.114
server=/swallow.apple.com/114.114.114.114
server=/swcatalog-cdn.apple.com.akadns.net/114.114.114.114
server=/swcatalog.apple.com/114.114.114.114
server=/swcdn.apple.com/114.114.114.114
server=/swcdn.g.aaplimg.com/114.114.114.114
server=/swdist.apple.com.akadns.net/114.114.114.114
server=/swdist.apple.com/114.114.114.114
server=/swscan-cdn.apple.com.akadns.net/114.114.114.114
server=/swscan.apple.com/114.114.114.114
server=/sylvan.apple.com/114.114.114.114
server=/tj-pod1-smp-device.apple.com/114.114.114.114
server=/updates-http.cdn-apple.com.akadns.net/114.114.114.114
server=/updates-http.cdn-apple.com/114.114.114.114
server=/updates.cdn-apple.com/114.114.114.114
server=/valid.apple.com/114.114.114.114
server=/valid.origin-apple.com.akadns.net/114.114.114.114
server=/weather-data.apple.com.akadns.net/114.114.114.114
server=/weather-data.apple.com/114.114.114.114
server=/weather-map.apple.com/114.114.114.114
server=/weather-map2.apple.com/114.114.114.114
server=/weatherkit.apple.com/114.114.114.114
server=/www.apple.com.edgekey.net.globalredir.akadns.net/114.114.114.114
server=/www.apple.com.edgekey.net/114.114.114.114
server=/www.apple.com/114.114.114.114
server=/xp.apple.com/114.114.114.114
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,14 @@
gvod.aiseejapp.atianqi.com
stat.pandora.xiaomi.com
upgrade.mishop.pandora.xiaomi.com
logonext.tv.kuyun.com
config.kuyun.com
mishop.pandora.xiaomi.com
dvb.pandora.xiaomi.com
api.ad.xiaomi.com
de.pandora.xiaomi.com
data.mistat.xiaomi.com
jellyfish.pandora.xiaomi.com
gallery.pandora.xiaomi.com
o2o.api.xiaomi.com
bss.pandora.xiaomi.com
@@ -0,0 +1,17 @@
# cloudflare-dns.com
sdns://AgcAAAAAAAAADjEwNC4xNi4yNDkuMjQ5ABJjbG91ZGZsYXJlLWRucy5jb20KL2Rucy1xdWVyeQ
# Google
sdns://AgUAAAAAAAAABzguOC40LjQgsKKKE4EwvtIbNjGjagI2607EdKSVHowYZtyvD9iPrkkHOC44LjQuNAovZG5zLXF1ZXJ5
# dns.sb
sdns://AgcAAAAAAAAADzE4NS4yMjIuMjIyLjIyMiAOp5Svj-oV-Fz-65-8H2VKHLKJ0egmfEgrdPeAQlUFFA8xODUuMjIyLjIyMi4yMjIKL2Rucy1xdWVyeQ
# Quad9
sdns://AgMAAAAAAAAADTE0OS4xMTIuMTEyLjkgsBkgdEu7dsmrBT4B4Ht-BQ5HPSD3n3vqQ1-v5DydJC8SZG5zOS5xdWFkOS5uZXQ6NDQzCi9kbnMtcXVlcnk
sdns://AQMAAAAAAAAADDkuOS45Ljk6ODQ0MyBnyEe4yHWM0SAkVUO-dWdG3zTfHYTAC4xHA2jfgh2GPhkyLmRuc2NyeXB0LWNlcnQucXVhZDkubmV0
# AdGuard
sdns://AQMAAAAAAAAAETk0LjE0MC4xNC4xNDo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20
# Cloudflare
sdns://AgcAAAAAAAAABzEuMC4wLjGgENk8mGSlIfMGXMOlIlCcKvq7AVgcrZxtjon911-ep0cg63Ul-I8NlFj4GplQGb_TTLiczclX57DvMV8Q-JdjgRgSZG5zLmNsb3VkZmxhcmUuY29tCi9kbnMtcXVlcnk
# TWNIC-101
sdns://AgcAAAAAAAAAACC2vD25TAYM7EnyCH8Xw1-0g5OccnTsGH9vQUUH0njRtAxkbnMudHduaWMudHcKL2Rucy1xdWVyeQ
# cs-tx
sdns://AQYAAAAAAAAADTIwOS41OC4xNDcuMzYgMTNyrVlWMsJBa4cvCY-FG925ZShMbL6aTxkJZDDbqVoeMi5kbnNjcnlwdC1jZXJ0LmNyeXB0b3N0b3JtLmlz
@@ -0,0 +1,110 @@
server=/91smartyun.pt/127.0.0.1#5335
ipset=/91smartyun.pt/gfwlist
server=/adobe.com/127.0.0.1#5335
ipset=/adobe.com/gfwlist
server=/amazonaws.com/127.0.0.1#5335
ipset=/amazonaws.com/gfwlist
server=/ampproject.org/127.0.0.1#5335
ipset=/ampproject.org/gfwlist
server=/apple.news/127.0.0.1#5335
ipset=/apple.news/gfwlist
server=/aws.amazon.com/127.0.0.1#5335
ipset=/aws.amazon.com/gfwlist
server=/azureedge.net/127.0.0.1#5335
ipset=/azureedge.net/gfwlist
server=/backpackers.com.tw/127.0.0.1#5335
ipset=/backpackers.com.tw/gfwlist
server=/bitfinex.com/127.0.0.1#5335
ipset=/bitfinex.com/gfwlist
server=/buzzfeed.com/127.0.0.1#5335
ipset=/buzzfeed.com/gfwlist
server=/clockwise.ee/127.0.0.1#5335
ipset=/clockwise.ee/gfwlist
server=/cloudfront.net/127.0.0.1#5335
ipset=/cloudfront.net/gfwlist
server=/coindesk.com/127.0.0.1#5335
ipset=/coindesk.com/gfwlist
server=/coinsquare.io/127.0.0.1#5335
ipset=/coinsquare.io/gfwlist
server=/cryptocompare.com/127.0.0.1#5335
ipset=/cryptocompare.com/gfwlist
server=/dropboxstatic.com/127.0.0.1#5335
ipset=/dropboxstatic.com/gfwlist
server=/eurecom.fr/127.0.0.1#5335
ipset=/eurecom.fr/gfwlist
server=/gdax.com/127.0.0.1#5335
ipset=/gdax.com/gfwlist
server=/github.com/127.0.0.1#5335
ipset=/github.com/gfwlist
server=/kknews.cc/127.0.0.1#5335
ipset=/kknews.cc/gfwlist
server=/nutaq.com/127.0.0.1#5335
ipset=/nutaq.com/gfwlist
server=/openairinterface.org/127.0.0.1#5335
ipset=/openairinterface.org/gfwlist
server=/skype.com/127.0.0.1#5335
ipset=/skype.com/gfwlist
server=/sublimetext.com/127.0.0.1#5335
ipset=/sublimetext.com/gfwlist
server=/textnow.com/127.0.0.1#5335
ipset=/textnow.com/gfwlist
server=/textnow.me/127.0.0.1#5335
ipset=/textnow.me/gfwlist
server=/trouter.io/127.0.0.1#5335
ipset=/trouter.io/gfwlist
server=/t66y.com/127.0.0.1#5335
ipset=/t66y.com/gfwlist
server=/uploaded.net/127.0.0.1#5335
ipset=/uploaded.net/gfwlist
server=/v2rayssr.com/127.0.0.1#5335
ipset=/v2rayssr.com/gfwlist
server=/whatsapp.com/127.0.0.1#5335
ipset=/whatsapp.com/gfwlist
server=/whatsapp.net/127.0.0.1#5335
ipset=/whatsapp.net/gfwlist
server=/wsj.net/127.0.0.1#5335
ipset=/wsj.net/gfwlist
server=/google.com/127.0.0.1#5335
ipset=/google.com/gfwlist
server=/google.com.hk/127.0.0.1#5335
ipset=/google.com.hk/gfwlist
server=/gstatic.com/127.0.0.1#5335
ipset=/gstatic.com/gfwlist
server=/googleusercontent.com/127.0.0.1#5335
ipset=/googleusercontent.com/gfwlist
server=/googlepages.com/127.0.0.1#5335
ipset=/googlepages.com/gfwlist
server=/googlevideo.com/127.0.0.1#5335
ipset=/googlevideo.com/gfwlist
server=/googlecode.com/127.0.0.1#5335
ipset=/googlecode.com/gfwlist
server=/googleapis.com/127.0.0.1#5335
ipset=/googleapis.com/gfwlist
server=/googlesource.com/127.0.0.1#5335
ipset=/googlesource.com/gfwlist
server=/googledrive.com/127.0.0.1#5335
ipset=/googledrive.com/gfwlist
server=/ggpht.com/127.0.0.1#5335
ipset=/ggpht.com/gfwlist
server=/youtube.com/127.0.0.1#5335
ipset=/youtube.com/gfwlist
server=/youtu.be/127.0.0.1#5335
ipset=/youtu.be/gfwlist
server=/ytimg.com/127.0.0.1#5335
ipset=/ytimg.com/gfwlist
server=/twitter.com/127.0.0.1#5335
ipset=/twitter.com/gfwlist
server=/facebook.com/127.0.0.1#5335
ipset=/facebook.com/gfwlist
server=/fastly.net/127.0.0.1#5335
ipset=/fastly.net/gfwlist
server=/akamai.net/127.0.0.1#5335
ipset=/akamai.net/gfwlist
server=/akamaiedge.net/127.0.0.1#5335
ipset=/akamaiedge.net/gfwlist
server=/akamaihd.net/127.0.0.1#5335
ipset=/akamaihd.net/gfwlist
server=/edgesuite.net/127.0.0.1#5335
ipset=/edgesuite.net/gfwlist
server=/edgekey.net/127.0.0.1#5335
ipset=/edgekey.net/gfwlist
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,43 @@
log:
level: info
plugins:
- tag: lazy_cache
type: cache
args:
size: 8000
lazy_cache_ttl: 86400
- tag: forward_google
type: forward
args:
concurrent: 2
upstreams:
- tag: main_sequence_disable_IPv6
type: sequence
args:
- exec: $lazy_cache
- exec: prefer_ipv4
- exec: $forward_google
- matches:
- qtype 28 65
exec: reject 0
- tag: main_sequence_with_IPv6
type: sequence
args:
- exec: $lazy_cache
- exec: $forward_google
- tag: udp_server
type: udp_server
args:
entry: DNS_MODE
listen: 0.0.0.0:DNS_PORT
- tag: tcp_server
type: tcp_server
args:
entry: DNS_MODE
listen: 0.0.0.0:DNS_PORT
@@ -0,0 +1,30 @@
8.41.4.0/24
23.23.189.144/28
23.246.0.0/18
34.195.253.0/25
34.210.42.111/32
37.77.184.0/21
38.72.126.0/24
45.57.0.0/17
52.24.178.0/24
52.35.140.0/24
52.89.124.203/32
54.148.37.5/32
54.204.25.0/28
54.213.167.0/24
64.120.128.0/17
66.197.128.0/17
69.53.224.0/19
103.87.204.0/22
108.175.32.0/20
185.2.220.0/22
185.9.188.0/22
192.173.64.0/18
198.38.96.0/19
198.45.48.0/20
203.75.84.0/24
203.198.13.0/24
203.198.80.0/24
207.45.72.0/22
208.75.76.0/22
210.0.153.0/24
@@ -0,0 +1,7 @@
bilibili.com
bilibili.cn
bilivideo.com
bilivideo.cn
biliapi.com
biliapi.net
apple.com
+137
View File
@@ -0,0 +1,137 @@
#!/bin/sh
if [ -e "/etc/config/ucitrack" ]; then
uci -q batch <<-EOF
delete ucitrack.@shadowsocksr[-1]
add ucitrack shadowsocksr
set ucitrack.@shadowsocksr[-1].init=shadowsocksr
commit ucitrack
EOF
fi
uci -q batch <<-EOF
delete firewall.shadowsocksr
set firewall.shadowsocksr=include
set firewall.shadowsocksr.type=script
set firewall.shadowsocksr.path=/var/etc/shadowsocksr.include
$(command -v fw4 >/dev/null 2>&1 || echo "set firewall.shadowsocksr.reload=1")
commit firewall
EOF
rm -rf /etc/config/shadowsocksr-opkg /etc/ssrplus/*opkg
touch /etc/ssrplus/china_ssr.txt
touch /etc/ssrplus/deny.list
touch /etc/ssrplus/white.list
touch /etc/ssrplus/black.list
touch /etc/ssrplus/gfw_base.conf
touch /etc/ssrplus/gfw_list.conf
touch /etc/ssrplus/ad.conf
touch /etc/config/shadowsocksr
if [ -s "/etc/config/shadowsocksr" ]; then
if uci -q get shadowsocksr.@server_subscribe[0].auto_update_time > /dev/null; then
uci -q delete shadowsocksr.@server_subscribe[0].auto_update_time
fi
if ! uci -q get shadowsocksr.@server_subscribe[0].auto_update_week_time > /dev/null; then
uci -q set shadowsocksr.@server_subscribe[0].auto_update_week_time='*'
uci -q set shadowsocksr.@server_subscribe[0].auto_update_day_time='2'
uci -q set shadowsocksr.@server_subscribe[0].auto_update_min_time='0'
fi
if ! uci -q get shadowsocksr.@server_subscribe[0].config_auto_update_mode > /dev/null; then
uci -q set shadowsocksr.@server_subscribe[0].config_auto_update_mode='0'
fi
if ! uci -q get shadowsocksr.@server_subscribe[0].config_update_interval > /dev/null; then
uci -q set shadowsocksr.@server_subscribe[0].config_update_interval='60'
fi
if ! uci -q get shadowsocksr.@server_subscribe[0].user_agent > /dev/null; then
uci -q set shadowsocksr.@server_subscribe[0].user_agent='v2rayN/9.99'
fi
if ! uci -q get shadowsocksr.@server_subscribe[0].proxy > /dev/null; then
uci -q set shadowsocksr.@server_subscribe[0].proxy='1'
fi
if ! uci -q get shadowsocksr.@global_xray_fragment[0] > /dev/null; then
uci -q add shadowsocksr global_xray_fragment
uci -q set shadowsocksr.@global_xray_fragment[0].fragment='0'
uci -q set shadowsocksr.@global_xray_fragment[0].noise='0'
fi
if ! uci -q get shadowsocksr.@global[0].component_mirror > /dev/null; then
uci -q set shadowsocksr.@global[0].component_mirror='direct'
fi
if ! uci -q get shadowsocksr.@global[0].filter_aaaa > /dev/null; then
legacy_filter_aaaa="$(uci -q get shadowsocksr.@global[0].mosdns_ipv6)"
uci -q set shadowsocksr.@global[0].filter_aaaa="${legacy_filter_aaaa:-1}"
fi
legacy_dns_mode="$(uci -q get shadowsocksr.@global[0].pdnsd_enable)"
if [ "$legacy_dns_mode" = "2" ] || [ "$legacy_dns_mode" = "3" ]; then
uci -q set shadowsocksr.@global[0].pdnsd_enable='1'
fi
if uci -q get shadowsocksr.@global[0].mosdns_ipv6 > /dev/null; then
uci -q delete shadowsocksr.@global[0].mosdns_ipv6
fi
if ! uci -q get shadowsocksr.@http_proxy[0] > /dev/null; then
uci -q add shadowsocksr http_proxy
uci -q set shadowsocksr.@http_proxy[0].server='nil'
uci -q set shadowsocksr.@http_proxy[0].local_port='3128'
uci -q set shadowsocksr.@http_proxy[0].http_auth='none'
fi
ss_rust_bin="$(command -v ssserver 2>/dev/null)"
[ -n "$ss_rust_bin" ] || [ ! -x /usr/libexec/ssserver ] || ss_rust_bin="/usr/libexec/ssserver"
mihomo_bin="$(command -v mihomo 2>/dev/null)"
[ -n "$mihomo_bin" ] || [ ! -x /usr/libexec/mihomo ] || mihomo_bin="/usr/libexec/mihomo"
xray_bin="$(command -v xray 2>/dev/null)"
[ -n "$xray_bin" ] || [ ! -x /usr/libexec/xray ] || xray_bin="/usr/libexec/xray"
for section in $(uci -q show shadowsocksr | sed -n "s/^shadowsocksr\\.\\([^.=][^.=]*\\)=servers$/\\1/p"); do
node_type="$(uci -q get shadowsocksr.${section}.type)"
case "$node_type" in
ss|ss-libev)
if [ -n "$mihomo_bin" ]; then
uci -q set shadowsocksr.${section}.type='ss'
elif [ -n "$ss_rust_bin" ]; then
uci -q set shadowsocksr.${section}.type='ss-rust'
elif [ -n "$xray_bin" ]; then
uci -q set shadowsocksr.${section}.type='v2ray'
uci -q set shadowsocksr.${section}.v2ray_protocol='shadowsocks'
fi
;;
v2ray)
if [ "$(uci -q get shadowsocksr.${section}.v2ray_protocol)" = "shadowsocks" ] && [ -n "$mihomo_bin" ]; then
uci -q set shadowsocksr.${section}.type='ss'
uci -q delete shadowsocksr.${section}.v2ray_protocol
fi
;;
esac
done
uci -q commit shadowsocksr
fi
[ -s "/etc/config/shadowsocksr" ] || /etc/init.d/shadowsocksr reset
sed -i "s/option type 'vmess'/option type 'v2ray'\n\toption v2ray_protocol 'vmess'/g" /etc/config/shadowsocksr
sed -i "s/option type 'vless'/option type 'v2ray'\n\toption v2ray_protocol 'vless'/g" /etc/config/shadowsocksr
sed -i "s/option encrypt_method_v2ray_ss/option encrypt_method_ss/g" /etc/config/shadowsocksr
sed -i "s/option xtls/option tls/g" /etc/config/shadowsocksr
sed -i "/option vless_flow/d" /etc/config/shadowsocksr
sed -i "/option fingerprint 'disable'/d" /etc/config/shadowsocksr
if [ -s "/etc/uwsgi/vassals/luci-webui.ini" ];then
limit=$(cat /etc/uwsgi/vassals/luci-webui.ini | grep -Eo "limit-as.*"|grep -Eo "[0-9]+")
[ $limit -lt 5000 ] && sed -i '/limit-as/c\limit-as = 5000' /etc/uwsgi/vassals/luci-webui.ini && \
/etc/init.d/uwsgi restart
fi
rm -f /tmp/luci-indexcache /tmp/luci-indexcache.*
rm -rf /tmp/luci-modulecache/
exit 0
@@ -0,0 +1,11 @@
--- luci-app-ssr-plus/root/etc/uci-defaults/luci-ssr-plus
+++ luci-app-ssr-plus/root/etc/uci-defaults/luci-ssr-plus
@@ -43,5 +43,8 @@ if [ -s "/etc/uwsgi/vassals/luci-webui.ini" ];then
/etc/init.d/uwsgi restart
fi
+grep -q ip-api.com /etc/ssrplus/black.list ||
+ sed -i '$a ip-api.com' /etc/ssrplus/black.list
+
rm -rf /tmp/luci-modulecache /tmp/luci-indexcache
exit 0
+225
View File
@@ -0,0 +1,225 @@
#!/bin/sh
#
# Copyright (C) 2017 openwrt-ssr
# Copyright (C) 2017 yushi studio <ywb94@qq.com>
#
# This is free software, licensed under the GNU General Public License v3.
# See /LICENSE for more information.
#
. $IPKG_INSTROOT/etc/init.d/shadowsocksr
LOCK_FILE="/var/lock/ssr-monitor.lock"
[ -f "$LOCK_FILE" ] && exit 2
touch "$LOCK_FILE"
ps_list() {
if busybox ps -w >/dev/null 2>&1; then
busybox ps -w
else
busybox ps
fi
}
match_proc_count() {
local pattern="$1"
ps_list | grep -E "$pattern" | grep -v grep | wc -l
}
redir_tcp_running() {
local ssr_count
local ipt_count
ssr_count=$(match_proc_count 'ssr-retcp')
ipt_count=$(ps_list | grep 'ipt2socks' | grep -v grep | grep -E ' -T( |$)| --tcp-only( |$)' | wc -l)
echo $((ssr_count + ipt_count))
}
redir_udp_running() {
local ssr_count
local ipt_count
ssr_count=$(match_proc_count 'ssr-reudp')
ipt_count=$(ps_list | grep 'ipt2socks' | grep -v grep | grep -E ' -U( |$)| --udp-only( |$)' | wc -l)
echo $((ssr_count + ipt_count))
}
kill_matched_procs() {
local pattern="$1"
ps_list | grep -E "$pattern" | grep -v grep | awk '{print $1}' | xargs kill -9 >/dev/null 2>&1
}
get_expected_threads() {
local threads
threads=$(uci_get_by_type global threads 0)
if [ "$threads" = "0" ] || [ -z "$threads" ]; then
threads=$(grep -c '^processor' /proc/cpuinfo 2>/dev/null)
fi
case "$threads" in
''|*[!0-9]*)
threads=1
;;
esac
[ "$threads" -lt 1 ] && threads=1
echo "$threads"
}
server_process_count=$1
redir_tcp_process=$2
redir_udp_process=$3
kcp_process=$4
local_process=$5
pdnsd_process=$6
if [ -z "$pdnsd_process" ]; then
pdnsd_process=0
fi
i=0
GLOBAL_SERVER=$(uci_get_by_type global global_server)
GLOBAL_TYPE=$(uci_get_by_name $GLOBAL_SERVER type)
EXPECTED_THREADS=$(get_expected_threads)
server=$(uci_get_by_name $GLOBAL_SERVER server)
kcp_port=$(uci_get_by_name $GLOBAL_SERVER kcp_port)
server_port=$(uci_get_by_name $GLOBAL_SERVER server_port)
password=$(uci_get_by_name $GLOBAL_SERVER kcp_password)
kcp_param=$(uci_get_by_name $GLOBAL_SERVER kcp_param)
[ "$password" != "" ] && password="--key "${password}
while [ "1" == "1" ]; do #死循环
sleep 000030s
#redir tcp
if [ "$redir_tcp_process" -gt 0 ]; then
icount=$(redir_tcp_running)
local tcp_expected="$redir_tcp_process"
if [ "$GLOBAL_TYPE" = "socks5" ]; then
tcp_expected="$EXPECTED_THREADS"
fi
if [ "$icount" -lt "$tcp_expected" ]; then
logger -t "$NAME" "ssrplus redir tcp error.restart!"
echolog "ssrplus redir tcp error.restart!"
/etc/init.d/shadowsocksr restart
exit 0
fi
fi
#redir udp
if [ "$redir_udp_process" -gt 0 ]; then
icount=$(redir_udp_running)
local udp_expected="$redir_udp_process"
if [ "$GLOBAL_TYPE" = "socks5" ]; then
udp_expected="$EXPECTED_THREADS"
fi
if [ "$icount" -lt "$udp_expected" ]; then
logger -t "$NAME" "ssrplus redir udp error.restart!"
echolog "ssrplus redir udp error.restart!"
/etc/init.d/shadowsocksr restart
exit 0
fi
fi
#server
if [ "$server_process_count" -gt 0 ]; then
icount=$(match_proc_count 'ssr-server|ss-server-|xray-server-')
if [ "$icount" -lt "$server_process_count" ]; then #如果进程挂掉就重启它
logger -t "$NAME" "ssrplus server error.restart!"
echolog "ssrplus server error.restart!"
kill_matched_procs 'ssr-server|ss-server-'
/etc/init.d/shadowsocksr restart
exit 0
fi
fi
#kcptun
if [ "$kcp_process" -gt 0 ]; then
icount=$(ps_list | grep kcptun-client | grep -v grep | wc -l)
if [ "$icount" -lt "$kcp_process" ]; then #如果进程挂掉就重启它
logger -t "$NAME" "ssrplus kcptun error.restart!"
echolog "ssrplus kcptun error.restart!"
killall -q -9 kcptun-client
(/usr/bin/kcptun-client -r $server:$kcp_port -l :$server_port $password $kcp_param &)
fi
fi
#localsocks
if [ "$local_process" -gt 0 ]; then
icount=$(match_proc_count 'ssr-local|ss-local|tuic-local')
if [ "$icount" -lt "$local_process" ]; then #如果进程挂掉就重启它
logger -t "$NAME" "global socks server error.restart!"
echolog "global socks server error.restart!"
kill_matched_procs 'ssr-local|ss-local|tuic-local'
/etc/init.d/shadowsocksr restart
exit 0
fi
fi
#dns2tcp
if [ "$pdnsd_process" -eq 1 ]; then
icount=$(ps_list | grep $TMP_BIN_PATH/dns2tcp | grep -v grep | wc -l)
if [ "$icount" -lt 1 ]; then #如果进程挂掉就重启它
logger -t "$NAME" "dns2tcp tunnel error.restart!"
echolog "dns2tcp tunnel error.restart!"
dnsserver=$(uci_get_by_type global tunnel_forward 8.8.4.4:53)
kill -9 $(ps_list | grep $TMP_BIN_PATH/dns2tcp | grep -v grep | awk '{print $1}') >/dev/null 2>&1
start_dns2tcp "$dnsserver"
fi
#mosdns
elif [ "$pdnsd_process" -eq 4 ]; then
icount=$(ps_list | grep $TMP_BIN_PATH/mosdns | grep -v grep | wc -l)
if [ "$icount" -lt 1 ]; then #如果进程挂掉就重启它
logger -t "$NAME" "mosdns tunnel error.restart!"
echolog "mosdns tunnel error.restart!"
dnsserver=$(uci_get_by_type global tunnel_forward 8.8.4.4:53)
kill -9 $(ps_list | grep $TMP_BIN_PATH/mosdns | grep -v grep | awk '{print $1}') >/dev/null 2>&1
ln_start_bin $(first_type mosdns) mosdns start -c /etc/mosdns/config.yaml
fi
#chinadns-ng(proxy)
elif [ "$pdnsd_process" -eq 6 ]; then
icount=$(ps_list | grep -e ssrplus-dns -e "chinadns-ng -b 127.0.0.1 -l $tmp_dns_port" | grep -v grep | wc -l)
if [ "$icount" -lt 1 ]; then #如果进程挂掉就重启它
logger -t "$NAME" "chinadns-ng $dnsserver tunnel error.restart!"
echolog "chinadns-ng $dnsserver tunnel error.restart!"
dnsserver=$(uci_get_by_type global chinadns_ng_tunnel_forward 8.8.4.4:53)
kill -9 $(ps_list | grep "chinadns-ng -b 127.0.0.1 -l $tmp_dns_port" | grep -v grep | awk '{print $1}') >/dev/null 2>&1
local chinadns_ng_proto="$(uci_get_by_type global chinadns_ng_proto)"
local chinadns_ng_dns=""
IFS=','
for chinadns_ng_server in $dnsserver; do
local chinadns_ng_ip="${chinadns_ng_server%%:*}"
local chinadns_ng_port="${chinadns_ng_server##*:}"
[ "$chinadns_ng_ip" = "$chinadns_ng_port" ] && chinadns_ng_port="53"
chinadns_ng_tls_port="853"
case "$chinadns_ng_proto" in
"none")
chinadns_ng_server="${chinadns_ng_ip}#${chinadns_ng_port}"
;;
"tls")
chinadns_ng_server="${chinadns_ng_proto}://${chinadns_ng_ip}#${chinadns_ng_tls_port}"
;;
*)
chinadns_ng_server="${chinadns_ng_proto}://${chinadns_ng_ip}#${chinadns_ng_port}"
;;
esac
chinadns_ng_dns="${chinadns_ng_dns} -t ${chinadns_ng_server}"
done
unset IFS
dnsserver="$chinadns_ng_dns"
ln_start_bin $(first_type chinadns-ng) chinadns-ng -b 127.0.0.1 -l $tmp_dns_port -l $dns_port -p 3 -d gfw $dnsserver -N --filter-qtype 64,65 -f -r --cache 4096 --cache-stale 86400 --cache-refresh 20
fi
#built-in dns
elif [ "$pdnsd_process" -eq 7 ]; then
if ! is_builtin_dns_active; then
logger -t "$NAME" "builtin dns error.restart!"
echolog "builtin dns error.restart!"
/etc/init.d/shadowsocksr restart
exit 0
fi
fi
#chinadns-ng(china)
if [ "$(uci -q get "dhcp.@dnsmasq[0]._unused_ssrp_changed")" = "1" ]; then
icount=$(ps_list | grep $TMP_BIN_PATH/chinadns-ng | grep -v grep | wc -l)
if [ "$icount" -lt 1 ]; then #如果进程挂掉就重启它
logger -t "$NAME" "chinadns-ng tunnel error.restart!"
echolog "chinadns-ng tunnel error.restart!"
chinadns=$(uci_get_by_type global chinadns_forward)
wandns="$(ifstatus wan | jsonfilter -e '@["dns-server"][0]' || echo "119.29.29.29")"
case "$chinadns" in
"wan") chinadns="$wandns" ;;
""|"wan_114") chinadns="$wandns,114.114.114.114" ;;
esac
kill -9 $(ps_list | grep $TMP_BIN_PATH/chinadns-ng | grep -v grep | awk '{print $1}') >/dev/null 2>&1
ln_start_bin $(first_type chinadns-ng) chinadns-ng -l $china_dns_port -4 china -p 3 -c ${chinadns/:/#} -t 127.0.0.1#$dns_port -N -f -r
fi
fi
done
File diff suppressed because it is too large Load Diff
+282
View File
@@ -0,0 +1,282 @@
#!/bin/sh
#
# Copyright (C) 2017 openwrt-ssr
# Copyright (C) 2017 yushi studio <ywb94@qq.com>
#
# This is free software, licensed under the GNU General Public License v3.
# See /LICENSE for more information.
#
ORIG_ARG1="${1:-}"
ORIG_ARG2="${2:-}"
ORIG_ARG3="${3:-}"
[ -f /lib/functions.sh ] && . /lib/functions.sh
. $IPKG_INSTROOT/etc/init.d/shadowsocksr
LOCK_FILE="/var/lock/ssr-switch.lock"
PROBE_SID=""
cycle_time=60
switch_time=3
probe_socks_port=10800
direct_fail_count=0
direct_fail_limit=3
probe_url="$(uci_get_by_type server_subscribe url_test_url)"
[ -n "$probe_url" ] || probe_url="https://www.google.com/generate_204"
PROBE_INSTANCE_KEY="ssr-switch-probe-local"
DEFAULT_SERVER="$(uci_get_by_type global global_server)"
CURRENT_SERVER="$DEFAULT_SERVER"
ENABLE_SERVER="nil"
cleanup_probe_socks() {
ps_list | grep -v "grep" | grep -E "tcp-http-ssr-switch-local\\.json|ss-${PROBE_INSTANCE_KEY}/config\\.yaml|tuic-${PROBE_INSTANCE_KEY}/config\\.yaml" | awk '{print $1}' | xargs kill >/dev/null 2>&1
sleep 1
ps_list | grep -v "grep" | grep -E "tcp-http-ssr-switch-local\\.json|ss-${PROBE_INSTANCE_KEY}/config\\.yaml|tuic-${PROBE_INSTANCE_KEY}/config\\.yaml" | awk '{print $1}' | xargs kill -9 >/dev/null 2>&1
rm -f "$TMP_PATH/tcp-http-ssr-switch-local.json" 2>/dev/null
rm -rf "$TMP_PATH/ss-${PROBE_INSTANCE_KEY}" "$TMP_PATH/tuic-${PROBE_INSTANCE_KEY}" 2>/dev/null
rm -f /tmp/ssr-switch-probe.log 2>/dev/null
}
start_probe_socks() {
local sid="$1"
local server_type
server_type="$(uci_get_by_name "$sid" type)"
[ "$server_type" = "clash" ] && return 1
[ "$server_type" = "tuic" ] && return 1
[ "$server_type" = "socks5" ] && return 1
cleanup_probe_socks
LOCAL_SERVER="$sid"
local_config_file="$TMP_PATH/tcp-http-ssr-switch-local.json"
_local="2"
local_enable=0
tmp_local_port=
mode="tcp,udp"
if [ "$server_type" = "v2ray" ]; then
export SSR_SWITCH_PROBE=1
gen_config_file "$LOCAL_SERVER" "$server_type" 4 0 "$probe_socks_port" >/tmp/ssr-switch-probe.log 2>&1 || return 1
unset SSR_SWITCH_PROBE
local xray_bin
xray_bin="$(first_type xray)"
[ -x "$xray_bin" ] || return 1
"$xray_bin" run -c "$local_config_file" >>/tmp/ssr-switch-probe.log 2>&1 &
else
export SSR_SWITCH_PROBE=1
export SSR_SWITCH_PROBE_INSTANCE_KEY="$PROBE_INSTANCE_KEY"
start_local_with_port "$probe_socks_port" >/tmp/ssr-switch-probe.log 2>&1 || return 1
unset SSR_SWITCH_PROBE_INSTANCE_KEY
unset SSR_SWITCH_PROBE
fi
sleep 2
return 0
}
probe_http_ok() {
case "$1" in
[1-5][0-9][0-9])
return 0
;;
esac
return 1
}
check_direct_probe() {
local code
code="$(curl -k -sS -L -o /dev/null \
--connect-timeout "$switch_time" \
--max-time "$switch_time" \
-w '%{http_code}' \
"$probe_url" 2>/dev/null || true)"
probe_http_ok "$code"
}
check_proxy_via_socks() {
local try_count
local code
local proxy_host
try_count="$(uci_get_by_type global switch_try_count 3)"
for i in $(seq 1 "$try_count"); do
for proxy_host in "127.0.0.1:$probe_socks_port" "[::1]:$probe_socks_port"; do
code="$(curl -k -sS -L -o /dev/null \
--connect-timeout "$switch_time" \
--max-time "$switch_time" \
--socks5-hostname "$proxy_host" \
-w '%{http_code}' \
"$probe_url" 2>/dev/null || true)"
probe_http_ok "$code" && return 0
done
sleep 1
done
return 1
}
test_proxy() {
local sid="$1"
[ "$(uci_get_by_name "$sid" type)" = "clash" ] && return 1
[ "$(uci_get_by_name "$sid" type)" = "tuic" ] && return 1
start_probe_socks "$sid" || {
cleanup_probe_socks
return 1
}
check_proxy_via_socks
local ret=$?
cleanup_probe_socks
return $ret
}
search_proxy() {
local sid="$1"
[ "$(uci_get_by_name "$sid" switch_enable 0)" = "1" ] || return 1
[ "$(uci_get_by_name "$sid" type)" = "clash" ] && return 1
[ "$(uci_get_by_name "$sid" type)" = "tuic" ] && return 1
[ "$sid" = "$CURRENT_SERVER" ] && return 1
[ "$ENABLE_SERVER" != "nil" ] && return 0
if test_proxy "$sid"; then
ENABLE_SERVER="$sid"
return 0
fi
return 1
}
select_proxy() {
config_load "$NAME"
ENABLE_SERVER="nil"
config_foreach search_proxy servers
}
set_main_server() {
local sid="$1"
[ -n "$sid" ] || return 1
uci set shadowsocksr.@global[0].global_server="$sid"
uci commit shadowsocksr
return 0
}
switch_proxy() {
set_main_server "$1" || return 1
/etc/init.d/shadowsocksr restart "$1"
return 0
}
start() {
while [ "1" = "1" ]; do
sleep "0000$cycle_time"
run_once
done
}
run_once() {
DEFAULT_SERVER="$(uci_get_by_type global global_server)"
[ "$DEFAULT_SERVER" = "nil" ] && {
direct_fail_count=0
return 0
}
[ "$(uci_get_by_name "$DEFAULT_SERVER" type)" = "clash" ] && {
direct_fail_count=0
cleanup_probe_socks
return 0
}
[ "$(uci_get_by_name "$DEFAULT_SERVER" type)" = "tuic" ] && {
direct_fail_count=0
cleanup_probe_socks
return 0
}
[ "$(uci_get_by_name "$DEFAULT_SERVER" kcp_enable 0)" = "1" ] && {
direct_fail_count=0
cleanup_probe_socks
return 1
}
CURRENT_SERVER="$DEFAULT_SERVER"
if check_direct_probe; then
direct_fail_count=0
return 0
fi
direct_fail_count=$((direct_fail_count + 1))
[ "$direct_fail_count" -lt "$direct_fail_limit" ] && return 0
direct_fail_count=0
if test_proxy "$CURRENT_SERVER"; then
return 0
fi
echolog "Current server error, try to switch another server."
select_proxy
if [ "$ENABLE_SERVER" != "nil" ]; then
CURRENT_SERVER="$ENABLE_SERVER"
switch_proxy "$CURRENT_SERVER"
echolog "Switch to $(uci_get_by_name "$CURRENT_SERVER" alias) proxy!"
else
switch_proxy "$CURRENT_SERVER"
echolog "Try restart current server."
fi
return 0
}
probe_once() {
local sid="$1"
[ -n "$sid" ] || {
echo "missing_sid"
return 1
}
cleanup_probe_socks
if ! start_probe_socks "$sid"; then
echo "probe_start:fail"
return 1
fi
echo "probe_start:ok"
echo "probe:curl_ipv4"
curl -k -sS -L -o /dev/null \
--connect-timeout "$switch_time" \
--max-time "$switch_time" \
--socks5-hostname "127.0.0.1:$probe_socks_port" \
-w 'code=%{http_code}\n' \
"$probe_url" 2>/dev/null || true
echo "probe:curl_ipv6"
curl -g -k -sS -L -o /dev/null \
--connect-timeout "$switch_time" \
--max-time "$switch_time" \
--socks5-hostname "[::1]:$probe_socks_port" \
-w 'code=%{http_code}\n' \
"$probe_url" 2>/dev/null || true
cleanup_probe_socks
return 0
}
main() {
case "${ORIG_ARG1:-}" in
probe)
PROBE_SID="${ORIG_ARG2:-}"
probe_once "$PROBE_SID"
;;
once)
run_once
;;
start)
[ -n "$ORIG_ARG2" ] && cycle_time="$ORIG_ARG2"
[ -n "$ORIG_ARG3" ] && switch_time="$ORIG_ARG3"
[ -f "$LOCK_FILE" ] && exit 2
touch "$LOCK_FILE"
start
;;
*)
[ -n "$ORIG_ARG1" ] && cycle_time="$ORIG_ARG1"
[ -n "$ORIG_ARG2" ] && switch_time="$ORIG_ARG2"
[ -f "$LOCK_FILE" ] && exit 2
touch "$LOCK_FILE"
start
;;
esac
}
if [ "${0##*/}" = "ssr-switch" ]; then
main "$@"
fi
@@ -0,0 +1,17 @@
{
"luci-app-ssr-plus": {
"description": "Grant UCI access for luci-app-ssr-plus",
"read": {
"file": {
"/etc/ssrplus/*": [ "read" ]
},
"uci": ["shadowsocksr"]
},
"write": {
"file": {
"/etc/ssrplus/*": [ "write" ]
},
"uci": ["shadowsocksr"]
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

@@ -0,0 +1,53 @@
#!/bin/sh
. $IPKG_INSTROOT/etc/init.d/shadowsocksr
check_run_environment
# 设置 china_ip 变量并检查文件是否存在
china_ip="${1:-${china_ip:-/etc/ssrplus/china_ssr.txt}}"
[ -f "$china_ip" ] || exit 1
case "$USE_TABLES" in
nftables)
skip_inet="${SKIP_INET:-0}"
case "$skip_inet" in
1)
{
# ss_spec / inet (仅在表和 set 存在时添加)
if nft list set inet ss_spec china >/dev/null 2>&1; then
echo "add element inet ss_spec china {"
grep -vE '^\s*#|^\s*$' "$china_ip" | sed 's/^/ /;s/$/,/'
echo "}"
fi
} | nft -f - || exit 1
;;
2)
{
# ss_spec_mangle / ip (仅在表和 set 存在时添加)
if nft list set ip ss_spec_mangle china >/dev/null 2>&1; then
echo "add element ip ss_spec_mangle china {"
grep -vE '^\s*#|^\s*$' "$china_ip" | sed 's/^/ /;s/$/,/'
echo "}"
fi
} | nft -f - || exit 1
;;
*)
echolog "chinaipset: invalid SKIP_INET=$skip_inet"
exit 1
;;
esac
;;
iptables)
ipset -! flush china 2>/dev/null
ipset -! -R <<-EOF || exit 1
create china hash:net
$(grep -vE '^\s*#|^\s*$' "$china_ip" | sed 's/^/add china /')
EOF
;;
*)
echolog "ERROR: No supported firewall backend detected"
exit 1
;;
esac
@@ -0,0 +1,939 @@
#!/usr/bin/lua
require "nixio.fs"
require "luci.model.uci"
local ok_lyaml, lyaml = pcall(require, "lyaml")
if not ok_lyaml then
io.stderr:write("lyaml_not_found\n")
os.exit(2)
end
local uci = require "luci.model.uci".cursor()
local ok_jsonc, jsonc = pcall(require, "luci.jsonc")
local function read_file(path)
local data = nixio.fs.readfile(path)
if not data or data == "" then
return nil
end
return data
end
local function write_file(path, data)
return nixio.fs.writefile(path, data)
end
local function load_yaml(path)
local raw = read_file(path)
if not raw then
return nil, "read_failed"
end
local ok, parsed = pcall(lyaml.load, raw)
if not ok or type(parsed) ~= "table" then
return nil, "parse_failed"
end
return parsed
end
local function dump_yaml(path, data)
local ok, rendered = pcall(lyaml.dump, { data })
if not ok or not rendered then
return nil, "dump_failed"
end
write_file(path, rendered)
return true
end
local function split_filter_words(text)
local items = {}
for part in tostring(text or ""):gmatch("[^/]+") do
if part ~= "" then
items[#items + 1] = part
end
end
return items
end
local function trim(value)
return tostring(value or ""):gsub("^%s+", ""):gsub("%s+$", "")
end
local function parse_csv_line(line)
local cols = {}
local cur = ""
local in_quote = false
local i = 1
while i <= #line do
local ch = line:sub(i, i)
if ch == '"' then
if in_quote and line:sub(i + 1, i + 1) == '"' then
cur = cur .. '"'
i = i + 1
else
in_quote = not in_quote
end
elseif ch == "," and not in_quote then
cols[#cols + 1] = cur
cur = ""
else
cur = cur .. ch
end
i = i + 1
end
cols[#cols + 1] = cur
return cols
end
local function read_clash_client_rules_csv(sid)
local rows = {}
sid = trim(sid)
if sid == "" then
return rows
end
local csv_path = string.format("/etc/ssrplus/clash/%s.csv", sid)
local raw = read_file(csv_path)
if not raw or raw == "" then
return rows
end
local first = true
for line in tostring(raw):gsub("\r", ""):gmatch("[^\n]+") do
local text = trim(line)
if text ~= "" then
if first and text:lower() == "enabled,client,policy,remarks,client_mac" then
first = false
else
local cols = parse_csv_line(line)
if #cols >= 4 then
rows[#rows + 1] = {
enabled = cols[1],
ip_addr = trim(cols[2] or ""),
policy_group = trim(cols[3] or ""),
remarks = trim(cols[4] or ""),
client_mac = trim(cols[5] or "")
}
end
end
end
end
return rows
end
local function has_proxy_sections(doc)
return type(doc.proxies) == "table" or type(doc["proxy-providers"]) == "table"
end
local function validate(path)
local doc = load_yaml(path)
if not doc then
return false
end
return has_proxy_sections(doc)
end
local function filter(path, filter_words)
local doc, err = load_yaml(path)
if not doc then
io.stderr:write(err or "parse_failed", "\n")
return false
end
local words = split_filter_words(filter_words)
if #words == 0 then
return true
end
local removed = {}
local proxies = {}
for _, proxy in ipairs(doc.proxies or {}) do
local name = tostring(proxy.name or "")
local matched = false
for _, word in ipairs(words) do
if name:find(word, 1, true) then
matched = true
removed[name] = true
break
end
end
if not matched then
proxies[#proxies + 1] = proxy
end
end
doc.proxies = proxies
for _, group in ipairs(doc["proxy-groups"] or {}) do
if type(group.proxies) == "table" then
local kept = {}
for _, name in ipairs(group.proxies) do
if not removed[tostring(name)] then
kept[#kept + 1] = name
end
end
group.proxies = kept
end
end
local count = 0
for _ in pairs(removed) do
count = count + 1
end
dump_yaml(path, doc)
io.stdout:write(tostring(count), "\n")
return true
end
local function deep_merge(dst, src)
if type(dst) ~= "table" or type(src) ~= "table" then
return src
end
for k, v in pairs(src) do
if type(v) == "table" and type(dst[k]) == "table" then
dst[k] = deep_merge(dst[k], v)
else
dst[k] = v
end
end
return dst
end
local function strip_runtime_conflicts(doc)
doc.tun = nil
doc.listeners = nil
doc["redir-port"] = nil
doc["tproxy-port"] = nil
doc["socks-port"] = nil
doc["mixed-port"] = nil
doc.port = nil
doc["external-controller"] = nil
doc.secret = nil
doc["allow-lan"] = nil
if type(doc.dns) == "table" then
doc.dns["fake-ip-range"] = nil
doc.dns["fake-ip-filter"] = nil
end
end
local function group_requires_candidates(group)
local gtype = tostring(group and group.type or ""):lower()
return gtype == "select"
or gtype == "fallback"
or gtype == "load-balance"
or gtype == "url-test"
or gtype == "relay"
end
local function has_nonempty_sequence(value)
return type(value) == "table" and next(value) ~= nil
end
local function fill_empty_proxy_groups(doc)
local changed = 0
for _, group in ipairs(doc["proxy-groups"] or {}) do
if type(group) == "table"
and group_requires_candidates(group)
and not has_nonempty_sequence(group.proxies)
and not has_nonempty_sequence(group.use)
then
group.proxies = { "DIRECT" }
changed = changed + 1
end
end
return changed
end
local function strip_incompatible_script_rules(doc)
local kept = {}
local removed = 0
local has_script_rule = false
for _, rule in ipairs(doc.rules or {}) do
local text = tostring(rule or "")
if text:match("^SCRIPT,") then
removed = removed + 1
else
kept[#kept + 1] = rule
if text:match("^SCRIPT,") then
has_script_rule = true
end
end
end
if removed > 0 then
doc.rules = kept
end
if not has_script_rule then
doc.script = nil
end
return removed
end
local function prepare(input_path, output_path)
local doc, err = load_yaml(input_path)
if not doc then
io.stderr:write(err or "parse_failed", "\n")
return false
end
if not has_proxy_sections(doc) then
io.stderr:write("missing_proxy_sections\n")
return false
end
strip_runtime_conflicts(doc)
local filled_groups = fill_empty_proxy_groups(doc)
local stripped_rules = strip_incompatible_script_rules(doc)
local ok, rendered = pcall(lyaml.dump, { doc })
if not ok or not rendered then
io.stderr:write("dump_failed\n")
return false
end
write_file(output_path, rendered)
io.stdout:write(string.format("filled_groups=%d stripped_script_rules=%d\n", filled_groups, stripped_rules))
return true
end
local function merge(raw_path, overlay_path, output_path)
local raw_doc, raw_err = load_yaml(raw_path)
if not raw_doc then
io.stderr:write(raw_err or "parse_failed", "\n")
return false
end
local overlay_doc, overlay_err = load_yaml(overlay_path)
if not overlay_doc then
io.stderr:write(overlay_err or "parse_failed", "\n")
return false
end
strip_runtime_conflicts(raw_doc)
local filled_groups = fill_empty_proxy_groups(raw_doc)
local stripped_rules = strip_incompatible_script_rules(raw_doc)
local merged = deep_merge(raw_doc, overlay_doc)
local ok, rendered = pcall(lyaml.dump, { merged })
if not ok or not rendered then
io.stderr:write("dump_failed\n")
return false
end
write_file(output_path, rendered)
io.stdout:write(string.format("filled_groups=%d stripped_script_rules=%d\n", filled_groups, stripped_rules))
return true
end
local function append_client_policy_rules(runtime_path, sid)
local doc, err = load_yaml(runtime_path)
if not doc then
io.stderr:write(err or "parse_failed", "\n")
return false
end
local valid_policies = {}
for _, proxy in ipairs(doc.proxies or {}) do
if type(proxy) == "table" and proxy.name and proxy.name ~= "" then
valid_policies[tostring(proxy.name)] = true
end
end
for _, group in ipairs(doc["proxy-groups"] or {}) do
if type(group) == "table" and group.name and group.name ~= "" then
valid_policies[tostring(group.name)] = true
end
end
local custom_rules = {}
for _, section in ipairs(read_clash_client_rules_csv(sid)) do
if tostring(section.enabled or "0") == "1" then
local ip_addr = tostring(section.ip_addr or "")
local policy_group = tostring(section.policy_group or "")
if ip_addr ~= "" and policy_group ~= "" and valid_policies[policy_group] then
if not ip_addr:find("/", 1, true) then
ip_addr = ip_addr .. "/32"
end
custom_rules[#custom_rules + 1] = string.format("SRC-IP-CIDR,%s,%s", ip_addr, policy_group)
end
end
end
if #custom_rules == 0 then
io.stdout:write("client_rules=0\n")
return true
end
local existing_rules = {}
for _, rule in ipairs(doc.rules or {}) do
local text = tostring(rule or "")
if not text:match("^SRC%-IP%-CIDR,") then
existing_rules[#existing_rules + 1] = rule
end
end
doc.rules = {}
for _, rule in ipairs(custom_rules) do
doc.rules[#doc.rules + 1] = rule
end
for _, rule in ipairs(existing_rules) do
doc.rules[#doc.rules + 1] = rule
end
local ok, rendered = pcall(lyaml.dump, { doc })
if not ok or not rendered then
io.stderr:write("dump_failed\n")
return false
end
write_file(runtime_path, rendered)
io.stdout:write(string.format("client_rules=%d\n", #custom_rules))
return true
end
local function bool_enabled(value)
return value == "1" or value == 1 or value == true or value == "true"
end
local function split_csv(value)
local items = {}
for part in tostring(value or ""):gmatch("[^,%s]+") do
items[#items + 1] = part
end
return items
end
local function get_server_field(sid, option, default)
local value = uci:get("shadowsocksr", sid, option)
if value == nil or value == "" then
return default
end
return value
end
local function get_filter_aaaa()
local value = uci:get_first("shadowsocksr", "global", "filter_aaaa", "1")
if value == nil or value == "" then
value = uci:get_first("shadowsocksr", "global", "mosdns_ipv6", "1")
end
return value
end
local function build_tuic_runtime_doc(sid, local_port, socks_port, mode)
local server = get_server_field(sid, "server", "")
local server_port = tonumber(get_server_field(sid, "server_port", "0")) or 0
local tuic_ip = get_server_field(sid, "tuic_ip", "")
local tls_host = get_server_field(sid, "tls_host", "")
local ipstack_prefer = get_server_field(sid, "ipstack_prefer", "")
local dns_mode = uci:get_first("shadowsocksr", "global", "pdnsd_enable", "0")
local proxy = {
name = sid,
type = "tuic",
server = server,
port = server_port,
uuid = get_server_field(sid, "tuic_uuid", ""),
password = get_server_field(sid, "tuic_passwd", ""),
["udp-relay-mode"] = get_server_field(sid, "udp_relay_mode", "native"),
["congestion-controller"] = get_server_field(sid, "congestion_control", "cubic"),
["skip-cert-verify"] = bool_enabled(get_server_field(sid, "insecure", "0")),
["disable-sni"] = bool_enabled(get_server_field(sid, "disable_sni", "0")),
["reduce-rtt"] = bool_enabled(get_server_field(sid, "zero_rtt_handshake", "0"))
}
if tuic_ip ~= "" then
proxy.ip = tuic_ip
end
if tls_host ~= "" then
proxy.sni = tls_host
end
local alpn = split_csv(get_server_field(sid, "tuic_alpn", ""))
if #alpn > 0 then
proxy.alpn = alpn
end
local heartbeat = tonumber(get_server_field(sid, "heartbeat", "0"))
if heartbeat and heartbeat > 0 then
proxy["heartbeat-interval"] = heartbeat * 1000
end
local timeout = tonumber(get_server_field(sid, "timeout", "0"))
if timeout and timeout > 0 then
proxy["request-timeout"] = timeout * 1000
end
local max_udp_packet_size = tonumber(get_server_field(sid, "tuic_max_package_size", "0"))
if max_udp_packet_size and max_udp_packet_size > 0 then
proxy["max-udp-relay-packet-size"] = max_udp_packet_size
end
if ipstack_prefer ~= "" then
proxy["ip-version"] = ipstack_prefer == "v6first" and "ipv6-prefer" or "ipv4-prefer"
end
local listen_port = tonumber(local_port)
local socks_listen = tonumber(socks_port)
local doc = {
["allow-lan"] = true,
["bind-address"] = "0.0.0.0",
mode = "rule",
["log-level"] = "silent",
["find-process-mode"] = "off",
["unified-delay"] = true,
["tcp-concurrent"] = true,
["routing-mark"] = 255,
proxies = { proxy },
["proxy-groups"] = {
{
name = "PROXY",
type = "select",
proxies = { sid }
}
},
rules = { "MATCH,PROXY" },
tun = { enable = false },
profile = { ["store-selected"] = true },
dns = {
enable = dns_mode == "7",
["enhanced-mode"] = "redir-host",
listen = "127.0.0.1:5335",
ipv6 = get_filter_aaaa() ~= "1"
}
}
if mode == "socks" then
doc["socks-port"] = listen_port
else
doc["redir-port"] = listen_port
doc["tproxy-port"] = listen_port
if socks_listen and socks_listen > 0 then
doc["socks-port"] = socks_listen
end
end
return doc
end
local function generate_tuic_runtime(sid, output_path, local_port, socks_port, mode)
local doc = build_tuic_runtime_doc(sid, local_port, socks_port, mode)
local ok, rendered = pcall(lyaml.dump, { doc })
if not ok or not rendered then
io.stderr:write("dump_failed\n")
return false
end
write_file(output_path, rendered)
return true
end
local function parse_plugin_opts(value)
local result = {}
for part in tostring(value or ""):gmatch("[^;]+") do
local key, val = part:match("^%s*([^=]+)=?(.*)%s*$")
if key and key ~= "" then
result[key] = val or ""
end
end
return result
end
local function split_host_port(value)
local text = tostring(value or "")
if text == "" then
return "", ""
end
local host, port = text:match("^%[(.-)%]:(%d+)$")
if host and port then
return host, port
end
host, port = text:match("^(.-):(%d+)$")
if host and port then
return host, port
end
return text, ""
end
local function bool_default(value, default)
if value == nil or value == "" then
return default
end
return bool_enabled(value)
end
local function number_or_nil(value)
if value == nil or value == "" then
return nil
end
return tonumber(value)
end
local function string_or_nil(value)
if value == nil or value == "" then
return nil
end
return tostring(value)
end
local function pick_plugin_opt(plugin_opts, ...)
for i = 1, select("#", ...) do
local key = select(i, ...)
local value = plugin_opts[key]
if value ~= nil and value ~= "" then
return value
end
end
return nil
end
local function parse_plugin_headers(plugin_opts)
local headers = {}
local raw_headers = pick_plugin_opt(plugin_opts, "headers", "header")
if raw_headers and ok_jsonc and jsonc then
local decoded = jsonc.parse(raw_headers)
if type(decoded) == "table" then
for key, value in pairs(decoded) do
headers[tostring(key)] = tostring(value)
end
end
end
if raw_headers and next(headers) == nil then
for part in tostring(raw_headers):gmatch("[^|,]+") do
local key, value = part:match("^%s*([^=:]+)%s*[:=]%s*(.-)%s*$")
if key and key ~= "" and value and value ~= "" then
headers[key] = value
end
end
end
for key, value in pairs(plugin_opts) do
local header_name = key:match("^headers[%.:](.+)$")
or key:match("^header[%.:](.+)$")
or key:match("^header_(.+)$")
if header_name and header_name ~= "" and value ~= "" then
headers[header_name] = value
end
end
return next(headers) and headers or nil
end
local function get_plugin_client_fingerprint(sid, plugin_opts)
return string_or_nil(
pick_plugin_opt(
plugin_opts,
"client-fingerprint",
"client_fingerprint",
"fingerprint"
) or get_server_field(sid, "fingerprint", "")
)
end
local function normalize_plugin_name(plugin)
local value = tostring(plugin or ""):lower()
if value == "" or value == "none" then
return ""
end
if value == "simple-obfs" then
return "obfs-local"
end
if value == "obfs" then
return "obfs-local"
end
if value == "shadowtls" then
return "shadow-tls"
end
if value == "gost" then
return "gost-plugin"
end
if value == "kcp-tun" then
return "kcptun"
end
return value
end
local function build_shadowsocks_plugin(proxy, sid)
local plugin = normalize_plugin_name(get_server_field(sid, "plugin", ""))
local plugin_opts = parse_plugin_opts(get_server_field(sid, "plugin_opts", ""))
if plugin == "" then
return
end
if plugin == "obfs-local" then
proxy.plugin = "obfs"
proxy["plugin-opts"] = {
mode = pick_plugin_opt(plugin_opts, "obfs", "mode") or "http",
host = string_or_nil(pick_plugin_opt(plugin_opts, "obfs-host", "obfs_host", "host"))
}
return
end
if plugin == "v2ray-plugin" or plugin == "xray-plugin" then
proxy.plugin = "v2ray-plugin"
proxy["plugin-opts"] = {
mode = pick_plugin_opt(plugin_opts, "mode") or "websocket",
tls = bool_default(pick_plugin_opt(plugin_opts, "tls"), false),
fingerprint = string_or_nil(pick_plugin_opt(plugin_opts, "fingerprint")),
["skip-cert-verify"] = bool_default(pick_plugin_opt(plugin_opts, "skip-cert-verify", "skip_cert_verify", "insecure"), false),
host = string_or_nil(pick_plugin_opt(plugin_opts, "host")),
path = string_or_nil(pick_plugin_opt(plugin_opts, "path")),
mux = bool_default(pick_plugin_opt(plugin_opts, "mux"), false),
headers = parse_plugin_headers(plugin_opts),
["v2ray-http-upgrade"] = bool_default(pick_plugin_opt(plugin_opts, "v2ray-http-upgrade", "v2ray_http_upgrade"), false)
}
return
end
if plugin == "gost-plugin" then
proxy.plugin = "gost-plugin"
proxy["plugin-opts"] = {
mode = pick_plugin_opt(plugin_opts, "mode") or "websocket",
tls = bool_default(pick_plugin_opt(plugin_opts, "tls"), false),
fingerprint = string_or_nil(pick_plugin_opt(plugin_opts, "fingerprint")),
["skip-cert-verify"] = bool_default(pick_plugin_opt(plugin_opts, "skip-cert-verify", "skip_cert_verify", "insecure"), false),
host = string_or_nil(pick_plugin_opt(plugin_opts, "host")),
path = string_or_nil(pick_plugin_opt(plugin_opts, "path")),
mux = bool_default(pick_plugin_opt(plugin_opts, "mux"), false),
headers = parse_plugin_headers(plugin_opts)
}
return
end
if plugin == "shadow-tls" then
local host, port = split_host_port(pick_plugin_opt(plugin_opts, "host") or "")
local version
if plugin_opts.v3 == "1" or plugin_opts.version == "3" then
version = 3
elseif plugin_opts.v2 == "1" or plugin_opts.version == "2" then
version = 2
elseif plugin_opts.v1 == "1" or plugin_opts.version == "1" then
version = 1
end
proxy.plugin = "shadow-tls"
proxy["client-fingerprint"] = get_plugin_client_fingerprint(sid, plugin_opts)
proxy["plugin-opts"] = {
host = host ~= "" and host or nil,
port = number_or_nil(port),
password = string_or_nil(pick_plugin_opt(plugin_opts, "passwd", "password")),
version = version
}
return
end
if plugin == "restls" then
proxy.plugin = "restls"
proxy["client-fingerprint"] = get_plugin_client_fingerprint(sid, plugin_opts)
proxy["plugin-opts"] = {
host = string_or_nil(pick_plugin_opt(plugin_opts, "host")),
password = string_or_nil(pick_plugin_opt(plugin_opts, "passwd", "password")),
["version-hint"] = string_or_nil(pick_plugin_opt(plugin_opts, "version-hint", "version_hint")),
["restls-script"] = string_or_nil(pick_plugin_opt(plugin_opts, "restls-script", "restls_script"))
}
return
end
if plugin == "kcptun" then
proxy.plugin = "kcptun"
proxy["plugin-opts"] = {
key = string_or_nil(pick_plugin_opt(plugin_opts, "key", "passwd", "password")),
crypt = string_or_nil(pick_plugin_opt(plugin_opts, "crypt")),
mode = string_or_nil(pick_plugin_opt(plugin_opts, "mode")),
conn = number_or_nil(pick_plugin_opt(plugin_opts, "conn")),
autoexpire = number_or_nil(pick_plugin_opt(plugin_opts, "autoexpire")),
scavengettl = number_or_nil(pick_plugin_opt(plugin_opts, "scavengettl")),
mtu = number_or_nil(pick_plugin_opt(plugin_opts, "mtu")),
ratelimit = number_or_nil(pick_plugin_opt(plugin_opts, "ratelimit")),
sndwnd = number_or_nil(pick_plugin_opt(plugin_opts, "sndwnd")),
rcvwnd = number_or_nil(pick_plugin_opt(plugin_opts, "rcvwnd")),
datashard = number_or_nil(pick_plugin_opt(plugin_opts, "datashard")),
parityshard = number_or_nil(pick_plugin_opt(plugin_opts, "parityshard")),
dscp = number_or_nil(pick_plugin_opt(plugin_opts, "dscp")),
nocomp = bool_default(pick_plugin_opt(plugin_opts, "nocomp"), false),
acknodelay = bool_default(pick_plugin_opt(plugin_opts, "acknodelay"), false),
nodelay = number_or_nil(pick_plugin_opt(plugin_opts, "nodelay")),
interval = number_or_nil(pick_plugin_opt(plugin_opts, "interval")),
resend = number_or_nil(pick_plugin_opt(plugin_opts, "resend")),
sockbuf = number_or_nil(pick_plugin_opt(plugin_opts, "sockbuf")),
smuxver = number_or_nil(pick_plugin_opt(plugin_opts, "smuxver")),
smuxbuf = number_or_nil(pick_plugin_opt(plugin_opts, "smuxbuf")),
framesize = number_or_nil(pick_plugin_opt(plugin_opts, "framesize")),
streambuf = number_or_nil(pick_plugin_opt(plugin_opts, "streambuf")),
keepalive = number_or_nil(pick_plugin_opt(plugin_opts, "keepalive"))
}
return
end
proxy.plugin = plugin
if next(plugin_opts) then
proxy["plugin-opts"] = plugin_opts
end
end
local function build_kcptun_plugin(proxy, sid)
if not bool_enabled(get_server_field(sid, "kcp_enable", "0")) then
return
end
proxy.plugin = "kcptun"
proxy.port = tonumber(get_server_field(sid, "kcp_port", "0")) or proxy.port
proxy["plugin-opts"] = {
key = get_server_field(sid, "kcp_password", ""),
mode = "fast",
mtu = 1350
}
end
local function build_shadowsocks_runtime_doc(sid, local_port, socks_port, mode)
local dns_mode = uci:get_first("shadowsocksr", "global", "pdnsd_enable", "0")
local server = get_server_field(sid, "server", "")
local server_port = tonumber(get_server_field(sid, "server_port", "0")) or 0
local method = get_server_field(sid, "encrypt_method_ss", "none")
local password = get_server_field(sid, "password", "")
local proxy = {
name = sid,
type = "ss",
server = server,
port = server_port,
cipher = method,
password = password,
udp = true,
tfo = bool_enabled(get_server_field(sid, "fast_open", "0"))
}
if get_server_field(sid, "type", "") == "ss" then
build_kcptun_plugin(proxy, sid)
end
if proxy.plugin == nil then
build_shadowsocks_plugin(proxy, sid)
end
local doc = {
["allow-lan"] = true,
["bind-address"] = "0.0.0.0",
mode = "rule",
["log-level"] = "silent",
["find-process-mode"] = "off",
["unified-delay"] = true,
["tcp-concurrent"] = true,
["routing-mark"] = 255,
proxies = { proxy },
["proxy-groups"] = {
{
name = "PROXY",
type = "select",
proxies = { sid }
}
},
rules = { "MATCH,PROXY" },
tun = { enable = false },
profile = { ["store-selected"] = true },
dns = {
enable = dns_mode == "7",
["enhanced-mode"] = "redir-host",
listen = "127.0.0.1:5335",
ipv6 = get_filter_aaaa() ~= "1"
}
}
local listen_port = tonumber(local_port)
local socks_listen = tonumber(socks_port)
if mode == "socks" then
doc["socks-port"] = listen_port
else
doc["redir-port"] = listen_port
doc["tproxy-port"] = listen_port
if socks_listen and socks_listen > 0 then
doc["socks-port"] = socks_listen
end
end
return doc
end
local function generate_shadowsocks_runtime(sid, output_path, local_port, socks_port, mode)
local doc = build_shadowsocks_runtime_doc(sid, local_port, socks_port, mode)
local ok, rendered = pcall(lyaml.dump, { doc })
if not ok or not rendered then
io.stderr:write("dump_failed\n")
return false
end
write_file(output_path, rendered)
return true
end
local function build_shadowsocks_server_doc(sid)
local server_port = tonumber(get_server_field(sid, "server_port", "0")) or 0
local method = get_server_field(sid, "encrypt_method_ss", "aes-128-gcm")
local password = get_server_field(sid, "password", "")
local listener = {
name = sid,
type = "shadowsocks",
listen = "::",
port = server_port,
cipher = method,
password = password,
udp = true,
tfo = bool_enabled(get_server_field(sid, "fast_open", "0"))
}
local plugin = normalize_plugin_name(get_server_field(sid, "plugin", ""))
if plugin == "obfs-local" then
local plugin_opts = parse_plugin_opts(get_server_field(sid, "plugin_opts", ""))
listener.obfs = plugin_opts.obfs or plugin_opts.mode or "http"
listener.obfs_opts = {
mode = plugin_opts.obfs or plugin_opts.mode or "http",
host = plugin_opts["obfs-host"] or plugin_opts.obfs_host or plugin_opts.host or nil
}
end
return {
["allow-lan"] = true,
["bind-address"] = "*",
["log-level"] = "silent",
["find-process-mode"] = "off",
listeners = { listener }
}
end
local function generate_shadowsocks_server(sid, output_path)
local doc = build_shadowsocks_server_doc(sid)
local ok, rendered = pcall(lyaml.dump, { doc })
if not ok or not rendered then
io.stderr:write("dump_failed\n")
return false
end
write_file(output_path, rendered)
return true
end
local action = arg[1]
if action == "validate" then
os.exit(validate(arg[2]) and 0 or 1)
elseif action == "filter" then
os.exit(filter(arg[2], arg[3]) and 0 or 1)
elseif action == "prepare" then
os.exit(prepare(arg[2], arg[3]) and 0 or 1)
elseif action == "merge" then
os.exit(merge(arg[2], arg[3], arg[4]) and 0 or 1)
elseif action == "append_client_policy_rules" then
os.exit(append_client_policy_rules(arg[2], arg[3]) and 0 or 1)
elseif action == "tuic" then
os.exit(generate_tuic_runtime(arg[2], arg[3], arg[4], arg[5], arg[6]) and 0 or 1)
elseif action == "ss" then
os.exit(generate_shadowsocks_runtime(arg[2], arg[3], arg[4], arg[5], arg[6]) and 0 or 1)
elseif action == "ss_server" then
os.exit(generate_shadowsocks_server(arg[2], arg[3]) and 0 or 1)
else
io.stderr:write("usage: clash_yaml.lua validate <yaml> | filter <yaml> <words> | prepare <input> <output> | merge <raw> <overlay> <output> | append_client_policy_rules <runtime_yaml> <sid> | tuic <sid> <output> <local_port> [socks_port] [mode] | ss <sid> <output> <local_port> [socks_port] [mode] | ss_server <sid> <output>\n")
os.exit(1)
end
File diff suppressed because it is too large Load Diff
+137
View File
@@ -0,0 +1,137 @@
#!/bin/sh
. $IPKG_INSTROOT/etc/init.d/shadowsocksr
check_run_environment
case "$USE_TABLES" in
nftables)
nft_support=1
echolog "gfw2ipset: Using nftables"
;;
iptables)
nft_support=0
echolog "gfw2ipset: Using iptables"
;;
*)
echolog "ERROR: No supported firewall backend detected"
exit 1
;;
esac
mkdir -p $TMP_DNSMASQ_PATH
run_mode=$(normalize_run_mode)
cp -rf /etc/ssrplus/gfw_list.conf $TMP_DNSMASQ_PATH/
cp -rf /etc/ssrplus/gfw_base.conf $TMP_DNSMASQ_PATH/
for conf_file in gfw_base.conf gfw_list.conf; do
conf="$TMP_DNSMASQ_PATH/$conf_file"
[ -f "$conf" ] || continue
if [ "$run_mode" = "gfw" ]; then
if [ "$nft_support" = "1" ]; then
# gfw + nftipset → nftset
sed -i 's|ipset=/\([^/]*\)/\([^[:space:]]*\)|nftset=/\1/inet#ss_spec#\2|g' "$conf"
fi
else
# 非 gfw:无条件清理所有分流引用
# sed -i '/^[[:space:]]*\(ipset=\|nftset=\)/d' "$conf"
sed -i '/^[[:space:]]*ipset=/d' "$conf"
fi
done
# 此处使用 for 方式读取 防止 /etc/ssrplus/ 目录下的 black.list white.list deny.list 等2个或多个文件一行中存在空格 比如:# abc.com 而丢失:server
# Optimize: Batch filter using grep
for list_file in /etc/ssrplus/black.list /etc/ssrplus/white.list /etc/ssrplus/deny.list; do
if [ -s "$list_file" ]; then
# 清理注释和空行
grep -vE '^\s*#|^\s*$' "$list_file" | sed 's/\r//g' > "${list_file}.clean"
if [ -s "${list_file}.clean" ]; then
for target_file in "$TMP_DNSMASQ_PATH/gfw_list.conf" "$TMP_DNSMASQ_PATH/gfw_base.conf"; do
[ -f "$target_file" ] || continue
tmp_file="${target_file}.tmp"
awk -v list="${list_file}.clean" '
BEGIN {
while ((getline line < list) > 0) {
gsub(/\r/, "", line)
if (line != "") {
domain[line] = 1
# 同时支持 *.domain
domain["*." line] = 1
}
}
close(list)
}
{
# 提取 server=/domain/xxx
if (match($0, /^server=\/([^\/]+)\//, m)) {
if (m[1] in domain) next
}
# 提取 ipset=/domain/xxx
if (match($0, /^ipset=\/([^\/]+)\//, m)) {
if (m[1] in domain) next
}
print
}
' "$target_file" > "$tmp_file"
mv "$tmp_file" "$target_file"
done
fi
rm -f "${list_file}.clean"
fi
done
# 此处直接使用 cat 因为有 sed '/#/d' 删除了 数据
if [ "$nft_support" = "1" ]; then
cat /etc/ssrplus/black.list | sed '/^$/d' | sed '/#/d' | sed "/.*/s/.*/server=\/&\/127.0.0.1#$dns_port\nnftset=\/&\/inet#ss_spec#blacklist/" >$TMP_DNSMASQ_PATH/blacklist_forward.conf
cat /etc/ssrplus/white.list | sed '/^$/d' | sed '/#/d' | sed "/.*/s/.*/server=\/&\/127.0.0.1\nnftset=\/&\/inet#ss_spec#whitelist/" >$TMP_DNSMASQ_PATH/whitelist_forward.conf
elif [ "$nft_support" = "0" ]; then
cat /etc/ssrplus/black.list | sed '/^$/d' | sed '/#/d' | sed "/.*/s/.*/server=\/&\/127.0.0.1#$dns_port\nipset=\/&\/blacklist/" >$TMP_DNSMASQ_PATH/blacklist_forward.conf
cat /etc/ssrplus/white.list | sed '/^$/d' | sed '/#/d' | sed "/.*/s/.*/server=\/&\/127.0.0.1\nipset=\/&\/whitelist/" >$TMP_DNSMASQ_PATH/whitelist_forward.conf
fi
cat /etc/ssrplus/deny.list | sed '/^$/d' | sed '/#/d' | sed "/.*/s/.*/address=\/&\//" >$TMP_DNSMASQ_PATH/denylist.conf
if [ "$(uci_get_by_type global adblock 0)" == "1" ]; then
cp -f /etc/ssrplus/ad.conf "$TMP_DNSMASQ_PATH/"
if [ -f "$TMP_DNSMASQ_PATH/ad.conf" ]; then
for list_file in /etc/ssrplus/black.list /etc/ssrplus/white.list /etc/ssrplus/deny.list; do
if [ -s "$list_file" ]; then
# 清理注释 & 空行
grep -vE '^\s*#|^\s*$' "$list_file" | sed 's/\r//g' > "${list_file}.clean"
if [ -s "${list_file}.clean" ]; then
tmp_file="$TMP_DNSMASQ_PATH/ad.conf.tmp"
awk -v list="${list_file}.clean" '
BEGIN {
while ((getline line < list) > 0) {
gsub(/\r/, "", line)
if (line != "") {
domain[line] = 1
# 支持泛域名
domain["*." line] = 1
}
}
close(list)
}
{
keep = 1
# 精确匹配 server=/domain/
if (match($0, /^server=\/([^\/]+)\//, m)) {
if (m[1] in domain) keep = 0
}
# 精确匹配 ipset=/domain/
if (match($0, /^ipset=\/([^\/]+)\//, m)) {
if (m[1] in domain) keep = 0
}
if (keep) print
}
' "$TMP_DNSMASQ_PATH/ad.conf" > "$tmp_file"
mv "$tmp_file" "$TMP_DNSMASQ_PATH/ad.conf"
fi
rm -f "${list_file}.clean"
fi
done
fi
else
rm -f "$TMP_DNSMASQ_PATH/ad.conf"
fi
@@ -0,0 +1,67 @@
config global
option global_server 'nil'
option threads '0'
option run_mode 'router'
option dports '2'
option custom_ports '80,443'
option pdnsd_enable '1'
option filter_aaaa '1'
option tunnel_forward '8.8.4.4:53'
option monitor_enable '1'
option enable_switch '1'
option switch_time '667'
option switch_timeout '5'
option switch_try_count '3'
option default_node_local_port '1234'
option component_mirror 'direct'
option gfwlist_url 'https://fastly.jsdelivr.net/gh/YW5vbnltb3Vz/domain-list-community@release/gfwlist.txt'
option chnroute_url 'https://ispip.clang.cn/all_cn.txt'
option adblock_url 'https://anti-ad.net/anti-ad-for-dnsmasq.conf'
config server_subscribe
option proxy '1'
option auto_update '1'
option config_auto_update_mode '0'
option config_update_interval '60'
option auto_update_week_time '*'
option auto_update_day_time '2'
option auto_update_min_time '0'
option url_test_url 'https://www.google.com/generate_204'
option user_agent 'v2rayN/9.99'
option filter_words '过期/套餐/剩余/网址/QQ群/官网/防失联/回国'
config access_control
option lan_ac_mode '0'
option router_proxy '1'
list wan_fw_ips '149.154.160.0/20'
list wan_fw_ips '67.198.55.0/24'
list wan_fw_ips '91.108.4.0/22'
list wan_fw_ips '91.108.56.0/22'
list wan_fw_ips '109.239.140.0/24'
list wan_fw_ips '8.8.8.8'
list wan_fw_ips '1.1.1.1'
list Interface 'lan'
config socks5_proxy
option server 'nil'
option local_port '1080'
config http_proxy
option server 'nil'
option local_port '3128'
option http_auth 'none'
config server_global
option enable_server '0'
config global_xray_fragment
option fragment '0'
option noise '0'
config clash_client_group
option enabled '0'
option remarks ''
option ip_addr ''
option client_mac ''
option policy_group ''
@@ -0,0 +1,43 @@
#!/bin/sh
LOCK_DIR="/var/run/ssrplusupdate.lock"
LOOP_STAMP="/var/run/ssrplusupdate.loop"
MODE="$(uci -q get shadowsocksr.@server_subscribe[0].config_auto_update_mode 2>/dev/null || echo 0)"
mkdir "$LOCK_DIR" 2>/dev/null || exit 0
trap 'rmdir "$LOCK_DIR" 2>/dev/null' EXIT INT TERM
if [ "$1" = "loop" ]; then
[ "$(uci -q get shadowsocksr.@server_subscribe[0].auto_update 2>/dev/null || echo 0)" = "1" ] || exit 0
[ "$MODE" = "1" ] || exit 0
INTERVAL="$(uci -q get shadowsocksr.@server_subscribe[0].config_update_interval 2>/dev/null || echo 60)"
case "$INTERVAL" in
''|*[!0-9]*)
INTERVAL=60
;;
esac
[ "$INTERVAL" -gt 0 ] 2>/dev/null || INTERVAL=60
NOW="$(date +%s)"
LAST_RUN=0
[ -f "$LOOP_STAMP" ] && LAST_RUN="$(cat "$LOOP_STAMP" 2>/dev/null || echo 0)"
case "$LAST_RUN" in
''|*[!0-9]*)
LAST_RUN=0
;;
esac
if [ $((NOW - LAST_RUN)) -lt $((INTERVAL * 60)) ]; then
exit 0
fi
fi
/usr/bin/lua /usr/share/shadowsocksr/update.lua
sleep 2s
/usr/share/shadowsocksr/chinaipset.sh /var/etc/ssrplus/china_ssr.txt
sleep 2s
/usr/bin/lua /usr/share/shadowsocksr/subscribe.lua
if [ "$1" = "loop" ]; then
date +%s > "$LOOP_STAMP"
fi
File diff suppressed because it is too large Load Diff
+266
View File
@@ -0,0 +1,266 @@
#!/usr/bin/lua
------------------------------------------------
-- This file is part of the luci-app-ssr-plus update.lua
-- By Mattraks
------------------------------------------------
require "luci.sys"
require "luci.model.uci"
local icount = 0
local args = arg[1]
local uci = require "luci.model.uci".cursor()
-- 以下设置更新数据库至 DNSMASQ 配置路径
-- 获取 DNSMASQ 配置 ID
local DNSMASQ_UCI_CONFIG = uci:get_first("dhcp", "dnsmasq", ".name")
-- 获取 DNSMASQ 默认配置文件
local DNSMASQ_CONF_PATH = "/tmp/etc/dnsmasq.conf." .. DNSMASQ_UCI_CONFIG
-- 检查 DNSMASQ 配置文件是否存在,如果存在则提取 conf-dir
for line in io.lines(DNSMASQ_CONF_PATH) do
local conf_dir = line:match("^conf%-dir=(.+)")
if conf_dir then
DNSMASQ_CONF_DIR = conf_dir:gsub("%s+", "") -- 去除空白字符
break
end
end
-- 设置 dnsmasq-ssrplus.d 目录路径,并去除路径末尾的斜杠
local TMP_DNSMASQ_PATH = DNSMASQ_CONF_DIR:match("^(.-)/?$") .. "/dnsmasq-ssrplus.d"
local TMP_PATH = "/var/etc/ssrplus"
-- match comments/title/whitelist/ip address/excluded_domain
local comment_pattern = "^[!\\[@]+"
local ip_pattern = "^%d+%.%d+%.%d+%.%d+"
local domain_pattern = "([%w%-%_]+%.[%w%.%-%_]+)[%/%*]*"
local excluded_domain = {
"apple.com", "sina.cn", "sina.com.cn", "baidu.com", "byr.cn", "jlike.com",
"weibo.com", "zhongsou.com", "youdao.com", "sogou.com", "so.com", "soso.com",
"aliyun.com", "taobao.com", "jd.com", "qq.com"
}
-- gfwlist parameter
local mydnsip = '127.0.0.1'
local mydnsport = '5335'
local ipsetname = 'gfwlist'
local new_appledns = uci:get_first("shadowsocksr", "global", "apple_dns")
local bc = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
-- base64decoding
local function base64_dec(data)
data = string.gsub(data, '[^' .. bc .. '=]', '')
return (data:gsub('.', function(x)
if (x == '=') then
return ''
end
local r, f = '', (bc:find(x) - 1)
for i = 6, 1, -1 do
r = r .. (f % 2 ^ i - f % 2 ^ (i - 1) > 0 and '1' or '0')
end
return r;
end):gsub('%d%d%d?%d?%d?%d?%d?%d?', function(x)
if (#x ~= 8) then
return ''
end
local c = 0
for i = 1, 8 do
c = c + (x:sub(i, i) == '1' and 2 ^ (8 - i) or 0)
end
return string.char(c)
end))
end
-- check if domain is excluded
local function check_excluded_domain(value)
for _, domain in ipairs(excluded_domain) do
if value:find(domain) then
return true
end
end
end
-- gfwlist转码至dnsmasq格式
local function generate_gfwlist(type)
local domains, domains_map = {}, {}
local out = io.open("/tmp/ssr-update." .. type, "w")
for line in io.lines("/tmp/ssr-update.tmp") do
if not (string.find(line, comment_pattern) or string.find(line, ip_pattern) or check_excluded_domain(line)) then
local start, finish, match = string.find(line, domain_pattern)
if start and not domains_map[match] then
domains_map[match] = true
table.insert(domains, match)
end
end
end
for _, domain in ipairs(domains) do
out:write(string.format("server=/%s/%s#%s\n", domain, mydnsip, mydnsport))
out:write(string.format("ipset=/%s/%s\n", domain, ipsetname))
end
out:close()
os.remove("/tmp/ssr-update.tmp")
end
-- 更换 Apple dns
local function generate_apple(type)
local domains, domains_map = {}, {}
local out = io.open("/tmp/ssr-update." .. type, "w")
for line in io.lines("/tmp/ssr-update.tmp") do
if not (string.find(line, comment_pattern)) then
local start, finish, match = string.find(line, domain_pattern)
if start and not domains_map[match] then
domains_map[match] = true
match = string.gsub(match, "%s", "") --从域名中去除所有空白字符
table.insert(domains, match)
end
end
end
for _, domain in ipairs(domains) do
if new_appledns and new_appledns ~= "" then
out:write(string.format("server=/%s/%s\n", domain, new_appledns))
end
end
out:close()
os.remove("/tmp/ssr-update.tmp")
end
-- adblock转码至dnsmasq格式
local function generate_adblock(type)
local domains, domains_map = {}, {}
local out = io.open("/tmp/ssr-update." .. type, "w")
for line in io.lines("/tmp/ssr-update.tmp") do
if not (string.find(line, comment_pattern)) then
local start, finish, match = string.find(line, domain_pattern)
if start and not domains_map[match] then
domains_map[match] = true
table.insert(domains, match)
end
end
end
for _, domain in ipairs(domains) do
out:write(string.format("address=/%s/\n", domain))
end
out:close()
os.remove("/tmp/ssr-update.tmp")
end
local log = function(...)
if args then
print("{ret=" .. table.concat({...}, ",retcount=") .. "}")
else
print(os.date("%Y-%m-%d %H:%M:%S ") .. table.concat({...}, " "))
end
end
local function update(url, file, type, file2)
local Num = 1
local refresh_cmd = "curl -sSL --insecure -o /tmp/ssr-update." .. type .. " " .. url
local sret = luci.sys.call(refresh_cmd)
if sret == 0 then
if type == "gfw_data" then
local gfwlist = io.open("/tmp/ssr-update." .. type, "r")
local decode = gfwlist:read("*a")
if not decode:find("google") then
decode = base64_dec(decode)
end
gfwlist:close()
-- 写回gfwlist
gfwlist = io.open("/tmp/ssr-update.tmp", "w")
gfwlist:write(decode)
gfwlist:close()
generate_gfwlist(type)
Num = 2
end
if type == "apple_data" then
local apple = io.open("/tmp/ssr-update." .. type, "r")
local decode = apple:read("*a")
if not decode:find("apple") then
decode = base64_dec(decode)
end
apple:close()
-- 写回applechina
apple = io.open("/tmp/ssr-update.tmp", "w")
apple:write(decode)
apple:close()
if new_appledns and new_appledns ~= "" then
generate_apple(type)
end
end
if type == "ad_data" then
local adblock = io.open("/tmp/ssr-update." .. type, "r")
local decode = adblock:read("*a")
if decode:find("address=") then
adblock:close()
else
adblock:close()
-- 写回adblock
adblock = io.open("/tmp/ssr-update.tmp", "w")
adblock:write(decode)
adblock:close()
generate_adblock(type)
end
end
local new_md5 = luci.sys.exec("echo -n $([ -f '/tmp/ssr-update." .. type .. "' ] && md5sum /tmp/ssr-update." .. type .. " | awk '{print $1}')")
local old_md5 = luci.sys.exec("echo -n $([ -f '" .. file .. "' ] && md5sum " .. file .. " | awk '{print $1}')")
if new_md5 == old_md5 then
if args then
log(1)
else
log("你已经是最新数据,无需更新!")
end
else
icount = luci.sys.exec("cat /tmp/ssr-update." .. type .. " | wc -l")
luci.sys.exec("cp -f /tmp/ssr-update." .. type .. " " .. file)
if file2 then
luci.sys.exec("cp -f /tmp/ssr-update." .. type .. " " .. file2)
end
if type == "gfw_data" or type == "ad_data" then
luci.sys.call("/usr/share/shadowsocksr/gfw2ipset.sh")
else
if luci.sys.call("command -v ipset >/dev/null 2>&1") == 0 then
luci.sys.call("/usr/share/shadowsocksr/chinaipset.sh " .. TMP_PATH .. "/china_ssr.txt")
end
end
if args then
log(0, tonumber(icount) / Num)
else
log("更新成功! 新的总记录数:" .. tostring(tonumber(icount) / Num))
end
end
else
if args then
log(-1)
else
log("更新失败!")
end
end
os.remove("/tmp/ssr-update." .. type)
end
if args then
if args == "gfw_data" then
update(uci:get_first("shadowsocksr", "global", "gfwlist_url"), "/etc/ssrplus/gfw_list.conf", args, TMP_DNSMASQ_PATH .. "/gfw_list.conf")
os.exit(0)
end
if args == "ip_data" then
update(uci:get_first("shadowsocksr", "global", "chnroute_url"), "/etc/ssrplus/china_ssr.txt", args, TMP_PATH .. "/china_ssr.txt")
os.exit(0)
end
if args == "apple_data" then
update(uci:get_first("shadowsocksr", "global", "apple_url"), "/etc/ssrplus/applechina.conf", args, TMP_DNSMASQ_PATH .. "/applechina.conf")
os.exit(0)
end
if args == "ad_data" then
update(uci:get_first("shadowsocksr", "global", "adblock_url"), "/etc/ssrplus/ad.conf", args, TMP_DNSMASQ_PATH .. "/ad.conf")
os.exit(0)
end
else
log("正在更新【GFW列表】数据库")
update(uci:get_first("shadowsocksr", "global", "gfwlist_url"), "/etc/ssrplus/gfw_list.conf", "gfw_data", TMP_DNSMASQ_PATH .. "/gfw_list.conf")
log("正在更新【国内IP段】数据库")
update(uci:get_first("shadowsocksr", "global", "chnroute_url"), "/etc/ssrplus/china_ssr.txt", "ip_data", TMP_PATH .. "/china_ssr.txt")
if uci:get_first("shadowsocksr", "global", "apple_optimization", "0") == "1" then
log("正在更新【Apple域名】数据库")
update(uci:get_first("shadowsocksr", "global", "apple_url"), "/etc/ssrplus/applechina.conf", "apple_data", TMP_DNSMASQ_PATH .. "/applechina.conf")
end
if uci:get_first("shadowsocksr", "global", "adblock", "0") == "1" then
log("正在更新【广告屏蔽】数据库")
update(uci:get_first("shadowsocksr", "global", "adblock_url"), "/etc/ssrplus/ad.conf", "ad_data", TMP_DNSMASQ_PATH .. "/ad.conf")
end
end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,4 @@
{
"config": "shadowsocksr",
"init": "shadowsocksr"
}