From 6393ec357e7a4b4086a09dc9f384a4cfb62c05bb Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Tue, 4 Apr 2017 11:25:00 +0100 Subject: [PATCH] Give rageshake script its own repo This was previously at https://github.com/vector-im/riot-web/blob/develop/scripts/rageshake.go, but it's not really exclusive to riot-web, and given I'm about to add some more complexity to it, I'd like for it to be in its own repo. --- hooks/install.sh | 5 + hooks/pre-commit | 9 ++ src/github.com/matrix-org/rageshake/main.go | 146 ++++++++++++++++++++ 3 files changed, 160 insertions(+) create mode 100755 hooks/install.sh create mode 100755 hooks/pre-commit create mode 100644 src/github.com/matrix-org/rageshake/main.go diff --git a/hooks/install.sh b/hooks/install.sh new file mode 100755 index 0000000..f8aa331 --- /dev/null +++ b/hooks/install.sh @@ -0,0 +1,5 @@ +#! /bin/bash + +DOT_GIT="$(dirname $0)/../.git" + +ln -s "../../hooks/pre-commit" "$DOT_GIT/hooks/pre-commit" \ No newline at end of file diff --git a/hooks/pre-commit b/hooks/pre-commit new file mode 100755 index 0000000..d9ffbfb --- /dev/null +++ b/hooks/pre-commit @@ -0,0 +1,9 @@ +#! /bin/bash + +set -eu + +golint src/... +go fmt ./src/... +go tool vet --shadow ./src +gocyclo -over 12 src/ +gb test diff --git a/src/github.com/matrix-org/rageshake/main.go b/src/github.com/matrix-org/rageshake/main.go new file mode 100644 index 0000000..a150eab --- /dev/null +++ b/src/github.com/matrix-org/rageshake/main.go @@ -0,0 +1,146 @@ +// Run a web server capable of dumping bug reports sent by Riot. +// Requires Go 1.5+ +// Usage: BUGS_USER=user BUGS_PASS=password go run rageshake.go PORT +// Example: BUGS_USER=alice BUGS_PASS=secret go run rageshake.go 8080 +package main + +import ( + "bytes" + "compress/gzip" + "crypto/subtle" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "path/filepath" + "strconv" + "time" +) + +var maxPayloadSize = 1024 * 1024 * 55 // 55 MB + +type LogEntry struct { + ID string `json:"id"` + Lines string `json:"lines"` +} + +type Payload struct { + Text string `json:"text"` + Version string `json:"version"` + UserAgent string `json:"user_agent"` + Logs []LogEntry `json:"logs"` +} + +func respond(code int, w http.ResponseWriter) { + w.WriteHeader(code) + w.Write([]byte("{}")) +} + +func gzipAndSave(data []byte, dirname, fpath string) error { + _ = os.MkdirAll(filepath.Join("bugs", dirname), os.ModePerm) + fpath = filepath.Join("bugs", dirname, fpath) + + if _, err := os.Stat(fpath); err == nil { + return fmt.Errorf("file already exists") // the user can just retry + } + var b bytes.Buffer + gz := gzip.NewWriter(&b) + if _, err := gz.Write(data); err != nil { + return err + } + if err := gz.Flush(); err != nil { + return err + } + if err := gz.Close(); err != nil { + return err + } + if err := ioutil.WriteFile(fpath, b.Bytes(), 0644); err != nil { + return err + } + return nil +} + +func basicAuth(handler http.Handler, username, password, realm string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + user, pass, ok := r.BasicAuth() // pull creds from the request + + // check user and pass securely + if !ok || subtle.ConstantTimeCompare([]byte(user), []byte(username)) != 1 || subtle.ConstantTimeCompare([]byte(pass), []byte(password)) != 1 { + w.Header().Set("WWW-Authenticate", `Basic realm="`+realm+`"`) + w.WriteHeader(401) + w.Write([]byte("Unauthorised.\n")) + return + } + + handler.ServeHTTP(w, r) + }) +} + +func main() { + http.HandleFunc("/api/submit", func(w http.ResponseWriter, req *http.Request) { + if req.Method != "POST" && req.Method != "OPTIONS" { + respond(405, w) + return + } + // Set CORS + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept") + if req.Method == "OPTIONS" { + respond(200, w) + return + } + if length, err := strconv.Atoi(req.Header.Get("Content-Length")); err != nil || length > maxPayloadSize { + respond(413, w) + return + } + var p Payload + if err := json.NewDecoder(req.Body).Decode(&p); err != nil { + respond(400, w) + return + } + // Dump bug report to disk as form: + // "bugreport-20170115-112233.log.gz" => user text, version, user agent, # logs + // "bugreport-20170115-112233-0.log.gz" => most recent log + // "bugreport-20170115-112233-1.log.gz" => ... + // "bugreport-20170115-112233-N.log.gz" => oldest log + t := time.Now().UTC() + prefix := t.Format("2006-01-02/150405") + summary := fmt.Sprintf( + "%s\n\nNumber of logs: %d\nVersion: %s\nUser-Agent: %s\n", p.Text, len(p.Logs), p.Version, p.UserAgent, + ) + if err := gzipAndSave([]byte(summary), prefix, "details.log.gz"); err != nil { + respond(500, w) + return + } + for i, log := range p.Logs { + if err := gzipAndSave([]byte(log.Lines), prefix, fmt.Sprintf("logs-%d.log.gz", i)); err != nil { + respond(500, w) + return // TODO: Rollback? + } + } + respond(200, w) + }) + + // Make sure bugs directory exists + _ = os.Mkdir("bugs", os.ModePerm) + + // serve files under "bugs" + fs := http.FileServer(http.Dir("bugs")) + fs = http.StripPrefix("/api/listing/", fs) + + // set auth if env vars exist + usr := os.Getenv("BUGS_USER") + pass := os.Getenv("BUGS_PASS") + if usr == "" || pass == "" { + fmt.Println("BUGS_USER and BUGS_PASS env vars not found. No authentication is running for /api/listing") + } else { + fs = basicAuth(fs, usr, pass, "Riot bug reports") + } + http.Handle("/api/listing/", fs) + + port := os.Args[1] + log.Fatal(http.ListenAndServe(":"+port, nil)) +}