1
0

remote_routing.go 18 KB

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