stcrashreceiver.go 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  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. "bytes"
  15. "compress/gzip"
  16. "flag"
  17. "io"
  18. "io/ioutil"
  19. "log"
  20. "net/http"
  21. "os"
  22. "path"
  23. "path/filepath"
  24. "strings"
  25. )
  26. const maxRequestSize = 1 << 20 // 1 MiB
  27. func main() {
  28. dir := flag.String("dir", ".", "Directory to store reports in")
  29. dsn := flag.String("dsn", "", "Sentry DSN")
  30. listen := flag.String("listen", ":22039", "HTTP listen address")
  31. flag.Parse()
  32. cr := &crashReceiver{
  33. dir: *dir,
  34. dsn: *dsn,
  35. }
  36. log.SetOutput(os.Stdout)
  37. if err := http.ListenAndServe(*listen, cr); err != nil {
  38. log.Fatalln("HTTP serve:", err)
  39. }
  40. }
  41. type crashReceiver struct {
  42. dir string
  43. dsn string
  44. }
  45. func (r *crashReceiver) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  46. // The final path component should be a SHA256 hash in hex, so 64 hex
  47. // characters. We don't care about case on the request but use lower
  48. // case internally.
  49. reportID := strings.ToLower(path.Base(req.URL.Path))
  50. if len(reportID) != 64 {
  51. http.Error(w, "Bad request", http.StatusBadRequest)
  52. return
  53. }
  54. for _, c := range reportID {
  55. if c >= 'a' && c <= 'f' {
  56. continue
  57. }
  58. if c >= '0' && c <= '9' {
  59. continue
  60. }
  61. http.Error(w, "Bad request", http.StatusBadRequest)
  62. return
  63. }
  64. // The location of the report on disk, compressed
  65. fullPath := filepath.Join(r.dir, r.dirFor(reportID), reportID) + ".gz"
  66. switch req.Method {
  67. case http.MethodGet:
  68. r.serveGet(fullPath, w, req)
  69. case http.MethodHead:
  70. r.serveHead(fullPath, w, req)
  71. case http.MethodPut:
  72. r.servePut(reportID, fullPath, w, req)
  73. default:
  74. http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
  75. }
  76. }
  77. // serveGet responds to GET requests by serving the uncompressed report.
  78. func (r *crashReceiver) serveGet(fullPath string, w http.ResponseWriter, _ *http.Request) {
  79. fd, err := os.Open(fullPath)
  80. if err != nil {
  81. http.Error(w, "Not found", http.StatusNotFound)
  82. return
  83. }
  84. defer fd.Close()
  85. gr, err := gzip.NewReader(fd)
  86. if err != nil {
  87. http.Error(w, "Internal server error", http.StatusInternalServerError)
  88. return
  89. }
  90. _, _ = io.Copy(w, gr) // best effort
  91. }
  92. // serveHead responds to HEAD requests by checking if the named report
  93. // already exists in the system.
  94. func (r *crashReceiver) serveHead(fullPath string, w http.ResponseWriter, _ *http.Request) {
  95. if _, err := os.Lstat(fullPath); err != nil {
  96. http.Error(w, "Not found", http.StatusNotFound)
  97. }
  98. }
  99. // servePut accepts and stores the given report.
  100. func (r *crashReceiver) servePut(reportID, fullPath string, w http.ResponseWriter, req *http.Request) {
  101. // Ensure the destination directory exists
  102. if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
  103. log.Println("Creating directory:", err)
  104. http.Error(w, "Internal server error", http.StatusInternalServerError)
  105. return
  106. }
  107. // Read at most maxRequestSize of report data.
  108. log.Println("Receiving report", reportID)
  109. lr := io.LimitReader(req.Body, maxRequestSize)
  110. bs, err := ioutil.ReadAll(lr)
  111. if err != nil {
  112. log.Println("Reading report:", err)
  113. http.Error(w, "Internal server error", http.StatusInternalServerError)
  114. return
  115. }
  116. // Compress the report for storage
  117. buf := new(bytes.Buffer)
  118. gw := gzip.NewWriter(buf)
  119. _, _ = gw.Write(bs) // can't fail
  120. gw.Close()
  121. // Create an output file with the compressed report
  122. err = ioutil.WriteFile(fullPath, buf.Bytes(), 0644)
  123. if err != nil {
  124. log.Println("Saving report:", err)
  125. http.Error(w, "Internal server error", http.StatusInternalServerError)
  126. return
  127. }
  128. // Send the report to Sentry
  129. if r.dsn != "" {
  130. go func() {
  131. // There's no need for the client to have to wait for this part.
  132. if err := sendReport(r.dsn, reportID, bs); err != nil {
  133. log.Println("Failed to send report:", err)
  134. }
  135. }()
  136. }
  137. }
  138. // 01234567890abcdef... => 01/23
  139. func (r *crashReceiver) dirFor(base string) string {
  140. return filepath.Join(base[0:2], base[2:4])
  141. }