diff --git a/component/geodata/utils.go b/component/geodata/utils.go index facc72e1..9e7e50b1 100644 --- a/component/geodata/utils.go +++ b/component/geodata/utils.go @@ -21,16 +21,16 @@ func SetLoader(newLoader string) { geoLoaderName = newLoader } -func Verify(name string) bool { +func Verify(name string) error { switch name { case C.GeositeName: _, _, err := LoadGeoSiteMatcher("CN") - return err == nil + return err case C.GeoipName: _, _, err := LoadGeoIPMatcher("CN") - return err == nil + return err default: - return false + return fmt.Errorf("not support name") } } diff --git a/config/config.go b/config/config.go index a8c5be9d..0b968665 100644 --- a/config/config.go +++ b/config/config.go @@ -219,11 +219,18 @@ type RawConfig struct { IPTables IPTables `yaml:"iptables"` Experimental Experimental `yaml:"experimental"` Profile Profile `yaml:"profile"` + GeoXUrl RawGeoXUrl `yaml:"geox-url"` Proxy []map[string]any `yaml:"proxies"` ProxyGroup []map[string]any `yaml:"proxy-groups"` Rule []string `yaml:"rules"` } +type RawGeoXUrl struct { + GeoIp string `yaml:"geoip" json:"geoip"` + Mmdb string `yaml:"mmdb" json:"mmdb"` + GeoSite string `yaml:"geosite" json:"geosite"` +} + type RawSniffer struct { Enable bool `yaml:"enable" json:"enable"` Sniffing []string `yaml:"sniffing" json:"sniffing"` @@ -309,6 +316,11 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) { Profile: Profile{ StoreSelected: true, }, + GeoXUrl: RawGeoXUrl{ + GeoIp: "https://raw.githubusercontents.com/Loyalsoldier/v2ray-rules-dat/release/geoip.dat", + Mmdb: "https://raw.githubusercontents.com/Loyalsoldier/geoip/release/Country.mmdb", + GeoSite: "https://raw.githubusercontents.com/Loyalsoldier/v2ray-rules-dat/release/geosite.dat", + }, } if err := yaml.Unmarshal(buf, rawCfg); err != nil { diff --git a/config/initial.go b/config/initial.go index e547ef5d..83c3e38b 100644 --- a/config/initial.go +++ b/config/initial.go @@ -15,7 +15,7 @@ import ( var initMode = true func downloadMMDB(path string) (err error) { - resp, err := http.Get("https://raw.githubusercontents.com/Loyalsoldier/geoip/release/Country.mmdb") + resp, err := http.Get(C.MmdbUrl) if err != nil { return } @@ -32,7 +32,7 @@ func downloadMMDB(path string) (err error) { } func downloadGeoIP(path string) (err error) { - resp, err := http.Get("https://raw.githubusercontents.com/Loyalsoldier/v2ray-rules-dat/release/geoip.dat") + resp, err := http.Get(C.GeoIpUrl) if err != nil { return } @@ -49,7 +49,7 @@ func downloadGeoIP(path string) (err error) { } func downloadGeoSite(path string) (err error) { - resp, err := http.Get("https://raw.githubusercontents.com/Loyalsoldier/v2ray-rules-dat/release/geosite.dat") + resp, err := http.Get(C.GeoSiteUrl) if err != nil { return } @@ -74,8 +74,8 @@ func initGeoSite() error { log.Infoln("Download GeoSite.dat finish") } if initMode { - if !geodata.Verify(C.GeositeName) { - log.Warnln("GeoSite.dat invalid, remove and download") + if err := geodata.Verify(C.GeositeName); err != nil { + log.Warnln("GeoSite.dat invalid, remove and download: %s", err) if err := os.Remove(C.Path.GeoSite()); err != nil { return fmt.Errorf("can't remove invalid GeoSite.dat: %s", err.Error()) } @@ -97,8 +97,8 @@ func initGeoIP() error { log.Infoln("Download GeoIP.dat finish") } - if !geodata.Verify(C.GeoipName) { - log.Warnln("GeoIP.dat invalid, remove and download") + if err := geodata.Verify(C.GeoipName); err != nil { + log.Warnln("GeoIP.dat invalid, remove and download: %s", err) if err := os.Remove(C.Path.GeoIP()); err != nil { return fmt.Errorf("can't remove invalid GeoIP.dat: %s", err.Error()) } @@ -158,6 +158,9 @@ func Init(dir string) error { } if !C.GeodataMode { C.GeodataMode = rawCfg.GeodataMode + C.GeoIpUrl = rawCfg.GeoXUrl.GeoIp + C.GeoSiteUrl = rawCfg.GeoXUrl.GeoSite + C.MmdbUrl = rawCfg.GeoXUrl.Mmdb } // initial GeoIP if err := initGeoIP(); err != nil { diff --git a/config/updateGeo.go b/config/updateGeo.go new file mode 100644 index 00000000..2dfb8fa0 --- /dev/null +++ b/config/updateGeo.go @@ -0,0 +1,82 @@ +package config + +import ( + "fmt" + "github.com/Dreamacro/clash/component/geodata" + _ "github.com/Dreamacro/clash/component/geodata/standard" + C "github.com/Dreamacro/clash/constant" + "github.com/oschwald/geoip2-golang" + "os" + "runtime" +) + +func UpdateGeoDatabases() error { + var ( + tmpMMDB = C.Path.Resolve("temp_country.mmdb") + tmpGepIP = C.Path.Resolve("temp_geoip.dat") + tmpGeoSite = C.Path.Resolve("temp_geosite.dat") + ) + + if C.GeodataMode { + if err := downloadGeoIP(tmpGepIP); err != nil { + return fmt.Errorf("can't download MMDB database file: %w", err) + } + + if err := verifyGeoSite("temp_geoip.dat"); err != nil { + _ = os.Remove(tmpGepIP) + return fmt.Errorf("invalid GeoIP database file: %s", err) + } + + if err := os.Rename(tmpGepIP, C.Path.GeoIP()); err != nil { + return fmt.Errorf("can't rename MMDB database file: %w", err) + } + + } else { + if err := downloadMMDB(tmpMMDB); err != nil { + return fmt.Errorf("can't download MMDB database file: %w", err) + } + + if err := verifyMMDB("temp_country.mmdb"); err != nil { + _ = os.Remove(tmpMMDB) + return fmt.Errorf("invalid MMDB database file: %s", err) + } + + if err := os.Rename(tmpMMDB, C.Path.MMDB()); err != nil { + return fmt.Errorf("can't rename MMDB database file: %w", err) + } + } + + if err := downloadGeoSite(tmpGeoSite); err != nil { + return fmt.Errorf("can't download GeoSite database file: %w", err) + } + + if err := verifyGeoSite("temp_geosite.dat"); err != nil { + _ = os.Remove(tmpGeoSite) + return fmt.Errorf("invalid GeoSite database file: %s", err) + } + + if err := os.Rename(tmpGeoSite, C.Path.GeoSite()); err != nil { + return fmt.Errorf("can't rename GeoSite database file: %w", err) + } + runtime.GC() + return nil +} + +func verifyMMDB(path string) error { + instance, err := geoip2.Open(path) + if err == nil { + _ = instance.Close() + } + return err +} + +func verifyGeoSite(path string) error { + geoLoader, err := geodata.GetGeoDataLoader("standard") + if err != nil { + return err + } + + _, err = geoLoader.LoadSite(path, "cn") + + return err +} diff --git a/constant/geodata.go b/constant/geodata.go index 7548356a..72452270 100644 --- a/constant/geodata.go +++ b/constant/geodata.go @@ -1,3 +1,8 @@ package constant -var GeodataMode bool +var ( + GeodataMode bool + GeoIpUrl string + MmdbUrl string + GeoSiteUrl string +) diff --git a/hub/route/configs.go b/hub/route/configs.go index 78927cc9..b60ad586 100644 --- a/hub/route/configs.go +++ b/hub/route/configs.go @@ -3,6 +3,7 @@ package route import ( "net/http" "path/filepath" + "sync" "github.com/Dreamacro/clash/component/resolver" "github.com/Dreamacro/clash/config" @@ -16,10 +17,16 @@ import ( "github.com/go-chi/render" ) +var ( + updateGeoMux sync.Mutex + updatingGeo = false +) + func configRouter() http.Handler { r := chi.NewRouter() r.Get("/", getConfigs) r.Put("/", updateConfigs) + r.Post("/geo", updateGeoDatabases) r.Patch("/", patchConfigs) return r } @@ -143,3 +150,42 @@ func updateConfigs(w http.ResponseWriter, r *http.Request) { executor.ApplyConfig(cfg, force) render.NoContent(w, r) } + +func updateGeoDatabases(w http.ResponseWriter, r *http.Request) { + updateGeoMux.Lock() + + if updatingGeo { + updateGeoMux.Unlock() + render.Status(r, http.StatusBadRequest) + render.JSON(w, r, newError("updating...")) + return + } + + updatingGeo = true + updateGeoMux.Unlock() + + go func() { + defer func() { + updatingGeo = false + }() + + log.Warnln("[REST-API] updating GEO databases...") + + if err := config.UpdateGeoDatabases(); err != nil { + log.Errorln("[REST-API] update GEO databases failed: %v", err) + return + } + + cfg, err := executor.ParseWithPath(constant.Path.Config()) + if err != nil { + log.Errorln("[REST-API] update GEO databases failed: %v", err) + return + } + + log.Warnln("[REST-API] update GEO databases successful, apply config...") + + executor.ApplyConfig(cfg, false) + }() + + render.NoContent(w, r) +}