main.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  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. // Command stcrashreceiver is a trivial HTTP server that allows two things:
  7. //
  8. // - uploading files (crash reports) named like a SHA256 hash using a PUT request
  9. // - checking whether such file exists using a HEAD request
  10. //
  11. // Typically this should be deployed behind something that manages HTTPS.
  12. package main
  13. import (
  14. "context"
  15. "crypto/sha256"
  16. "encoding/json"
  17. "fmt"
  18. "io"
  19. "log"
  20. "net/http"
  21. "os"
  22. "path/filepath"
  23. "regexp"
  24. "strings"
  25. "github.com/alecthomas/kong"
  26. raven "github.com/getsentry/raven-go"
  27. "github.com/prometheus/client_golang/prometheus/promhttp"
  28. _ "github.com/syncthing/syncthing/lib/automaxprocs"
  29. "github.com/syncthing/syncthing/lib/build"
  30. "github.com/syncthing/syncthing/lib/ur"
  31. )
  32. const maxRequestSize = 1 << 20 // 1 MiB
  33. type cli struct {
  34. Dir string `help:"Parent directory to store crash and failure reports in" env:"REPORTS_DIR" default:"."`
  35. DSN string `help:"Sentry DSN" env:"SENTRY_DSN"`
  36. Listen string `help:"HTTP listen address" default:":8080" env:"LISTEN_ADDRESS"`
  37. MaxDiskFiles int `help:"Maximum number of reports on disk" default:"100000" env:"MAX_DISK_FILES"`
  38. MaxDiskSizeMB int64 `help:"Maximum disk space to use for reports" default:"1024" env:"MAX_DISK_SIZE_MB"`
  39. SentryQueue int `help:"Maximum number of reports to queue for sending to Sentry" default:"64" env:"SENTRY_QUEUE"`
  40. DiskQueue int `help:"Maximum number of reports to queue for writing to disk" default:"64" env:"DISK_QUEUE"`
  41. MetricsListen string `help:"HTTP listen address for metrics" default:":8081" env:"METRICS_LISTEN_ADDRESS"`
  42. IngorePatterns string `help:"File containing ignore patterns (regexp)" env:"IGNORE_PATTERNS" type:"existingfile"`
  43. }
  44. func main() {
  45. var params cli
  46. kong.Parse(&params)
  47. mux := http.NewServeMux()
  48. ds := &diskStore{
  49. dir: filepath.Join(params.Dir, "crash_reports"),
  50. inbox: make(chan diskEntry, params.DiskQueue),
  51. maxFiles: params.MaxDiskFiles,
  52. maxBytes: params.MaxDiskSizeMB << 20,
  53. }
  54. go ds.Serve(context.Background())
  55. ss := &sentryService{
  56. dsn: params.DSN,
  57. inbox: make(chan sentryRequest, params.SentryQueue),
  58. }
  59. go ss.Serve(context.Background())
  60. var ip *ignorePatterns
  61. if params.IngorePatterns != "" {
  62. var err error
  63. ip, err = loadIgnorePatterns(params.IngorePatterns)
  64. if err != nil {
  65. log.Fatalf("Failed to load ignore patterns: %v", err)
  66. }
  67. }
  68. cr := &crashReceiver{
  69. store: ds,
  70. sentry: ss,
  71. ignore: ip,
  72. }
  73. mux.Handle("/", cr)
  74. mux.HandleFunc("/ping", func(w http.ResponseWriter, req *http.Request) {
  75. w.Write([]byte("OK"))
  76. })
  77. if params.MetricsListen != "" {
  78. mmux := http.NewServeMux()
  79. mmux.Handle("/metrics", promhttp.Handler())
  80. go func() {
  81. if err := http.ListenAndServe(params.MetricsListen, mmux); err != nil {
  82. log.Fatalln("HTTP serve metrics:", err)
  83. }
  84. }()
  85. }
  86. if params.DSN != "" {
  87. mux.HandleFunc("/newcrash/failure", handleFailureFn(params.DSN, filepath.Join(params.Dir, "failure_reports"), ip))
  88. }
  89. log.SetOutput(os.Stdout)
  90. if err := http.ListenAndServe(params.Listen, mux); err != nil {
  91. log.Fatalln("HTTP serve:", err)
  92. }
  93. }
  94. func handleFailureFn(dsn, failureDir string, ignore *ignorePatterns) func(w http.ResponseWriter, req *http.Request) {
  95. return func(w http.ResponseWriter, req *http.Request) {
  96. result := "failure"
  97. defer func() {
  98. metricFailureReportsTotal.WithLabelValues(result).Inc()
  99. }()
  100. lr := io.LimitReader(req.Body, maxRequestSize)
  101. bs, err := io.ReadAll(lr)
  102. req.Body.Close()
  103. if err != nil {
  104. http.Error(w, err.Error(), 500)
  105. return
  106. }
  107. if ignore.match(bs) {
  108. result = "ignored"
  109. return
  110. }
  111. var reports []ur.FailureReport
  112. err = json.Unmarshal(bs, &reports)
  113. if err != nil {
  114. http.Error(w, err.Error(), 400)
  115. return
  116. }
  117. if len(reports) == 0 {
  118. // Shouldn't happen
  119. log.Printf("Got zero failure reports")
  120. return
  121. }
  122. version, err := build.ParseVersion(reports[0].Version)
  123. if err != nil {
  124. http.Error(w, err.Error(), 400)
  125. return
  126. }
  127. for _, r := range reports {
  128. pkt := packet(version, "failure")
  129. pkt.Message = r.Description
  130. pkt.Extra = raven.Extra{
  131. "count": r.Count,
  132. }
  133. for k, v := range r.Extra {
  134. pkt.Extra[k] = v
  135. }
  136. if r.Goroutines != "" {
  137. url, err := saveFailureWithGoroutines(r.FailureData, failureDir)
  138. if err != nil {
  139. log.Println("Saving failure report:", err)
  140. http.Error(w, "Internal server error", http.StatusInternalServerError)
  141. return
  142. }
  143. pkt.Extra["goroutinesURL"] = url
  144. }
  145. message := sanitizeMessageLDB(r.Description)
  146. pkt.Fingerprint = []string{message}
  147. if err := sendReport(dsn, pkt, userIDFor(req)); err != nil {
  148. log.Println("Failed to send failure report:", err)
  149. } else {
  150. log.Println("Sent failure report:", r.Description)
  151. result = "success"
  152. }
  153. }
  154. }
  155. }
  156. func saveFailureWithGoroutines(data ur.FailureData, failureDir string) (string, error) {
  157. bs := make([]byte, len(data.Description)+len(data.Goroutines))
  158. copy(bs, data.Description)
  159. copy(bs[len(data.Description):], data.Goroutines)
  160. id := fmt.Sprintf("%x", sha256.Sum256(bs))
  161. path := fullPathCompressed(failureDir, id)
  162. err := compressAndWrite(bs, path)
  163. if err != nil {
  164. return "", err
  165. }
  166. return reportServer + path, nil
  167. }
  168. type ignorePatterns struct {
  169. patterns []*regexp.Regexp
  170. }
  171. func loadIgnorePatterns(path string) (*ignorePatterns, error) {
  172. bs, err := os.ReadFile(path)
  173. if err != nil {
  174. return nil, err
  175. }
  176. var patterns []*regexp.Regexp
  177. for _, line := range strings.Split(string(bs), "\n") {
  178. line = strings.TrimSpace(line)
  179. if line == "" {
  180. continue
  181. }
  182. re, err := regexp.Compile(line)
  183. if err != nil {
  184. return nil, err
  185. }
  186. patterns = append(patterns, re)
  187. }
  188. log.Printf("Loaded %d ignore patterns", len(patterns))
  189. return &ignorePatterns{patterns: patterns}, nil
  190. }
  191. func (i *ignorePatterns) match(report []byte) bool {
  192. if i == nil {
  193. return false
  194. }
  195. for _, re := range i.patterns {
  196. if re.Match(report) {
  197. return true
  198. }
  199. }
  200. return false
  201. }