custom_geo.go 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760
  1. package service
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "io"
  7. "net"
  8. "net/http"
  9. "net/url"
  10. "os"
  11. "path/filepath"
  12. "regexp"
  13. "strings"
  14. "time"
  15. "github.com/mhsanaei/3x-ui/v2/config"
  16. "github.com/mhsanaei/3x-ui/v2/database"
  17. "github.com/mhsanaei/3x-ui/v2/database/model"
  18. "github.com/mhsanaei/3x-ui/v2/logger"
  19. )
  20. const (
  21. customGeoTypeGeosite = "geosite"
  22. customGeoTypeGeoip = "geoip"
  23. minDatBytes = 64
  24. customGeoProbeTimeout = 12 * time.Second
  25. )
  26. var (
  27. customGeoAliasPattern = regexp.MustCompile(`^[a-z0-9_-]+$`)
  28. reservedCustomAliases = map[string]struct{}{
  29. "geoip": {}, "geosite": {},
  30. "geoip_ir": {}, "geosite_ir": {},
  31. "geoip_ru": {}, "geosite_ru": {},
  32. }
  33. ErrCustomGeoInvalidType = errors.New("custom_geo_invalid_type")
  34. ErrCustomGeoAliasRequired = errors.New("custom_geo_alias_required")
  35. ErrCustomGeoAliasPattern = errors.New("custom_geo_alias_pattern")
  36. ErrCustomGeoAliasReserved = errors.New("custom_geo_alias_reserved")
  37. ErrCustomGeoURLRequired = errors.New("custom_geo_url_required")
  38. ErrCustomGeoInvalidURL = errors.New("custom_geo_invalid_url")
  39. ErrCustomGeoURLScheme = errors.New("custom_geo_url_scheme")
  40. ErrCustomGeoURLHost = errors.New("custom_geo_url_host")
  41. ErrCustomGeoDuplicateAlias = errors.New("custom_geo_duplicate_alias")
  42. ErrCustomGeoNotFound = errors.New("custom_geo_not_found")
  43. ErrCustomGeoDownload = errors.New("custom_geo_download")
  44. ErrCustomGeoSSRFBlocked = errors.New("custom_geo_ssrf_blocked")
  45. ErrCustomGeoPathTraversal = errors.New("custom_geo_path_traversal")
  46. )
  47. type CustomGeoUpdateAllItem struct {
  48. Id int `json:"id"`
  49. Alias string `json:"alias"`
  50. FileName string `json:"fileName"`
  51. }
  52. type CustomGeoUpdateAllFailure struct {
  53. Id int `json:"id"`
  54. Alias string `json:"alias"`
  55. FileName string `json:"fileName"`
  56. Err string `json:"error"`
  57. }
  58. type CustomGeoUpdateAllResult struct {
  59. Succeeded []CustomGeoUpdateAllItem `json:"succeeded"`
  60. Failed []CustomGeoUpdateAllFailure `json:"failed"`
  61. }
  62. type CustomGeoService struct {
  63. serverService *ServerService
  64. updateAllGetAll func() ([]model.CustomGeoResource, error)
  65. updateAllApply func(id int, onStartup bool) (string, error)
  66. updateAllRestart func() error
  67. }
  68. func NewCustomGeoService() *CustomGeoService {
  69. s := &CustomGeoService{
  70. serverService: &ServerService{},
  71. }
  72. s.updateAllGetAll = s.GetAll
  73. s.updateAllApply = s.applyDownloadAndPersist
  74. s.updateAllRestart = func() error { return s.serverService.RestartXrayService() }
  75. return s
  76. }
  77. func NormalizeAliasKey(alias string) string {
  78. return strings.ToLower(strings.ReplaceAll(alias, "-", "_"))
  79. }
  80. func (s *CustomGeoService) fileNameFor(typ, alias string) string {
  81. if typ == customGeoTypeGeoip {
  82. return fmt.Sprintf("geoip_%s.dat", alias)
  83. }
  84. return fmt.Sprintf("geosite_%s.dat", alias)
  85. }
  86. func (s *CustomGeoService) validateType(typ string) error {
  87. if typ != customGeoTypeGeosite && typ != customGeoTypeGeoip {
  88. return ErrCustomGeoInvalidType
  89. }
  90. return nil
  91. }
  92. func (s *CustomGeoService) validateAlias(alias string) error {
  93. if alias == "" {
  94. return ErrCustomGeoAliasRequired
  95. }
  96. if !customGeoAliasPattern.MatchString(alias) {
  97. return ErrCustomGeoAliasPattern
  98. }
  99. if _, ok := reservedCustomAliases[NormalizeAliasKey(alias)]; ok {
  100. return ErrCustomGeoAliasReserved
  101. }
  102. return nil
  103. }
  104. func (s *CustomGeoService) sanitizeURL(raw string) (string, error) {
  105. if raw == "" {
  106. return "", ErrCustomGeoURLRequired
  107. }
  108. u, err := url.Parse(raw)
  109. if err != nil {
  110. return "", ErrCustomGeoInvalidURL
  111. }
  112. if u.Scheme != "http" && u.Scheme != "https" {
  113. return "", ErrCustomGeoURLScheme
  114. }
  115. if u.Host == "" {
  116. return "", ErrCustomGeoURLHost
  117. }
  118. if err := checkSSRF(context.Background(), u.Hostname()); err != nil {
  119. return "", err
  120. }
  121. // Reconstruct URL from parsed components to break taint propagation.
  122. clean := &url.URL{
  123. Scheme: u.Scheme,
  124. Host: u.Host,
  125. Path: u.Path,
  126. RawPath: u.RawPath,
  127. RawQuery: u.RawQuery,
  128. Fragment: u.Fragment,
  129. }
  130. return clean.String(), nil
  131. }
  132. func localDatFileNeedsRepair(path string) bool {
  133. safePath, err := sanitizeDestPath(path)
  134. if err != nil {
  135. return true
  136. }
  137. fi, err := os.Stat(safePath)
  138. if err != nil {
  139. return true
  140. }
  141. if fi.IsDir() {
  142. return true
  143. }
  144. return fi.Size() < int64(minDatBytes)
  145. }
  146. func CustomGeoLocalFileNeedsRepair(path string) bool {
  147. return localDatFileNeedsRepair(path)
  148. }
  149. func isBlockedIP(ip net.IP) bool {
  150. return ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() ||
  151. ip.IsLinkLocalMulticast() || ip.IsUnspecified()
  152. }
  153. // checkSSRFDefault validates that the given host does not resolve to a private/internal IP.
  154. // It is context-aware so that dial context cancellation/deadlines are respected during DNS resolution.
  155. func checkSSRFDefault(ctx context.Context, hostname string) error {
  156. ips, err := net.DefaultResolver.LookupIPAddr(ctx, hostname)
  157. if err != nil {
  158. return fmt.Errorf("%w: cannot resolve host %s", ErrCustomGeoSSRFBlocked, hostname)
  159. }
  160. for _, ipAddr := range ips {
  161. if isBlockedIP(ipAddr.IP) {
  162. return fmt.Errorf("%w: %s resolves to blocked address %s", ErrCustomGeoSSRFBlocked, hostname, ipAddr.IP)
  163. }
  164. }
  165. return nil
  166. }
  167. // checkSSRF is the active SSRF guard. Override in tests to allow localhost test servers.
  168. var checkSSRF = checkSSRFDefault
  169. func ssrfSafeTransport() http.RoundTripper {
  170. base, ok := http.DefaultTransport.(*http.Transport)
  171. if !ok {
  172. base = &http.Transport{}
  173. }
  174. cloned := base.Clone()
  175. cloned.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
  176. host, _, err := net.SplitHostPort(addr)
  177. if err != nil {
  178. return nil, fmt.Errorf("%w: %v", ErrCustomGeoSSRFBlocked, err)
  179. }
  180. if err := checkSSRF(ctx, host); err != nil {
  181. return nil, err
  182. }
  183. var dialer net.Dialer
  184. return dialer.DialContext(ctx, network, addr)
  185. }
  186. return cloned
  187. }
  188. func probeCustomGeoURLWithGET(rawURL string) error {
  189. sanitizedURL, err := (&CustomGeoService{}).sanitizeURL(rawURL)
  190. if err != nil {
  191. return err
  192. }
  193. client := &http.Client{Timeout: customGeoProbeTimeout, Transport: ssrfSafeTransport()}
  194. req, err := http.NewRequest(http.MethodGet, sanitizedURL, nil)
  195. if err != nil {
  196. return err
  197. }
  198. req.Header.Set("Range", "bytes=0-0")
  199. resp, err := client.Do(req)
  200. if err != nil {
  201. return err
  202. }
  203. defer resp.Body.Close()
  204. _, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 256))
  205. switch resp.StatusCode {
  206. case http.StatusOK, http.StatusPartialContent:
  207. return nil
  208. default:
  209. return fmt.Errorf("get range status %d", resp.StatusCode)
  210. }
  211. }
  212. func probeCustomGeoURL(rawURL string) error {
  213. sanitizedURL, err := (&CustomGeoService{}).sanitizeURL(rawURL)
  214. if err != nil {
  215. return err
  216. }
  217. client := &http.Client{Timeout: customGeoProbeTimeout, Transport: ssrfSafeTransport()}
  218. req, err := http.NewRequest(http.MethodHead, sanitizedURL, nil)
  219. if err != nil {
  220. return err
  221. }
  222. resp, err := client.Do(req)
  223. if err != nil {
  224. return err
  225. }
  226. _ = resp.Body.Close()
  227. sc := resp.StatusCode
  228. if sc >= 200 && sc < 300 {
  229. return nil
  230. }
  231. if sc == http.StatusMethodNotAllowed || sc == http.StatusNotImplemented {
  232. return probeCustomGeoURLWithGET(rawURL)
  233. }
  234. return fmt.Errorf("head status %d", sc)
  235. }
  236. func (s *CustomGeoService) EnsureOnStartup() {
  237. list, err := s.GetAll()
  238. if err != nil {
  239. logger.Warning("custom geo startup: load list:", err)
  240. return
  241. }
  242. n := len(list)
  243. if n == 0 {
  244. logger.Info("custom geo startup: no custom geofiles configured")
  245. return
  246. }
  247. logger.Infof("custom geo startup: checking %d custom geofile(s)", n)
  248. for i := range list {
  249. r := &list[i]
  250. sanitizedURL, err := s.sanitizeURL(r.Url)
  251. if err != nil {
  252. logger.Warningf("custom geo startup id=%d: invalid url: %v", r.Id, err)
  253. continue
  254. }
  255. r.Url = sanitizedURL
  256. s.syncLocalPath(r)
  257. localPath := r.LocalPath
  258. if !localDatFileNeedsRepair(localPath) {
  259. logger.Infof("custom geo startup id=%d alias=%s path=%s: present", r.Id, r.Alias, localPath)
  260. continue
  261. }
  262. logger.Infof("custom geo startup id=%d alias=%s path=%s: missing or needs repair, probing source", r.Id, r.Alias, localPath)
  263. if err := probeCustomGeoURL(r.Url); err != nil {
  264. logger.Warningf("custom geo startup id=%d alias=%s url=%s: probe: %v (attempting download anyway)", r.Id, r.Alias, r.Url, err)
  265. }
  266. _, _ = s.applyDownloadAndPersist(r.Id, true)
  267. }
  268. }
  269. func (s *CustomGeoService) downloadToPath(resourceURL, destPath string, lastModifiedHeader string) (skipped bool, newLastModified string, err error) {
  270. safeDestPath, err := sanitizeDestPath(destPath)
  271. if err != nil {
  272. return false, "", fmt.Errorf("%w: %v", ErrCustomGeoDownload, err)
  273. }
  274. skipped, lm, err := s.downloadToPathOnce(resourceURL, safeDestPath, lastModifiedHeader, false)
  275. if err != nil {
  276. return false, "", err
  277. }
  278. if skipped {
  279. if _, statErr := os.Stat(safeDestPath); statErr == nil && !localDatFileNeedsRepair(safeDestPath) {
  280. return true, lm, nil
  281. }
  282. return s.downloadToPathOnce(resourceURL, safeDestPath, lastModifiedHeader, true)
  283. }
  284. return false, lm, nil
  285. }
  286. // sanitizeDestPath ensures destPath is inside the bin folder, preventing path traversal.
  287. // It resolves symlinks to prevent symlink-based escapes.
  288. // Returns the cleaned absolute path that is safe to use in file operations.
  289. func sanitizeDestPath(destPath string) (string, error) {
  290. baseDirAbs, err := filepath.Abs(config.GetBinFolderPath())
  291. if err != nil {
  292. return "", fmt.Errorf("%w: %v", ErrCustomGeoPathTraversal, err)
  293. }
  294. // Resolve symlinks in base directory to get the real path.
  295. if resolved, evalErr := filepath.EvalSymlinks(baseDirAbs); evalErr == nil {
  296. baseDirAbs = resolved
  297. }
  298. destPathAbs, err := filepath.Abs(destPath)
  299. if err != nil {
  300. return "", fmt.Errorf("%w: %v", ErrCustomGeoPathTraversal, err)
  301. }
  302. // Resolve symlinks for the parent directory of the destination path.
  303. destDir := filepath.Dir(destPathAbs)
  304. if resolved, evalErr := filepath.EvalSymlinks(destDir); evalErr == nil {
  305. destPathAbs = filepath.Join(resolved, filepath.Base(destPathAbs))
  306. }
  307. // Verify the resolved path is within the safe base directory using prefix check.
  308. safeDirPrefix := baseDirAbs + string(filepath.Separator)
  309. if !strings.HasPrefix(destPathAbs, safeDirPrefix) {
  310. return "", ErrCustomGeoPathTraversal
  311. }
  312. return destPathAbs, nil
  313. }
  314. func (s *CustomGeoService) downloadToPathOnce(resourceURL, destPath string, lastModifiedHeader string, forceFull bool) (skipped bool, newLastModified string, err error) {
  315. safeDestPath, err := sanitizeDestPath(destPath)
  316. if err != nil {
  317. return false, "", fmt.Errorf("%w: %v", ErrCustomGeoDownload, err)
  318. }
  319. sanitizedURL, err := s.sanitizeURL(resourceURL)
  320. if err != nil {
  321. return false, "", fmt.Errorf("%w: %v", ErrCustomGeoDownload, err)
  322. }
  323. var req *http.Request
  324. req, err = http.NewRequest(http.MethodGet, sanitizedURL, nil)
  325. if err != nil {
  326. return false, "", fmt.Errorf("%w: %v", ErrCustomGeoDownload, err)
  327. }
  328. if !forceFull {
  329. if fi, statErr := os.Stat(safeDestPath); statErr == nil && !localDatFileNeedsRepair(safeDestPath) {
  330. if !fi.ModTime().IsZero() {
  331. req.Header.Set("If-Modified-Since", fi.ModTime().UTC().Format(http.TimeFormat))
  332. } else if lastModifiedHeader != "" {
  333. if t, perr := time.Parse(http.TimeFormat, lastModifiedHeader); perr == nil {
  334. req.Header.Set("If-Modified-Since", t.UTC().Format(http.TimeFormat))
  335. }
  336. }
  337. }
  338. }
  339. client := &http.Client{Timeout: 10 * time.Minute, Transport: ssrfSafeTransport()}
  340. // lgtm[go/request-forgery]
  341. resp, err := client.Do(req)
  342. if err != nil {
  343. return false, "", fmt.Errorf("%w: %v", ErrCustomGeoDownload, err)
  344. }
  345. defer resp.Body.Close()
  346. var serverModTime time.Time
  347. if lm := resp.Header.Get("Last-Modified"); lm != "" {
  348. if parsed, perr := time.Parse(http.TimeFormat, lm); perr == nil {
  349. serverModTime = parsed
  350. newLastModified = lm
  351. }
  352. }
  353. updateModTime := func() {
  354. if !serverModTime.IsZero() {
  355. _ = os.Chtimes(safeDestPath, serverModTime, serverModTime)
  356. }
  357. }
  358. if resp.StatusCode == http.StatusNotModified {
  359. if forceFull {
  360. return false, "", fmt.Errorf("%w: unexpected 304 on unconditional get", ErrCustomGeoDownload)
  361. }
  362. updateModTime()
  363. return true, newLastModified, nil
  364. }
  365. if resp.StatusCode != http.StatusOK {
  366. return false, "", fmt.Errorf("%w: unexpected status %d", ErrCustomGeoDownload, resp.StatusCode)
  367. }
  368. binDir := filepath.Dir(safeDestPath)
  369. if err = os.MkdirAll(binDir, 0o755); err != nil {
  370. return false, "", fmt.Errorf("%w: %v", ErrCustomGeoDownload, err)
  371. }
  372. safeTmpPath, err := sanitizeDestPath(safeDestPath + ".tmp")
  373. if err != nil {
  374. return false, "", fmt.Errorf("%w: %v", ErrCustomGeoDownload, err)
  375. }
  376. out, err := os.Create(safeTmpPath)
  377. if err != nil {
  378. return false, "", fmt.Errorf("%w: %v", ErrCustomGeoDownload, err)
  379. }
  380. n, err := io.Copy(out, resp.Body)
  381. closeErr := out.Close()
  382. if err != nil {
  383. _ = os.Remove(safeTmpPath)
  384. return false, "", fmt.Errorf("%w: %v", ErrCustomGeoDownload, err)
  385. }
  386. if closeErr != nil {
  387. _ = os.Remove(safeTmpPath)
  388. return false, "", fmt.Errorf("%w: %v", ErrCustomGeoDownload, closeErr)
  389. }
  390. if n < minDatBytes {
  391. _ = os.Remove(safeTmpPath)
  392. return false, "", fmt.Errorf("%w: file too small", ErrCustomGeoDownload)
  393. }
  394. if err = os.Rename(safeTmpPath, safeDestPath); err != nil {
  395. _ = os.Remove(safeTmpPath)
  396. return false, "", fmt.Errorf("%w: %v", ErrCustomGeoDownload, err)
  397. }
  398. updateModTime()
  399. if newLastModified == "" && resp.Header.Get("Last-Modified") != "" {
  400. newLastModified = resp.Header.Get("Last-Modified")
  401. }
  402. return false, newLastModified, nil
  403. }
  404. func (s *CustomGeoService) resolveDestPath(r *model.CustomGeoResource) string {
  405. if r.LocalPath != "" {
  406. return r.LocalPath
  407. }
  408. return filepath.Join(config.GetBinFolderPath(), s.fileNameFor(r.Type, r.Alias))
  409. }
  410. func (s *CustomGeoService) syncLocalPath(r *model.CustomGeoResource) {
  411. p := filepath.Join(config.GetBinFolderPath(), s.fileNameFor(r.Type, r.Alias))
  412. r.LocalPath = p
  413. }
  414. func (s *CustomGeoService) syncAndSanitizeLocalPath(r *model.CustomGeoResource) error {
  415. s.syncLocalPath(r)
  416. safePath, err := sanitizeDestPath(r.LocalPath)
  417. if err != nil {
  418. return err
  419. }
  420. r.LocalPath = safePath
  421. return nil
  422. }
  423. func removeSafePathIfExists(path string) error {
  424. safePath, err := sanitizeDestPath(path)
  425. if err != nil {
  426. return err
  427. }
  428. if _, err := os.Stat(safePath); err == nil {
  429. if err := os.Remove(safePath); err != nil {
  430. return err
  431. }
  432. }
  433. return nil
  434. }
  435. func (s *CustomGeoService) Create(r *model.CustomGeoResource) error {
  436. if err := s.validateType(r.Type); err != nil {
  437. return err
  438. }
  439. if err := s.validateAlias(r.Alias); err != nil {
  440. return err
  441. }
  442. sanitizedURL, err := s.sanitizeURL(r.Url)
  443. if err != nil {
  444. return err
  445. }
  446. r.Url = sanitizedURL
  447. var existing int64
  448. database.GetDB().Model(&model.CustomGeoResource{}).
  449. Where("geo_type = ? AND alias = ?", r.Type, r.Alias).Count(&existing)
  450. if existing > 0 {
  451. return ErrCustomGeoDuplicateAlias
  452. }
  453. if err := s.syncAndSanitizeLocalPath(r); err != nil {
  454. return err
  455. }
  456. skipped, lm, err := s.downloadToPath(r.Url, r.LocalPath, r.LastModified)
  457. if err != nil {
  458. return err
  459. }
  460. now := time.Now().Unix()
  461. r.LastUpdatedAt = now
  462. r.LastModified = lm
  463. if err = database.GetDB().Create(r).Error; err != nil {
  464. _ = removeSafePathIfExists(r.LocalPath)
  465. return err
  466. }
  467. logger.Infof("custom geo created id=%d type=%s alias=%s skipped=%v", r.Id, r.Type, r.Alias, skipped)
  468. if err = s.serverService.RestartXrayService(); err != nil {
  469. logger.Warning("custom geo create: restart xray:", err)
  470. }
  471. return nil
  472. }
  473. func (s *CustomGeoService) Update(id int, r *model.CustomGeoResource) error {
  474. var cur model.CustomGeoResource
  475. if err := database.GetDB().First(&cur, id).Error; err != nil {
  476. if database.IsNotFound(err) {
  477. return ErrCustomGeoNotFound
  478. }
  479. return err
  480. }
  481. if err := s.validateType(r.Type); err != nil {
  482. return err
  483. }
  484. if err := s.validateAlias(r.Alias); err != nil {
  485. return err
  486. }
  487. sanitizedURL, err := s.sanitizeURL(r.Url)
  488. if err != nil {
  489. return err
  490. }
  491. r.Url = sanitizedURL
  492. if cur.Type != r.Type || cur.Alias != r.Alias {
  493. var cnt int64
  494. database.GetDB().Model(&model.CustomGeoResource{}).
  495. Where("geo_type = ? AND alias = ? AND id <> ?", r.Type, r.Alias, id).
  496. Count(&cnt)
  497. if cnt > 0 {
  498. return ErrCustomGeoDuplicateAlias
  499. }
  500. }
  501. oldPath := s.resolveDestPath(&cur)
  502. r.Id = id
  503. if err := s.syncAndSanitizeLocalPath(r); err != nil {
  504. return err
  505. }
  506. if oldPath != r.LocalPath && oldPath != "" {
  507. if err := removeSafePathIfExists(oldPath); err != nil && !errors.Is(err, ErrCustomGeoPathTraversal) {
  508. logger.Warningf("custom geo remove old path %s: %v", oldPath, err)
  509. }
  510. }
  511. _, lm, err := s.downloadToPath(r.Url, r.LocalPath, cur.LastModified)
  512. if err != nil {
  513. return err
  514. }
  515. r.LastUpdatedAt = time.Now().Unix()
  516. r.LastModified = lm
  517. err = database.GetDB().Model(&model.CustomGeoResource{}).Where("id = ?", id).Updates(map[string]any{
  518. "geo_type": r.Type,
  519. "alias": r.Alias,
  520. "url": r.Url,
  521. "local_path": r.LocalPath,
  522. "last_updated_at": r.LastUpdatedAt,
  523. "last_modified": r.LastModified,
  524. }).Error
  525. if err != nil {
  526. return err
  527. }
  528. logger.Infof("custom geo updated id=%d", id)
  529. if err = s.serverService.RestartXrayService(); err != nil {
  530. logger.Warning("custom geo update: restart xray:", err)
  531. }
  532. return nil
  533. }
  534. func (s *CustomGeoService) Delete(id int) (displayName string, err error) {
  535. var r model.CustomGeoResource
  536. if err := database.GetDB().First(&r, id).Error; err != nil {
  537. if database.IsNotFound(err) {
  538. return "", ErrCustomGeoNotFound
  539. }
  540. return "", err
  541. }
  542. displayName = s.fileNameFor(r.Type, r.Alias)
  543. p := s.resolveDestPath(&r)
  544. if _, err := sanitizeDestPath(p); err != nil {
  545. return displayName, err
  546. }
  547. if err := database.GetDB().Delete(&model.CustomGeoResource{}, id).Error; err != nil {
  548. return displayName, err
  549. }
  550. if p != "" {
  551. if err := removeSafePathIfExists(p); err != nil {
  552. logger.Warningf("custom geo delete file %s: %v", p, err)
  553. }
  554. }
  555. logger.Infof("custom geo deleted id=%d", id)
  556. if err := s.serverService.RestartXrayService(); err != nil {
  557. logger.Warning("custom geo delete: restart xray:", err)
  558. }
  559. return displayName, nil
  560. }
  561. func (s *CustomGeoService) GetAll() ([]model.CustomGeoResource, error) {
  562. var list []model.CustomGeoResource
  563. err := database.GetDB().Order("id asc").Find(&list).Error
  564. return list, err
  565. }
  566. func (s *CustomGeoService) applyDownloadAndPersist(id int, onStartup bool) (displayName string, err error) {
  567. var r model.CustomGeoResource
  568. if err := database.GetDB().First(&r, id).Error; err != nil {
  569. if database.IsNotFound(err) {
  570. return "", ErrCustomGeoNotFound
  571. }
  572. return "", err
  573. }
  574. displayName = s.fileNameFor(r.Type, r.Alias)
  575. if err := s.syncAndSanitizeLocalPath(&r); err != nil {
  576. return displayName, err
  577. }
  578. sanitizedURL, sanitizeErr := s.sanitizeURL(r.Url)
  579. if sanitizeErr != nil {
  580. return displayName, sanitizeErr
  581. }
  582. skipped, lm, err := s.downloadToPath(sanitizedURL, r.LocalPath, r.LastModified)
  583. if err != nil {
  584. if onStartup {
  585. logger.Warningf("custom geo startup download id=%d: %v", id, err)
  586. } else {
  587. logger.Warningf("custom geo manual update id=%d: %v", id, err)
  588. }
  589. return displayName, err
  590. }
  591. now := time.Now().Unix()
  592. updates := map[string]any{
  593. "last_modified": lm,
  594. "local_path": r.LocalPath,
  595. "last_updated_at": now,
  596. }
  597. if err = database.GetDB().Model(&model.CustomGeoResource{}).Where("id = ?", id).Updates(updates).Error; err != nil {
  598. if onStartup {
  599. logger.Warningf("custom geo startup id=%d: persist metadata: %v", id, err)
  600. } else {
  601. logger.Warningf("custom geo manual update id=%d: persist metadata: %v", id, err)
  602. }
  603. return displayName, err
  604. }
  605. if skipped {
  606. if onStartup {
  607. logger.Infof("custom geo startup download skipped (not modified) id=%d", id)
  608. } else {
  609. logger.Infof("custom geo manual update skipped (not modified) id=%d", id)
  610. }
  611. } else {
  612. if onStartup {
  613. logger.Infof("custom geo startup download ok id=%d", id)
  614. } else {
  615. logger.Infof("custom geo manual update ok id=%d", id)
  616. }
  617. }
  618. return displayName, nil
  619. }
  620. func (s *CustomGeoService) TriggerUpdate(id int) (string, error) {
  621. displayName, err := s.applyDownloadAndPersist(id, false)
  622. if err != nil {
  623. return displayName, err
  624. }
  625. if err = s.serverService.RestartXrayService(); err != nil {
  626. logger.Warning("custom geo manual update: restart xray:", err)
  627. }
  628. return displayName, nil
  629. }
  630. func (s *CustomGeoService) TriggerUpdateAll() (*CustomGeoUpdateAllResult, error) {
  631. var list []model.CustomGeoResource
  632. var err error
  633. if s.updateAllGetAll != nil {
  634. list, err = s.updateAllGetAll()
  635. } else {
  636. list, err = s.GetAll()
  637. }
  638. if err != nil {
  639. return nil, err
  640. }
  641. res := &CustomGeoUpdateAllResult{}
  642. if len(list) == 0 {
  643. return res, nil
  644. }
  645. for _, r := range list {
  646. var name string
  647. var applyErr error
  648. if s.updateAllApply != nil {
  649. name, applyErr = s.updateAllApply(r.Id, false)
  650. } else {
  651. name, applyErr = s.applyDownloadAndPersist(r.Id, false)
  652. }
  653. if applyErr != nil {
  654. res.Failed = append(res.Failed, CustomGeoUpdateAllFailure{
  655. Id: r.Id, Alias: r.Alias, FileName: name, Err: applyErr.Error(),
  656. })
  657. continue
  658. }
  659. res.Succeeded = append(res.Succeeded, CustomGeoUpdateAllItem{
  660. Id: r.Id, Alias: r.Alias, FileName: name,
  661. })
  662. }
  663. if len(res.Succeeded) > 0 {
  664. var restartErr error
  665. if s.updateAllRestart != nil {
  666. restartErr = s.updateAllRestart()
  667. } else {
  668. restartErr = s.serverService.RestartXrayService()
  669. }
  670. if restartErr != nil {
  671. logger.Warning("custom geo update all: restart xray:", restartErr)
  672. }
  673. }
  674. return res, nil
  675. }
  676. type CustomGeoAliasItem struct {
  677. Alias string `json:"alias"`
  678. Type string `json:"type"`
  679. FileName string `json:"fileName"`
  680. ExtExample string `json:"extExample"`
  681. }
  682. type CustomGeoAliasesResponse struct {
  683. Geosite []CustomGeoAliasItem `json:"geosite"`
  684. Geoip []CustomGeoAliasItem `json:"geoip"`
  685. }
  686. func (s *CustomGeoService) GetAliasesForUI() (CustomGeoAliasesResponse, error) {
  687. list, err := s.GetAll()
  688. if err != nil {
  689. logger.Warning("custom geo GetAliasesForUI:", err)
  690. return CustomGeoAliasesResponse{}, err
  691. }
  692. var out CustomGeoAliasesResponse
  693. for _, r := range list {
  694. fn := s.fileNameFor(r.Type, r.Alias)
  695. ex := fmt.Sprintf("ext:%s:tag", fn)
  696. item := CustomGeoAliasItem{
  697. Alias: r.Alias,
  698. Type: r.Type,
  699. FileName: fn,
  700. ExtExample: ex,
  701. }
  702. if r.Type == customGeoTypeGeoip {
  703. out.Geoip = append(out.Geoip, item)
  704. } else {
  705. out.Geosite = append(out.Geosite, item)
  706. }
  707. }
  708. return out, nil
  709. }