remote_routing.go 18 KB

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