diff --git a/.github/update.log b/.github/update.log index becb45fb39..a62f727a95 100644 --- a/.github/update.log +++ b/.github/update.log @@ -1135,3 +1135,4 @@ Update On Thu Sep 25 20:42:08 CEST 2025 Update On Fri Sep 26 20:35:20 CEST 2025 Update On Sat Sep 27 20:33:29 CEST 2025 Update On Sun Sep 28 20:32:41 CEST 2025 +Update On Mon Sep 29 20:38:41 CEST 2025 diff --git a/clash-meta/component/resolver/local.go b/clash-meta/component/resolver/service.go similarity index 73% rename from clash-meta/component/resolver/local.go rename to clash-meta/component/resolver/service.go index e8505118b4..8b8f115842 100644 --- a/clash-meta/component/resolver/local.go +++ b/clash-meta/component/resolver/service.go @@ -6,15 +6,15 @@ import ( D "github.com/miekg/dns" ) -var DefaultLocalServer LocalServer +var DefaultService Service -type LocalServer interface { +type Service interface { ServeMsg(ctx context.Context, msg *D.Msg) (*D.Msg, error) } // ServeMsg with a dns.Msg, return resolve dns.Msg func ServeMsg(ctx context.Context, msg *D.Msg) (*D.Msg, error) { - if server := DefaultLocalServer; server != nil { + if server := DefaultService; server != nil { return server.ServeMsg(ctx, msg) } diff --git a/clash-meta/config/config.go b/clash-meta/config/config.go index bcd3cc7ad2..dec0955b2d 100644 --- a/clash-meta/config/config.go +++ b/clash-meta/config/config.go @@ -146,6 +146,7 @@ type DNS struct { PreferH3 bool IPv6 bool IPv6Timeout uint + UseHosts bool UseSystemHosts bool NameServer []dns.NameServer Fallback []dns.NameServer @@ -157,7 +158,6 @@ type DNS struct { CacheAlgorithm string CacheMaxSize int FakeIPRange *fakeip.Pool - Hosts *trie.DomainTrie[resolver.HostValue] NameServerPolicy []dns.Policy ProxyServerNameserver []dns.NameServer DirectNameServer []dns.NameServer @@ -680,7 +680,7 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { } config.Hosts = hosts - dnsCfg, err := parseDNS(rawCfg, hosts, ruleProviders) + dnsCfg, err := parseDNS(rawCfg, ruleProviders) if err != nil { return nil, err } @@ -1341,7 +1341,7 @@ func parseNameServerPolicy(nsPolicy *orderedmap.OrderedMap[string, any], rulePro return policy, nil } -func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[resolver.HostValue], ruleProviders map[string]providerTypes.RuleProvider) (*DNS, error) { +func parseDNS(rawCfg *RawConfig, ruleProviders map[string]providerTypes.RuleProvider) (*DNS, error) { cfg := rawCfg.DNS if cfg.Enable && len(cfg.NameServer) == 0 { return nil, fmt.Errorf("if DNS configuration is turned on, NameServer cannot be empty") @@ -1357,6 +1357,7 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[resolver.HostValue], rul PreferH3: cfg.PreferH3, IPv6Timeout: cfg.IPv6Timeout, IPv6: cfg.IPv6, + UseHosts: cfg.UseHosts, UseSystemHosts: cfg.UseSystemHosts, EnhancedMode: cfg.EnhancedMode, CacheAlgorithm: cfg.CacheAlgorithm, @@ -1490,10 +1491,6 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[resolver.HostValue], rul } } - if cfg.UseHosts { - dnsCfg.Hosts = hosts - } - return dnsCfg, nil } diff --git a/clash-meta/context/dns.go b/clash-meta/context/dns.go index 1cc2067d8d..15143102ca 100644 --- a/clash-meta/context/dns.go +++ b/clash-meta/context/dns.go @@ -2,10 +2,10 @@ package context import ( "context" + "github.com/metacubex/mihomo/common/utils" "github.com/gofrs/uuid/v5" - "github.com/miekg/dns" ) const ( @@ -17,17 +17,15 @@ const ( type DNSContext struct { context.Context - id uuid.UUID - msg *dns.Msg - tp string + id uuid.UUID + tp string } -func NewDNSContext(ctx context.Context, msg *dns.Msg) *DNSContext { +func NewDNSContext(ctx context.Context) *DNSContext { return &DNSContext{ Context: ctx, - id: utils.NewUUIDV4(), - msg: msg, + id: utils.NewUUIDV4(), } } diff --git a/clash-meta/dns/enhancer.go b/clash-meta/dns/enhancer.go index 9ea3ae84ac..36923ca624 100644 --- a/clash-meta/dns/enhancer.go +++ b/clash-meta/dns/enhancer.go @@ -12,6 +12,7 @@ type ResolverEnhancer struct { mode C.DNSMode fakePool *fakeip.Pool mapping *lru.LruCache[netip.Addr, string] + useHosts bool } func (h *ResolverEnhancer) FakeIPEnabled() bool { @@ -103,7 +104,13 @@ func (h *ResolverEnhancer) StoreFakePoolState() { } } -func NewEnhancer(cfg Config) *ResolverEnhancer { +type EnhancerConfig struct { + EnhancedMode C.DNSMode + Pool *fakeip.Pool + UseHosts bool +} + +func NewEnhancer(cfg EnhancerConfig) *ResolverEnhancer { var fakePool *fakeip.Pool var mapping *lru.LruCache[netip.Addr, string] @@ -116,5 +123,6 @@ func NewEnhancer(cfg Config) *ResolverEnhancer { mode: cfg.EnhancedMode, fakePool: fakePool, mapping: mapping, + useHosts: cfg.UseHosts, } } diff --git a/clash-meta/dns/local.go b/clash-meta/dns/local.go deleted file mode 100644 index 37b5d41b04..0000000000 --- a/clash-meta/dns/local.go +++ /dev/null @@ -1,20 +0,0 @@ -package dns - -import ( - "context" - - D "github.com/miekg/dns" -) - -type LocalServer struct { - handler handler -} - -// ServeMsg implement resolver.LocalServer ResolveMsg -func (s *LocalServer) ServeMsg(ctx context.Context, msg *D.Msg) (*D.Msg, error) { - return handlerWithContext(ctx, s.handler, msg) -} - -func NewLocalServer(resolver *Resolver, mapper *ResolverEnhancer) *LocalServer { - return &LocalServer{handler: NewHandler(resolver, mapper)} -} diff --git a/clash-meta/dns/middleware.go b/clash-meta/dns/middleware.go index e6461e91a3..502a37e5e5 100644 --- a/clash-meta/dns/middleware.go +++ b/clash-meta/dns/middleware.go @@ -7,22 +7,22 @@ import ( "github.com/metacubex/mihomo/common/lru" "github.com/metacubex/mihomo/component/fakeip" - R "github.com/metacubex/mihomo/component/resolver" + "github.com/metacubex/mihomo/component/resolver" C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/context" + icontext "github.com/metacubex/mihomo/context" "github.com/metacubex/mihomo/log" D "github.com/miekg/dns" ) type ( - handler func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) + handler func(ctx *icontext.DNSContext, r *D.Msg) (*D.Msg, error) middleware func(next handler) handler ) -func withHosts(hosts R.Hosts, mapping *lru.LruCache[netip.Addr, string]) middleware { +func withHosts(mapping *lru.LruCache[netip.Addr, string]) middleware { return func(next handler) handler { - return func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) { + return func(ctx *icontext.DNSContext, r *D.Msg) (*D.Msg, error) { q := r.Question[0] if !isIPRequest(q) { @@ -36,7 +36,7 @@ func withHosts(hosts R.Hosts, mapping *lru.LruCache[netip.Addr, string]) middlew rr.Target = domain + "." resp.Answer = append([]D.RR{rr}, resp.Answer...) } - record, ok := hosts.Search(host, q.Qtype != D.TypeA && q.Qtype != D.TypeAAAA) + record, ok := resolver.DefaultHosts.Search(host, q.Qtype != D.TypeA && q.Qtype != D.TypeAAAA) if !ok { if record != nil && record.IsDomain { // replace request domain @@ -88,7 +88,7 @@ func withHosts(hosts R.Hosts, mapping *lru.LruCache[netip.Addr, string]) middlew return next(ctx, r) } - ctx.SetType(context.DNSTypeHost) + ctx.SetType(icontext.DNSTypeHost) msg.SetRcode(r, D.RcodeSuccess) msg.Authoritative = true msg.RecursionAvailable = true @@ -99,7 +99,7 @@ func withHosts(hosts R.Hosts, mapping *lru.LruCache[netip.Addr, string]) middlew func withMapping(mapping *lru.LruCache[netip.Addr, string]) middleware { return func(next handler) handler { - return func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) { + return func(ctx *icontext.DNSContext, r *D.Msg) (*D.Msg, error) { q := r.Question[0] if !isIPRequest(q) { @@ -149,7 +149,7 @@ func withMapping(mapping *lru.LruCache[netip.Addr, string]) middleware { func withFakeIP(fakePool *fakeip.Pool) middleware { return func(next handler) handler { - return func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) { + return func(ctx *icontext.DNSContext, r *D.Msg) (*D.Msg, error) { q := r.Question[0] host := strings.TrimRight(q.Name, ".") @@ -173,7 +173,7 @@ func withFakeIP(fakePool *fakeip.Pool) middleware { msg := r.Copy() msg.Answer = []D.RR{rr} - ctx.SetType(context.DNSTypeFakeIP) + ctx.SetType(icontext.DNSTypeFakeIP) setMsgTTL(msg, 1) msg.SetRcode(r, D.RcodeSuccess) msg.Authoritative = true @@ -185,8 +185,8 @@ func withFakeIP(fakePool *fakeip.Pool) middleware { } func withResolver(resolver *Resolver) handler { - return func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) { - ctx.SetType(context.DNSTypeRaw) + return func(ctx *icontext.DNSContext, r *D.Msg) (*D.Msg, error) { + ctx.SetType(icontext.DNSTypeRaw) q := r.Question[0] @@ -218,11 +218,11 @@ func compose(middlewares []middleware, endpoint handler) handler { return h } -func NewHandler(resolver *Resolver, mapper *ResolverEnhancer) handler { - middlewares := []middleware{} +func newHandler(resolver *Resolver, mapper *ResolverEnhancer) handler { + var middlewares []middleware - if resolver.hosts != nil { - middlewares = append(middlewares, withHosts(R.NewHosts(resolver.hosts), mapper.mapping)) + if mapper.useHosts { + middlewares = append(middlewares, withHosts(mapper.mapping)) } if mapper.mode == C.DNSFakeIP { diff --git a/clash-meta/dns/resolver.go b/clash-meta/dns/resolver.go index f5f69c5f4a..f7d4d42968 100644 --- a/clash-meta/dns/resolver.go +++ b/clash-meta/dns/resolver.go @@ -9,7 +9,6 @@ import ( "github.com/metacubex/mihomo/common/arc" "github.com/metacubex/mihomo/common/lru" "github.com/metacubex/mihomo/common/singleflight" - "github.com/metacubex/mihomo/component/fakeip" "github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/component/trie" C "github.com/metacubex/mihomo/constant" @@ -40,7 +39,6 @@ type result struct { type Resolver struct { ipv6 bool ipv6Timeout time.Duration - hosts *trie.DomainTrie[resolver.HostValue] main []dnsClient fallback []dnsClient fallbackDomainFilters []C.DomainMatcher @@ -452,11 +450,8 @@ type Config struct { DirectFollowPolicy bool IPv6 bool IPv6Timeout uint - EnhancedMode C.DNSMode FallbackIPFilter []C.IpMatcher FallbackDomainFilter []C.DomainMatcher - Pool *fakeip.Pool - Hosts *trie.DomainTrie[resolver.HostValue] Policy []Policy CacheAlgorithm string CacheMaxSize int @@ -530,7 +525,6 @@ func NewResolver(config Config) (rs Resolvers) { ipv6: config.IPv6, main: cacheTransform(config.Main), cache: config.newCache(), - hosts: config.Hosts, ipv6Timeout: time.Duration(config.IPv6Timeout) * time.Millisecond, } r.defaultResolver = defaultResolver @@ -541,7 +535,6 @@ func NewResolver(config Config) (rs Resolvers) { ipv6: config.IPv6, main: cacheTransform(config.ProxyServer), cache: config.newCache(), - hosts: config.Hosts, ipv6Timeout: time.Duration(config.IPv6Timeout) * time.Millisecond, } } @@ -551,7 +544,6 @@ func NewResolver(config Config) (rs Resolvers) { ipv6: config.IPv6, main: cacheTransform(config.DirectServer), cache: config.newCache(), - hosts: config.Hosts, ipv6Timeout: time.Duration(config.IPv6Timeout) * time.Millisecond, } } diff --git a/clash-meta/dns/server.go b/clash-meta/dns/server.go index caf1c2891a..b1224c6212 100644 --- a/clash-meta/dns/server.go +++ b/clash-meta/dns/server.go @@ -1,13 +1,12 @@ package dns import ( - stdContext "context" - "errors" + "context" "net" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/common/sockopt" - "github.com/metacubex/mihomo/context" + "github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/log" D "github.com/miekg/dns" @@ -21,39 +20,32 @@ var ( ) type Server struct { - handler handler + service resolver.Service tcpServer *D.Server udpServer *D.Server } // ServeDNS implement D.Handler ServeDNS func (s *Server) ServeDNS(w D.ResponseWriter, r *D.Msg) { - msg, err := handlerWithContext(stdContext.Background(), s.handler, r) + msg, err := s.service.ServeMsg(context.Background(), r) if err != nil { - D.HandleFailed(w, r) + m := new(D.Msg) + m.SetRcode(r, D.RcodeServerFailure) + // does not matter if this write fails + w.WriteMsg(m) return } msg.Compress = true w.WriteMsg(msg) } -func handlerWithContext(stdCtx stdContext.Context, handler handler, msg *D.Msg) (*D.Msg, error) { - if len(msg.Question) == 0 { - return nil, errors.New("at least one question is required") - } - - ctx := context.NewDNSContext(stdCtx, msg) - return handler(ctx, msg) +func (s *Server) SetService(service resolver.Service) { + s.service = service } -func (s *Server) SetHandler(handler handler) { - s.handler = handler -} - -func ReCreateServer(addr string, resolver *Resolver, mapper *ResolverEnhancer) { - if addr == address && resolver != nil { - handler := NewHandler(resolver, mapper) - server.SetHandler(handler) +func ReCreateServer(addr string, service resolver.Service) { + if addr == address && service != nil { + server.SetService(service) return } @@ -67,10 +59,10 @@ func ReCreateServer(addr string, resolver *Resolver, mapper *ResolverEnhancer) { server.udpServer = nil } - server.handler = nil + server.service = nil address = "" - if addr == "" { + if addr == "" || service == nil { return } @@ -87,8 +79,7 @@ func ReCreateServer(addr string, resolver *Resolver, mapper *ResolverEnhancer) { } address = addr - handler := NewHandler(resolver, mapper) - server = &Server{handler: handler} + server = &Server{service: service} go func() { p, err := inbound.ListenPacket("udp", addr) diff --git a/clash-meta/dns/service.go b/clash-meta/dns/service.go new file mode 100644 index 0000000000..4a7c1bb2ea --- /dev/null +++ b/clash-meta/dns/service.go @@ -0,0 +1,29 @@ +package dns + +import ( + "context" + "errors" + + "github.com/metacubex/mihomo/component/resolver" + icontext "github.com/metacubex/mihomo/context" + D "github.com/miekg/dns" +) + +type Service struct { + handler handler +} + +// ServeMsg implement [resolver.Service] ResolveMsg +func (s *Service) ServeMsg(ctx context.Context, msg *D.Msg) (*D.Msg, error) { + if len(msg.Question) == 0 { + return nil, errors.New("at least one question is required") + } + + return s.handler(icontext.NewDNSContext(ctx), msg) +} + +var _ resolver.Service = (*Service)(nil) + +func NewService(resolver *Resolver, mapper *ResolverEnhancer) *Service { + return &Service{handler: newHandler(resolver, mapper)} +} diff --git a/clash-meta/hub/executor/executor.go b/clash-meta/hub/executor/executor.go index fcf176e0e0..041e6fc36b 100644 --- a/clash-meta/hub/executor/executor.go +++ b/clash-meta/hub/executor/executor.go @@ -240,20 +240,18 @@ func updateDNS(c *config.DNS, generalIPv6 bool) { if !c.Enable { resolver.DefaultResolver = nil resolver.DefaultHostMapper = nil - resolver.DefaultLocalServer = nil + resolver.DefaultService = nil resolver.ProxyServerHostResolver = nil resolver.DirectHostResolver = nil - dns.ReCreateServer("", nil, nil) + dns.ReCreateServer("", nil) return } - cfg := dns.Config{ + + r := dns.NewResolver(dns.Config{ Main: c.NameServer, Fallback: c.Fallback, IPv6: c.IPv6 && generalIPv6, IPv6Timeout: c.IPv6Timeout, - EnhancedMode: c.EnhancedMode, - Pool: c.FakeIPRange, - Hosts: c.Hosts, FallbackIPFilter: c.FallbackIPFilter, FallbackDomainFilter: c.FallbackDomainFilter, Default: c.DefaultNameserver, @@ -263,19 +261,23 @@ func updateDNS(c *config.DNS, generalIPv6 bool) { DirectFollowPolicy: c.DirectFollowPolicy, CacheAlgorithm: c.CacheAlgorithm, CacheMaxSize: c.CacheMaxSize, - } - - r := dns.NewResolver(cfg) - m := dns.NewEnhancer(cfg) + }) + m := dns.NewEnhancer(dns.EnhancerConfig{ + EnhancedMode: c.EnhancedMode, + Pool: c.FakeIPRange, + UseHosts: c.UseHosts, + }) // reuse cache of old host mapper if old := resolver.DefaultHostMapper; old != nil { m.PatchFrom(old.(*dns.ResolverEnhancer)) } + s := dns.NewService(r.Resolver, m) + resolver.DefaultResolver = r resolver.DefaultHostMapper = m - resolver.DefaultLocalServer = dns.NewLocalServer(r.Resolver, m) + resolver.DefaultService = s resolver.UseSystemHosts = c.UseSystemHosts if r.ProxyResolver.Invalid() { @@ -290,7 +292,7 @@ func updateDNS(c *config.DNS, generalIPv6 bool) { resolver.DirectHostResolver = r.Resolver } - dns.ReCreateServer(c.Listen, r.Resolver, m) + dns.ReCreateServer(c.Listen, s) } func updateHosts(tree *trie.DomainTrie[resolver.HostValue]) { diff --git a/clash-nyanpasu/manifest/version.json b/clash-nyanpasu/manifest/version.json index 697c6fcc7e..6dd2385c13 100644 --- a/clash-nyanpasu/manifest/version.json +++ b/clash-nyanpasu/manifest/version.json @@ -2,7 +2,7 @@ "manifest_version": 1, "latest": { "mihomo": "v1.19.14", - "mihomo_alpha": "alpha-f45c6f5", + "mihomo_alpha": "alpha-f7bd8b8", "clash_rs": "v0.9.0", "clash_premium": "2023-09-05-gdcc8d87", "clash_rs_alpha": "0.9.0-alpha+sha.2784d7a" @@ -69,5 +69,5 @@ "linux-armv7hf": "clash-armv7-unknown-linux-gnueabihf" } }, - "updated_at": "2025-09-26T22:21:10.718Z" + "updated_at": "2025-09-28T22:20:43.982Z" } diff --git a/lede/README.md b/lede/README.md index a45cd91b41..5361b16163 100644 --- a/lede/README.md +++ b/lede/README.md @@ -39,7 +39,7 @@ ArmSoM-Sige 系列:软路由、单板计算机、小型服务器与智能家 ## 编译命令 -1. 首先装好 Linux 系统,推荐 Debian 或 Ubuntu LTS +1. 首先装好 Linux 系统,推荐 Debian 或 Ubuntu LTS 22/24 2. 安装编译依赖 @@ -50,7 +50,7 @@ ArmSoM-Sige 系列:软路由、单板计算机、小型服务器与智能家 bzip2 ccache clang cmake cpio curl device-tree-compiler flex gawk gcc-multilib g++-multilib gettext \ genisoimage git gperf haveged help2man intltool libc6-dev-i386 libelf-dev libfuse-dev libglib2.0-dev \ libgmp3-dev libltdl-dev libmpc-dev libmpfr-dev libncurses5-dev libncursesw5-dev libpython3-dev \ - libreadline-dev libssl-dev libtool llvm lrzsz msmtp ninja-build p7zip p7zip-full patch pkgconf \ + libreadline-dev libssl-dev libtool llvm lrzsz libnsl-dev ninja-build p7zip p7zip-full patch pkgconf \ python3 python3-pyelftools python3-setuptools qemu-utils rsync scons squashfs-tools subversion \ swig texinfo uglifyjs upx-ucl unzip vim wget xmlto xxd zlib1g-dev ``` diff --git a/lede/package/network/services/dnsmasq/Makefile b/lede/package/network/services/dnsmasq/Makefile index aabac5d3ee..d2fe1e51e7 100644 --- a/lede/package/network/services/dnsmasq/Makefile +++ b/lede/package/network/services/dnsmasq/Makefile @@ -1,5 +1,5 @@ # -# Copyright (C) 2006-2016 OpenWrt.org +# Copyright (C) 2006-2022 OpenWrt.org # # This is free software, licensed under the GNU General Public License v2. # See /LICENSE for more information. @@ -10,7 +10,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=dnsmasq PKG_UPSTREAM_VERSION:=2.91 PKG_VERSION:=$(subst test,~~test,$(subst rc,~rc,$(PKG_UPSTREAM_VERSION))) -PKG_RELEASE:=2 +PKG_RELEASE:=1 PKG_SOURCE:=$(PKG_NAME)-$(PKG_UPSTREAM_VERSION).tar.xz PKG_SOURCE_URL:=https://thekelleys.org.uk/dnsmasq/ @@ -24,7 +24,6 @@ PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(BUILD_VARIANT)/$(PKG_NAME)-$(PKG_UPSTR PKG_INSTALL:=1 PKG_BUILD_PARALLEL:=1 -PKG_BUILD_FLAGS:=lto PKG_ASLR_PIE_REGULAR:=1 PKG_CONFIG_DEPENDS:= CONFIG_PACKAGE_dnsmasq_$(BUILD_VARIANT)_dhcp \ CONFIG_PACKAGE_dnsmasq_$(BUILD_VARIANT)_dhcpv6 \ @@ -109,16 +108,16 @@ define Package/dnsmasq-full/config default n config PACKAGE_dnsmasq_full_auth bool "Build with the facility to act as an authoritative DNS server." - default y + default n config PACKAGE_dnsmasq_full_ipset bool "Build with IPset support." default y config PACKAGE_dnsmasq_full_nftset bool "Build with Nftset support." - default y + default n config PACKAGE_dnsmasq_full_conntrack bool "Build with Conntrack support." - default y + default n config PACKAGE_dnsmasq_full_noid bool "Build with NO_ID. (hide *.bind pseudo domain)" default n @@ -134,6 +133,9 @@ endef Package/dnsmasq-dhcpv6/conffiles = $(Package/dnsmasq/conffiles) Package/dnsmasq-full/conffiles = $(Package/dnsmasq/conffiles) +TARGET_CFLAGS += -flto +TARGET_LDFLAGS += -flto=jobserver + COPTS = -DHAVE_UBUS -DHAVE_POLL_H \ $(if $(CONFIG_IPV6),,-DNO_IPV6) diff --git a/lede/package/network/services/dnsmasq/files/dhcp-script.sh b/lede/package/network/services/dnsmasq/files/dhcp-script.sh index f0c8b50902..470097bf6b 100755 --- a/lede/package/network/services/dnsmasq/files/dhcp-script.sh +++ b/lede/package/network/services/dnsmasq/files/dhcp-script.sh @@ -8,15 +8,6 @@ json_init json_add_array env hotplugobj="" -oldIFS=$IFS -IFS=$'\n' -for var in $(env); do - if [ "${var}" != "${var#DNSMASQ_}" ]; then - json_add_string "" "${var%%=*}=${var#*=}" - fi -done -IFS=$oldIFS - case "$1" in add | del | old | arp-add | arp-del) json_add_string "" "MACADDR=$2" diff --git a/lede/package/network/services/dnsmasq/files/dhcp.conf b/lede/package/network/services/dnsmasq/files/dhcp.conf index d5b9dfa018..3f054f5feb 100644 --- a/lede/package/network/services/dnsmasq/files/dhcp.conf +++ b/lede/package/network/services/dnsmasq/files/dhcp.conf @@ -10,7 +10,7 @@ config dnsmasq option domain 'lan' option expandhosts 1 option nonegcache 0 - option cachesize 1000 + option cachesize 8192 option authoritative 1 option readethers 1 option leasefile '/tmp/dhcp.leases' diff --git a/lede/package/network/services/dnsmasq/files/dnsmasq.init b/lede/package/network/services/dnsmasq/files/dnsmasq.init index 6f31636a0d..7fa50803f9 100755 --- a/lede/package/network/services/dnsmasq/files/dnsmasq.init +++ b/lede/package/network/services/dnsmasq/files/dnsmasq.init @@ -12,7 +12,6 @@ ADD_WAN_FQDN=0 ADD_LOCAL_FQDN="" BASECONFIGFILE="/var/etc/dnsmasq.conf" -EXTRACONFFILE="extraconfig.conf" BASEHOSTFILE="/tmp/hosts/dhcp" TRUSTANCHORSFILE="/usr/share/dnsmasq/trust-anchors.conf" TIMEVALIDFILE="/var/state/dnsmasqsec" @@ -20,7 +19,7 @@ BASEDHCPSTAMPFILE="/var/run/dnsmasq" DHCPBOGUSHOSTNAMEFILE="/usr/share/dnsmasq/dhcpbogushostname.conf" RFC6761FILE="/usr/share/dnsmasq/rfc6761.conf" DHCPSCRIPT="/usr/lib/dnsmasq/dhcp-script.sh" -DHCPSCRIPT_DEPENDS="/usr/share/libubox/jshn.sh /usr/bin/jshn /bin/ubus /usr/bin/env" +DHCPSCRIPT_DEPENDS="/usr/share/libubox/jshn.sh /usr/bin/jshn /bin/ubus" DNSMASQ_DHCP_VER=4 @@ -34,7 +33,6 @@ dnsmasq_ignore_opt() { [ "${dnsmasq_features#* DNSSEC }" = "$dnsmasq_features" ] || dnsmasq_has_dnssec=1 [ "${dnsmasq_features#* TFTP }" = "$dnsmasq_features" ] || dnsmasq_has_tftp=1 [ "${dnsmasq_features#* ipset }" = "$dnsmasq_features" ] || dnsmasq_has_ipset=1 - [ "${dnsmasq_features#* nftset }" = "$dnsmasq_features" ] || dnsmasq_has_nftset=1 fi case "$opt" in @@ -57,8 +55,6 @@ dnsmasq_ignore_opt() { [ -z "$dnsmasq_has_tftp" ] ;; ipset) [ -z "$dnsmasq_has_ipset" ] ;; - nftset) - [ -z "$dnsmasq_has_nftset" ] ;; *) return 1 esac @@ -69,7 +65,7 @@ xappend() { local opt="${value%%=*}" if ! dnsmasq_ignore_opt "$opt"; then - echo "$value" >>"$CONFIGFILE_TMP" + echo "$value" >>$CONFIGFILE_TMP fi } @@ -173,6 +169,10 @@ append_address() { xappend "--address=$1" } +append_ipset() { + xappend "--ipset=$1" +} + append_connmark_allowlist() { xappend "--connmark-allowlist=$1" } @@ -205,12 +205,8 @@ ismounted() { return 1 } -append_extramount() { - ismounted "$1" || append EXTRA_MOUNT "$1" -} - append_addnhosts() { - append_extramount "$1" + ismounted "$1" || append EXTRA_MOUNT "$1" xappend "--addn-hosts=$1" } @@ -226,14 +222,6 @@ append_interface_name() { xappend "--interface-name=$1,$2" } -append_filter_rr() { - xappend "--filter-rr=$1" -} - -append_cache_rr() { - xappend "--cache-rr=$1" -} - filter_dnsmasq() { local cfg="$1" func="$2" match_cfg="$3" found_cfg @@ -362,7 +350,7 @@ dhcp_host_add() { config_get_bool dns "$cfg" dns 0 [ "$dns" = "1" ] && [ -n "$ip" ] && [ -n "$name" ] && { - echo "$ip $name${DOMAIN:+.$DOMAIN}" >> "$HOSTFILE_TMP" + echo "$ip $name${DOMAIN:+.$DOMAIN}" >> $HOSTFILE_TMP } config_get mac "$cfg" mac @@ -511,13 +499,14 @@ dhcp_boot_add() { [ -n "$serveraddress" ] && [ ! -n "$servername" ] && return 0 - xappend "--dhcp-boot=${networkid:+tag:$networkid,}${filename}${servername:+,$servername}${serveraddress:+,$serveraddress}" + xappend "--dhcp-boot=${networkid:+net:$networkid,}${filename}${servername:+,$servername}${serveraddress:+,$serveraddress}" config_get_bool force "$cfg" force 0 dhcp_option_add "$cfg" "$networkid" "$force" } + dhcp_add() { local cfg="$1" local dhcp6range="::" @@ -548,13 +537,8 @@ dhcp_add() { # Do not support non-static interfaces for now [ static = "$proto" ] || return 0 - ipaddr="${subnet%%/*}" - prefix_or_netmask="${subnet##*/}" - # Override interface netmask with dhcp config if applicable - config_get netmask "$cfg" netmask - - [ -n "$netmask" ] && prefix_or_netmask="$netmask" + config_get netmask "$cfg" netmask "${subnet##*/}" #check for an already active dhcp server on the interface, unless 'force' is set config_get_bool force "$cfg" force 0 @@ -570,8 +554,6 @@ dhcp_add() { config_get leasetime "$cfg" leasetime 12h config_get options "$cfg" options config_get_bool dynamicdhcp "$cfg" dynamicdhcp 1 - config_get_bool dynamicdhcpv4 "$cfg" dynamicdhcpv4 $dynamicdhcp - config_get_bool dynamicdhcpv6 "$cfg" dynamicdhcpv6 $dynamicdhcp config_get dhcpv4 "$cfg" dhcpv4 config_get dhcpv6 "$cfg" dhcpv6 @@ -596,30 +578,25 @@ dhcp_add() { nettag="${networkid:+set:${networkid},}" - # make sure the DHCP range is not empty - if [ "$dhcpv4" != "disabled" ]; then - unset START - unset END - unset NETMASK - ipcalc "$ipaddr/$prefix_or_netmask" "$start" "$limit" - - if [ -z "$START" ] || [ -z "$END" ] || [ -z "$NETMASK" ]; then - logger -t dnsmasq \ - "unable to set dhcp-range for dhcp uci config section '$cfg'" \ - "on interface '$ifname', please check your config" - else - [ "$dynamicdhcpv4" = "0" ] && END="static" - xappend "--dhcp-range=$tags$nettag$START,$END,$NETMASK,$leasetime${options:+ $options}" - fi + if [ "$limit" -gt 0 ] ; then + limit=$((limit-1)) fi - if [ "$dynamicdhcpv6" = "0" ] ; then + eval "$(ipcalc.sh "${subnet%%/*}" $netmask $start $limit)" + + if [ "$dynamicdhcp" = "0" ] ; then + END="static" dhcp6range="::,static" else dhcp6range="::1000,::ffff" fi + if [ "$dhcpv4" != "disabled" ] ; then + xappend "--dhcp-range=$tags$nettag$START,$END,$NETMASK,$leasetime${options:+ $options}" + fi + + if [ $DNSMASQ_DHCP_VER -eq 6 ] && [ "$ra" = "server" ] ; then # Note: dnsmasq cannot just be a DHCPv6 server (all-in-1) # and let some other machine(s) send RA pointing to it. @@ -732,7 +709,7 @@ dhcp_domain_add() { record="${record:+$record }$name" done - echo "$ip $record" >> "$HOSTFILE_TMP" + echo "$ip $record" >> $HOSTFILE_TMP } dhcp_srv_add() { @@ -806,29 +783,6 @@ dhcp_hostrecord_add() { xappend "--host-record=$record" } -dhcp_dnsrr_add() { - #This adds arbitrary resource record types (of IN class) whose optional data must be hex - local cfg="$1" - local rrname rrnumber hexdata - - config_get rrname "$cfg" rrname - [ -n "$rrname" ] || return 0 - - config_get rrnumber "$cfg" rrnumber - [ -n "$rrnumber" ] && [ "$rrnumber" -gt 0 ] || return 0 - - config_get hexdata "$cfg" hexdata - - # dnsmasq accepts colon XX:XX:.., space XX XX .., or contiguous XXXX.. hex forms or mixtures thereof - if [ -n "${hexdata//[0-9a-fA-F\:\ ]/}" ]; then - # is invalid hex literal - echo "dnsmasq: \"$hexdata\" is malformed hexadecimal (separate hex with colon, space or not at all)." >&2 - return 1 - fi - - xappend "--dns-rr=${rrname},${rrnumber}${hexdata:+,$hexdata}" -} - dhcp_relay_add() { local cfg="$1" local local_addr server_addr interface @@ -850,61 +804,30 @@ dhcp_relay_add() { dnsmasq_ipset_add() { local cfg="$1" - local ipsets nftsets domains + local ipsets add_ipset() { ipsets="${ipsets:+$ipsets,}$1" } - add_nftset() { - local IFS=, - for set in $1; do - local fam="$family" - [ -n "$fam" ] || fam=$(echo "$set" | sed -nre \ - 's#^.*[^0-9]([46])$|^.*[-_]([46])[-_].*$|^([46])[^0-9].*$#\1\2\3#p') - [ -n "$fam" ] || \ - fam=$(nft -t list set "$table_family" "$table" "$set" 2>&1 | sed -nre \ - 's#^\t\ttype .*\bipv([46])_addr\b.*$#\1#p') - - [ -n "$fam" ] || \ - logger -t dnsmasq "Cannot infer address family from non-existent nftables set '$set'" - - nftsets="${nftsets:+$nftsets,}${fam:+$fam#}$table_family#$table#$set" - done - } - add_domain() { - # leading '/' is expected - domains="$domains/$1" + xappend "--ipset=/$1/$ipsets" } - config_get table "$cfg" table 'fw4' - config_get table_family "$cfg" table_family 'inet' - if [ "$table_family" = "ip" ] ; then - family="4" - elif [ "$table_family" = "ip6" ] ; then - family="6" - else - config_get family "$cfg" family - fi - config_list_foreach "$cfg" "name" add_ipset - config_list_foreach "$cfg" "name" add_nftset - config_list_foreach "$cfg" "domain" add_domain - if [ -z "$ipsets" ] || [ -z "$nftsets" ] || [ -z "$domains" ]; then + if [ -z "$ipsets" ]; then return 0 fi - xappend "--ipset=$domains/$ipsets" - xappend "--nftset=$domains/$nftsets" + config_list_foreach "$cfg" "domain" add_domain } dnsmasq_start() { local cfg="$1" - local disabled user_dhcpscript logfacility - local resolvfile resolvdir localuse=1 + local disabled user_dhcpscript + local resolvfile resolvdir localuse=0 config_get_bool disabled "$cfg" disabled 0 [ "$disabled" -gt 0 ] && return 0 @@ -923,13 +846,13 @@ dnsmasq_start() # before we can call xappend umask u=rwx,g=rx,o=rx mkdir -p /var/run/dnsmasq/ - mkdir -p "$(dirname "$CONFIGFILE")" + mkdir -p $(dirname $CONFIGFILE) mkdir -p "$HOSTFILE_DIR" mkdir -p /var/lib/misc chown dnsmasq:dnsmasq /var/run/dnsmasq - echo "# auto-generated config file from /etc/config/dhcp" > "$CONFIGFILE_TMP" - echo "# auto-generated config file from /etc/config/dhcp" > "$HOSTFILE_TMP" + echo "# auto-generated config file from /etc/config/dhcp" > $CONFIGFILE_TMP + echo "# auto-generated config file from /etc/config/dhcp" > $HOSTFILE_TMP local dnsmasqconffile="/etc/dnsmasq.${cfg}.conf" if [ ! -r "$dnsmasqconffile" ]; then @@ -1015,14 +938,11 @@ dnsmasq_start() append_bool "$cfg" rapidcommit "--dhcp-rapid-commit" append_bool "$cfg" scriptarp "--script-arp" - # deprecate or remove filter-X in favor of filter-rr? append_bool "$cfg" filter_aaaa "--filter-AAAA" append_bool "$cfg" filter_a "--filter-A" - config_list_foreach "$cfg" filter_rr append_filter_rr - config_list_foreach "$cfg" cache_rr append_cache_rr append_parm "$cfg" logfacility "--log-facility" - config_get logfacility "$cfg" "logfacility" + append_parm "$cfg" cachesize "--cache-size" append_parm "$cfg" dnsforwardmax "--dns-forward-max" append_parm "$cfg" port "--port" @@ -1037,6 +957,7 @@ dnsmasq_start() config_list_foreach "$cfg" "server" append_server config_list_foreach "$cfg" "rev_server" append_rev_server config_list_foreach "$cfg" "address" append_address + config_list_foreach "$cfg" "ipset" append_ipset local connmark_allowlist_enable config_get connmark_allowlist_enable "$cfg" connmark_allowlist_enable 0 @@ -1060,14 +981,7 @@ dnsmasq_start() config_list_foreach "$cfg" "addnhosts" append_addnhosts config_list_foreach "$cfg" "bogusnxdomain" append_bogusnxdomain append_parm "$cfg" "leasefile" "--dhcp-leasefile" "/tmp/dhcp.leases" - - local serversfile - config_get serversfile "$cfg" "serversfile" - [ -n "$serversfile" ] && { - xappend "--servers-file=$serversfile" - append EXTRA_MOUNT "$serversfile" - } - + append_parm "$cfg" "serversfile" "--servers-file" append_parm "$cfg" "tftp_root" "--tftp-root" append_parm "$cfg" "dhcp_boot" "--dhcp-boot" append_parm "$cfg" "local_ttl" "--local-ttl" @@ -1104,7 +1018,7 @@ dnsmasq_start() config_get resolvfile "$cfg" resolvfile /tmp/resolv.conf.d/resolv.conf.auto [ -n "$resolvfile" ] && [ ! -e "$resolvfile" ] && touch "$resolvfile" xappend "--resolv-file=$resolvfile" - [ "$resolvfile" != "/tmp/resolv.conf.d/resolv.conf.auto" ] && localuse=0 + [ "$resolvfile" = "/tmp/resolv.conf.d/resolv.conf.auto" ] && localuse=1 resolvdir="$(dirname "$resolvfile")" fi config_get_bool localuse "$cfg" localuse "$localuse" @@ -1153,9 +1067,6 @@ dnsmasq_start() [ "$addmac" = "1" ] && addmac= xappend "--add-mac${addmac:+="$addmac"}" } - append_bool "$cfg" stripmac "--strip-mac" - append_parm "$cfg" addsubnet "--add-subnet" - append_bool "$cfg" stripsubnet "--strip-subnet" dhcp_option_add "$cfg" "" 0 dhcp_option_add "$cfg" "" 2 @@ -1169,7 +1080,7 @@ dnsmasq_start() [ ! -d "$dnsmasqconfdir" ] && mkdir -p $dnsmasqconfdir xappend "--user=dnsmasq" xappend "--group=dnsmasq" - echo >> "$CONFIGFILE_TMP" + echo >> $CONFIGFILE_TMP config_get_bool enable_tftp "$cfg" enable_tftp 0 [ "$enable_tftp" -gt 0 ] && { @@ -1178,7 +1089,7 @@ dnsmasq_start() } config_foreach filter_dnsmasq host dhcp_host_add "$cfg" - echo >> "$CONFIGFILE_TMP" + echo >> $CONFIGFILE_TMP config_get_bool dhcpbogushostname "$cfg" dhcpbogushostname 1 [ "$dhcpbogushostname" -gt 0 ] && { @@ -1197,13 +1108,12 @@ dnsmasq_start() config_foreach filter_dnsmasq match dhcp_match_add "$cfg" config_foreach filter_dnsmasq domain dhcp_domain_add "$cfg" config_foreach filter_dnsmasq hostrecord dhcp_hostrecord_add "$cfg" - config_foreach filter_dnsmasq dnsrr dhcp_dnsrr_add "$cfg" [ -n "$BOOT" ] || config_foreach filter_dnsmasq relay dhcp_relay_add "$cfg" - echo >> "$CONFIGFILE_TMP" + echo >> $CONFIGFILE_TMP config_foreach filter_dnsmasq srvhost dhcp_srv_add "$cfg" config_foreach filter_dnsmasq mxhost dhcp_mx_add "$cfg" - echo >> "$CONFIGFILE_TMP" + echo >> $CONFIGFILE_TMP config_get_bool boguspriv "$cfg" boguspriv 1 [ "$boguspriv" -gt 0 ] && { @@ -1225,16 +1135,16 @@ dnsmasq_start() fi - echo >> "$CONFIGFILE_TMP" + echo >> $CONFIGFILE_TMP config_foreach filter_dnsmasq cname dhcp_cname_add "$cfg" - echo >> "$CONFIGFILE_TMP" + echo >> $CONFIGFILE_TMP - echo >> "$CONFIGFILE_TMP" + echo >> $CONFIGFILE_TMP config_foreach filter_dnsmasq ipset dnsmasq_ipset_add "$cfg" - echo >> "$CONFIGFILE_TMP" + echo >> $CONFIGFILE_TMP - mv -f "$CONFIGFILE_TMP" "$CONFIGFILE" - mv -f "$HOSTFILE_TMP" "$HOSTFILE" + mv -f $CONFIGFILE_TMP $CONFIGFILE + mv -f $HOSTFILE_TMP $HOSTFILE [ "$localuse" -gt 0 ] && { rm -f /tmp/resolv.conf @@ -1248,30 +1158,18 @@ dnsmasq_start() done } - config_list_foreach "$cfg" addnmount append_extramount - procd_open_instance $cfg procd_set_param command $PROG -C $CONFIGFILE -k -x /var/run/dnsmasq/dnsmasq."${cfg}".pid procd_set_param file $CONFIGFILE [ -n "$user_dhcpscript" ] && procd_set_param env USER_DHCPSCRIPT="$user_dhcpscript" procd_set_param respawn - local instance_ifc instance_netdev - config_get instance_ifc "$cfg" interface - [ -n "$instance_ifc" ] && network_get_device instance_netdev "$instance_ifc" && - [ -n "$instance_netdev" ] && procd_set_param netdev $instance_netdev - procd_add_jail dnsmasq ubus log procd_add_jail_mount $CONFIGFILE $DHCPBOGUSHOSTNAMEFILE $DHCPSCRIPT $DHCPSCRIPT_DEPENDS procd_add_jail_mount $EXTRA_MOUNT $RFC6761FILE $TRUSTANCHORSFILE procd_add_jail_mount $dnsmasqconffile $dnsmasqconfdir $resolvdir $user_dhcpscript procd_add_jail_mount /etc/passwd /etc/group /etc/TZ /etc/hosts /etc/ethers procd_add_jail_mount_rw /var/run/dnsmasq/ $leasefile - case "$logfacility" in */*) - [ ! -e "$logfacility" ] && touch "$logfacility" - procd_add_jail_mount_rw "$logfacility" - esac - [ -e "$hostsfile" ] && procd_add_jail_mount $hostsfile procd_close_instance } @@ -1279,12 +1177,12 @@ dnsmasq_start() dnsmasq_stop() { local cfg="$1" - local noresolv resolvfile localuse=1 + local noresolv resolvfile localuse=0 config_get_bool noresolv "$cfg" noresolv 0 config_get resolvfile "$cfg" "resolvfile" - [ "$noresolv" = 0 ] && [ "$resolvfile" != "/tmp/resolv.conf.d/resolv.conf.auto" ] && localuse=0 + [ "$noresolv" = 0 ] && [ "$resolvfile" = "/tmp/resolv.conf.d/resolv.conf.auto" ] && localuse=1 config_get_bool localuse "$cfg" localuse "$localuse" [ "$localuse" -gt 0 ] && ln -sf "/tmp/resolv.conf.d/resolv.conf.auto" /tmp/resolv.conf @@ -1293,11 +1191,10 @@ dnsmasq_stop() add_interface_trigger() { - local interface ifname ignore + local interface ignore config_get interface "$1" interface config_get_bool ignore "$1" ignore 0 - network_get_device ifname "$interface" || ignore=0 [ -n "$interface" ] && [ $ignore -eq 0 ] && procd_add_interface_trigger "interface.*" "$interface" /etc/init.d/dnsmasq reload } diff --git a/lede/package/network/services/dnsmasq/patches/200-ubus_dns.patch b/lede/package/network/services/dnsmasq/patches/200-ubus_dns.patch index a1a668818e..21e9e57c9c 100644 --- a/lede/package/network/services/dnsmasq/patches/200-ubus_dns.patch +++ b/lede/package/network/services/dnsmasq/patches/200-ubus_dns.patch @@ -275,4 +275,4 @@ + void ubus_event_bcast(const char *type, const char *mac, const char *ip, const char *name, const char *interface) { - struct ubus_context *ubus = (struct ubus_context *)daemon->ubus; + struct ubus_context *ubus = (struct ubus_context *)daemon->ubus; \ No newline at end of file diff --git a/mihomo/component/resolver/local.go b/mihomo/component/resolver/service.go similarity index 73% rename from mihomo/component/resolver/local.go rename to mihomo/component/resolver/service.go index e8505118b4..8b8f115842 100644 --- a/mihomo/component/resolver/local.go +++ b/mihomo/component/resolver/service.go @@ -6,15 +6,15 @@ import ( D "github.com/miekg/dns" ) -var DefaultLocalServer LocalServer +var DefaultService Service -type LocalServer interface { +type Service interface { ServeMsg(ctx context.Context, msg *D.Msg) (*D.Msg, error) } // ServeMsg with a dns.Msg, return resolve dns.Msg func ServeMsg(ctx context.Context, msg *D.Msg) (*D.Msg, error) { - if server := DefaultLocalServer; server != nil { + if server := DefaultService; server != nil { return server.ServeMsg(ctx, msg) } diff --git a/mihomo/config/config.go b/mihomo/config/config.go index bcd3cc7ad2..dec0955b2d 100644 --- a/mihomo/config/config.go +++ b/mihomo/config/config.go @@ -146,6 +146,7 @@ type DNS struct { PreferH3 bool IPv6 bool IPv6Timeout uint + UseHosts bool UseSystemHosts bool NameServer []dns.NameServer Fallback []dns.NameServer @@ -157,7 +158,6 @@ type DNS struct { CacheAlgorithm string CacheMaxSize int FakeIPRange *fakeip.Pool - Hosts *trie.DomainTrie[resolver.HostValue] NameServerPolicy []dns.Policy ProxyServerNameserver []dns.NameServer DirectNameServer []dns.NameServer @@ -680,7 +680,7 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { } config.Hosts = hosts - dnsCfg, err := parseDNS(rawCfg, hosts, ruleProviders) + dnsCfg, err := parseDNS(rawCfg, ruleProviders) if err != nil { return nil, err } @@ -1341,7 +1341,7 @@ func parseNameServerPolicy(nsPolicy *orderedmap.OrderedMap[string, any], rulePro return policy, nil } -func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[resolver.HostValue], ruleProviders map[string]providerTypes.RuleProvider) (*DNS, error) { +func parseDNS(rawCfg *RawConfig, ruleProviders map[string]providerTypes.RuleProvider) (*DNS, error) { cfg := rawCfg.DNS if cfg.Enable && len(cfg.NameServer) == 0 { return nil, fmt.Errorf("if DNS configuration is turned on, NameServer cannot be empty") @@ -1357,6 +1357,7 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[resolver.HostValue], rul PreferH3: cfg.PreferH3, IPv6Timeout: cfg.IPv6Timeout, IPv6: cfg.IPv6, + UseHosts: cfg.UseHosts, UseSystemHosts: cfg.UseSystemHosts, EnhancedMode: cfg.EnhancedMode, CacheAlgorithm: cfg.CacheAlgorithm, @@ -1490,10 +1491,6 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[resolver.HostValue], rul } } - if cfg.UseHosts { - dnsCfg.Hosts = hosts - } - return dnsCfg, nil } diff --git a/mihomo/context/dns.go b/mihomo/context/dns.go index 1cc2067d8d..15143102ca 100644 --- a/mihomo/context/dns.go +++ b/mihomo/context/dns.go @@ -2,10 +2,10 @@ package context import ( "context" + "github.com/metacubex/mihomo/common/utils" "github.com/gofrs/uuid/v5" - "github.com/miekg/dns" ) const ( @@ -17,17 +17,15 @@ const ( type DNSContext struct { context.Context - id uuid.UUID - msg *dns.Msg - tp string + id uuid.UUID + tp string } -func NewDNSContext(ctx context.Context, msg *dns.Msg) *DNSContext { +func NewDNSContext(ctx context.Context) *DNSContext { return &DNSContext{ Context: ctx, - id: utils.NewUUIDV4(), - msg: msg, + id: utils.NewUUIDV4(), } } diff --git a/mihomo/dns/enhancer.go b/mihomo/dns/enhancer.go index 9ea3ae84ac..36923ca624 100644 --- a/mihomo/dns/enhancer.go +++ b/mihomo/dns/enhancer.go @@ -12,6 +12,7 @@ type ResolverEnhancer struct { mode C.DNSMode fakePool *fakeip.Pool mapping *lru.LruCache[netip.Addr, string] + useHosts bool } func (h *ResolverEnhancer) FakeIPEnabled() bool { @@ -103,7 +104,13 @@ func (h *ResolverEnhancer) StoreFakePoolState() { } } -func NewEnhancer(cfg Config) *ResolverEnhancer { +type EnhancerConfig struct { + EnhancedMode C.DNSMode + Pool *fakeip.Pool + UseHosts bool +} + +func NewEnhancer(cfg EnhancerConfig) *ResolverEnhancer { var fakePool *fakeip.Pool var mapping *lru.LruCache[netip.Addr, string] @@ -116,5 +123,6 @@ func NewEnhancer(cfg Config) *ResolverEnhancer { mode: cfg.EnhancedMode, fakePool: fakePool, mapping: mapping, + useHosts: cfg.UseHosts, } } diff --git a/mihomo/dns/local.go b/mihomo/dns/local.go deleted file mode 100644 index 37b5d41b04..0000000000 --- a/mihomo/dns/local.go +++ /dev/null @@ -1,20 +0,0 @@ -package dns - -import ( - "context" - - D "github.com/miekg/dns" -) - -type LocalServer struct { - handler handler -} - -// ServeMsg implement resolver.LocalServer ResolveMsg -func (s *LocalServer) ServeMsg(ctx context.Context, msg *D.Msg) (*D.Msg, error) { - return handlerWithContext(ctx, s.handler, msg) -} - -func NewLocalServer(resolver *Resolver, mapper *ResolverEnhancer) *LocalServer { - return &LocalServer{handler: NewHandler(resolver, mapper)} -} diff --git a/mihomo/dns/middleware.go b/mihomo/dns/middleware.go index e6461e91a3..502a37e5e5 100644 --- a/mihomo/dns/middleware.go +++ b/mihomo/dns/middleware.go @@ -7,22 +7,22 @@ import ( "github.com/metacubex/mihomo/common/lru" "github.com/metacubex/mihomo/component/fakeip" - R "github.com/metacubex/mihomo/component/resolver" + "github.com/metacubex/mihomo/component/resolver" C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/context" + icontext "github.com/metacubex/mihomo/context" "github.com/metacubex/mihomo/log" D "github.com/miekg/dns" ) type ( - handler func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) + handler func(ctx *icontext.DNSContext, r *D.Msg) (*D.Msg, error) middleware func(next handler) handler ) -func withHosts(hosts R.Hosts, mapping *lru.LruCache[netip.Addr, string]) middleware { +func withHosts(mapping *lru.LruCache[netip.Addr, string]) middleware { return func(next handler) handler { - return func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) { + return func(ctx *icontext.DNSContext, r *D.Msg) (*D.Msg, error) { q := r.Question[0] if !isIPRequest(q) { @@ -36,7 +36,7 @@ func withHosts(hosts R.Hosts, mapping *lru.LruCache[netip.Addr, string]) middlew rr.Target = domain + "." resp.Answer = append([]D.RR{rr}, resp.Answer...) } - record, ok := hosts.Search(host, q.Qtype != D.TypeA && q.Qtype != D.TypeAAAA) + record, ok := resolver.DefaultHosts.Search(host, q.Qtype != D.TypeA && q.Qtype != D.TypeAAAA) if !ok { if record != nil && record.IsDomain { // replace request domain @@ -88,7 +88,7 @@ func withHosts(hosts R.Hosts, mapping *lru.LruCache[netip.Addr, string]) middlew return next(ctx, r) } - ctx.SetType(context.DNSTypeHost) + ctx.SetType(icontext.DNSTypeHost) msg.SetRcode(r, D.RcodeSuccess) msg.Authoritative = true msg.RecursionAvailable = true @@ -99,7 +99,7 @@ func withHosts(hosts R.Hosts, mapping *lru.LruCache[netip.Addr, string]) middlew func withMapping(mapping *lru.LruCache[netip.Addr, string]) middleware { return func(next handler) handler { - return func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) { + return func(ctx *icontext.DNSContext, r *D.Msg) (*D.Msg, error) { q := r.Question[0] if !isIPRequest(q) { @@ -149,7 +149,7 @@ func withMapping(mapping *lru.LruCache[netip.Addr, string]) middleware { func withFakeIP(fakePool *fakeip.Pool) middleware { return func(next handler) handler { - return func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) { + return func(ctx *icontext.DNSContext, r *D.Msg) (*D.Msg, error) { q := r.Question[0] host := strings.TrimRight(q.Name, ".") @@ -173,7 +173,7 @@ func withFakeIP(fakePool *fakeip.Pool) middleware { msg := r.Copy() msg.Answer = []D.RR{rr} - ctx.SetType(context.DNSTypeFakeIP) + ctx.SetType(icontext.DNSTypeFakeIP) setMsgTTL(msg, 1) msg.SetRcode(r, D.RcodeSuccess) msg.Authoritative = true @@ -185,8 +185,8 @@ func withFakeIP(fakePool *fakeip.Pool) middleware { } func withResolver(resolver *Resolver) handler { - return func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) { - ctx.SetType(context.DNSTypeRaw) + return func(ctx *icontext.DNSContext, r *D.Msg) (*D.Msg, error) { + ctx.SetType(icontext.DNSTypeRaw) q := r.Question[0] @@ -218,11 +218,11 @@ func compose(middlewares []middleware, endpoint handler) handler { return h } -func NewHandler(resolver *Resolver, mapper *ResolverEnhancer) handler { - middlewares := []middleware{} +func newHandler(resolver *Resolver, mapper *ResolverEnhancer) handler { + var middlewares []middleware - if resolver.hosts != nil { - middlewares = append(middlewares, withHosts(R.NewHosts(resolver.hosts), mapper.mapping)) + if mapper.useHosts { + middlewares = append(middlewares, withHosts(mapper.mapping)) } if mapper.mode == C.DNSFakeIP { diff --git a/mihomo/dns/resolver.go b/mihomo/dns/resolver.go index f5f69c5f4a..f7d4d42968 100644 --- a/mihomo/dns/resolver.go +++ b/mihomo/dns/resolver.go @@ -9,7 +9,6 @@ import ( "github.com/metacubex/mihomo/common/arc" "github.com/metacubex/mihomo/common/lru" "github.com/metacubex/mihomo/common/singleflight" - "github.com/metacubex/mihomo/component/fakeip" "github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/component/trie" C "github.com/metacubex/mihomo/constant" @@ -40,7 +39,6 @@ type result struct { type Resolver struct { ipv6 bool ipv6Timeout time.Duration - hosts *trie.DomainTrie[resolver.HostValue] main []dnsClient fallback []dnsClient fallbackDomainFilters []C.DomainMatcher @@ -452,11 +450,8 @@ type Config struct { DirectFollowPolicy bool IPv6 bool IPv6Timeout uint - EnhancedMode C.DNSMode FallbackIPFilter []C.IpMatcher FallbackDomainFilter []C.DomainMatcher - Pool *fakeip.Pool - Hosts *trie.DomainTrie[resolver.HostValue] Policy []Policy CacheAlgorithm string CacheMaxSize int @@ -530,7 +525,6 @@ func NewResolver(config Config) (rs Resolvers) { ipv6: config.IPv6, main: cacheTransform(config.Main), cache: config.newCache(), - hosts: config.Hosts, ipv6Timeout: time.Duration(config.IPv6Timeout) * time.Millisecond, } r.defaultResolver = defaultResolver @@ -541,7 +535,6 @@ func NewResolver(config Config) (rs Resolvers) { ipv6: config.IPv6, main: cacheTransform(config.ProxyServer), cache: config.newCache(), - hosts: config.Hosts, ipv6Timeout: time.Duration(config.IPv6Timeout) * time.Millisecond, } } @@ -551,7 +544,6 @@ func NewResolver(config Config) (rs Resolvers) { ipv6: config.IPv6, main: cacheTransform(config.DirectServer), cache: config.newCache(), - hosts: config.Hosts, ipv6Timeout: time.Duration(config.IPv6Timeout) * time.Millisecond, } } diff --git a/mihomo/dns/server.go b/mihomo/dns/server.go index caf1c2891a..b1224c6212 100644 --- a/mihomo/dns/server.go +++ b/mihomo/dns/server.go @@ -1,13 +1,12 @@ package dns import ( - stdContext "context" - "errors" + "context" "net" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/common/sockopt" - "github.com/metacubex/mihomo/context" + "github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/log" D "github.com/miekg/dns" @@ -21,39 +20,32 @@ var ( ) type Server struct { - handler handler + service resolver.Service tcpServer *D.Server udpServer *D.Server } // ServeDNS implement D.Handler ServeDNS func (s *Server) ServeDNS(w D.ResponseWriter, r *D.Msg) { - msg, err := handlerWithContext(stdContext.Background(), s.handler, r) + msg, err := s.service.ServeMsg(context.Background(), r) if err != nil { - D.HandleFailed(w, r) + m := new(D.Msg) + m.SetRcode(r, D.RcodeServerFailure) + // does not matter if this write fails + w.WriteMsg(m) return } msg.Compress = true w.WriteMsg(msg) } -func handlerWithContext(stdCtx stdContext.Context, handler handler, msg *D.Msg) (*D.Msg, error) { - if len(msg.Question) == 0 { - return nil, errors.New("at least one question is required") - } - - ctx := context.NewDNSContext(stdCtx, msg) - return handler(ctx, msg) +func (s *Server) SetService(service resolver.Service) { + s.service = service } -func (s *Server) SetHandler(handler handler) { - s.handler = handler -} - -func ReCreateServer(addr string, resolver *Resolver, mapper *ResolverEnhancer) { - if addr == address && resolver != nil { - handler := NewHandler(resolver, mapper) - server.SetHandler(handler) +func ReCreateServer(addr string, service resolver.Service) { + if addr == address && service != nil { + server.SetService(service) return } @@ -67,10 +59,10 @@ func ReCreateServer(addr string, resolver *Resolver, mapper *ResolverEnhancer) { server.udpServer = nil } - server.handler = nil + server.service = nil address = "" - if addr == "" { + if addr == "" || service == nil { return } @@ -87,8 +79,7 @@ func ReCreateServer(addr string, resolver *Resolver, mapper *ResolverEnhancer) { } address = addr - handler := NewHandler(resolver, mapper) - server = &Server{handler: handler} + server = &Server{service: service} go func() { p, err := inbound.ListenPacket("udp", addr) diff --git a/mihomo/dns/service.go b/mihomo/dns/service.go new file mode 100644 index 0000000000..4a7c1bb2ea --- /dev/null +++ b/mihomo/dns/service.go @@ -0,0 +1,29 @@ +package dns + +import ( + "context" + "errors" + + "github.com/metacubex/mihomo/component/resolver" + icontext "github.com/metacubex/mihomo/context" + D "github.com/miekg/dns" +) + +type Service struct { + handler handler +} + +// ServeMsg implement [resolver.Service] ResolveMsg +func (s *Service) ServeMsg(ctx context.Context, msg *D.Msg) (*D.Msg, error) { + if len(msg.Question) == 0 { + return nil, errors.New("at least one question is required") + } + + return s.handler(icontext.NewDNSContext(ctx), msg) +} + +var _ resolver.Service = (*Service)(nil) + +func NewService(resolver *Resolver, mapper *ResolverEnhancer) *Service { + return &Service{handler: newHandler(resolver, mapper)} +} diff --git a/mihomo/hub/executor/executor.go b/mihomo/hub/executor/executor.go index fcf176e0e0..041e6fc36b 100644 --- a/mihomo/hub/executor/executor.go +++ b/mihomo/hub/executor/executor.go @@ -240,20 +240,18 @@ func updateDNS(c *config.DNS, generalIPv6 bool) { if !c.Enable { resolver.DefaultResolver = nil resolver.DefaultHostMapper = nil - resolver.DefaultLocalServer = nil + resolver.DefaultService = nil resolver.ProxyServerHostResolver = nil resolver.DirectHostResolver = nil - dns.ReCreateServer("", nil, nil) + dns.ReCreateServer("", nil) return } - cfg := dns.Config{ + + r := dns.NewResolver(dns.Config{ Main: c.NameServer, Fallback: c.Fallback, IPv6: c.IPv6 && generalIPv6, IPv6Timeout: c.IPv6Timeout, - EnhancedMode: c.EnhancedMode, - Pool: c.FakeIPRange, - Hosts: c.Hosts, FallbackIPFilter: c.FallbackIPFilter, FallbackDomainFilter: c.FallbackDomainFilter, Default: c.DefaultNameserver, @@ -263,19 +261,23 @@ func updateDNS(c *config.DNS, generalIPv6 bool) { DirectFollowPolicy: c.DirectFollowPolicy, CacheAlgorithm: c.CacheAlgorithm, CacheMaxSize: c.CacheMaxSize, - } - - r := dns.NewResolver(cfg) - m := dns.NewEnhancer(cfg) + }) + m := dns.NewEnhancer(dns.EnhancerConfig{ + EnhancedMode: c.EnhancedMode, + Pool: c.FakeIPRange, + UseHosts: c.UseHosts, + }) // reuse cache of old host mapper if old := resolver.DefaultHostMapper; old != nil { m.PatchFrom(old.(*dns.ResolverEnhancer)) } + s := dns.NewService(r.Resolver, m) + resolver.DefaultResolver = r resolver.DefaultHostMapper = m - resolver.DefaultLocalServer = dns.NewLocalServer(r.Resolver, m) + resolver.DefaultService = s resolver.UseSystemHosts = c.UseSystemHosts if r.ProxyResolver.Invalid() { @@ -290,7 +292,7 @@ func updateDNS(c *config.DNS, generalIPv6 bool) { resolver.DirectHostResolver = r.Resolver } - dns.ReCreateServer(c.Listen, r.Resolver, m) + dns.ReCreateServer(c.Listen, s) } func updateHosts(tree *trie.DomainTrie[resolver.HostValue]) { diff --git a/openwrt-packages/adguardhome/Makefile b/openwrt-packages/adguardhome/Makefile index d672bb213a..eef2c1d5b1 100644 --- a/openwrt-packages/adguardhome/Makefile +++ b/openwrt-packages/adguardhome/Makefile @@ -6,12 +6,12 @@ include $(TOPDIR)/rules.mk PKG_NAME:=adguardhome -PKG_VERSION:=0.107.66 +PKG_VERSION:=0.107.67 PKG_RELEASE:=1 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/AdguardTeam/AdGuardHome/tar.gz/v$(PKG_VERSION)? -PKG_HASH:=823ccfed64b1472e9f4a23867e892a681642b2b3a6e64e285dbe22a57a384d84 +PKG_HASH:=0d74004fd17c8f185174fa09deb130ad48e2f46e946eb9fa8c66ce186d2af9cf PKG_BUILD_DIR:=$(BUILD_DIR)/AdGuardHome-$(PKG_VERSION) PKG_LICENSE:=GPL-3.0-only @@ -58,7 +58,7 @@ define Download/adguardhome-frontend URL:=https://github.com/AdguardTeam/AdGuardHome/releases/download/v$(PKG_VERSION)/ URL_FILE:=AdGuardHome_frontend.tar.gz FILE:=$(FRONTEND_FILE) - HASH:=18ead3a9a0c710a05d63a3f967795709120a8f50e8938462860022ada3c950e4 + HASH:=8709396e05f812f3e2085a64074384b6363fe1871b9bbb7e8f9886c1aa64b579 endef define Build/Prepare diff --git a/small/luci-app-fchomo/htdocs/luci-static/resources/fchomo.js b/small/luci-app-fchomo/htdocs/luci-static/resources/fchomo.js index bcfa13b060..be786084a8 100644 --- a/small/luci-app-fchomo/htdocs/luci-static/resources/fchomo.js +++ b/small/luci-app-fchomo/htdocs/luci-static/resources/fchomo.js @@ -356,11 +356,11 @@ const vless_flow = [ /* Prototype */ const CBIGridSection = form.GridSection.extend({ modaltitle(/* ... */) { - return loadModalTitle.call(this, ...this.hm_modaltitle || [null,null], ...arguments) + return loadModalTitle.call(this, ...this.hm_modaltitle || [null,null], ...arguments); }, sectiontitle(/* ... */) { - return loadDefaultLabel.call(this, ...arguments); + return loadDefaultLabel.apply(this, arguments); }, renderSectionAdd(extra_class) { @@ -1184,28 +1184,6 @@ function textvalue2Value(section_id) { return this.vallist[i]; } -function validatePresetIDs(disoption_list, section_id) { - let node; - let hm_prefmt = glossary[this.section.sectiontype].prefmt; - let preset_ids = [ - 'fchomo_direct_list', - 'fchomo_proxy_list', - 'fchomo_china_list', - 'fchomo_gfw_list' - ]; - - if (preset_ids.map((v) => hm_prefmt.format(v)).includes(section_id)) { - disoption_list.forEach(([typ, opt]) => { - node = this.section.getUIElement(section_id, opt)?.node; - (typ ? node?.querySelector(typ) : node)?.setAttribute(typ === 'textarea' ? 'readOnly' : 'disabled', ''); - }); - - this.map.findElement('id', 'cbi-fchomo-' + section_id)?.lastChild.querySelector('.cbi-button-remove')?.remove(); - } - - return true; -} - function validateAuth(section_id, value) { if (!value) return true; @@ -1270,59 +1248,6 @@ function validateCommonPort(section_id, value) { return true; } -function validateJson(section_id, value) { - if (!value) - return true; - - try { - let obj = JSON.parse(value.trim()); - if (!obj) - return _('Expecting: %s').format(_('valid JSON format')); - } - catch(e) { - return _('Expecting: %s').format(_('valid JSON format')); - } - - return true; -} - -function validateMTLSClientAuth(type_option, section_id, value) { - // If `client-auth-type` is set to "verify-if-given" or "require-and-verify", `client-auth-cert` must not be empty. - const auth_type = this.section.getOption(type_option).formvalue(section_id); - //this.section.getUIElement('tls_client_auth_type').getValue(); - if (!value && ["verify-if-given", "require-and-verify"].includes(auth_type)) - return _('Expecting: %s').format(_('non-empty value')); - - return true; -} - -function validateBase64Key(length, section_id, value) { - /* Thanks to luci-proto-wireguard */ - if (value) - if (value.length !== length || !value.match(/^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/) || value[length-1] !== '=') - return _('Expecting: %s').format(_('valid base64 key with %d characters').format(length)); - - return true; -} - -function validateShadowsocksPassword(encmode, section_id, value) { - let length = shadowsocks_cipher_length[encmode]; - if (typeof length !== 'undefined') { - length = Math.ceil(length/3)*4; - if (encmode.match(/^2022-/)) { - return validateBase64Key(length, section_id, value); - } else { - if (length === 0 && !value) - return _('Expecting: %s').format(_('non-empty value')); - if (length !== 0 && value.length !== length) - return _('Expecting: %s').format(_('valid key length with %d characters').format(length)); - } - } else - return true; - - return true; -} - function validateBytesize(section_id, value) { if (!value) return true; @@ -1342,18 +1267,27 @@ function validateTimeDuration(section_id, value) { return true; } -function validateUniqueValue(section_id, value) { +function validateJson(section_id, value) { if (!value) - return _('Expecting: %s').format(_('non-empty value')); + return true; - let duplicate = false; - uci.sections(this.config, this.section.sectiontype, (res) => { - if (res['.name'] !== section_id) - if (res[this.option] === value) - duplicate = true; - }); - if (duplicate) - return _('Expecting: %s').format(_('unique value')); + try { + let obj = JSON.parse(value.trim()); + if (!obj) + return _('Expecting: %s').format(_('valid JSON format')); + } + catch(e) { + return _('Expecting: %s').format(_('valid JSON format')); + } + + return true; +} + +function validateUUID(section_id, value) { + if (!value) + return true; + else if (value.match('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') === null) + return _('Expecting: %s').format(_('valid uuid')); return true; } @@ -1374,11 +1308,77 @@ function validateUrl(section_id, value) { return true; } -function validateUUID(section_id, value) { - if (!value) +function validateBase64Key(length, section_id, value) { + /* Thanks to luci-proto-wireguard */ + if (value) + if (value.length !== length || !value.match(/^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/) || value[length-1] !== '=') + return _('Expecting: %s').format(_('valid base64 key with %d characters').format(length)); + + return true; +} + +function validateMTLSClientAuth(type_option, section_id, value) { + // If `client-auth-type` is set to "verify-if-given" or "require-and-verify", `client-auth-cert` must not be empty. + const auth_type = this.section.getOption(type_option).formvalue(section_id); + //this.section.getUIElement('tls_client_auth_type').getValue(); + if (!value && ["verify-if-given", "require-and-verify"].includes(auth_type)) + return _('Expecting: %s').format(_('non-empty value')); + + return true; +} + +function validatePresetIDs(disoption_list, section_id) { + let node; + let hm_prefmt = glossary[this.section.sectiontype].prefmt; + let preset_ids = [ + 'fchomo_direct_list', + 'fchomo_proxy_list', + 'fchomo_china_list', + 'fchomo_gfw_list' + ]; + + if (preset_ids.map((v) => hm_prefmt.format(v)).includes(section_id)) { + disoption_list.forEach(([typ, opt]) => { + node = this.section.getUIElement(section_id, opt)?.node; + (typ ? node?.querySelector(typ) : node)?.setAttribute(typ === 'textarea' ? 'readOnly' : 'disabled', ''); + }); + + this.map.findElement('id', 'cbi-fchomo-' + section_id)?.lastChild.querySelector('.cbi-button-remove')?.remove(); + } + + return true; +} + +function validateShadowsocksPassword(encmode, section_id, value) { + let length = shadowsocks_cipher_length[encmode]; + if (typeof length !== 'undefined') { + length = Math.ceil(length/3)*4; + if (encmode.match(/^2022-/)) { + return validateBase64Key.call(this, length, section_id, value); + } else { + if (length === 0 && !value) + return _('Expecting: %s').format(_('non-empty value')); + if (length !== 0 && value.length !== length) + return _('Expecting: %s').format(_('valid key length with %d characters').format(length)); + } + } else return true; - else if (value.match('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') === null) - return _('Expecting: %s').format(_('valid uuid')); + + return true; +} + +function validateUniqueValue(section_id, value) { + if (!value) + return _('Expecting: %s').format(_('non-empty value')); + + let duplicate = false; + uci.sections(this.config, this.section.sectiontype, (res) => { + if (res['.name'] !== section_id) + if (res[this.option] === value) + duplicate = true; + }); + if (duplicate) + return _('Expecting: %s').format(_('unique value')); return true; } @@ -1563,6 +1563,7 @@ return baseclass.extend({ getFeatures, getServiceStatus, getClashAPI, + // load loadDefaultLabel, loadModalTitle, loadProxyGroupLabel, @@ -1570,6 +1571,7 @@ return baseclass.extend({ loadProviderLabel, loadRulesetLabel, loadSubRuleGroup, + // render renderStatus, updateStatus, getDashURL, @@ -1578,20 +1580,23 @@ return baseclass.extend({ handleReload, handleRemoveIdles, textvalue2Value, - validatePresetIDs, + // validate validateAuth, validateAuthUsername, validateAuthPassword, validateCommonPort, - validateJson, - validateMTLSClientAuth, - validateBase64Key, - validateShadowsocksPassword, validateBytesize, validateTimeDuration, - validateUniqueValue, - validateUrl, + validateJson, validateUUID, + validateUrl, + // validate with bind this + validateBase64Key, + validateMTLSClientAuth, + validatePresetIDs, + validateShadowsocksPassword, + validateUniqueValue, + // file operations lsDir, readFile, writeFile, diff --git a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/client.js b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/client.js index 65dcf6b669..3719e6bd7b 100644 --- a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/client.js +++ b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/client.js @@ -658,8 +658,8 @@ function renderRules(s, uciconfig) { o.load = function(section_id) { return form.DummyValue.prototype.load.call(this, section_id) || new RulesEntry().toString('json'); } - o.write = L.bind(form.AbstractValue.prototype.write, o); - o.remove = L.bind(form.AbstractValue.prototype.remove, o); + o.write = form.AbstractValue.prototype.write; + o.remove = form.AbstractValue.prototype.remove; o.editable = true; o = s.option(form.ListValue, 'type', _('Type')); @@ -894,7 +894,7 @@ return view.extend({ /* General fields */ so = ss.taboption('field_general', form.Value, 'label', _('Label')); - so.load = L.bind(hm.loadDefaultLabel, so); + so.load = hm.loadDefaultLabel; so.validate = function(section_id, value) { if (value.match(/[,]/)) return _('Expecting: %s').format(_('not included ","')); @@ -975,14 +975,14 @@ return view.extend({ hm.health_checkurls.forEach((res) => { so.value.apply(so, res); }) - so.validate = L.bind(hm.validateUrl, so); + so.validate = hm.validateUrl; so.depends({type: 'select', '!reverse': true}); so.modalonly = true; so = ss.taboption('field_health', form.Value, 'interval', _('Health check interval'), _('In seconds. %s will be used if empty.').format('600')); so.placeholder = '600'; - so.validate = L.bind(hm.validateTimeDuration, so); + so.validate = hm.validateTimeDuration; so.depends({type: 'select', '!reverse': true}); so.modalonly = true; @@ -1128,14 +1128,14 @@ return view.extend({ /* Import mihomo config end */ so = ss.option(form.Value, 'label', _('Label')); - so.load = L.bind(hm.loadDefaultLabel, so); - so.validate = L.bind(hm.validateUniqueValue, so); + so.load = hm.loadDefaultLabel; + so.validate = hm.validateUniqueValue; so.modalonly = true; so = ss.option(form.Flag, 'enabled', _('Enable')); so.default = so.enabled; so.editable = true; - so.validate = function(section_id, value) { + so.validate = function(/* ... */) { let n = 0; return hm.validatePresetIDs.call(this, [ @@ -1224,8 +1224,8 @@ return view.extend({ /* Import mihomo config end */ so = ss.option(form.Value, 'label', _('Label')); - so.load = L.bind(hm.loadDefaultLabel, so); - so.validate = L.bind(hm.validateUniqueValue, so); + so.load = hm.loadDefaultLabel; + so.validate = hm.validateUniqueValue; so.modalonly = true; so = ss.option(form.Flag, 'enabled', _('Enable')); @@ -1235,7 +1235,7 @@ return view.extend({ so = ss.option(form.Value, 'group', _('Sub rule group')); so.value('sub-rule1'); so.rmempty = false; - so.validate = L.bind(hm.validateAuthUsername, so); + so.validate = hm.validateAuthUsername; so.editable = true; renderRules(ss, data[0]); @@ -1259,28 +1259,28 @@ return view.extend({ so = ss.option(form.MultiValue, 'boot_server', _('Bootstrap DNS server'), _('Used to resolve the domain of the DNS server. Must be IP.')); so.default = 'default-dns'; - so.load = L.bind(loadDNSServerLabel, so); - so.validate = L.bind(validateNameserver, so); + so.load = loadDNSServerLabel; + so.validate = validateNameserver; so.rmempty = false; so = ss.option(form.MultiValue, 'bootnode_server', _('Bootstrap DNS server (Node)'), _('Used to resolve the domain of the Proxy node.')); so.default = 'default-dns'; - so.load = L.bind(loadDNSServerLabel, so); - so.validate = L.bind(validateNameserver, so); + so.load = loadDNSServerLabel; + so.validate = validateNameserver; so.rmempty = false; so = ss.option(form.MultiValue, 'default_server', _('Default DNS server')); so.description = uci.get(data[0], so.section.section, 'fallback_server') ? _('Final DNS server (For non-poisoned domains)') : _('Final DNS server'); so.default = 'default-dns'; - so.load = L.bind(loadDNSServerLabel, so); - so.validate = L.bind(validateNameserver, so); + so.load = loadDNSServerLabel; + so.validate = validateNameserver; so.rmempty = false; so = ss.option(form.MultiValue, 'fallback_server', _('Fallback DNS server')); so.description = uci.get(data[0], so.section.section, 'fallback_server') ? _('Final DNS server (For poisoned domains)') : _('Fallback DNS server'); - so.load = L.bind(loadDNSServerLabel, so); - so.validate = L.bind(validateNameserver, so); + so.load = loadDNSServerLabel; + so.validate = validateNameserver; so.onchange = function(ev, section_id, value) { let ddesc = this.section.getUIElement(section_id, 'default_server').node.nextSibling; let fdesc = ev.target.nextSibling; @@ -1358,8 +1358,8 @@ return view.extend({ /* Import mihomo config end */ so = ss.option(form.Value, 'label', _('Label')); - so.load = L.bind(hm.loadDefaultLabel, so); - so.validate = L.bind(hm.validateUniqueValue, so); + so.load = hm.loadDefaultLabel; + so.validate = hm.validateUniqueValue; so.modalonly = true; so = ss.option(form.Flag, 'enabled', _('Enable')); @@ -1367,8 +1367,8 @@ return view.extend({ so.editable = true; so = ss.option(form.DummyValue, 'address', _('Address')); - so.write = L.bind(form.AbstractValue.prototype.write, so); - so.remove = L.bind(form.AbstractValue.prototype.remove, so); + so.write = form.AbstractValue.prototype.write; + so.remove = form.AbstractValue.prototype.remove; so.editable = true; so = ss.option(form.Value, 'addr', _('Address')); @@ -1580,14 +1580,14 @@ return view.extend({ /* Import mihomo config end */ so = ss.option(form.Value, 'label', _('Label')); - so.load = L.bind(hm.loadDefaultLabel, so); - so.validate = L.bind(hm.validateUniqueValue, so); + so.load = hm.loadDefaultLabel; + so.validate = hm.validateUniqueValue; so.modalonly = true; so = ss.option(form.Flag, 'enabled', _('Enable')); so.default = so.enabled; so.editable = true; - so.validate = function(section_id, value) { + so.validate = function(/* ... */) { return hm.validatePresetIDs.call(this, [ ['select', 'type'], ['', 'rule_set'] @@ -1628,8 +1628,8 @@ return view.extend({ so = ss.option(form.MultiValue, 'server', _('DNS server')); so.value('default-dns'); so.default = 'default-dns'; - so.load = L.bind(loadDNSServerLabel, so); - so.validate = L.bind(validateNameserver, so); + so.load = loadDNSServerLabel; + so.validate = validateNameserver; so.rmempty = false; so.editable = true; diff --git a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/global.js b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/global.js index 4f3589f936..11796df5b3 100644 --- a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/global.js +++ b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/global.js @@ -304,7 +304,7 @@ return view.extend({ .format('https://raw.githubusercontent.com/fcshark-org/openwrt-fchomo/refs/heads/initialpack/initial.tgz')); so.inputstyle = 'action'; so.inputtitle = _('Upload...'); - so.onclick = L.bind(hm.uploadInitialPack, so); + so.onclick = hm.uploadInitialPack; } so = ss.option(form.Flag, 'auto_update', _('Auto update'), @@ -424,12 +424,12 @@ return view.extend({ so = ss.option(form.Value, 'keep_alive_interval', _('TCP-Keep-Alive interval'), _('In seconds. %s will be used if empty.').format('30')); so.placeholder = '30'; - so.validate = L.bind(hm.validateTimeDuration, so); + so.validate = hm.validateTimeDuration; so = ss.option(form.Value, 'keep_alive_idle', _('TCP-Keep-Alive idle timeout'), _('In seconds. %s will be used if empty.').format('600')); so.placeholder = '600'; - so.validate = L.bind(hm.validateTimeDuration, so); + so.validate = hm.validateTimeDuration; /* Global Authentication */ o = s.taboption('general', form.SectionValue, '_global', form.NamedSection, 'global', 'fchomo', _('Global Authentication')); @@ -438,7 +438,7 @@ return view.extend({ so = ss.option(form.DynamicList, 'authentication', _('User Authentication')); so.datatype = 'list(string)'; so.placeholder = 'user1:pass1'; - so.validate = L.bind(hm.validateAuth, so); + so.validate = hm.validateAuth; so = ss.option(form.DynamicList, 'skip_auth_prefixes', _('No Authentication IP ranges')); so.datatype = 'list(cidr)'; @@ -514,7 +514,7 @@ return view.extend({ _('Aging time of NAT map maintained by client.
') + _('In seconds. %s will be used if empty.').format('300')); so.placeholder = '300'; - so.validate = L.bind(hm.validateTimeDuration, so); + so.validate = hm.validateTimeDuration; so = ss.option(form.Flag, 'tun_endpoint_independent_nat', _('Endpoint-Independent NAT'), _('Performance may degrade slightly, so it is not recommended to enable on when it is not needed.')); @@ -573,7 +573,7 @@ return view.extend({ } } so.renderWidget = function(section_id, option_index, cfgvalue) { - let node = hm.TextValue.prototype.renderWidget.apply(this, arguments); + let node = hm.TextValue.prototype.renderWidget.call(this, section_id, option_index, cfgvalue); const cbid = this.cbid(section_id) + '._outer_sni'; node.appendChild(E('div', { 'class': 'control-group' }, [ @@ -826,7 +826,7 @@ return view.extend({ if (!res[0].match(/_udpport$/)) so.value.apply(so, res); }) - so.validate = L.bind(hm.validateCommonPort, so); + so.validate = hm.validateCommonPort; so = ss.taboption('routing_control', hm.RichMultiValue, 'routing_udpport', _('Routing ports') + ' (UDP)', _('Specify target ports to be proxied. Multiple ports must be separated by commas.')); @@ -835,7 +835,7 @@ return view.extend({ if (!res[0].match(/_tcpport$/)) so.value.apply(so, res); }) - so.validate = L.bind(hm.validateCommonPort, so); + so.validate = hm.validateCommonPort; so = ss.taboption('routing_control', form.ListValue, 'routing_mode', _('Routing mode'), _('Routing mode of the traffic enters mihomo via firewall rules.')); diff --git a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/log.js b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/log.js index aa6a2b12f2..cccc952581 100644 --- a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/log.js +++ b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/log.js @@ -84,7 +84,7 @@ function getRuntimeLog(name, option_index, section_id, in_table) { ); let log; - poll.add(L.bind(function() { + poll.add(function() { return fs.read_direct(String.format('%s/%s.log', hm_dir, filename), 'text') .then((res) => { log = E('pre', { 'wrap': 'pre' }, [ @@ -104,7 +104,7 @@ function getRuntimeLog(name, option_index, section_id, in_table) { dom.content(log_textarea, log); }); - })); + }); return E([ E('style', [ css ]), diff --git a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/node.js b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/node.js index 223141c1c6..b448e03061 100644 --- a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/node.js +++ b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/node.js @@ -143,8 +143,8 @@ return view.extend({ ss.tab('field_dial', _('Dial fields')); so = ss.taboption('field_general', form.Value, 'label', _('Label')); - so.load = L.bind(hm.loadDefaultLabel, so); - so.validate = L.bind(hm.validateUniqueValue, so); + so.load = hm.loadDefaultLabel; + so.validate = hm.validateUniqueValue; so.modalonly = true; so = ss.taboption('field_general', form.Flag, 'enabled', _('Enable')); @@ -170,19 +170,19 @@ return view.extend({ /* HTTP / SOCKS fields */ /* hm.validateAuth */ so = ss.taboption('field_general', form.Value, 'username', _('Username')); - so.validate = L.bind(hm.validateAuthUsername, so); + so.validate = hm.validateAuthUsername; so.depends({type: /^(http|socks5|mieru|ssh)$/}); so.modalonly = true; so = ss.taboption('field_general', form.Value, 'password', _('Password')); so.password = true; - so.validate = L.bind(hm.validateAuthPassword, so); + so.validate = hm.validateAuthPassword; so.depends({type: /^(http|socks5|mieru|trojan|anytls|hysteria2|tuic|ssh)$/}); so.modalonly = true; so = ss.taboption('field_general', hm.TextValue, 'headers', _('HTTP header')); so.placeholder = '{\n "User-Agent": [\n "Clash/v1.18.0",\n "mihomo/1.18.3"\n ],\n "Authorization": [\n //"token 1231231"\n ]\n}'; - so.validate = L.bind(hm.validateJson, so); + so.validate = hm.validateJson; so.depends('type', 'http'); so.modalonly = true; @@ -288,7 +288,7 @@ return view.extend({ so = ss.taboption('field_general', form.Value, 'snell_psk', _('Pre-shared key')); so.password = true; so.rmempty = false; - so.validate = L.bind(hm.validateAuthPassword, so); + so.validate = hm.validateAuthPassword; so.depends('type', 'snell'); so.modalonly = true; @@ -303,7 +303,7 @@ return view.extend({ /* TUIC fields */ so = ss.taboption('field_general', form.Value, 'uuid', _('UUID')); so.rmempty = false; - so.validate = L.bind(hm.validateUUID, so); + so.validate = hm.validateUUID; so.depends('type', 'tuic'); so.modalonly = true; @@ -401,14 +401,14 @@ return view.extend({ so = ss.taboption('field_general', form.Value, 'anytls_idle_session_check_interval', _('Idle session check interval'), _('In seconds.')); so.placeholder = '30'; - so.validate = L.bind(hm.validateTimeDuration, so); + so.validate = hm.validateTimeDuration; so.depends('type', 'anytls'); so.modalonly = true; so = ss.taboption('field_general', form.Value, 'anytls_idle_session_timeout', _('Idle session timeout'), _('In seconds.')); so.placeholder = '30'; - so.validate = L.bind(hm.validateTimeDuration, so); + so.validate = hm.validateTimeDuration; so.depends('type', 'anytls'); so.modalonly = true; @@ -421,7 +421,7 @@ return view.extend({ /* VMess / VLESS fields */ so = ss.taboption('field_general', form.Value, 'vmess_uuid', _('UUID')); so.rmempty = false; - so.validate = L.bind(hm.validateUUID, so); + so.validate = hm.validateUUID; so.depends({type: /^(vmess|vless)$/}); so.modalonly = true; @@ -630,13 +630,13 @@ return view.extend({ } so = ss.taboption('field_vless_encryption', form.Value, 'vless_encryption_encryption', _('encryption')); - so.renderWidget = function(section_id, option_index, cfgvalue) { + so.renderWidget = function(/* ... */) { let node = form.Value.prototype.renderWidget.apply(this, arguments); node.firstChild.style.width = '30em'; return node; - }, + } so.rmempty = false; so.depends('vless_encryption', '1'); so.modalonly = true; @@ -904,7 +904,7 @@ return view.extend({ so = ss.taboption('field_transport', hm.TextValue, 'transport_http_headers', _('HTTP header')); so.placeholder = '{\n "Host": "example.com",\n "Connection": [\n "keep-alive"\n ]\n}'; - so.validate = L.bind(hm.validateJson, so); + so.validate = hm.validateJson; so.depends({transport_enabled: '1', transport_type: /^(http|ws)$/}); so.modalonly = true; @@ -1162,8 +1162,8 @@ return view.extend({ /* General fields */ so = ss.taboption('field_general', form.Value, 'label', _('Label')); - so.load = L.bind(hm.loadDefaultLabel, so); - so.validate = L.bind(hm.validateUniqueValue, so); + so.load = hm.loadDefaultLabel; + so.validate = hm.validateUniqueValue; so.modalonly = true; so = ss.taboption('field_general', form.Flag, 'enabled', _('Enable')); @@ -1216,7 +1216,7 @@ return view.extend({ so.modalonly = true; so = ss.taboption('field_general', form.Value, 'url', _('Provider URL')); - so.validate = L.bind(hm.validateUrl, so); + so.validate = hm.validateUrl; so.rmempty = false; so.depends('type', 'http'); so.modalonly = true; @@ -1224,13 +1224,13 @@ return view.extend({ so = ss.taboption('field_general', form.Value, 'size_limit', _('Size limit'), _('In bytes. %s will be used if empty.').format('0')); so.placeholder = '0'; - so.validate = L.bind(hm.validateBytesize, so); + so.validate = hm.validateBytesize; so.depends('type', 'http'); so = ss.taboption('field_general', form.Value, 'interval', _('Update interval'), _('In seconds. %s will be used if empty.').format('86400')); so.placeholder = '86400'; - so.validate = L.bind(hm.validateTimeDuration, so); + so.validate = hm.validateTimeDuration; so.depends('type', 'http'); so = ss.taboption('field_general', form.ListValue, 'proxy', _('Proxy group'), @@ -1240,14 +1240,14 @@ return view.extend({ so.value.apply(so, res); }) so.load = L.bind(hm.loadProxyGroupLabel, so, hm.preset_outbound.direct); - so.textvalue = L.bind(hm.textvalue2Value, so); + so.textvalue = hm.textvalue2Value; //so.editable = true; so.depends('type', 'http'); so = ss.taboption('field_general', hm.TextValue, 'header', _('HTTP header'), _('Custom HTTP header.')); so.placeholder = '{\n "User-Agent": [\n "Clash/v1.18.0",\n "mihomo/1.18.3"\n ],\n "Accept": [\n //"application/vnd.github.v3.raw"\n ],\n "Authorization": [\n //"token 1231231"\n ]\n}'; - so.validate = L.bind(hm.validateJson, so); + so.validate = hm.validateJson; so.depends('type', 'http'); so.modalonly = true; @@ -1267,7 +1267,7 @@ return view.extend({ _('For format see %s.') .format('https://wiki.metacubex.one/config/proxy-providers/#overrideproxy-name', _('override.proxy-name'))); so.placeholder = '{"pattern": "IPLC-(.*?)倍", "target": "iplc x $1"}'; - so.validate = L.bind(hm.validateJson, so); + so.validate = hm.validateJson; so.depends({type: 'inline', '!reverse': true}); so.modalonly = true; @@ -1360,7 +1360,7 @@ return view.extend({ hm.health_checkurls.forEach((res) => { so.value.apply(so, res); }) - so.validate = L.bind(hm.validateUrl, so); + so.validate = hm.validateUrl; so.retain = true; so.depends({type: 'inline', '!reverse': true}); so.modalonly = true; @@ -1368,7 +1368,7 @@ return view.extend({ so = ss.taboption('field_health', form.Value, 'health_interval', _('Health check interval'), _('In seconds. %s will be used if empty.').format('600')); so.placeholder = '600'; - so.validate = L.bind(hm.validateTimeDuration, so); + so.validate = hm.validateTimeDuration; so.depends({type: 'inline', '!reverse': true}); so.modalonly = true; @@ -1414,7 +1414,7 @@ return view.extend({ so.modalonly = true; so = ss.option(form.DummyValue, '_update'); - so.cfgvalue = L.bind(hm.renderResDownload, so); + so.cfgvalue = hm.renderResDownload; so.editable = true; so.modalonly = false; /* Provider END */ @@ -1435,8 +1435,8 @@ return view.extend({ ss.hm_lowcase_only = true; so = ss.option(form.Value, 'label', _('Label')); - so.load = L.bind(hm.loadDefaultLabel, so); - so.validate = L.bind(hm.validateUniqueValue, so); + so.load = hm.loadDefaultLabel; + so.validate = hm.validateUniqueValue; so.modalonly = true; so = ss.option(form.Flag, 'enabled', _('Enable')); @@ -1447,7 +1447,7 @@ return view.extend({ so.value('node', _('Proxy Node')); so.value('provider', _('Provider')); so.default = 'node'; - so.textvalue = L.bind(hm.textvalue2Value, so); + so.textvalue = hm.textvalue2Value; so = ss.option(form.DummyValue, '_value', _('Value')); so.load = function(section_id) { diff --git a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/ruleset.js b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/ruleset.js index b5db959a41..e7617790cd 100644 --- a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/ruleset.js +++ b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/ruleset.js @@ -188,7 +188,7 @@ return view.extend({ o.placeholder = 'http(s)://github.com/ACL4SSR/ACL4SSR/raw/refs/heads/master/Clash/Providers/BanAD.yaml?fmt=yaml&behav=classical&rawq=good%3Djob#BanAD\n' + 'file:///example.txt?fmt=text&behav=domain&fill=LmNuCg#CN%20TLD\n' + 'inline://LSAnLmhrJwoK?behav=domain#HK%20TLD\n'; - o.handleFn = L.bind(function(textarea) { + o.handleFn = function(textarea) { let input_links = textarea.getValue().trim().split('\n'); let imported_count = 0; @@ -216,7 +216,7 @@ return view.extend({ return this.save(); else return ui.hideModal(); - }, o); + } return o.render(); } @@ -246,14 +246,14 @@ return view.extend({ /* Import mihomo config and Import rule-set links and Remove idle files end */ o = s.option(form.Value, 'label', _('Label')); - o.load = L.bind(hm.loadDefaultLabel, o); - o.validate = L.bind(hm.validateUniqueValue, o); + o.load = hm.loadDefaultLabel; + o.validate = hm.validateUniqueValue; o.modalonly = true; o = s.option(form.Flag, 'enabled', _('Enable')); o.default = o.enabled; o.editable = true; - o.validate = function(section_id, value) { + o.validate = function(/* ... */) { return hm.validatePresetIDs.call(this, [ ['select', 'type'], ['select', 'behavior'], @@ -345,7 +345,7 @@ return view.extend({ o.modalonly = true; o = s.option(form.Value, 'url', _('Rule set URL')); - o.validate = L.bind(hm.validateUrl, o); + o.validate = hm.validateUrl; o.rmempty = false; o.depends('type', 'http'); o.modalonly = true; @@ -353,13 +353,13 @@ return view.extend({ o = s.option(form.Value, 'size_limit', _('Size limit'), _('In bytes. %s will be used if empty.').format('0')); o.placeholder = '0'; - o.validate = L.bind(hm.validateBytesize, o); + o.validate = hm.validateBytesize; o.depends('type', 'http'); o = s.option(form.Value, 'interval', _('Update interval'), _('In seconds. %s will be used if empty.').format('259200')); o.placeholder = '259200'; - o.validate = L.bind(hm.validateTimeDuration, o); + o.validate = hm.validateTimeDuration; o.depends('type', 'http'); o = s.option(form.ListValue, 'proxy', _('Proxy group'), @@ -369,12 +369,12 @@ return view.extend({ o.value.apply(o, res); }) o.load = L.bind(hm.loadProxyGroupLabel, o, hm.preset_outbound.direct); - o.textvalue = L.bind(hm.textvalue2Value, o); + o.textvalue = hm.textvalue2Value; //o.editable = true; o.depends('type', 'http'); o = s.option(form.DummyValue, '_update'); - o.cfgvalue = L.bind(hm.renderResDownload, o); + o.cfgvalue = hm.renderResDownload; o.editable = true; o.modalonly = false; /* Rule set END */ diff --git a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/server.js b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/server.js index 98aefaf899..437af04433 100644 --- a/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/server.js +++ b/small/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/server.js @@ -13,7 +13,7 @@ const CBIDummyCopyValue = form.Value.extend({ readonly: true, renderWidget: function(section_id, option_index, cfgvalue) { - let node = form.Value.prototype.renderWidget.apply(this, arguments); + let node = form.Value.prototype.renderWidget.call(this, section_id, option_index, cfgvalue); node.classList.add('control-group'); node.firstChild.style.width = '30em'; @@ -199,8 +199,8 @@ return view.extend({ /* General fields */ o = s.taboption('field_general', form.Value, 'label', _('Label')); - o.load = L.bind(hm.loadDefaultLabel, o); - o.validate = L.bind(hm.validateUniqueValue, o); + o.load = hm.loadDefaultLabel; + o.validate = hm.validateUniqueValue; o.modalonly = true; o = s.taboption('field_general', form.Flag, 'enabled', _('Enable')); @@ -227,7 +227,7 @@ return view.extend({ o.datatype = 'or(port, portrange)'; //o.placeholder = '1080,2079-2080,3080'; // @fw4 does not support port lists with commas o.rmempty = false; - //o.validate = L.bind(hm.validateCommonPort, o); // @fw4 does not support port lists with commas + //o.validate = hm.validateCommonPort; // @fw4 does not support port lists with commas // @dev: Features under development // @rule @@ -236,13 +236,13 @@ return view.extend({ /* HTTP / SOCKS fields */ /* hm.validateAuth */ o = s.taboption('field_general', form.Value, 'username', _('Username')); - o.validate = L.bind(hm.validateAuthUsername, o); + o.validate = hm.validateAuthUsername; o.depends({type: /^(http|socks|mixed|trojan|anytls|hysteria2)$/}); o.modalonly = true; o = s.taboption('field_general', hm.GenValue, 'password', _('Password')); o.password = true; - o.validate = L.bind(hm.validateAuthPassword, o); + o.validate = hm.validateAuthPassword; o.rmempty = false; o.depends({type: /^(http|socks|mixed|trojan|anytls|hysteria2)$/, username: /.+/}); o.depends({type: /^(tuic)$/, uuid: /.+/}); @@ -308,7 +308,7 @@ return view.extend({ /* Tuic fields */ o = s.taboption('field_general', hm.GenValue, 'uuid', _('UUID')); o.rmempty = false; - o.validate = L.bind(hm.validateUUID, o); + o.validate = hm.validateUUID; o.depends('type', 'tuic'); o.modalonly = true; @@ -330,14 +330,14 @@ return view.extend({ o = s.taboption('field_general', form.Value, 'tuic_max_idle_time', _('Idle timeout'), _('In seconds.')); o.default = '15000'; - o.validate = L.bind(hm.validateTimeDuration, o); + o.validate = hm.validateTimeDuration; o.depends('type', 'tuic'); o.modalonly = true; o = s.taboption('field_general', form.Value, 'tuic_authentication_timeout', _('Auth timeout'), _('In seconds.')); o.default = '1000'; - o.validate = L.bind(hm.validateTimeDuration, o); + o.validate = hm.validateTimeDuration; o.depends('type', 'tuic'); o.modalonly = true; @@ -372,7 +372,7 @@ return view.extend({ /* VMess / VLESS fields */ o = s.taboption('field_general', hm.GenValue, 'vmess_uuid', _('UUID')); o.rmempty = false; - o.validate = L.bind(hm.validateUUID, o); + o.validate = hm.validateUUID; o.depends({type: /^(vmess|vless)$/}); o.modalonly = true; @@ -573,7 +573,7 @@ return view.extend({ } } o.renderWidget = function(section_id, option_index, cfgvalue) { - let node = hm.TextValue.prototype.renderWidget.apply(this, arguments); + let node = hm.TextValue.prototype.renderWidget.call(this, section_id, option_index, cfgvalue); const cbid = this.cbid(section_id) + '._keytype_select'; const selected = this.hm_options.type; @@ -608,7 +608,7 @@ return view.extend({ return JSON.stringify(new VlessEncryption(uci.get(data[0], section_id, 'vless_encryption_hmpayload'))['keypairs'], null, 2); } o.validate = function(section_id, value) { - let result = hm.validateJson.apply(this, arguments); + let result = hm.validateJson.call(this, section_id, value); if (result === true) { let keypairs = JSON.parse(value.trim()); @@ -715,8 +715,8 @@ return view.extend({ o = s.taboption('field_tls', form.Value, 'tls_client_auth_cert_path', _('Client Auth Certificate path') + _(' (mTLS)'), _('The %s public key, in PEM format.').format(_('Client'))); o.value('/etc/fchomo/certs/client_publickey.pem'); - o.validate = function(section_id, value) { - return hm.validateMTLSClientAuth.call(this, 'tls_client_auth_type', section_id, value); + o.validate = function(/* ... */) { + return hm.validateMTLSClientAuth.call(this, 'tls_client_auth_type', ...arguments); } o.depends({tls: '1', type: /^(http|socks|mixed|vmess|vless|trojan|anytls|hysteria2|tuic)$/}); o.modalonly = true; @@ -745,7 +745,7 @@ return view.extend({ } } o.renderWidget = function(section_id, option_index, cfgvalue) { - let node = hm.TextValue.prototype.renderWidget.apply(this, arguments); + let node = hm.TextValue.prototype.renderWidget.call(this, section_id, option_index, cfgvalue); const cbid = this.cbid(section_id) + '._outer_sni'; node.appendChild(E('div', { 'class': 'control-group' }, [ diff --git a/small/v2ray-geodata/Makefile b/small/v2ray-geodata/Makefile index d90f8a67cf..dc0a9769a3 100644 --- a/small/v2ray-geodata/Makefile +++ b/small/v2ray-geodata/Makefile @@ -30,7 +30,7 @@ define Download/geosite HASH:=1a7dad0ceaaf1f6d12fef585576789699bd1c6ea014c887c04b94cb9609350e9 endef -GEOSITE_IRAN_VER:=202509220041 +GEOSITE_IRAN_VER:=202509290038 GEOSITE_IRAN_FILE:=iran.dat.$(GEOSITE_IRAN_VER) define Download/geosite-ir URL:=https://github.com/bootmortis/iran-hosted-domains/releases/download/$(GEOSITE_IRAN_VER)/ diff --git a/youtube-dl/.github/workflows/ci.yml b/youtube-dl/.github/workflows/ci.yml index 8234e0ccb6..c7a8fff844 100644 --- a/youtube-dl/.github/workflows/ci.yml +++ b/youtube-dl/.github/workflows/ci.yml @@ -122,12 +122,12 @@ jobs: ytdl-test-set: ${{ fromJSON(needs.select.outputs.test-set) }} run-tests-ext: [sh] include: - - os: windows-2019 + - os: windows-2022 python-version: 3.4 python-impl: cpython ytdl-test-set: ${{ contains(needs.select.outputs.test-set, 'core') && 'core' || 'nocore' }} run-tests-ext: bat - - os: windows-2019 + - os: windows-2022 python-version: 3.4 python-impl: cpython ytdl-test-set: ${{ contains(needs.select.outputs.test-set, 'download') && 'download' || 'nodownload' }} diff --git a/youtube-dl/youtube_dl/__init__.py b/youtube-dl/youtube_dl/__init__.py index 3c1272e7b0..202f2c9b9a 100644 --- a/youtube-dl/youtube_dl/__init__.py +++ b/youtube-dl/youtube_dl/__init__.py @@ -409,6 +409,8 @@ def _real_main(argv=None): 'include_ads': opts.include_ads, 'default_search': opts.default_search, 'youtube_include_dash_manifest': opts.youtube_include_dash_manifest, + 'youtube_player_js_version': opts.youtube_player_js_version, + 'youtube_player_js_variant': opts.youtube_player_js_variant, 'encoding': opts.encoding, 'extract_flat': opts.extract_flat, 'mark_watched': opts.mark_watched, diff --git a/youtube-dl/youtube_dl/downloader/common.py b/youtube-dl/youtube_dl/downloader/common.py index 91e691776b..8354030a9f 100644 --- a/youtube-dl/youtube_dl/downloader/common.py +++ b/youtube-dl/youtube_dl/downloader/common.py @@ -11,6 +11,7 @@ from ..utils import ( decodeArgument, encodeFilename, error_to_compat_str, + float_or_none, format_bytes, shell_quote, timeconvert, @@ -367,14 +368,27 @@ class FileDownloader(object): }) return True - min_sleep_interval = self.params.get('sleep_interval') - if min_sleep_interval: - max_sleep_interval = self.params.get('max_sleep_interval', min_sleep_interval) - sleep_interval = random.uniform(min_sleep_interval, max_sleep_interval) + min_sleep_interval, max_sleep_interval = ( + float_or_none(self.params.get(interval), default=0) + for interval in ('sleep_interval', 'max_sleep_interval')) + + sleep_note = '' + available_at = info_dict.get('available_at') + if available_at: + forced_sleep_interval = available_at - int(time.time()) + if forced_sleep_interval > min_sleep_interval: + sleep_note = 'as required by the site' + min_sleep_interval = forced_sleep_interval + if forced_sleep_interval > max_sleep_interval: + max_sleep_interval = forced_sleep_interval + + sleep_interval = random.uniform( + min_sleep_interval, max_sleep_interval or min_sleep_interval) + + if sleep_interval > 0: self.to_screen( - '[download] Sleeping %s seconds...' % ( - int(sleep_interval) if sleep_interval.is_integer() - else '%.2f' % sleep_interval)) + '[download] Sleeping %.2f seconds %s...' % ( + sleep_interval, sleep_note)) time.sleep(sleep_interval) return self.real_download(filename, info_dict) diff --git a/youtube-dl/youtube_dl/extractor/youtube.py b/youtube-dl/youtube_dl/extractor/youtube.py index b31798729e..0b802351d2 100644 --- a/youtube-dl/youtube_dl/extractor/youtube.py +++ b/youtube-dl/youtube_dl/extractor/youtube.py @@ -1,5 +1,4 @@ # coding: utf-8 - from __future__ import unicode_literals import collections @@ -110,7 +109,7 @@ class YoutubeBaseInfoExtractor(InfoExtractor): 'INNERTUBE_CONTEXT': { 'client': { 'clientName': 'MWEB', - 'clientVersion': '2.20250311.03.00', + 'clientVersion': '2.2.20250925.01.00', # mweb previously did not require PO Token with this UA 'userAgent': 'Mozilla/5.0 (iPad; CPU OS 16_7_10 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1,gzip(gfe)', }, @@ -124,23 +123,36 @@ class YoutubeBaseInfoExtractor(InfoExtractor): 'client': { 'clientName': 'TVHTML5', 'clientVersion': '7.20250312.16.00', - 'userAgent': 'Mozilla/5.0 (ChromiumStylePlatform) Cobalt/Version', + # See: https://github.com/youtube/cobalt/blob/main/cobalt/browser/user_agent/user_agent_platform_info.cc#L506 + 'userAgent': 'Mozilla/5.0 (ChromiumStylePlatform) Cobalt/25.lts.30.1034943-gold (unlike Gecko), Unknown_TV_Unknown_0/Unknown (Unknown, Unknown)', }, }, 'INNERTUBE_CONTEXT_CLIENT_NAME': 7, 'SUPPORTS_COOKIES': True, }, + 'web': { 'INNERTUBE_CONTEXT': { 'client': { 'clientName': 'WEB', - 'clientVersion': '2.20250312.04.00', + 'clientVersion': '2.20250925.01.00', + 'userAgent': 'Mozilla/5.0', }, }, 'INNERTUBE_CONTEXT_CLIENT_NAME': 1, 'REQUIRE_PO_TOKEN': True, 'SUPPORTS_COOKIES': True, }, + # Safari UA returns pre-merged video+audio 144p/240p/360p/720p/1080p HLS formats + 'web_safari': { + 'INNERTUBE_CONTEXT': { + 'client': { + 'clientName': 'WEB', + 'clientVersion': '2.20250925.01.00', + 'userAgent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Safari/605.1.15,gzip(gfe)', + }, + }, + }, } def _login(self): @@ -419,10 +431,15 @@ class YoutubeBaseInfoExtractor(InfoExtractor): T(compat_str))) def _extract_ytcfg(self, video_id, webpage): - return self._parse_json( - self._search_regex( - r'ytcfg\.set\s*\(\s*({.+?})\s*\)\s*;', webpage, 'ytcfg', - default='{}'), video_id, fatal=False) or {} + ytcfg = self._search_json( + r'ytcfg\.set\s*\(', webpage, 'ytcfg', video_id, + end_pattern=r'\)\s*;', default={}) + + traverse_obj(ytcfg, ( + 'INNERTUBE_CONTEXT', 'client', 'configInfo', + T(lambda x: x.pop('appInstallData', None)))) + + return ytcfg def _extract_video(self, renderer): video_id = renderer['videoId'] @@ -694,7 +711,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor): r'/(?P[a-zA-Z0-9_-]{8,})/player(?:_ias(?:_tce)?\.vflset(?:/[a-zA-Z]{2,3}_[a-zA-Z]{2,3})?|-plasma-ias-(?:phone|tablet)-[a-z]{2}_[A-Z]{2}\.vflset)/base\.js$', r'\b(?Pvfl[a-zA-Z0-9_-]{6,})\b.*?\.js$', ) - _SUBTITLE_FORMATS = ('json3', 'srv1', 'srv2', 'srv3', 'ttml', 'vtt') + _SUBTITLE_FORMATS = ('json3', 'srv1', 'srv2', 'srv3', 'ttml', 'srt', 'vtt') _GEO_BYPASS = False @@ -1587,7 +1604,10 @@ class YoutubeIE(YoutubeBaseInfoExtractor): _PLAYER_JS_VARIANT_MAP = ( ('main', 'player_ias.vflset/en_US/base.js'), + ('tcc', 'player_ias_tcc.vflset/en_US/base.js'), ('tce', 'player_ias_tce.vflset/en_US/base.js'), + ('es5', 'player_es5.vflset/en_US/base.js'), + ('es6', 'player_es6.vflset/en_US/base.js'), ('tv', 'tv-player-ias.vflset/tv-player-ias.js'), ('tv_es6', 'tv-player-es6.vflset/tv-player-es6.js'), ('phone', 'player-plasma-ias-phone-en_US.vflset/base.js'), @@ -1605,6 +1625,19 @@ class YoutubeIE(YoutubeBaseInfoExtractor): self._code_cache = {} self._player_cache = {} + def _get_player_js_version(self): + player_js_version = self.get_param('youtube_player_js_version') or '20348@0004de42' + sts_hash = self._search_regex( + ('^actual$(^)?(^)?', r'^([0-9]{5,})@([0-9a-f]{8,})$'), + player_js_version, 'player_js_version', group=(1, 2), default=None) + if sts_hash: + return sts_hash + self.report_warning( + 'Invalid player JS version "{0}" specified. ' + 'It should be "{1}" or in the format of {2}'.format( + player_js_version, 'actual', 'SignatureTimeStamp@Hash'), only_once=True) + return None, None + # *ytcfgs, webpage=None def _extract_player_url(self, *ytcfgs, **kw_webpage): if ytcfgs and not isinstance(ytcfgs[0], dict): @@ -1615,19 +1648,43 @@ class YoutubeIE(YoutubeBaseInfoExtractor): webpage or '', 'player URL', fatal=False) if player_url: ytcfgs = ytcfgs + ({'PLAYER_JS_URL': player_url},) - return traverse_obj( + player_url = traverse_obj( ytcfgs, (Ellipsis, 'PLAYER_JS_URL'), (Ellipsis, 'WEB_PLAYER_CONTEXT_CONFIGS', Ellipsis, 'jsUrl'), get_all=False, expected_type=lambda u: urljoin('https://www.youtube.com', u)) + player_id_override = self._get_player_js_version()[1] + + requested_js_variant = self.get_param('youtube_player_js_variant') or 'main' + variant_js = next( + (v for k, v in self._PLAYER_JS_VARIANT_MAP if k == requested_js_variant), + None) + if variant_js: + player_id = player_id_override or self._extract_player_info(player_url) + original_url = player_url + player_url = '/s/player/{0}/{1}'.format(player_id, variant_js) + if original_url != player_url: + self.write_debug( + 'Forcing "{0}" player JS variant for player {1}\n' + ' original url = {2}'.format( + requested_js_variant, player_id, original_url), + only_once=True) + elif requested_js_variant != 'actual': + self.report_warning( + 'Invalid player JS variant name "{0}" requested. ' + 'Valid choices are: {1}'.format( + requested_js_variant, ','.join(k for k, _ in self._PLAYER_JS_VARIANT_MAP)), + only_once=True) + + return urljoin('https://www.youtube.com', player_url) + def _download_player_url(self, video_id, fatal=False): res = self._download_webpage( 'https://www.youtube.com/iframe_api', note='Downloading iframe API JS', video_id=video_id, fatal=fatal) player_version = self._search_regex( r'player\\?/([0-9a-fA-F]{8})\\?/', res or '', 'player version', fatal=fatal, - default=NO_DEFAULT if res else None) - if player_version: - return 'https://www.youtube.com/s/player/{0}/player_ias.vflset/en_US/base.js'.format(player_version) + default=NO_DEFAULT if res else None) or None + return player_version and 'https://www.youtube.com/s/player/{0}/player_ias.vflset/en_US/base.js'.format(player_version) def _signature_cache_id(self, example_sig): """ Return a string representation of a signature """ @@ -2014,9 +2071,15 @@ class YoutubeIE(YoutubeBaseInfoExtractor): def _extract_signature_timestamp(self, video_id, player_url, ytcfg=None, fatal=False): """ Extract signatureTimestamp (sts) + Required to tell API what sig/player version is in use. """ - sts = traverse_obj(ytcfg, 'STS', expected_type=int) + sts = traverse_obj( + (self._get_player_js_version(), ytcfg), + (0, 0), + (1, 'STS'), + expected_type=int_or_none) + if sts: return sts @@ -2163,8 +2226,12 @@ class YoutubeIE(YoutubeBaseInfoExtractor): video_id = self._match_id(url) base_url = self.http_scheme() + '//www.youtube.com/' webpage_url = base_url + 'watch?v=' + video_id + ua = traverse_obj(self._INNERTUBE_CLIENTS, ( + 'web', 'INNERTUBE_CONTEXT', 'client', 'userAgent')) + headers = {'User-Agent': ua} if ua else None webpage = self._download_webpage( - webpage_url + '&bpctr=9999999999&has_verified=1', video_id, fatal=False) + webpage_url + '&bpctr=9999999999&has_verified=1', video_id, + headers=headers, fatal=False) player_response = None player_url = None @@ -2174,12 +2241,14 @@ class YoutubeIE(YoutubeBaseInfoExtractor): video_id, 'initial player response') is_live = traverse_obj(player_response, ('videoDetails', 'isLive')) + fetched_timestamp = None if False and not player_response: player_response = self._call_api( 'player', {'videoId': video_id}, video_id) if True or not player_response: origin = 'https://www.youtube.com' pb_context = {'html5Preference': 'HTML5_PREF_WANTS'} + fetched_timestamp = int(time.time()) player_url = self._extract_player_url(webpage) ytcfg = self._extract_ytcfg(video_id, webpage or '') @@ -2246,6 +2315,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor): hls = traverse_obj( (player_response, api_player_response), (Ellipsis, 'streamingData', 'hlsManifestUrl', T(url_or_none))) + fetched_timestamp = int(time.time()) if len(hls) == 2 and not hls[0] and hls[1]: player_response['streamingData']['hlsManifestUrl'] = hls[1] else: @@ -2257,13 +2327,14 @@ class YoutubeIE(YoutubeBaseInfoExtractor): player_response['videoDetails'] = video_details def is_agegated(playability): - if not isinstance(playability, dict): - return + # playability: dict + if not playability: + return False if playability.get('desktopLegacyAgeGateReason'): return True - reasons = filter(None, (playability.get(r) for r in ('status', 'reason'))) + reasons = traverse_obj(playability, (('status', 'reason'),)) AGE_GATE_REASONS = ( 'confirm your age', 'age-restricted', 'inappropriate', # reason 'age_verification_required', 'age_check_required', # status @@ -2321,15 +2392,9 @@ class YoutubeIE(YoutubeBaseInfoExtractor): trailer_video_id, self.ie_key(), trailer_video_id) def get_text(x): - if not x: - return - text = x.get('simpleText') - if text and isinstance(text, compat_str): - return text - runs = x.get('runs') - if not isinstance(runs, list): - return - return ''.join([r['text'] for r in runs if isinstance(r.get('text'), compat_str)]) + return ''.join(traverse_obj( + x, (('simpleText',),), ('runs', Ellipsis, 'text'), + expected_type=compat_str)) search_meta = ( (lambda x: self._html_search_meta(x, webpage, default=None)) @@ -2412,6 +2477,14 @@ class YoutubeIE(YoutubeBaseInfoExtractor): lower = lambda s: s.lower() + if is_live: + fetched_timestamp = None + elif fetched_timestamp is not None: + # Handle preroll waiting period + preroll_sleep = self.get_param('youtube_preroll_sleep') + preroll_sleep = int_or_none(preroll_sleep, default=6) + fetched_timestamp += preroll_sleep + for fmt in streaming_formats: if fmt.get('targetDurationSec'): continue @@ -2508,6 +2581,9 @@ class YoutubeIE(YoutubeBaseInfoExtractor): 'downloader_options': {'http_chunk_size': CHUNK_SIZE}, # No longer useful? }) + if fetched_timestamp: + dct['available_at'] = fetched_timestamp + formats.append(dct) def process_manifest_format(f, proto, client_name, itag, all_formats=False): @@ -2525,6 +2601,10 @@ class YoutubeIE(YoutubeBaseInfoExtractor): if f.get('source_preference') is None: f['source_preference'] = -1 + # Deprioritize since its pre-merged m3u8 formats may have lower quality audio streams + if client_name == 'web_safari' and proto == 'hls' and not is_live: + f['source_preference'] -= 1 + if itag in ('616', '235'): f['format_note'] = join_nonempty(f.get('format_note'), 'Premium', delim=' ') f['source_preference'] += 100 @@ -2541,15 +2621,15 @@ class YoutubeIE(YoutubeBaseInfoExtractor): hls_manifest_url = streaming_data.get('hlsManifestUrl') if hls_manifest_url: - for f in self._extract_m3u8_formats( + formats.extend( + f for f in self._extract_m3u8_formats( hls_manifest_url, video_id, 'mp4', - entry_protocol='m3u8_native', live=is_live, fatal=False): + entry_protocol='m3u8_native', live=is_live, fatal=False) if process_manifest_format( - f, 'hls', None, self._search_regex( - r'/itag/(\d+)', f['url'], 'itag', default=None)): - formats.append(f) + f, 'hls', None, self._search_regex( + r'/itag/(\d+)', f['url'], 'itag', default=None))) - if self._downloader.params.get('youtube_include_dash_manifest', True): + if self.get_param('youtube_include_dash_manifest', True): dash_manifest_url = streaming_data.get('dashManifestUrl') if dash_manifest_url: for f in self._extract_mpd_formats( @@ -2576,7 +2656,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor): playability_status, lambda x: x['errorScreen']['playerErrorMessageRenderer'], dict) or {} - reason = get_text(pemr.get('reason')) or playability_status.get('reason') + reason = get_text(pemr.get('reason')) or playability_status.get('reason') or '' subreason = pemr.get('subreason') if subreason: subreason = clean_html(get_text(subreason)) @@ -2588,7 +2668,12 @@ class YoutubeIE(YoutubeBaseInfoExtractor): self.raise_geo_restricted( subreason, countries) reason += '\n' + subreason + if reason: + if 'sign in' in reason.lower(): + self.raise_login_required(remove_end(reason, 'This helps protect our community. Learn more')) + elif traverse_obj(playability_status, ('errorScreen', 'playerCaptchaViewModel', T(dict))): + reason += '. YouTube is requiring a captcha challenge before playback' raise ExtractorError(reason, expected=True) self._sort_formats(formats) @@ -2691,6 +2776,9 @@ class YoutubeIE(YoutubeBaseInfoExtractor): for fmt in self._SUBTITLE_FORMATS: query.update({ 'fmt': fmt, + # xosf=1 causes undesirable text position data for vtt, json3 & srv* subtitles + # See: https://github.com/yt-dlp/yt-dlp/issues/13654 + 'xosf': [] }) lang_subs.append({ 'ext': fmt, @@ -2732,7 +2820,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor): for d_k, s_ks in [('start', ('start', 't')), ('end', ('end',))]: d_k += '_time' if d_k not in info and k in s_ks: - info[d_k] = parse_duration(query[k][0]) + info[d_k] = parse_duration(v[0]) if video_description: # Youtube Music Auto-generated description @@ -2761,6 +2849,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor): initial_data = self._call_api( 'next', {'videoId': video_id}, video_id, fatal=False) + initial_sdcr = None if initial_data: chapters = self._extract_chapters_from_json( initial_data, video_id, duration) @@ -2780,9 +2869,9 @@ class YoutubeIE(YoutubeBaseInfoExtractor): for next_num, content in enumerate(contents, start=1): mmlir = content.get('macroMarkersListItemRenderer') or {} start_time = chapter_time(mmlir) - end_time = chapter_time(try_get( - contents, lambda x: x[next_num]['macroMarkersListItemRenderer'])) \ - if next_num < len(contents) else duration + end_time = (traverse_obj( + contents, (next_num, 'macroMarkersListItemRenderer', T(chapter_time))) + if next_num < len(contents) else duration) if start_time is None or end_time is None: continue chapters.append({ @@ -2888,12 +2977,13 @@ class YoutubeIE(YoutubeBaseInfoExtractor): info['track'] = mrr_contents_text # this is not extraction but spelunking! - carousel_lockups = traverse_obj( - initial_data, - ('engagementPanels', Ellipsis, 'engagementPanelSectionListRenderer', - 'content', 'structuredDescriptionContentRenderer', 'items', Ellipsis, - 'videoDescriptionMusicSectionRenderer', 'carouselLockups', Ellipsis), - expected_type=dict) or [] + initial_sdcr = traverse_obj(initial_data, ( + 'engagementPanels', Ellipsis, 'engagementPanelSectionListRenderer', + 'content', 'structuredDescriptionContentRenderer', T(dict)), + get_all=False) + carousel_lockups = traverse_obj(initial_sdcr, ( + 'items', Ellipsis, 'videoDescriptionMusicSectionRenderer', + 'carouselLockups', Ellipsis, T(dict))) or [] # try to reproduce logic from metadataRowContainerRenderer above (if it still is) fields = (('ALBUM', 'album'), ('ARTIST', 'artist'), ('SONG', 'track'), ('LICENSES', 'license')) # multiple_songs ? @@ -2918,6 +3008,23 @@ class YoutubeIE(YoutubeBaseInfoExtractor): self.mark_watched(video_id, player_response) + # Fallbacks for missing metadata + if initial_sdcr: + if info.get('description') is None: + info['description'] = traverse_obj(initial_sdcr, ( + 'items', Ellipsis, 'expandableVideoDescriptionBodyRenderer', + 'attributedDescriptionBodyText', 'content', T(compat_str)), + get_all=False) + # videoDescriptionHeaderRenderer also has publishDate/channel/handle/ucid, but not needed + if info.get('title') is None: + info['title'] = traverse_obj( + (initial_sdcr, initial_data), + (0, 'items', Ellipsis, 'videoDescriptionHeaderRenderer', T(dict)), + (1, 'playerOverlays', 'playerOverlayRenderer', 'videoDetails', + 'playerOverlayVideoDetailsRenderer', T(dict)), + expected_type=lambda x: self._get_text(x, 'title'), + get_all=False) + return merge_dicts( info, { 'uploader_id': self._extract_uploader_id(owner_profile_url), @@ -3428,38 +3535,46 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor): if not content_id: return content_type = view_model.get('contentType') - if content_type not in ('LOCKUP_CONTENT_TYPE_PLAYLIST', 'LOCKUP_CONTENT_TYPE_PODCAST'): + if content_type == 'LOCKUP_CONTENT_TYPE_VIDEO': + ie = YoutubeIE + url = update_url_query( + 'https://www.youtube.com/watch', {'v': content_id}), + thumb_keys = (None,) + elif content_type in ('LOCKUP_CONTENT_TYPE_PLAYLIST', 'LOCKUP_CONTENT_TYPE_PODCAST'): + ie = YoutubeTabIE + url = update_url_query( + 'https://www.youtube.com/playlist', {'list': content_id}), + thumb_keys = ('collectionThumbnailViewModel', 'primaryThumbnail') + else: self.report_warning( - 'Unsupported lockup view model content type "{0}"{1}'.format(content_type, bug_reports_message()), only_once=True) + 'Unsupported lockup view model content type "{0}"{1}'.format(content_type, bug_reports_message()), + only_once=True) return + thumb_keys = ('contentImage',) + thumb_keys + ('thumbnailViewModel', 'image') return merge_dicts(self.url_result( - update_url_query('https://www.youtube.com/playlist', {'list': content_id}), - ie=YoutubeTabIE.ie_key(), video_id=content_id), { + url, ie=ie.ie_key(), video_id=content_id), { 'title': traverse_obj(view_model, ( - 'metadata', 'lockupMetadataViewModel', 'title', 'content', T(compat_str))), - 'thumbnails': self._extract_thumbnails(view_model, ( - 'contentImage', 'collectionThumbnailViewModel', 'primaryThumbnail', - 'thumbnailViewModel', 'image'), final_key='sources'), + 'metadata', 'lockupMetadataViewModel', 'title', + 'content', T(compat_str))), + 'thumbnails': self._extract_thumbnails( + view_model, thumb_keys, final_key='sources'), }) def _extract_shorts_lockup_view_model(self, view_model): content_id = traverse_obj(view_model, ( 'onTap', 'innertubeCommand', 'reelWatchEndpoint', 'videoId', T(lambda v: v if YoutubeIE.suitable(v) else None))) - if not content_id: - return return merge_dicts(self.url_result( content_id, ie=YoutubeIE.ie_key(), video_id=content_id), { 'title': traverse_obj(view_model, ( 'overlayMetadata', 'primaryText', 'content', T(compat_str))), 'thumbnails': self._extract_thumbnails( view_model, 'thumbnail', final_key='sources'), - }) + }) if content_id else None def _video_entry(self, video_renderer): video_id = video_renderer.get('videoId') - if video_id: - return self._extract_video(video_renderer) + return self._extract_video(video_renderer) if video_id else None def _post_thread_entries(self, post_thread_renderer): post_renderer = try_get( @@ -4119,6 +4234,7 @@ class YoutubeFeedsInfoExtractor(YoutubeTabIE): Subclasses must define the _FEED_NAME property. """ + _LOGIN_REQUIRED = True @property diff --git a/youtube-dl/youtube_dl/options.py b/youtube-dl/youtube_dl/options.py index 61705d1f02..ce3633c418 100644 --- a/youtube-dl/youtube_dl/options.py +++ b/youtube-dl/youtube_dl/options.py @@ -404,6 +404,10 @@ def parseOpts(overrideArguments=None): '-F', '--list-formats', action='store_true', dest='listformats', help='List all available formats of requested videos') + video_format.add_option( + '--no-list-formats', + action='store_false', dest='listformats', + help='Do not list available formats of requested videos (default)') video_format.add_option( '--youtube-include-dash-manifest', action='store_true', dest='youtube_include_dash_manifest', default=True, @@ -412,6 +416,17 @@ def parseOpts(overrideArguments=None): '--youtube-skip-dash-manifest', action='store_false', dest='youtube_include_dash_manifest', help='Do not download the DASH manifests and related data on YouTube videos') + video_format.add_option( + '--youtube-player-js-variant', + action='store', dest='youtube_player_js_variant', + help='For YouTube, the player javascript variant to use for n/sig deciphering; `actual` to follow the site; default `%default`.', + choices=('actual', 'main', 'tcc', 'tce', 'es5', 'es6', 'tv', 'tv_es6', 'phone', 'tablet'), + default='main', metavar='VARIANT') + video_format.add_option( + '--youtube-player-js-version', + action='store', dest='youtube_player_js_version', + help='For YouTube, the player javascript version to use for n/sig deciphering, specified as `signature_timestamp@hash`, or `actual` to follow the site; default `%default`', + default='20348@0004de42', metavar='STS@HASH') video_format.add_option( '--merge-output-format', action='store', dest='merge_output_format', metavar='FORMAT', default=None, diff --git a/yt-dlp/test/test_pot/test_pot_builtin_utils.py b/yt-dlp/test/test_pot/test_pot_builtin_utils.py index 7645ba601f..15a25cff2f 100644 --- a/yt-dlp/test/test_pot/test_pot_builtin_utils.py +++ b/yt-dlp/test/test_pot/test_pot_builtin_utils.py @@ -45,3 +45,8 @@ class TestGetWebPoContentBinding: def test_invalid_base64(self, pot_request): pot_request.visitor_data = 'invalid-base64' assert get_webpo_content_binding(pot_request, bind_to_visitor_id=True) == (pot_request.visitor_data, ContentBindingType.VISITOR_DATA) + + def test_gvs_video_id_binding_experiment(self, pot_request): + pot_request.context = PoTokenContext.GVS + pot_request._gvs_bind_to_video_id = True + assert get_webpo_content_binding(pot_request) == ('example-video-id', ContentBindingType.VIDEO_ID) diff --git a/yt-dlp/yt_dlp/extractor/youtube/_video.py b/yt-dlp/yt_dlp/extractor/youtube/_video.py index 79c183c6a5..9ef7f14dfa 100644 --- a/yt-dlp/yt_dlp/extractor/youtube/_video.py +++ b/yt-dlp/yt_dlp/extractor/youtube/_video.py @@ -2955,9 +2955,20 @@ class YoutubeIE(YoutubeBaseInfoExtractor): # TODO(future): This validation should be moved into pot framework. # Some sort of middleware or validation provider perhaps? + gvs_bind_to_video_id = False + experiments = traverse_obj(ytcfg, ( + 'WEB_PLAYER_CONTEXT_CONFIGS', ..., 'serializedExperimentFlags', {urllib.parse.parse_qs})) + if 'true' in traverse_obj(experiments, (..., 'html5_generate_content_po_token', -1)): + self.write_debug( + f'{video_id}: Detected experiment to bind GVS PO Token to video id.', only_once=True) + gvs_bind_to_video_id = True + # GVS WebPO Token is bound to visitor_data / Visitor ID when logged out. # Must have visitor_data for it to function. - if player_url and context == _PoTokenContext.GVS and not visitor_data and not self.is_authenticated: + if ( + player_url and context == _PoTokenContext.GVS + and not visitor_data and not self.is_authenticated and not gvs_bind_to_video_id + ): self.report_warning( f'Unable to fetch GVS PO Token for {client} client: Missing required Visitor Data. ' f'You may need to pass Visitor Data with --extractor-args "youtube:visitor_data=XXX"', only_once=True) @@ -2971,7 +2982,10 @@ class YoutubeIE(YoutubeBaseInfoExtractor): config_po_token = self._get_config_po_token(client, context) if config_po_token: # GVS WebPO token is bound to data_sync_id / account Session ID when logged in. - if player_url and context == _PoTokenContext.GVS and not data_sync_id and self.is_authenticated: + if ( + player_url and context == _PoTokenContext.GVS + and not data_sync_id and self.is_authenticated and not gvs_bind_to_video_id + ): self.report_warning( f'Got a GVS PO Token for {client} client, but missing Data Sync ID for account. Formats may not work.' f'You may need to pass a Data Sync ID with --extractor-args "youtube:data_sync_id=XXX"') @@ -2997,6 +3011,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor): video_id=video_id, video_webpage=webpage, required=required, + _gvs_bind_to_video_id=gvs_bind_to_video_id, **kwargs, ) @@ -3040,6 +3055,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor): data_sync_id=kwargs.get('data_sync_id'), video_id=kwargs.get('video_id'), request_cookiejar=self._downloader.cookiejar, + _gvs_bind_to_video_id=kwargs.get('_gvs_bind_to_video_id', False), # All requests that would need to be proxied should be in the # context of www.youtube.com or the innertube host diff --git a/yt-dlp/yt_dlp/extractor/youtube/pot/provider.py b/yt-dlp/yt_dlp/extractor/youtube/pot/provider.py index 13b3b1f9bb..2511edf015 100644 --- a/yt-dlp/yt_dlp/extractor/youtube/pot/provider.py +++ b/yt-dlp/yt_dlp/extractor/youtube/pot/provider.py @@ -58,6 +58,8 @@ class PoTokenRequest: visitor_data: str | None = None data_sync_id: str | None = None video_id: str | None = None + # Internal, YouTube experiment on whether to bind GVS PO Token to video_id. + _gvs_bind_to_video_id: bool = False # Networking parameters request_cookiejar: YoutubeDLCookieJar = dataclasses.field(default_factory=YoutubeDLCookieJar) diff --git a/yt-dlp/yt_dlp/extractor/youtube/pot/utils.py b/yt-dlp/yt_dlp/extractor/youtube/pot/utils.py index a27921d4af..7f9ca078d6 100644 --- a/yt-dlp/yt_dlp/extractor/youtube/pot/utils.py +++ b/yt-dlp/yt_dlp/extractor/youtube/pot/utils.py @@ -42,6 +42,9 @@ def get_webpo_content_binding( if not client_name or client_name not in webpo_clients: return None, None + if request.context == PoTokenContext.GVS and request._gvs_bind_to_video_id: + return request.video_id, ContentBindingType.VIDEO_ID + if request.context == PoTokenContext.GVS or client_name in ('WEB_REMIX', ): if request.is_authenticated: return request.data_sync_id, ContentBindingType.DATASYNC_ID