mirror of
https://github.com/octoleo/syncthing.git
synced 2024-11-09 23:00:58 +00:00
42ce6be9b9
* lib/ur: Implement crash (panic) reporting (fixes #959) This implements a simple crash reporting method. It piggybacks on the panic log files created by the monitor process, picking these up and uploading them from the usage reporting routine. A new config value points to the crash receiver base URL, which defaults to "https://crash.syncthing.net/newcrash" (following the pattern of "https://data.syncthing.net/newdata" for usage reports, but allowing us to separate the service as required).
136 lines
3.6 KiB
Go
136 lines
3.6 KiB
Go
// Copyright (C) 2019 The Syncthing Authors.
|
|
//
|
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
|
|
// Command stcrashreceiver is a trivial HTTP server that allows two things:
|
|
//
|
|
// - uploading files (crash reports) named like a SHA256 hash using a PUT request
|
|
// - checking whether such file exists using a HEAD request
|
|
//
|
|
// Typically this should be deployed behind something that manages HTTPS.
|
|
package main
|
|
|
|
import (
|
|
"flag"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
const maxRequestSize = 1 << 20 // 1 MiB
|
|
|
|
func main() {
|
|
dir := flag.String("dir", ".", "Directory to store reports in")
|
|
dsn := flag.String("dsn", "", "Sentry DSN")
|
|
listen := flag.String("listen", ":22039", "HTTP listen address")
|
|
flag.Parse()
|
|
|
|
cr := &crashReceiver{
|
|
dir: *dir,
|
|
dsn: *dsn,
|
|
}
|
|
|
|
log.SetOutput(os.Stdout)
|
|
if err := http.ListenAndServe(*listen, cr); err != nil {
|
|
log.Fatalln("HTTP serve:", err)
|
|
}
|
|
}
|
|
|
|
type crashReceiver struct {
|
|
dir string
|
|
dsn string
|
|
}
|
|
|
|
func (r *crashReceiver) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|
// The final path component should be a SHA256 hash in hex, so 64 hex
|
|
// characters. We don't care about case on the request but use lower
|
|
// case internally.
|
|
base := strings.ToLower(path.Base(req.URL.Path))
|
|
if len(base) != 64 {
|
|
http.Error(w, "Bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
for _, c := range base {
|
|
if c >= 'a' && c <= 'f' {
|
|
continue
|
|
}
|
|
if c >= '0' && c <= '9' {
|
|
continue
|
|
}
|
|
http.Error(w, "Bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
switch req.Method {
|
|
case http.MethodHead:
|
|
r.serveHead(base, w, req)
|
|
case http.MethodPut:
|
|
r.servePut(base, w, req)
|
|
default:
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
// serveHead responds to HEAD requests by checking if the named report
|
|
// already exists in the system.
|
|
func (r *crashReceiver) serveHead(base string, w http.ResponseWriter, _ *http.Request) {
|
|
path := filepath.Join(r.dirFor(base), base)
|
|
if _, err := os.Lstat(path); err != nil {
|
|
http.Error(w, "Not found", http.StatusNotFound)
|
|
}
|
|
// 200 OK
|
|
}
|
|
|
|
// servePut accepts and stores the given report.
|
|
func (r *crashReceiver) servePut(base string, w http.ResponseWriter, req *http.Request) {
|
|
path := filepath.Join(r.dirFor(base), base)
|
|
fullPath := filepath.Join(r.dir, path)
|
|
|
|
// Ensure the destination directory exists
|
|
if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
|
|
log.Printf("Creating directory for report %s: %v", base, err)
|
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Read at most maxRequestSize of report data.
|
|
log.Println("Receiving report", base)
|
|
lr := io.LimitReader(req.Body, maxRequestSize)
|
|
bs, err := ioutil.ReadAll(lr)
|
|
if err != nil {
|
|
log.Println("Reading report:", err)
|
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Create an output file
|
|
err = ioutil.WriteFile(fullPath, bs, 0644)
|
|
if err != nil {
|
|
log.Printf("Creating file for report %s: %v", base, err)
|
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Send the report to Sentry
|
|
if r.dsn != "" {
|
|
go func() {
|
|
// There's no need for the client to have to wait for this part.
|
|
if err := sendReport(r.dsn, path, bs); err != nil {
|
|
log.Println("Failed to send report:", err)
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
// 01234567890abcdef... => 01/23
|
|
func (r *crashReceiver) dirFor(base string) string {
|
|
return filepath.Join(base[0:2], base[2:4])
|
|
}
|