remote_routing.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641
  1. package sub
  2. import (
  3. "context"
  4. "crypto/tls"
  5. "encoding/base64"
  6. "encoding/json"
  7. "errors"
  8. "fmt"
  9. "io"
  10. "net/http"
  11. "net/url"
  12. "strings"
  13. "sync"
  14. "time"
  15. yaml "github.com/goccy/go-yaml"
  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/common"
  20. "github.com/mhsanaei/3x-ui/v3/internal/util/netsafe"
  21. )
  22. // Remote sources reuse the existing settings fields (one HTTPS URL = remote,
  23. // else inline) so no second mode toggle can disagree with the field contents.
  24. type remoteRoutingKind string
  25. const (
  26. remoteRoutingHapp remoteRoutingKind = "happ"
  27. remoteRoutingJson remoteRoutingKind = "jsonhapp"
  28. remoteRoutingClash remoteRoutingKind = "clash"
  29. remoteRoutingCacheTTL = 10 * time.Minute
  30. remoteRoutingRetryDelay = 30 * time.Second
  31. remoteRoutingHTTPTimeout = 6 * time.Second
  32. remoteRoutingHappMaxBody = 16 << 10 // 16 KiB; Happ emits the result in a response header
  33. remoteRoutingHappMaxValue = 8 << 10 // normalized Routing header value
  34. remoteRoutingClashMaxBody = 2 << 20 // 2 MiB
  35. )
  36. // isHappPayloadKind reports whether the kind carries a happ-payload source:
  37. // same validation and size caps, but a separate persisted cache row.
  38. func isHappPayloadKind(kind remoteRoutingKind) bool {
  39. return kind == remoteRoutingHapp || kind == remoteRoutingJson
  40. }
  41. var errRemoteRoutingUnavailable = errors.New("remote routing source is temporarily unavailable")
  42. type remoteRoutingKey struct {
  43. kind remoteRoutingKind
  44. source string
  45. }
  46. type remoteRoutingCacheEntry struct {
  47. Source string `json:"source"`
  48. Content string `json:"content"`
  49. FetchedAt int64 `json:"fetchedAt"`
  50. ETag string `json:"etag,omitempty"`
  51. LastModified string `json:"lastModified,omitempty"`
  52. Clash map[string]any `json:"-"`
  53. }
  54. func (e remoteRoutingCacheEntry) fetchedTime() time.Time {
  55. return time.Unix(e.FetchedAt, 0)
  56. }
  57. type remoteRoutingFetch struct {
  58. done chan struct{}
  59. err error
  60. }
  61. type remoteRoutingResolver struct {
  62. refreshWG sync.WaitGroup
  63. mu sync.Mutex
  64. loadMu sync.Mutex
  65. loaded bool
  66. loadInFlight bool
  67. entries map[remoteRoutingKey]remoteRoutingCacheEntry
  68. inflight map[remoteRoutingKey]*remoteRoutingFetch
  69. lastAttempt map[remoteRoutingKey]time.Time
  70. client *http.Client
  71. now func() time.Time
  72. persist bool
  73. }
  74. func newRemoteRoutingResolver(client *http.Client, persist bool) *remoteRoutingResolver {
  75. return &remoteRoutingResolver{
  76. entries: make(map[remoteRoutingKey]remoteRoutingCacheEntry),
  77. inflight: make(map[remoteRoutingKey]*remoteRoutingFetch),
  78. lastAttempt: make(map[remoteRoutingKey]time.Time),
  79. client: client,
  80. now: time.Now,
  81. persist: persist,
  82. }
  83. }
  84. var routingSourceResolver = newRemoteRoutingResolver(newRemoteRoutingHTTPClient(), true)
  85. // resolveRoutingSource serves a remote source from the validated cache without
  86. // ever blocking on network; inline values pass through (bool reports remote).
  87. func resolveRoutingSource(kind remoteRoutingKind, raw string) (string, bool, error) {
  88. return routingSourceResolver.resolve(kind, raw)
  89. }
  90. func (r *remoteRoutingResolver) resolve(kind remoteRoutingKind, raw string) (string, bool, error) {
  91. entry, remote, err := r.resolveEntry(kind, raw)
  92. if !remote {
  93. return raw, false, err
  94. }
  95. return entry.Content, true, err
  96. }
  97. func resolveClashRoutingSource(raw string) (string, map[string]any, bool, error) {
  98. entry, remote, err := routingSourceResolver.resolveEntry(remoteRoutingClash, raw)
  99. if !remote {
  100. return raw, nil, false, err
  101. }
  102. return entry.Content, entry.Clash, true, err
  103. }
  104. func (r *remoteRoutingResolver) resolveEntry(kind remoteRoutingKind, raw string) (remoteRoutingCacheEntry, bool, error) {
  105. source, remote, err := common.ParseRemoteRoutingURL(raw)
  106. if err != nil {
  107. return remoteRoutingCacheEntry{}, true, err
  108. }
  109. if !remote {
  110. return remoteRoutingCacheEntry{}, false, nil
  111. }
  112. r.triggerPersistedLoad()
  113. key := remoteRoutingKey{kind: kind, source: source}
  114. now := r.now()
  115. r.mu.Lock()
  116. cached, hasCached := r.entries[key]
  117. if hasCached && now.Sub(cached.fetchedTime()) < remoteRoutingCacheTTL {
  118. r.mu.Unlock()
  119. return cached, true, nil
  120. }
  121. if _, ok := r.inflight[key]; ok {
  122. r.mu.Unlock()
  123. if hasCached {
  124. return cached, true, nil
  125. }
  126. return remoteRoutingCacheEntry{}, true, errRemoteRoutingUnavailable
  127. }
  128. if attemptedAt, attempted := r.lastAttempt[key]; attempted && now.Sub(attemptedAt) < remoteRoutingRetryDelay {
  129. r.mu.Unlock()
  130. if hasCached {
  131. return cached, true, nil
  132. }
  133. return remoteRoutingCacheEntry{}, true, errRemoteRoutingUnavailable
  134. }
  135. fetch := &remoteRoutingFetch{done: make(chan struct{})}
  136. r.inflight[key] = fetch
  137. r.mu.Unlock()
  138. r.refreshWG.Add(1)
  139. common.GoRecover("remote-routing-refresh", func() {
  140. defer r.refreshWG.Done()
  141. r.refresh(key, cached, hasCached, fetch)
  142. })
  143. if hasCached {
  144. return cached, true, nil
  145. }
  146. return remoteRoutingCacheEntry{}, true, errRemoteRoutingUnavailable
  147. }
  148. // RefreshRemoteRoutingSources warms and refreshes configured remote sources
  149. // from the cron job; concurrent resolver reads are safe, fetches coalesce.
  150. func RefreshRemoteRoutingSources(happ, clash, jsonRouting string) {
  151. for kind, raw := range map[remoteRoutingKind][]string{
  152. remoteRoutingHapp: {happ},
  153. remoteRoutingJson: {jsonRouting},
  154. remoteRoutingClash: {clash},
  155. } {
  156. for _, source := range raw {
  157. _, remote, parseErr := common.ParseRemoteRoutingURL(source)
  158. if parseErr != nil {
  159. logger.Warningf("Remote %s routing source is invalid", kind)
  160. continue
  161. }
  162. if remote {
  163. _ = routingSourceResolver.refreshSource(kind, source)
  164. }
  165. }
  166. }
  167. }
  168. func (r *remoteRoutingResolver) refreshSource(kind remoteRoutingKind, raw string) error {
  169. source, remote, err := common.ParseRemoteRoutingURL(raw)
  170. if err != nil || !remote {
  171. return err
  172. }
  173. r.ensurePersistedLoaded()
  174. key := remoteRoutingKey{kind: kind, source: source}
  175. now := r.now()
  176. r.mu.Lock()
  177. previous, hasPrevious := r.entries[key]
  178. if hasPrevious && now.Sub(previous.fetchedTime()) < remoteRoutingCacheTTL {
  179. r.mu.Unlock()
  180. return nil
  181. }
  182. if fetch, ok := r.inflight[key]; ok {
  183. done := fetch.done
  184. r.mu.Unlock()
  185. <-done
  186. return fetch.err
  187. }
  188. if attemptedAt, attempted := r.lastAttempt[key]; attempted && now.Sub(attemptedAt) < remoteRoutingRetryDelay {
  189. r.mu.Unlock()
  190. return errRemoteRoutingUnavailable
  191. }
  192. fetch := &remoteRoutingFetch{done: make(chan struct{})}
  193. r.inflight[key] = fetch
  194. r.mu.Unlock()
  195. r.refresh(key, previous, hasPrevious, fetch)
  196. return fetch.err
  197. }
  198. func (r *remoteRoutingResolver) refresh(key remoteRoutingKey, previous remoteRoutingCacheEntry, hasPrevious bool, fetch *remoteRoutingFetch) {
  199. entry, err := r.fetch(key, previous, hasPrevious)
  200. now := r.now()
  201. r.mu.Lock()
  202. r.lastAttempt[key] = now
  203. if err == nil {
  204. r.entries[key] = entry
  205. }
  206. fetch.err = err
  207. delete(r.inflight, key)
  208. close(fetch.done)
  209. r.mu.Unlock()
  210. if err != nil {
  211. if hasPrevious {
  212. logger.Warningf("Remote %s routing refresh from %s failed; keeping the last valid value", key.kind, remoteRoutingHost(key.source))
  213. } else {
  214. logger.Warningf("Remote %s routing refresh from %s failed; no validated value is cached", key.kind, remoteRoutingHost(key.source))
  215. }
  216. return
  217. }
  218. if r.persist {
  219. r.persistEntry(key.kind, entry)
  220. }
  221. }
  222. func (r *remoteRoutingResolver) fetch(key remoteRoutingKey, previous remoteRoutingCacheEntry, hasPrevious bool) (entry remoteRoutingCacheEntry, err error) {
  223. // Remote bytes reach the YAML/JSON parsers below; a parser panic must
  224. // degrade to a failed refresh (keeping last-good), not crash the panel.
  225. defer func() {
  226. if panicValue := recover(); panicValue != nil {
  227. entry, err = remoteRoutingCacheEntry{}, fmt.Errorf("remote routing fetch panicked: %v", panicValue)
  228. }
  229. }()
  230. req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, key.source, nil)
  231. if err != nil {
  232. return remoteRoutingCacheEntry{}, err
  233. }
  234. req.Header.Set("User-Agent", "3x-ui-remote-routing/1.0")
  235. if hasPrevious {
  236. if previous.ETag != "" {
  237. req.Header.Set("If-None-Match", previous.ETag)
  238. }
  239. if previous.LastModified != "" {
  240. req.Header.Set("If-Modified-Since", previous.LastModified)
  241. }
  242. }
  243. resp, err := r.client.Do(req)
  244. if err != nil {
  245. return remoteRoutingCacheEntry{}, err
  246. }
  247. defer resp.Body.Close()
  248. if resp.StatusCode == http.StatusNotModified {
  249. if !hasPrevious {
  250. return remoteRoutingCacheEntry{}, errors.New("remote source returned 304 without a cached value")
  251. }
  252. previous.FetchedAt = r.now().Unix()
  253. if etag := strings.TrimSpace(resp.Header.Get("ETag")); etag != "" {
  254. previous.ETag = etag
  255. }
  256. if modified := strings.TrimSpace(resp.Header.Get("Last-Modified")); modified != "" {
  257. previous.LastModified = modified
  258. }
  259. return previous, nil
  260. }
  261. if isHappPayloadKind(key.kind) && isRemoteHappRedirect(resp.StatusCode) {
  262. location := strings.TrimSpace(resp.Header.Get("Location"))
  263. content, locationErr := normalizeHappRouting([]byte(location))
  264. if locationErr != nil {
  265. return remoteRoutingCacheEntry{}, fmt.Errorf("invalid Happ redirect target: %w", locationErr)
  266. }
  267. if len(content) > remoteRoutingHappMaxValue {
  268. return remoteRoutingCacheEntry{}, errors.New("Happ routing header exceeds the size limit")
  269. }
  270. return remoteRoutingCacheEntry{
  271. Source: key.source,
  272. Content: content,
  273. FetchedAt: r.now().Unix(),
  274. }, nil
  275. }
  276. if resp.StatusCode != http.StatusOK {
  277. return remoteRoutingCacheEntry{}, fmt.Errorf("remote source returned HTTP %d", resp.StatusCode)
  278. }
  279. limit := int64(remoteRoutingHappMaxBody)
  280. if key.kind == remoteRoutingClash {
  281. limit = remoteRoutingClashMaxBody
  282. }
  283. body, err := io.ReadAll(io.LimitReader(resp.Body, limit+1))
  284. if err != nil {
  285. return remoteRoutingCacheEntry{}, err
  286. }
  287. if int64(len(body)) > limit {
  288. return remoteRoutingCacheEntry{}, errors.New("remote routing response exceeds the size limit")
  289. }
  290. content, clash, err := normalizeRemoteRoutingContent(key.kind, body)
  291. if err != nil {
  292. return remoteRoutingCacheEntry{}, err
  293. }
  294. if isHappPayloadKind(key.kind) && len(content) > remoteRoutingHappMaxValue {
  295. return remoteRoutingCacheEntry{}, errors.New("Happ routing header exceeds the size limit")
  296. }
  297. return remoteRoutingCacheEntry{
  298. Source: key.source,
  299. Content: content,
  300. FetchedAt: r.now().Unix(),
  301. ETag: strings.TrimSpace(resp.Header.Get("ETag")),
  302. LastModified: strings.TrimSpace(resp.Header.Get("Last-Modified")),
  303. Clash: clash,
  304. }, nil
  305. }
  306. func isRemoteHappRedirect(status int) bool {
  307. switch status {
  308. case http.StatusMovedPermanently, http.StatusFound, http.StatusSeeOther,
  309. http.StatusTemporaryRedirect, http.StatusPermanentRedirect:
  310. return true
  311. default:
  312. return false
  313. }
  314. }
  315. func normalizeRemoteRoutingContent(kind remoteRoutingKind, body []byte) (string, map[string]any, error) {
  316. switch kind {
  317. case remoteRoutingHapp, remoteRoutingJson:
  318. content, err := normalizeHappRouting(body)
  319. return content, nil, err
  320. case remoteRoutingClash:
  321. return normalizeClashRouting(body)
  322. default:
  323. return "", nil, fmt.Errorf("unsupported remote routing kind %q", kind)
  324. }
  325. }
  326. func normalizeHappRouting(body []byte) (string, error) {
  327. text := strings.TrimSpace(string(body))
  328. if text == "" {
  329. return "", errors.New("empty Happ routing response")
  330. }
  331. if strings.HasPrefix(text, "{") {
  332. compact, err := validateAndCompactJSONObject([]byte(text))
  333. if err != nil {
  334. return "", fmt.Errorf("invalid Happ routing JSON: %w", err)
  335. }
  336. return "happ://routing/onadd/" + base64.StdEncoding.EncodeToString(compact), nil
  337. }
  338. if text == "happ://routing/off" {
  339. return text, nil
  340. }
  341. if strings.ContainsAny(text, "\r\n") {
  342. return "", errors.New("Happ deeplink must be a single line")
  343. }
  344. payload := ""
  345. for _, prefix := range []string{"happ://routing/onadd/", "happ://routing/add/"} {
  346. if after, ok := strings.CutPrefix(text, prefix); ok {
  347. payload = after
  348. break
  349. }
  350. }
  351. if payload == "" {
  352. return "", errors.New("Happ response is neither routing JSON nor a routing deeplink")
  353. }
  354. decoded, err := decodeRoutingBase64(payload)
  355. if err != nil {
  356. return "", fmt.Errorf("invalid Happ routing payload: %w", err)
  357. }
  358. if _, err := validateAndCompactJSONObject(decoded); err != nil {
  359. return "", fmt.Errorf("invalid Happ routing payload JSON: %w", err)
  360. }
  361. return text, nil
  362. }
  363. func validateAndCompactJSONObject(raw []byte) ([]byte, error) {
  364. var object map[string]any
  365. if err := json.Unmarshal(raw, &object); err != nil {
  366. return nil, err
  367. }
  368. if object == nil {
  369. return nil, errors.New("expected a JSON object")
  370. }
  371. return json.Marshal(object)
  372. }
  373. func decodeRoutingBase64(value string) ([]byte, error) {
  374. value = strings.TrimSpace(value)
  375. encodings := []*base64.Encoding{
  376. base64.StdEncoding,
  377. base64.RawStdEncoding,
  378. base64.URLEncoding,
  379. base64.RawURLEncoding,
  380. }
  381. var lastErr error
  382. for _, encoding := range encodings {
  383. decoded, err := encoding.DecodeString(value)
  384. if err == nil {
  385. return decoded, nil
  386. }
  387. lastErr = err
  388. }
  389. return nil, lastErr
  390. }
  391. func normalizeClashRouting(body []byte) (string, map[string]any, error) {
  392. text := strings.TrimSpace(string(body))
  393. if text == "" {
  394. return "", nil, errors.New("empty Clash routing response")
  395. }
  396. var document map[string]any
  397. if err := yaml.Unmarshal([]byte(text), &document); err != nil {
  398. return "", nil, fmt.Errorf("invalid Clash routing YAML: %w", err)
  399. }
  400. if len(document) == 0 {
  401. return "", nil, errors.New("Clash routing response must be a YAML map")
  402. }
  403. hasSupportedKey := false
  404. for key := range document {
  405. if remoteClashAllowedKey(key) {
  406. hasSupportedKey = true
  407. break
  408. }
  409. }
  410. if !hasSupportedKey {
  411. return "", nil, errors.New("Clash routing response has no supported routing keys")
  412. }
  413. base := map[string]any{
  414. "proxies": []map[string]any{{"name": "validation-node", "type": "vless"}},
  415. "proxy-groups": []map[string]any{{
  416. "name": "PROXY", "type": "select", "proxies": []string{"validation-node", "DIRECT"},
  417. }},
  418. "rules": []string{"MATCH,PROXY"},
  419. }
  420. if err := mergeRemoteClashRules(base, document); err != nil {
  421. return "", nil, fmt.Errorf("invalid remote Clash routing schema: %w", err)
  422. }
  423. return text, document, nil
  424. }
  425. func resolveIncyRoutingSource(raw string) (string, bool, error) {
  426. source, remote, err := common.ParseRemoteRoutingURL(raw)
  427. if err != nil || !remote {
  428. return raw, remote, err
  429. }
  430. return "incy://autorouting/onadd/" + source, true, nil
  431. }
  432. func newRemoteRoutingHTTPClient() *http.Client {
  433. transport := &http.Transport{
  434. Proxy: nil,
  435. DialContext: netsafe.SSRFGuardedDialContext,
  436. ForceAttemptHTTP2: true,
  437. TLSHandshakeTimeout: 4 * time.Second,
  438. ResponseHeaderTimeout: 5 * time.Second,
  439. TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12},
  440. }
  441. return &http.Client{
  442. Timeout: remoteRoutingHTTPTimeout,
  443. Transport: transport,
  444. CheckRedirect: checkRemoteRoutingRedirect,
  445. }
  446. }
  447. func checkRemoteRoutingRedirect(req *http.Request, via []*http.Request) error {
  448. if len(via) >= 5 {
  449. return errors.New("stopped after 5 redirects")
  450. }
  451. if strings.EqualFold(req.URL.Scheme, "happ") {
  452. // routing.help-style services publish the deeplink as the final Location;
  453. // hand the 3xx back to fetch(), which validates it without a request.
  454. return http.ErrUseLastResponse
  455. }
  456. if !strings.EqualFold(req.URL.Scheme, "https") || req.URL.Hostname() == "" || req.URL.User != nil {
  457. return errors.New("remote routing redirect must stay on an absolute HTTPS URL")
  458. }
  459. // The guarded dialer re-resolves, validates and connects to the same public
  460. // address, including on every HTTPS redirect hop.
  461. return nil
  462. }
  463. func remoteRoutingHost(source string) string {
  464. u, err := url.Parse(source)
  465. if err != nil || u.Hostname() == "" {
  466. return "unknown host"
  467. }
  468. return u.Hostname()
  469. }
  470. func remoteRoutingSettingKey(kind remoteRoutingKind) string {
  471. return "_subRemoteRoutingCache_" + string(kind)
  472. }
  473. func (r *remoteRoutingResolver) ensurePersistedLoaded() {
  474. if !r.persist {
  475. return
  476. }
  477. r.mu.Lock()
  478. loaded := r.loaded
  479. r.mu.Unlock()
  480. if loaded {
  481. return
  482. }
  483. r.loadMu.Lock()
  484. defer r.loadMu.Unlock()
  485. r.mu.Lock()
  486. loaded = r.loaded
  487. r.mu.Unlock()
  488. if loaded {
  489. return
  490. }
  491. db := database.GetDB()
  492. if db == nil {
  493. return
  494. }
  495. sqlDB, err := db.DB()
  496. if err != nil || sqlDB.Ping() != nil {
  497. return
  498. }
  499. r.loadPersisted()
  500. r.mu.Lock()
  501. r.loaded = true
  502. r.mu.Unlock()
  503. }
  504. // triggerPersistedLoad keeps SQLite off the subscription request path: requests
  505. // schedule at most one background load; the startup job loads synchronously.
  506. func (r *remoteRoutingResolver) triggerPersistedLoad() {
  507. if !r.persist {
  508. return
  509. }
  510. r.mu.Lock()
  511. if r.loaded || r.loadInFlight {
  512. r.mu.Unlock()
  513. return
  514. }
  515. r.loadInFlight = true
  516. r.mu.Unlock()
  517. common.GoRecover("remote-routing-cache-load", func() {
  518. defer func() {
  519. r.mu.Lock()
  520. r.loadInFlight = false
  521. r.mu.Unlock()
  522. }()
  523. r.ensurePersistedLoaded()
  524. })
  525. }
  526. func (r *remoteRoutingResolver) loadPersisted() {
  527. loaded := make(map[remoteRoutingKey]remoteRoutingCacheEntry, 2)
  528. for _, kind := range []remoteRoutingKind{remoteRoutingHapp, remoteRoutingJson, remoteRoutingClash} {
  529. var setting model.Setting
  530. err := database.GetDB().Where("key = ?", remoteRoutingSettingKey(kind)).First(&setting).Error
  531. if err != nil {
  532. continue
  533. }
  534. var entry remoteRoutingCacheEntry
  535. if json.Unmarshal([]byte(setting.Value), &entry) != nil || entry.Source == "" || entry.Content == "" || entry.FetchedAt <= 0 {
  536. continue
  537. }
  538. if _, remote, err := common.ParseRemoteRoutingURL(entry.Source); err != nil || !remote {
  539. continue
  540. }
  541. normalized, clash, err := normalizeRemoteRoutingContent(kind, []byte(entry.Content))
  542. if err != nil {
  543. continue
  544. }
  545. if isHappPayloadKind(kind) && len(normalized) > remoteRoutingHappMaxValue {
  546. continue
  547. }
  548. entry.Content = normalized
  549. entry.Clash = clash
  550. loaded[remoteRoutingKey{kind: kind, source: entry.Source}] = entry
  551. }
  552. r.mu.Lock()
  553. for key, entry := range loaded {
  554. current, exists := r.entries[key]
  555. if !exists || entry.FetchedAt > current.FetchedAt {
  556. r.entries[key] = entry
  557. }
  558. }
  559. r.mu.Unlock()
  560. }
  561. func (r *remoteRoutingResolver) persistEntry(kind remoteRoutingKind, entry remoteRoutingCacheEntry) {
  562. db := database.GetDB()
  563. if db == nil {
  564. return
  565. }
  566. encoded, err := json.Marshal(entry)
  567. if err != nil {
  568. return
  569. }
  570. key := remoteRoutingSettingKey(kind)
  571. var setting model.Setting
  572. err = db.Where("key = ?", key).First(&setting).Error
  573. if database.IsNotFound(err) {
  574. err = db.Create(&model.Setting{Key: key, Value: string(encoded)}).Error
  575. } else if err == nil {
  576. setting.Value = string(encoded)
  577. err = db.Save(&setting).Error
  578. }
  579. if err != nil {
  580. logger.Warningf("Could not persist the last valid %s remote routing value", kind)
  581. }
  582. }