remote_routing.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628
  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 strings.ContainsAny(text, "\r\n") {
  330. return "", errors.New("Happ deeplink must be a single line")
  331. }
  332. payload := ""
  333. for _, prefix := range []string{"happ://routing/onadd/", "happ://routing/add/"} {
  334. if after, ok := strings.CutPrefix(text, prefix); ok {
  335. payload = after
  336. break
  337. }
  338. }
  339. if payload == "" {
  340. return "", errors.New("Happ response is neither routing JSON nor a routing deeplink")
  341. }
  342. decoded, err := decodeRoutingBase64(payload)
  343. if err != nil {
  344. return "", fmt.Errorf("invalid Happ routing payload: %w", err)
  345. }
  346. if _, err := validateAndCompactJSONObject(decoded); err != nil {
  347. return "", fmt.Errorf("invalid Happ routing payload JSON: %w", err)
  348. }
  349. return text, nil
  350. }
  351. func validateAndCompactJSONObject(raw []byte) ([]byte, error) {
  352. var object map[string]any
  353. if err := json.Unmarshal(raw, &object); err != nil {
  354. return nil, err
  355. }
  356. if object == nil {
  357. return nil, errors.New("expected a JSON object")
  358. }
  359. return json.Marshal(object)
  360. }
  361. func decodeRoutingBase64(value string) ([]byte, error) {
  362. value = strings.TrimSpace(value)
  363. encodings := []*base64.Encoding{
  364. base64.StdEncoding,
  365. base64.RawStdEncoding,
  366. base64.URLEncoding,
  367. base64.RawURLEncoding,
  368. }
  369. var lastErr error
  370. for _, encoding := range encodings {
  371. decoded, err := encoding.DecodeString(value)
  372. if err == nil {
  373. return decoded, nil
  374. }
  375. lastErr = err
  376. }
  377. return nil, lastErr
  378. }
  379. func normalizeClashRouting(body []byte) (string, map[string]any, error) {
  380. text := strings.TrimSpace(string(body))
  381. if text == "" {
  382. return "", nil, errors.New("empty Clash routing response")
  383. }
  384. var document map[string]any
  385. if err := yaml.Unmarshal([]byte(text), &document); err != nil {
  386. return "", nil, fmt.Errorf("invalid Clash routing YAML: %w", err)
  387. }
  388. if len(document) == 0 {
  389. return "", nil, errors.New("Clash routing response must be a YAML map")
  390. }
  391. hasSupportedKey := false
  392. for key := range document {
  393. if remoteClashAllowedKey(key) {
  394. hasSupportedKey = true
  395. break
  396. }
  397. }
  398. if !hasSupportedKey {
  399. return "", nil, errors.New("Clash routing response has no supported routing keys")
  400. }
  401. base := map[string]any{
  402. "proxies": []map[string]any{{"name": "validation-node", "type": "vless"}},
  403. "proxy-groups": []map[string]any{{
  404. "name": "PROXY", "type": "select", "proxies": []string{"validation-node", "DIRECT"},
  405. }},
  406. "rules": []string{"MATCH,PROXY"},
  407. }
  408. if err := mergeRemoteClashRules(base, document); err != nil {
  409. return "", nil, fmt.Errorf("invalid remote Clash routing schema: %w", err)
  410. }
  411. return text, document, nil
  412. }
  413. func resolveIncyRoutingSource(raw string) (string, bool, error) {
  414. source, remote, err := common.ParseRemoteRoutingURL(raw)
  415. if err != nil || !remote {
  416. return raw, remote, err
  417. }
  418. return "incy://autorouting/onadd/" + source, true, nil
  419. }
  420. func newRemoteRoutingHTTPClient() *http.Client {
  421. transport := &http.Transport{
  422. Proxy: nil,
  423. DialContext: netsafe.SSRFGuardedDialContext,
  424. ForceAttemptHTTP2: true,
  425. TLSHandshakeTimeout: 4 * time.Second,
  426. ResponseHeaderTimeout: 5 * time.Second,
  427. TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12},
  428. }
  429. return &http.Client{
  430. Timeout: remoteRoutingHTTPTimeout,
  431. Transport: transport,
  432. CheckRedirect: checkRemoteRoutingRedirect,
  433. }
  434. }
  435. func checkRemoteRoutingRedirect(req *http.Request, via []*http.Request) error {
  436. if len(via) >= 5 {
  437. return errors.New("stopped after 5 redirects")
  438. }
  439. if strings.EqualFold(req.URL.Scheme, "happ") {
  440. // routing.help-style services publish the deeplink as the final Location;
  441. // hand the 3xx back to fetch(), which validates it without a request.
  442. return http.ErrUseLastResponse
  443. }
  444. if !strings.EqualFold(req.URL.Scheme, "https") || req.URL.Hostname() == "" || req.URL.User != nil {
  445. return errors.New("remote routing redirect must stay on an absolute HTTPS URL")
  446. }
  447. // The guarded dialer re-resolves, validates and connects to the same public
  448. // address, including on every HTTPS redirect hop.
  449. return nil
  450. }
  451. func remoteRoutingHost(source string) string {
  452. u, err := url.Parse(source)
  453. if err != nil || u.Hostname() == "" {
  454. return "unknown host"
  455. }
  456. return u.Hostname()
  457. }
  458. func remoteRoutingSettingKey(kind remoteRoutingKind) string {
  459. return "_subRemoteRoutingCache_" + string(kind)
  460. }
  461. func (r *remoteRoutingResolver) ensurePersistedLoaded() {
  462. if !r.persist {
  463. return
  464. }
  465. r.mu.Lock()
  466. loaded := r.loaded
  467. r.mu.Unlock()
  468. if loaded {
  469. return
  470. }
  471. r.loadMu.Lock()
  472. defer r.loadMu.Unlock()
  473. r.mu.Lock()
  474. loaded = r.loaded
  475. r.mu.Unlock()
  476. if loaded {
  477. return
  478. }
  479. db := database.GetDB()
  480. if db == nil {
  481. return
  482. }
  483. sqlDB, err := db.DB()
  484. if err != nil || sqlDB.Ping() != nil {
  485. return
  486. }
  487. r.loadPersisted()
  488. r.mu.Lock()
  489. r.loaded = true
  490. r.mu.Unlock()
  491. }
  492. // triggerPersistedLoad keeps SQLite off the subscription request path: requests
  493. // schedule at most one background load; the startup job loads synchronously.
  494. func (r *remoteRoutingResolver) triggerPersistedLoad() {
  495. if !r.persist {
  496. return
  497. }
  498. r.mu.Lock()
  499. if r.loaded || r.loadInFlight {
  500. r.mu.Unlock()
  501. return
  502. }
  503. r.loadInFlight = true
  504. r.mu.Unlock()
  505. common.GoRecover("remote-routing-cache-load", func() {
  506. defer func() {
  507. r.mu.Lock()
  508. r.loadInFlight = false
  509. r.mu.Unlock()
  510. }()
  511. r.ensurePersistedLoaded()
  512. })
  513. }
  514. func (r *remoteRoutingResolver) loadPersisted() {
  515. loaded := make(map[remoteRoutingKey]remoteRoutingCacheEntry, 2)
  516. for _, kind := range []remoteRoutingKind{remoteRoutingHapp, remoteRoutingClash} {
  517. var setting model.Setting
  518. err := database.GetDB().Where("key = ?", remoteRoutingSettingKey(kind)).First(&setting).Error
  519. if err != nil {
  520. continue
  521. }
  522. var entry remoteRoutingCacheEntry
  523. if json.Unmarshal([]byte(setting.Value), &entry) != nil || entry.Source == "" || entry.Content == "" || entry.FetchedAt <= 0 {
  524. continue
  525. }
  526. if _, remote, err := common.ParseRemoteRoutingURL(entry.Source); err != nil || !remote {
  527. continue
  528. }
  529. normalized, clash, err := normalizeRemoteRoutingContent(kind, []byte(entry.Content))
  530. if err != nil {
  531. continue
  532. }
  533. if kind == remoteRoutingHapp && len(normalized) > remoteRoutingHappMaxValue {
  534. continue
  535. }
  536. entry.Content = normalized
  537. entry.Clash = clash
  538. loaded[remoteRoutingKey{kind: kind, source: entry.Source}] = entry
  539. }
  540. r.mu.Lock()
  541. for key, entry := range loaded {
  542. current, exists := r.entries[key]
  543. if !exists || entry.FetchedAt > current.FetchedAt {
  544. r.entries[key] = entry
  545. }
  546. }
  547. r.mu.Unlock()
  548. }
  549. func (r *remoteRoutingResolver) persistEntry(kind remoteRoutingKind, entry remoteRoutingCacheEntry) {
  550. db := database.GetDB()
  551. if db == nil {
  552. return
  553. }
  554. encoded, err := json.Marshal(entry)
  555. if err != nil {
  556. return
  557. }
  558. key := remoteRoutingSettingKey(kind)
  559. var setting model.Setting
  560. err = db.Where("key = ?", key).First(&setting).Error
  561. if database.IsNotFound(err) {
  562. err = db.Create(&model.Setting{Key: key, Value: string(encoded)}).Error
  563. } else if err == nil {
  564. setting.Value = string(encoded)
  565. err = db.Save(&setting).Error
  566. }
  567. if err != nil {
  568. logger.Warningf("Could not persist the last valid %s remote routing value", kind)
  569. }
  570. }