main.go 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. // Copyright (C) 2019 The Syncthing Authors.
  2. //
  3. // This Source Code Form is subject to the terms of the Mozilla Public
  4. // License, v. 2.0. If a copy of the MPL was not distributed with this file,
  5. // You can obtain one at https://mozilla.org/MPL/2.0/.
  6. package main
  7. import (
  8. "bytes"
  9. "encoding/json"
  10. "fmt"
  11. "io"
  12. "log"
  13. "net/http"
  14. "os"
  15. "sort"
  16. "strings"
  17. "time"
  18. "github.com/alecthomas/kong"
  19. "github.com/syncthing/syncthing/lib/httpcache"
  20. "github.com/syncthing/syncthing/lib/upgrade"
  21. )
  22. type cli struct {
  23. Listen string `default:":8080" help:"Listen address"`
  24. URL string `short:"u" default:"https://api.github.com/repos/syncthing/syncthing/releases?per_page=25" help:"GitHub releases url"`
  25. Forward []string `short:"f" help:"Forwarded pages, format: /path->https://example/com/url"`
  26. CacheTime time.Duration `default:"15m" help:"Cache time"`
  27. }
  28. func main() {
  29. var params cli
  30. kong.Parse(&params)
  31. if err := server(&params); err != nil {
  32. fmt.Printf("Error: %v\n", err)
  33. os.Exit(1)
  34. }
  35. }
  36. func server(params *cli) error {
  37. http.Handle("/meta.json", httpcache.SinglePath(&githubReleases{url: params.URL}, params.CacheTime))
  38. for _, fwd := range params.Forward {
  39. path, url, ok := strings.Cut(fwd, "->")
  40. if !ok {
  41. return fmt.Errorf("invalid forward: %q", fwd)
  42. }
  43. http.Handle(path, httpcache.SinglePath(&proxy{url: url}, params.CacheTime))
  44. }
  45. return http.ListenAndServe(params.Listen, nil)
  46. }
  47. type githubReleases struct {
  48. url string
  49. }
  50. func (p *githubReleases) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  51. log.Println("Fetching", p.url)
  52. rels := upgrade.FetchLatestReleases(p.url, "")
  53. if rels == nil {
  54. http.Error(w, "no releases", http.StatusInternalServerError)
  55. return
  56. }
  57. sort.Sort(upgrade.SortByRelease(rels))
  58. rels = filterForLatest(rels)
  59. buf := new(bytes.Buffer)
  60. _ = json.NewEncoder(buf).Encode(rels)
  61. w.Header().Set("Content-Type", "application/json; charset=utf-8")
  62. w.Header().Set("Access-Control-Allow-Origin", "*")
  63. w.Header().Set("Access-Control-Allow-Methods", "GET")
  64. w.Write(buf.Bytes())
  65. }
  66. type proxy struct {
  67. url string
  68. }
  69. func (p *proxy) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  70. log.Println("Fetching", p.url)
  71. req, err := http.NewRequestWithContext(req.Context(), http.MethodGet, p.url, nil)
  72. if err != nil {
  73. http.Error(w, err.Error(), http.StatusInternalServerError)
  74. return
  75. }
  76. resp, err := http.DefaultClient.Do(req)
  77. if err != nil {
  78. http.Error(w, err.Error(), http.StatusInternalServerError)
  79. return
  80. }
  81. defer resp.Body.Close()
  82. ct := resp.Header.Get("Content-Type")
  83. w.Header().Set("Content-Type", ct)
  84. if resp.StatusCode == http.StatusOK {
  85. w.Header().Set("Cache-Control", "public, max-age=900")
  86. w.Header().Set("Access-Control-Allow-Origin", "*")
  87. w.Header().Set("Access-Control-Allow-Methods", "GET")
  88. }
  89. w.WriteHeader(resp.StatusCode)
  90. if strings.HasPrefix(ct, "application/json") {
  91. // Special JSON handling; clean it up a bit.
  92. var v interface{}
  93. if err := json.NewDecoder(resp.Body).Decode(&v); err != nil {
  94. http.Error(w, err.Error(), http.StatusInternalServerError)
  95. return
  96. }
  97. _ = json.NewEncoder(w).Encode(v)
  98. } else {
  99. _, _ = io.Copy(w, resp.Body)
  100. }
  101. }
  102. // filterForLatest returns the latest stable and prerelease only. If the
  103. // stable version is newer (comes first in the list) there is no need to go
  104. // looking for a prerelease at all.
  105. func filterForLatest(rels []upgrade.Release) []upgrade.Release {
  106. var filtered []upgrade.Release
  107. var havePre bool
  108. for _, rel := range rels {
  109. if !rel.Prerelease {
  110. // We found a stable version, we're good now.
  111. filtered = append(filtered, rel)
  112. break
  113. }
  114. if rel.Prerelease && !havePre {
  115. // We remember the first prerelease we find.
  116. filtered = append(filtered, rel)
  117. havePre = true
  118. }
  119. }
  120. return filtered
  121. }