| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760 |
- package service
- import (
- "context"
- "errors"
- "fmt"
- "io"
- "net"
- "net/http"
- "net/url"
- "os"
- "path/filepath"
- "regexp"
- "strings"
- "time"
- "github.com/mhsanaei/3x-ui/v2/config"
- "github.com/mhsanaei/3x-ui/v2/database"
- "github.com/mhsanaei/3x-ui/v2/database/model"
- "github.com/mhsanaei/3x-ui/v2/logger"
- )
- const (
- customGeoTypeGeosite = "geosite"
- customGeoTypeGeoip = "geoip"
- minDatBytes = 64
- customGeoProbeTimeout = 12 * time.Second
- )
- var (
- customGeoAliasPattern = regexp.MustCompile(`^[a-z0-9_-]+$`)
- reservedCustomAliases = map[string]struct{}{
- "geoip": {}, "geosite": {},
- "geoip_ir": {}, "geosite_ir": {},
- "geoip_ru": {}, "geosite_ru": {},
- }
- ErrCustomGeoInvalidType = errors.New("custom_geo_invalid_type")
- ErrCustomGeoAliasRequired = errors.New("custom_geo_alias_required")
- ErrCustomGeoAliasPattern = errors.New("custom_geo_alias_pattern")
- ErrCustomGeoAliasReserved = errors.New("custom_geo_alias_reserved")
- ErrCustomGeoURLRequired = errors.New("custom_geo_url_required")
- ErrCustomGeoInvalidURL = errors.New("custom_geo_invalid_url")
- ErrCustomGeoURLScheme = errors.New("custom_geo_url_scheme")
- ErrCustomGeoURLHost = errors.New("custom_geo_url_host")
- ErrCustomGeoDuplicateAlias = errors.New("custom_geo_duplicate_alias")
- ErrCustomGeoNotFound = errors.New("custom_geo_not_found")
- ErrCustomGeoDownload = errors.New("custom_geo_download")
- ErrCustomGeoSSRFBlocked = errors.New("custom_geo_ssrf_blocked")
- ErrCustomGeoPathTraversal = errors.New("custom_geo_path_traversal")
- )
- type CustomGeoUpdateAllItem struct {
- Id int `json:"id"`
- Alias string `json:"alias"`
- FileName string `json:"fileName"`
- }
- type CustomGeoUpdateAllFailure struct {
- Id int `json:"id"`
- Alias string `json:"alias"`
- FileName string `json:"fileName"`
- Err string `json:"error"`
- }
- type CustomGeoUpdateAllResult struct {
- Succeeded []CustomGeoUpdateAllItem `json:"succeeded"`
- Failed []CustomGeoUpdateAllFailure `json:"failed"`
- }
- type CustomGeoService struct {
- serverService *ServerService
- updateAllGetAll func() ([]model.CustomGeoResource, error)
- updateAllApply func(id int, onStartup bool) (string, error)
- updateAllRestart func() error
- }
- func NewCustomGeoService() *CustomGeoService {
- s := &CustomGeoService{
- serverService: &ServerService{},
- }
- s.updateAllGetAll = s.GetAll
- s.updateAllApply = s.applyDownloadAndPersist
- s.updateAllRestart = func() error { return s.serverService.RestartXrayService() }
- return s
- }
- func NormalizeAliasKey(alias string) string {
- return strings.ToLower(strings.ReplaceAll(alias, "-", "_"))
- }
- func (s *CustomGeoService) fileNameFor(typ, alias string) string {
- if typ == customGeoTypeGeoip {
- return fmt.Sprintf("geoip_%s.dat", alias)
- }
- return fmt.Sprintf("geosite_%s.dat", alias)
- }
- func (s *CustomGeoService) validateType(typ string) error {
- if typ != customGeoTypeGeosite && typ != customGeoTypeGeoip {
- return ErrCustomGeoInvalidType
- }
- return nil
- }
- func (s *CustomGeoService) validateAlias(alias string) error {
- if alias == "" {
- return ErrCustomGeoAliasRequired
- }
- if !customGeoAliasPattern.MatchString(alias) {
- return ErrCustomGeoAliasPattern
- }
- if _, ok := reservedCustomAliases[NormalizeAliasKey(alias)]; ok {
- return ErrCustomGeoAliasReserved
- }
- return nil
- }
- func (s *CustomGeoService) sanitizeURL(raw string) (string, error) {
- if raw == "" {
- return "", ErrCustomGeoURLRequired
- }
- u, err := url.Parse(raw)
- if err != nil {
- return "", ErrCustomGeoInvalidURL
- }
- if u.Scheme != "http" && u.Scheme != "https" {
- return "", ErrCustomGeoURLScheme
- }
- if u.Host == "" {
- return "", ErrCustomGeoURLHost
- }
- if err := checkSSRF(context.Background(), u.Hostname()); err != nil {
- return "", err
- }
- // Reconstruct URL from parsed components to break taint propagation.
- clean := &url.URL{
- Scheme: u.Scheme,
- Host: u.Host,
- Path: u.Path,
- RawPath: u.RawPath,
- RawQuery: u.RawQuery,
- Fragment: u.Fragment,
- }
- return clean.String(), nil
- }
- func localDatFileNeedsRepair(path string) bool {
- safePath, err := sanitizeDestPath(path)
- if err != nil {
- return true
- }
- fi, err := os.Stat(safePath)
- if err != nil {
- return true
- }
- if fi.IsDir() {
- return true
- }
- return fi.Size() < int64(minDatBytes)
- }
- func CustomGeoLocalFileNeedsRepair(path string) bool {
- return localDatFileNeedsRepair(path)
- }
- func isBlockedIP(ip net.IP) bool {
- return ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() ||
- ip.IsLinkLocalMulticast() || ip.IsUnspecified()
- }
- // checkSSRFDefault validates that the given host does not resolve to a private/internal IP.
- // It is context-aware so that dial context cancellation/deadlines are respected during DNS resolution.
- func checkSSRFDefault(ctx context.Context, hostname string) error {
- ips, err := net.DefaultResolver.LookupIPAddr(ctx, hostname)
- if err != nil {
- return fmt.Errorf("%w: cannot resolve host %s", ErrCustomGeoSSRFBlocked, hostname)
- }
- for _, ipAddr := range ips {
- if isBlockedIP(ipAddr.IP) {
- return fmt.Errorf("%w: %s resolves to blocked address %s", ErrCustomGeoSSRFBlocked, hostname, ipAddr.IP)
- }
- }
- return nil
- }
- // checkSSRF is the active SSRF guard. Override in tests to allow localhost test servers.
- var checkSSRF = checkSSRFDefault
- func ssrfSafeTransport() http.RoundTripper {
- base, ok := http.DefaultTransport.(*http.Transport)
- if !ok {
- base = &http.Transport{}
- }
- cloned := base.Clone()
- cloned.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
- host, _, err := net.SplitHostPort(addr)
- if err != nil {
- return nil, fmt.Errorf("%w: %v", ErrCustomGeoSSRFBlocked, err)
- }
- if err := checkSSRF(ctx, host); err != nil {
- return nil, err
- }
- var dialer net.Dialer
- return dialer.DialContext(ctx, network, addr)
- }
- return cloned
- }
- func probeCustomGeoURLWithGET(rawURL string) error {
- sanitizedURL, err := (&CustomGeoService{}).sanitizeURL(rawURL)
- if err != nil {
- return err
- }
- client := &http.Client{Timeout: customGeoProbeTimeout, Transport: ssrfSafeTransport()}
- req, err := http.NewRequest(http.MethodGet, sanitizedURL, nil)
- if err != nil {
- return err
- }
- req.Header.Set("Range", "bytes=0-0")
- resp, err := client.Do(req)
- if err != nil {
- return err
- }
- defer resp.Body.Close()
- _, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 256))
- switch resp.StatusCode {
- case http.StatusOK, http.StatusPartialContent:
- return nil
- default:
- return fmt.Errorf("get range status %d", resp.StatusCode)
- }
- }
- func probeCustomGeoURL(rawURL string) error {
- sanitizedURL, err := (&CustomGeoService{}).sanitizeURL(rawURL)
- if err != nil {
- return err
- }
- client := &http.Client{Timeout: customGeoProbeTimeout, Transport: ssrfSafeTransport()}
- req, err := http.NewRequest(http.MethodHead, sanitizedURL, nil)
- if err != nil {
- return err
- }
- resp, err := client.Do(req)
- if err != nil {
- return err
- }
- _ = resp.Body.Close()
- sc := resp.StatusCode
- if sc >= 200 && sc < 300 {
- return nil
- }
- if sc == http.StatusMethodNotAllowed || sc == http.StatusNotImplemented {
- return probeCustomGeoURLWithGET(rawURL)
- }
- return fmt.Errorf("head status %d", sc)
- }
- func (s *CustomGeoService) EnsureOnStartup() {
- list, err := s.GetAll()
- if err != nil {
- logger.Warning("custom geo startup: load list:", err)
- return
- }
- n := len(list)
- if n == 0 {
- logger.Info("custom geo startup: no custom geofiles configured")
- return
- }
- logger.Infof("custom geo startup: checking %d custom geofile(s)", n)
- for i := range list {
- r := &list[i]
- sanitizedURL, err := s.sanitizeURL(r.Url)
- if err != nil {
- logger.Warningf("custom geo startup id=%d: invalid url: %v", r.Id, err)
- continue
- }
- r.Url = sanitizedURL
- s.syncLocalPath(r)
- localPath := r.LocalPath
- if !localDatFileNeedsRepair(localPath) {
- logger.Infof("custom geo startup id=%d alias=%s path=%s: present", r.Id, r.Alias, localPath)
- continue
- }
- logger.Infof("custom geo startup id=%d alias=%s path=%s: missing or needs repair, probing source", r.Id, r.Alias, localPath)
- if err := probeCustomGeoURL(r.Url); err != nil {
- logger.Warningf("custom geo startup id=%d alias=%s url=%s: probe: %v (attempting download anyway)", r.Id, r.Alias, r.Url, err)
- }
- _, _ = s.applyDownloadAndPersist(r.Id, true)
- }
- }
- func (s *CustomGeoService) downloadToPath(resourceURL, destPath string, lastModifiedHeader string) (skipped bool, newLastModified string, err error) {
- safeDestPath, err := sanitizeDestPath(destPath)
- if err != nil {
- return false, "", fmt.Errorf("%w: %v", ErrCustomGeoDownload, err)
- }
- skipped, lm, err := s.downloadToPathOnce(resourceURL, safeDestPath, lastModifiedHeader, false)
- if err != nil {
- return false, "", err
- }
- if skipped {
- if _, statErr := os.Stat(safeDestPath); statErr == nil && !localDatFileNeedsRepair(safeDestPath) {
- return true, lm, nil
- }
- return s.downloadToPathOnce(resourceURL, safeDestPath, lastModifiedHeader, true)
- }
- return false, lm, nil
- }
- // sanitizeDestPath ensures destPath is inside the bin folder, preventing path traversal.
- // It resolves symlinks to prevent symlink-based escapes.
- // Returns the cleaned absolute path that is safe to use in file operations.
- func sanitizeDestPath(destPath string) (string, error) {
- baseDirAbs, err := filepath.Abs(config.GetBinFolderPath())
- if err != nil {
- return "", fmt.Errorf("%w: %v", ErrCustomGeoPathTraversal, err)
- }
- // Resolve symlinks in base directory to get the real path.
- if resolved, evalErr := filepath.EvalSymlinks(baseDirAbs); evalErr == nil {
- baseDirAbs = resolved
- }
- destPathAbs, err := filepath.Abs(destPath)
- if err != nil {
- return "", fmt.Errorf("%w: %v", ErrCustomGeoPathTraversal, err)
- }
- // Resolve symlinks for the parent directory of the destination path.
- destDir := filepath.Dir(destPathAbs)
- if resolved, evalErr := filepath.EvalSymlinks(destDir); evalErr == nil {
- destPathAbs = filepath.Join(resolved, filepath.Base(destPathAbs))
- }
- // Verify the resolved path is within the safe base directory using prefix check.
- safeDirPrefix := baseDirAbs + string(filepath.Separator)
- if !strings.HasPrefix(destPathAbs, safeDirPrefix) {
- return "", ErrCustomGeoPathTraversal
- }
- return destPathAbs, nil
- }
- func (s *CustomGeoService) downloadToPathOnce(resourceURL, destPath string, lastModifiedHeader string, forceFull bool) (skipped bool, newLastModified string, err error) {
- safeDestPath, err := sanitizeDestPath(destPath)
- if err != nil {
- return false, "", fmt.Errorf("%w: %v", ErrCustomGeoDownload, err)
- }
- sanitizedURL, err := s.sanitizeURL(resourceURL)
- if err != nil {
- return false, "", fmt.Errorf("%w: %v", ErrCustomGeoDownload, err)
- }
- var req *http.Request
- req, err = http.NewRequest(http.MethodGet, sanitizedURL, nil)
- if err != nil {
- return false, "", fmt.Errorf("%w: %v", ErrCustomGeoDownload, err)
- }
- if !forceFull {
- if fi, statErr := os.Stat(safeDestPath); statErr == nil && !localDatFileNeedsRepair(safeDestPath) {
- if !fi.ModTime().IsZero() {
- req.Header.Set("If-Modified-Since", fi.ModTime().UTC().Format(http.TimeFormat))
- } else if lastModifiedHeader != "" {
- if t, perr := time.Parse(http.TimeFormat, lastModifiedHeader); perr == nil {
- req.Header.Set("If-Modified-Since", t.UTC().Format(http.TimeFormat))
- }
- }
- }
- }
- client := &http.Client{Timeout: 10 * time.Minute, Transport: ssrfSafeTransport()}
- // lgtm[go/request-forgery]
- resp, err := client.Do(req)
- if err != nil {
- return false, "", fmt.Errorf("%w: %v", ErrCustomGeoDownload, err)
- }
- defer resp.Body.Close()
- var serverModTime time.Time
- if lm := resp.Header.Get("Last-Modified"); lm != "" {
- if parsed, perr := time.Parse(http.TimeFormat, lm); perr == nil {
- serverModTime = parsed
- newLastModified = lm
- }
- }
- updateModTime := func() {
- if !serverModTime.IsZero() {
- _ = os.Chtimes(safeDestPath, serverModTime, serverModTime)
- }
- }
- if resp.StatusCode == http.StatusNotModified {
- if forceFull {
- return false, "", fmt.Errorf("%w: unexpected 304 on unconditional get", ErrCustomGeoDownload)
- }
- updateModTime()
- return true, newLastModified, nil
- }
- if resp.StatusCode != http.StatusOK {
- return false, "", fmt.Errorf("%w: unexpected status %d", ErrCustomGeoDownload, resp.StatusCode)
- }
- binDir := filepath.Dir(safeDestPath)
- if err = os.MkdirAll(binDir, 0o755); err != nil {
- return false, "", fmt.Errorf("%w: %v", ErrCustomGeoDownload, err)
- }
- safeTmpPath, err := sanitizeDestPath(safeDestPath + ".tmp")
- if err != nil {
- return false, "", fmt.Errorf("%w: %v", ErrCustomGeoDownload, err)
- }
- out, err := os.Create(safeTmpPath)
- if err != nil {
- return false, "", fmt.Errorf("%w: %v", ErrCustomGeoDownload, err)
- }
- n, err := io.Copy(out, resp.Body)
- closeErr := out.Close()
- if err != nil {
- _ = os.Remove(safeTmpPath)
- return false, "", fmt.Errorf("%w: %v", ErrCustomGeoDownload, err)
- }
- if closeErr != nil {
- _ = os.Remove(safeTmpPath)
- return false, "", fmt.Errorf("%w: %v", ErrCustomGeoDownload, closeErr)
- }
- if n < minDatBytes {
- _ = os.Remove(safeTmpPath)
- return false, "", fmt.Errorf("%w: file too small", ErrCustomGeoDownload)
- }
- if err = os.Rename(safeTmpPath, safeDestPath); err != nil {
- _ = os.Remove(safeTmpPath)
- return false, "", fmt.Errorf("%w: %v", ErrCustomGeoDownload, err)
- }
- updateModTime()
- if newLastModified == "" && resp.Header.Get("Last-Modified") != "" {
- newLastModified = resp.Header.Get("Last-Modified")
- }
- return false, newLastModified, nil
- }
- func (s *CustomGeoService) resolveDestPath(r *model.CustomGeoResource) string {
- if r.LocalPath != "" {
- return r.LocalPath
- }
- return filepath.Join(config.GetBinFolderPath(), s.fileNameFor(r.Type, r.Alias))
- }
- func (s *CustomGeoService) syncLocalPath(r *model.CustomGeoResource) {
- p := filepath.Join(config.GetBinFolderPath(), s.fileNameFor(r.Type, r.Alias))
- r.LocalPath = p
- }
- func (s *CustomGeoService) syncAndSanitizeLocalPath(r *model.CustomGeoResource) error {
- s.syncLocalPath(r)
- safePath, err := sanitizeDestPath(r.LocalPath)
- if err != nil {
- return err
- }
- r.LocalPath = safePath
- return nil
- }
- func removeSafePathIfExists(path string) error {
- safePath, err := sanitizeDestPath(path)
- if err != nil {
- return err
- }
- if _, err := os.Stat(safePath); err == nil {
- if err := os.Remove(safePath); err != nil {
- return err
- }
- }
- return nil
- }
- func (s *CustomGeoService) Create(r *model.CustomGeoResource) error {
- if err := s.validateType(r.Type); err != nil {
- return err
- }
- if err := s.validateAlias(r.Alias); err != nil {
- return err
- }
- sanitizedURL, err := s.sanitizeURL(r.Url)
- if err != nil {
- return err
- }
- r.Url = sanitizedURL
- var existing int64
- database.GetDB().Model(&model.CustomGeoResource{}).
- Where("geo_type = ? AND alias = ?", r.Type, r.Alias).Count(&existing)
- if existing > 0 {
- return ErrCustomGeoDuplicateAlias
- }
- if err := s.syncAndSanitizeLocalPath(r); err != nil {
- return err
- }
- skipped, lm, err := s.downloadToPath(r.Url, r.LocalPath, r.LastModified)
- if err != nil {
- return err
- }
- now := time.Now().Unix()
- r.LastUpdatedAt = now
- r.LastModified = lm
- if err = database.GetDB().Create(r).Error; err != nil {
- _ = removeSafePathIfExists(r.LocalPath)
- return err
- }
- logger.Infof("custom geo created id=%d type=%s alias=%s skipped=%v", r.Id, r.Type, r.Alias, skipped)
- if err = s.serverService.RestartXrayService(); err != nil {
- logger.Warning("custom geo create: restart xray:", err)
- }
- return nil
- }
- func (s *CustomGeoService) Update(id int, r *model.CustomGeoResource) error {
- var cur model.CustomGeoResource
- if err := database.GetDB().First(&cur, id).Error; err != nil {
- if database.IsNotFound(err) {
- return ErrCustomGeoNotFound
- }
- return err
- }
- if err := s.validateType(r.Type); err != nil {
- return err
- }
- if err := s.validateAlias(r.Alias); err != nil {
- return err
- }
- sanitizedURL, err := s.sanitizeURL(r.Url)
- if err != nil {
- return err
- }
- r.Url = sanitizedURL
- if cur.Type != r.Type || cur.Alias != r.Alias {
- var cnt int64
- database.GetDB().Model(&model.CustomGeoResource{}).
- Where("geo_type = ? AND alias = ? AND id <> ?", r.Type, r.Alias, id).
- Count(&cnt)
- if cnt > 0 {
- return ErrCustomGeoDuplicateAlias
- }
- }
- oldPath := s.resolveDestPath(&cur)
- r.Id = id
- if err := s.syncAndSanitizeLocalPath(r); err != nil {
- return err
- }
- if oldPath != r.LocalPath && oldPath != "" {
- if err := removeSafePathIfExists(oldPath); err != nil && !errors.Is(err, ErrCustomGeoPathTraversal) {
- logger.Warningf("custom geo remove old path %s: %v", oldPath, err)
- }
- }
- _, lm, err := s.downloadToPath(r.Url, r.LocalPath, cur.LastModified)
- if err != nil {
- return err
- }
- r.LastUpdatedAt = time.Now().Unix()
- r.LastModified = lm
- err = database.GetDB().Model(&model.CustomGeoResource{}).Where("id = ?", id).Updates(map[string]any{
- "geo_type": r.Type,
- "alias": r.Alias,
- "url": r.Url,
- "local_path": r.LocalPath,
- "last_updated_at": r.LastUpdatedAt,
- "last_modified": r.LastModified,
- }).Error
- if err != nil {
- return err
- }
- logger.Infof("custom geo updated id=%d", id)
- if err = s.serverService.RestartXrayService(); err != nil {
- logger.Warning("custom geo update: restart xray:", err)
- }
- return nil
- }
- func (s *CustomGeoService) Delete(id int) (displayName string, err error) {
- var r model.CustomGeoResource
- if err := database.GetDB().First(&r, id).Error; err != nil {
- if database.IsNotFound(err) {
- return "", ErrCustomGeoNotFound
- }
- return "", err
- }
- displayName = s.fileNameFor(r.Type, r.Alias)
- p := s.resolveDestPath(&r)
- if _, err := sanitizeDestPath(p); err != nil {
- return displayName, err
- }
- if err := database.GetDB().Delete(&model.CustomGeoResource{}, id).Error; err != nil {
- return displayName, err
- }
- if p != "" {
- if err := removeSafePathIfExists(p); err != nil {
- logger.Warningf("custom geo delete file %s: %v", p, err)
- }
- }
- logger.Infof("custom geo deleted id=%d", id)
- if err := s.serverService.RestartXrayService(); err != nil {
- logger.Warning("custom geo delete: restart xray:", err)
- }
- return displayName, nil
- }
- func (s *CustomGeoService) GetAll() ([]model.CustomGeoResource, error) {
- var list []model.CustomGeoResource
- err := database.GetDB().Order("id asc").Find(&list).Error
- return list, err
- }
- func (s *CustomGeoService) applyDownloadAndPersist(id int, onStartup bool) (displayName string, err error) {
- var r model.CustomGeoResource
- if err := database.GetDB().First(&r, id).Error; err != nil {
- if database.IsNotFound(err) {
- return "", ErrCustomGeoNotFound
- }
- return "", err
- }
- displayName = s.fileNameFor(r.Type, r.Alias)
- if err := s.syncAndSanitizeLocalPath(&r); err != nil {
- return displayName, err
- }
- sanitizedURL, sanitizeErr := s.sanitizeURL(r.Url)
- if sanitizeErr != nil {
- return displayName, sanitizeErr
- }
- skipped, lm, err := s.downloadToPath(sanitizedURL, r.LocalPath, r.LastModified)
- if err != nil {
- if onStartup {
- logger.Warningf("custom geo startup download id=%d: %v", id, err)
- } else {
- logger.Warningf("custom geo manual update id=%d: %v", id, err)
- }
- return displayName, err
- }
- now := time.Now().Unix()
- updates := map[string]any{
- "last_modified": lm,
- "local_path": r.LocalPath,
- "last_updated_at": now,
- }
- if err = database.GetDB().Model(&model.CustomGeoResource{}).Where("id = ?", id).Updates(updates).Error; err != nil {
- if onStartup {
- logger.Warningf("custom geo startup id=%d: persist metadata: %v", id, err)
- } else {
- logger.Warningf("custom geo manual update id=%d: persist metadata: %v", id, err)
- }
- return displayName, err
- }
- if skipped {
- if onStartup {
- logger.Infof("custom geo startup download skipped (not modified) id=%d", id)
- } else {
- logger.Infof("custom geo manual update skipped (not modified) id=%d", id)
- }
- } else {
- if onStartup {
- logger.Infof("custom geo startup download ok id=%d", id)
- } else {
- logger.Infof("custom geo manual update ok id=%d", id)
- }
- }
- return displayName, nil
- }
- func (s *CustomGeoService) TriggerUpdate(id int) (string, error) {
- displayName, err := s.applyDownloadAndPersist(id, false)
- if err != nil {
- return displayName, err
- }
- if err = s.serverService.RestartXrayService(); err != nil {
- logger.Warning("custom geo manual update: restart xray:", err)
- }
- return displayName, nil
- }
- func (s *CustomGeoService) TriggerUpdateAll() (*CustomGeoUpdateAllResult, error) {
- var list []model.CustomGeoResource
- var err error
- if s.updateAllGetAll != nil {
- list, err = s.updateAllGetAll()
- } else {
- list, err = s.GetAll()
- }
- if err != nil {
- return nil, err
- }
- res := &CustomGeoUpdateAllResult{}
- if len(list) == 0 {
- return res, nil
- }
- for _, r := range list {
- var name string
- var applyErr error
- if s.updateAllApply != nil {
- name, applyErr = s.updateAllApply(r.Id, false)
- } else {
- name, applyErr = s.applyDownloadAndPersist(r.Id, false)
- }
- if applyErr != nil {
- res.Failed = append(res.Failed, CustomGeoUpdateAllFailure{
- Id: r.Id, Alias: r.Alias, FileName: name, Err: applyErr.Error(),
- })
- continue
- }
- res.Succeeded = append(res.Succeeded, CustomGeoUpdateAllItem{
- Id: r.Id, Alias: r.Alias, FileName: name,
- })
- }
- if len(res.Succeeded) > 0 {
- var restartErr error
- if s.updateAllRestart != nil {
- restartErr = s.updateAllRestart()
- } else {
- restartErr = s.serverService.RestartXrayService()
- }
- if restartErr != nil {
- logger.Warning("custom geo update all: restart xray:", restartErr)
- }
- }
- return res, nil
- }
- type CustomGeoAliasItem struct {
- Alias string `json:"alias"`
- Type string `json:"type"`
- FileName string `json:"fileName"`
- ExtExample string `json:"extExample"`
- }
- type CustomGeoAliasesResponse struct {
- Geosite []CustomGeoAliasItem `json:"geosite"`
- Geoip []CustomGeoAliasItem `json:"geoip"`
- }
- func (s *CustomGeoService) GetAliasesForUI() (CustomGeoAliasesResponse, error) {
- list, err := s.GetAll()
- if err != nil {
- logger.Warning("custom geo GetAliasesForUI:", err)
- return CustomGeoAliasesResponse{}, err
- }
- var out CustomGeoAliasesResponse
- for _, r := range list {
- fn := s.fileNameFor(r.Type, r.Alias)
- ex := fmt.Sprintf("ext:%s:tag", fn)
- item := CustomGeoAliasItem{
- Alias: r.Alias,
- Type: r.Type,
- FileName: fn,
- ExtExample: ex,
- }
- if r.Type == customGeoTypeGeoip {
- out.Geoip = append(out.Geoip, item)
- } else {
- out.Geosite = append(out.Geosite, item)
- }
- }
- return out, nil
- }
|