// 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]) }