stcrashreceiver.go 3.7 KB

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