local api = require "luci.passwall2.api" local appname = api.appname local datatypes = api.datatypes m = Map(appname, "Sing-Box/Xray " .. translate("Shunt Rule")) m.redirect = api.url("rule") api.set_apply_on_parse(m) if not arg[1] or not m:get(arg[1]) then luci.http.redirect(m.redirect) end function m.on_before_save(self) m:set("@global[0]", "flush_set", "1") end -- Add inline CSS to map description m.description = (m.description or "") .. [[ ]] function clean_text(text) local nbsp = string.char(0xC2, 0xA0) -- Non-breaking space (U+00A0) local fullwidth_space = string.char(0xE3, 0x80, 0x80) -- Full-width space (U+3000) return text :gsub("\t", " ") :gsub(nbsp, " ") :gsub(fullwidth_space, " ") :gsub("^%s+", "") :gsub("%s+$", "\n") :gsub("\r\n", "\n") :gsub("[ \t]*\n[ \t]*", "\n") end s = m:section(NamedSection, arg[1], "shunt_rules", "") s.addremove = false s.dynamic = false remarks = s:option(Value, "remarks", translate("Remarks")) remarks.default = arg[1] remarks.rmempty = false remarks.validate = function(self, value, section) value = api.trim(value) if value == "" then return nil, translate("Remark cannot be empty.") end return value end protocol = s:option(MultiValue, "protocol", translate("Protocol")) protocol:value("http") protocol:value("tls") protocol:value("quic") protocol:value("bittorrent") protocol.widget = "checkbox" protocol.default = nil protocol.optional = false o = s:option(MultiValue, "inbound", translate("Inbound Tag")) o:value("tproxy", translate("Transparent proxy")) o:value("socks", "Socks") o.widget = "checkbox" o.default = nil o.optional = false network = s:option(ListValue, "network", translate("Network")) network:value("tcp,udp", "TCP UDP") network:value("tcp", "TCP") network:value("udp", "UDP") source = s:option(DynamicList, "source", translate("Source")) source.description = "" source.cast = "string" source.cfgvalue = function(self, section) local value if self.tag_error[section] then value = self:formvalue(section) else value = self.map:get(section, self.option) if type(value) == "string" then local value2 = {} string.gsub(value, '[^' .. " " .. ']+', function(w) table.insert(value2, w) end) value = value2 end end return value end source.validate = function(self, value, t) local err = {} for _, v in ipairs(value) do local flag = false if datatypes.ip4addr(v) then flag = true end if flag == false and v:find("geoip:") and v:find("geoip:") == 1 then flag = true end if flag == false then err[#err + 1] = v end end if #err > 0 then self:add_error(t, "invalid", translate("Not true format, please re-enter!")) for _, v in ipairs(err) do self:add_error(t, "invalid", v) end end return value end local dynamicList_write = function(self, section, value) local t = {} local t2 = {} if type(value) == "table" then local x for _, x in ipairs(value) do if x and #x > 0 then if not t2[x] then t2[x] = x t[#t+1] = x end end end else t = { value } end t = table.concat(t, " ") return DynamicList.write(self, section, t) end source.write = dynamicList_write --[[ -- Too low usage rate, hidden sourcePort = s:option(Value, "sourcePort", translate("Source port")) ]]-- port = s:option(Value, "port", translate("Port")) domain_list = s:option(TextValue, "domain_list", translate("Domain")) domain_list.rows = 10 domain_list.wrap = "off" domain_list.validate = function(self, value) local hosts= {} value = clean_text(value) string.gsub(value, "[^\r\n]+", function(w) table.insert(hosts, w) end) for index, host in ipairs(hosts) do local flag = 1 local tmp_host = host if not host:find("#") and host:find("%s") then elseif host:find("regexp:") and host:find("regexp:") == 1 then flag = 0 elseif host:find("domain:.") and host:find("domain:.") == 1 then tmp_host = host:gsub("domain:", "") elseif host:find("full:.") and host:find("full:.") == 1 then tmp_host = host:gsub("full:", "") elseif host:find("geosite:") and host:find("geosite:") == 1 then flag = 0 elseif host:find("ext:") and host:find("ext:") == 1 then flag = 0 elseif host:find("rule-set:", 1, true) == 1 or host:find("rs:") == 1 then local w = host:sub(host:find(":") + 1, #host) if w:find("local:") == 1 or w:find("remote:") == 1 then flag = 0 end elseif host:find("#") and host:find("#") == 1 then flag = 0 end if flag == 1 then if not datatypes.hostname(tmp_host) then return nil, tmp_host .. " " .. translate("Not valid domain name, please re-enter!") end end end return value end domain_list.description = "
" ip_list = s:option(TextValue, "ip_list", "IP") ip_list.rows = 10 ip_list.wrap = "off" ip_list.validate = function(self, value) local ipmasks= {} value = clean_text(value) string.gsub(value, "[^\r\n]+", function(w) table.insert(ipmasks, w) end) for index, ipmask in ipairs(ipmasks) do if ipmask:find("geoip:") and ipmask:find("geoip:") == 1 and not ipmask:find("%s") then elseif ipmask:find("ext:") and ipmask:find("ext:") == 1 and not ipmask:find("%s") then elseif ipmask:find("rule-set:", 1, true) == 1 or ipmask:find("rs:") == 1 then local w = ipmask:sub(ipmask:find(":") + 1, #ipmask) if w:find("local:") == 1 or w:find("remote:") == 1 then flag = 0 end elseif ipmask:find("#") and ipmask:find("#") == 1 then else if not (datatypes.ipmask4(ipmask) or datatypes.ipmask6(ipmask)) then return nil, ipmask .. " " .. translate("Not valid IP format, please re-enter!") end end end return value end ip_list.description = "
" o = s:option(Flag, "invert", "invert", translate("Invert match result.") .. " " .. translate("Only support Sing-Box.")) return m