From 81865b019375675695eddb4278296d56869d5a28 Mon Sep 17 00:00:00 2001 From: Michael Kaye <1917473+michaelkaye@users.noreply.github.com> Date: Tue, 25 Jan 2022 09:58:50 +0000 Subject: [PATCH] Initial attempt at a generic webhook for reports. Background submission to a notification server that doesn't block rageshake submission. --- Makefile | 4 +++ README.md | 10 ++++++ docs/generic_webhook.md | 38 +++++++++++++++++++++ main.go | 13 +++++++- submit.go | 74 ++++++++++++++++++++++++++++++++++++----- 5 files changed, 130 insertions(+), 9 deletions(-) create mode 100644 Makefile create mode 100644 docs/generic_webhook.md diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..28187cb --- /dev/null +++ b/Makefile @@ -0,0 +1,4 @@ +docker: + DOCKER_BUILDKIT=1 docker build -t rageshake:latest . + docker run --rm --name rageshake --network rageshake-search --mount type=bind,source=$(shell pwd)/bugs,target=/bugs --mount type=bind,source=$(shell pwd)/rageshake.yaml,target=/rageshake.yaml -p 127.0.0.1:9110:9110 rageshake:latest -listen :9110 + diff --git a/README.md b/README.md index 6c5f228..a4112bc 100644 --- a/README.md +++ b/README.md @@ -105,3 +105,13 @@ You can get notifications when a new rageshake arrives on the server. Currently this tool supports pushing notifications as GitHub issues in a repo, through a Slack webhook or by email, cf sample config file for how to configure them. + +### Generic Webhook Notifications + +You can receive a webhook notifications when a new rageshake arrives on the server. + +These requests contain all the parsed metadata, and links to the uploaded files, and any github/gitlab +issues created. + +Details on the request and expected response are [available](docs/generic\_webhook.md). + diff --git a/docs/generic_webhook.md b/docs/generic_webhook.md new file mode 100644 index 0000000..5769ec9 --- /dev/null +++ b/docs/generic_webhook.md @@ -0,0 +1,38 @@ +## Generic webhook request + +If the configuration option `generic_webhook_url` is set, then a synchronous request to +the endpoint will be sent after the incoming request is parsed and the files are uploaded. + +The webhook is designed for notification or other tracking services, and does not contain +the original log files uploaded. + +(If you want the original log files, we suggest to implement the rageshake interface itself). + +A sample JSON body is as follows: + +``` +{ + 'user_text': 'test\r\n\r\nIssue: No issue link given', + 'app': 'element-web', + 'data': { + 'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:96.0) Gecko/20100101 Firefox/96.0', + 'Version': '0f15ba34cdf5-react-0f15ba34cdf5-js-0f15ba34cdf5', + ... + 'user_id': '@michaelgoesforawalk:matrix.org'}, + 'labels': None, + 'logs': [ + 'logs-0000.log.gz', + 'logs-0001.log.gz', + 'logs-0002.log.gz', + ], + 'logErrors': None, + 'files': [ + 'screenshot.png' + ], + 'fileErrors': None, + 'report_url': 'https://github.com/your-org/your-repo/issues/1251', + 'listing_url': 'http://your-rageshake-server/api/listing/2022-01-25/154742-OOXBVGIX' +} +``` + +The log and other files can be individually downloaded by concatenating the `listing_url` and the `logs` or `files` name. diff --git a/main.go b/main.go index dcaeaf8..d914a4e 100644 --- a/main.go +++ b/main.go @@ -71,6 +71,8 @@ type config struct { SMTPUsername string `yaml:"smtp_username"` SMTPPassword string `yaml:"smtp_password"` + + GenericWebhookURL string `yaml:"generic_webhook_url"` } func basicAuth(handler http.Handler, username, password, realm string) http.Handler { @@ -133,6 +135,15 @@ func main() { if len(cfg.EmailAddresses) > 0 && cfg.SMTPServer == "" { log.Fatal("Email address(es) specified but no smtp_server configured. Wrong configuration, aborting...") } + var genericWebhookClient *http.Client + if cfg.GenericWebhookURL == "" { + fmt.Println("No generic_webhook_url configured.") + } else { + fmt.Println("Will forward metadata of all requests to ", cfg.GenericWebhookURL) + genericWebhookClient = &http.Client{ + Timeout: time.Second * 300, + } + } apiPrefix := cfg.APIPrefix if apiPrefix == "" { @@ -148,7 +159,7 @@ func main() { log.Printf("Using %s/listing as public URI", apiPrefix) rand.Seed(time.Now().UnixNano()) - http.Handle("/api/submit", &submitServer{ghClient, glClient, apiPrefix, slack, cfg}) + http.Handle("/api/submit", &submitServer{ghClient, glClient, apiPrefix, slack, genericWebhookClient, cfg}) // Make sure bugs directory exists _ = os.Mkdir("bugs", os.ModePerm) diff --git a/submit.go b/submit.go index 0554a4f..def201c 100644 --- a/submit.go +++ b/submit.go @@ -57,6 +57,7 @@ type submitServer struct { slack *slackClient + genericWebhookClient *http.Client cfg *config } @@ -76,16 +77,23 @@ type jsonLogEntry struct { Lines string `json:"lines"` } + +type genericWebhookPayload struct { + parsedPayload + ReportURL string `json:"report_url"` + ListingURL string `json:"listing_url"` +} + // the payload after parsing type parsedPayload struct { - UserText string - AppName string - Data map[string]string - Labels []string - Logs []string - LogErrors []string - Files []string - FileErrors []string + UserText string `json:"user_text"` + AppName string `json:"app"` + Data map[string]string `json:"data"` + Labels []string `json:"labels"` + Logs []string `json:"logs"` + LogErrors []string `json:"logErrors"` + Files []string `json:"files"` + FileErrors []string `json:"fileErrors"` } func (p parsedPayload) WriteTo(out io.Writer) { @@ -492,9 +500,59 @@ func (s *submitServer) saveReport(ctx context.Context, p parsedPayload, reportDi return nil, err } + if err := s.submitGenericWebhook(p, listingURL, resp.ReportURL); err != nil { + return nil, err + } + return &resp, nil } +// submitGenericWebhook submits a basic JSON body to an endpoint configured in the config +// +// The request does not include the log body, only the metadata in the parsedPayload, +// with the required listingURL to obtain the logs over http if required. +// +// If a github or gitlab issue was previously made, the reportURL will also be passed. +// +// Uses a goroutine to handle the http request asynchronously as by this point all critical +// information has been stored. + +func (s *submitServer) submitGenericWebhook(p parsedPayload, listingURL string, reportURL string) error { + if s.genericWebhookClient == nil { + return nil + } + url := s.cfg.GenericWebhookURL + log.Println("Submitting json to URL", url) + // Enrich the parsedPayload with a reportURL and listingURL, to convert a single struct + // to JSON easily + genericHookPayload := genericWebhookPayload{ + parsedPayload: p, + ReportURL: reportURL, + ListingURL: listingURL, + } + + payloadBuffer := new(bytes.Buffer) + json.NewEncoder(payloadBuffer).Encode(genericHookPayload) + req, err := http.NewRequest("POST", url, payloadBuffer) + req.Header.Set("Content-Type", "application/json") + if err != nil { + return err + } + go s.sendGenericWebhook(req) + return nil +} + +func (s *submitServer) sendGenericWebhook(req *http.Request) { + resp, err := s.genericWebhookClient.Do(req) + if err != nil { + log.Println("Unable to submit notification", err) + } else { + defer resp.Body.Close() + log.Println("Got response", resp.Status) + } +} + + func (s *submitServer) submitGithubIssue(ctx context.Context, p parsedPayload, listingURL string, resp *submitResponse) error { if s.ghClient == nil { return nil