custom_geo.go 23 KB

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