main.go 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  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. "encoding/json"
  15. "flag"
  16. "fmt"
  17. "io"
  18. "io/ioutil"
  19. "log"
  20. "net"
  21. "net/http"
  22. "os"
  23. "time"
  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. func main() {
  30. dir := flag.String("dir", ".", "Directory to store reports in")
  31. dsn := flag.String("dsn", "", "Sentry DSN")
  32. listen := flag.String("listen", ":22039", "HTTP listen address")
  33. flag.Parse()
  34. mux := http.NewServeMux()
  35. cr := &crashReceiver{
  36. dir: *dir,
  37. dsn: *dsn,
  38. }
  39. mux.Handle("/", cr)
  40. if *dsn != "" {
  41. mux.HandleFunc("/newcrash/failure", handleFailureFn(*dsn))
  42. }
  43. log.SetOutput(os.Stdout)
  44. if err := http.ListenAndServe(*listen, mux); err != nil {
  45. log.Fatalln("HTTP serve:", err)
  46. }
  47. }
  48. func handleFailureFn(dsn string) func(w http.ResponseWriter, req *http.Request) {
  49. return func(w http.ResponseWriter, req *http.Request) {
  50. lr := io.LimitReader(req.Body, maxRequestSize)
  51. bs, err := ioutil.ReadAll(lr)
  52. req.Body.Close()
  53. if err != nil {
  54. http.Error(w, err.Error(), 500)
  55. return
  56. }
  57. var reports []ur.FailureReport
  58. err = json.Unmarshal(bs, &reports)
  59. if err != nil {
  60. http.Error(w, err.Error(), 400)
  61. return
  62. }
  63. if len(reports) == 0 {
  64. // Shouldn't happen
  65. log.Printf("Got zero failure reports")
  66. return
  67. }
  68. version, err := parseVersion(reports[0].Version)
  69. if err != nil {
  70. http.Error(w, err.Error(), 400)
  71. return
  72. }
  73. for _, r := range reports {
  74. pkt := packet(version, "failure")
  75. pkt.Message = r.Description
  76. pkt.Extra = raven.Extra{
  77. "count": r.Count,
  78. }
  79. pkt.Fingerprint = []string{r.Description}
  80. if err := sendReport(dsn, pkt, userIDFor(req)); err != nil {
  81. log.Println("Failed to send failure report:", err)
  82. } else {
  83. log.Println("Sent failure report:", r.Description)
  84. }
  85. }
  86. }
  87. }
  88. // userIDFor returns a string we can use as the user ID for the purpose of
  89. // counting affected users. It's the truncated hash of a salt, the user
  90. // remote IP, and the current month.
  91. func userIDFor(req *http.Request) string {
  92. addr := req.RemoteAddr
  93. if fwd := req.Header.Get("x-forwarded-for"); fwd != "" {
  94. addr = fwd
  95. }
  96. if host, _, err := net.SplitHostPort(addr); err == nil {
  97. addr = host
  98. }
  99. now := time.Now().Format("200601")
  100. salt := "stcrashreporter"
  101. hash := sha256.Sum256([]byte(salt + addr + now))
  102. return fmt.Sprintf("%x", hash[:8])
  103. }