mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-09-10 18:34:09 +08:00
update 2026-05-27 23:25:57
This commit is contained in:
@@ -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 + " " + 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
Symlink
+1
@@ -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
|
||||
+2453
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
@@ -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
|
||||
Executable
+225
@@ -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
|
||||
Executable
+1938
File diff suppressed because it is too large
Load Diff
Executable
+282
@@ -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
|
||||
+1123
File diff suppressed because it is too large
Load Diff
+137
@@ -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 + nft:ipset → 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
|
||||
+2253
File diff suppressed because it is too large
Load Diff
+266
@@ -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"
|
||||
}
|
||||
Reference in New Issue
Block a user