custom_geo.go 22 KB

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