main.go 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  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. "encoding/json"
  16. "fmt"
  17. "io"
  18. "log"
  19. "net/http"
  20. "os"
  21. "path/filepath"
  22. "time"
  23. "github.com/alecthomas/kong"
  24. "github.com/syncthing/syncthing/lib/sha256"
  25. "github.com/syncthing/syncthing/lib/ur"
  26. raven "github.com/getsentry/raven-go"
  27. )
  28. const maxRequestSize = 1 << 20 // 1 MiB
  29. type cli struct {
  30. Dir string `help:"Parent directory to store crash and failure reports in" env:"REPORTS_DIR" default:"."`
  31. DSN string `help:"Sentry DSN" env:"SENTRY_DSN"`
  32. Listen string `help:"HTTP listen address" default:":8080" env:"LISTEN_ADDRESS"`
  33. MaxDiskFiles int `help:"Maximum number of reports on disk" default:"100000" env:"MAX_DISK_FILES"`
  34. MaxDiskSizeMB int64 `help:"Maximum disk space to use for reports" default:"1024" env:"MAX_DISK_SIZE_MB"`
  35. CleanInterval time.Duration `help:"Interval between cleaning up old reports" default:"12h" env:"CLEAN_INTERVAL"`
  36. SentryQueue int `help:"Maximum number of reports to queue for sending to Sentry" default:"64" env:"SENTRY_QUEUE"`
  37. DiskQueue int `help:"Maximum number of reports to queue for writing to disk" default:"64" env:"DISK_QUEUE"`
  38. }
  39. func main() {
  40. var params cli
  41. kong.Parse(&params)
  42. mux := http.NewServeMux()
  43. ds := &diskStore{
  44. dir: filepath.Join(params.Dir, "crash_reports"),
  45. inbox: make(chan diskEntry, params.DiskQueue),
  46. maxFiles: params.MaxDiskFiles,
  47. maxBytes: params.MaxDiskSizeMB << 20,
  48. }
  49. go ds.Serve(context.Background())
  50. ss := &sentryService{
  51. dsn: params.DSN,
  52. inbox: make(chan sentryRequest, params.SentryQueue),
  53. }
  54. go ss.Serve(context.Background())
  55. cr := &crashReceiver{
  56. store: ds,
  57. sentry: ss,
  58. }
  59. mux.Handle("/", cr)
  60. if params.DSN != "" {
  61. mux.HandleFunc("/newcrash/failure", handleFailureFn(params.DSN, filepath.Join(params.Dir, "failure_reports")))
  62. }
  63. log.SetOutput(os.Stdout)
  64. if err := http.ListenAndServe(params.Listen, mux); err != nil {
  65. log.Fatalln("HTTP serve:", err)
  66. }
  67. }
  68. func handleFailureFn(dsn, failureDir string) func(w http.ResponseWriter, req *http.Request) {
  69. return func(w http.ResponseWriter, req *http.Request) {
  70. lr := io.LimitReader(req.Body, maxRequestSize)
  71. bs, err := io.ReadAll(lr)
  72. req.Body.Close()
  73. if err != nil {
  74. http.Error(w, err.Error(), 500)
  75. return
  76. }
  77. var reports []ur.FailureReport
  78. err = json.Unmarshal(bs, &reports)
  79. if err != nil {
  80. http.Error(w, err.Error(), 400)
  81. return
  82. }
  83. if len(reports) == 0 {
  84. // Shouldn't happen
  85. log.Printf("Got zero failure reports")
  86. return
  87. }
  88. version, err := parseVersion(reports[0].Version)
  89. if err != nil {
  90. http.Error(w, err.Error(), 400)
  91. return
  92. }
  93. for _, r := range reports {
  94. pkt := packet(version, "failure")
  95. pkt.Message = r.Description
  96. pkt.Extra = raven.Extra{
  97. "count": r.Count,
  98. }
  99. for k, v := range r.Extra {
  100. pkt.Extra[k] = v
  101. }
  102. if r.Goroutines != "" {
  103. url, err := saveFailureWithGoroutines(r.FailureData, failureDir)
  104. if err != nil {
  105. log.Println("Saving failure report:", err)
  106. http.Error(w, "Internal server error", http.StatusInternalServerError)
  107. return
  108. }
  109. pkt.Extra["goroutinesURL"] = url
  110. }
  111. message := sanitizeMessageLDB(r.Description)
  112. pkt.Fingerprint = []string{message}
  113. if err := sendReport(dsn, pkt, userIDFor(req)); err != nil {
  114. log.Println("Failed to send failure report:", err)
  115. } else {
  116. log.Println("Sent failure report:", r.Description)
  117. }
  118. }
  119. }
  120. }
  121. func saveFailureWithGoroutines(data ur.FailureData, failureDir string) (string, error) {
  122. bs := make([]byte, len(data.Description)+len(data.Goroutines))
  123. copy(bs, data.Description)
  124. copy(bs[len(data.Description):], data.Goroutines)
  125. id := fmt.Sprintf("%x", sha256.Sum256(bs))
  126. path := fullPathCompressed(failureDir, id)
  127. err := compressAndWrite(bs, path)
  128. if err != nil {
  129. return "", err
  130. }
  131. return reportServer + path, nil
  132. }