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/changelog.d/50.feature b/changelog.d/50.feature new file mode 100644 index 0000000..b297c26 --- /dev/null +++ b/changelog.d/50.feature @@ -0,0 +1 @@ +Allow forwarding of a request to a webhook endpoint. diff --git a/docs/generic_webhook.md b/docs/generic_webhook.md new file mode 100644 index 0000000..8519a9f --- /dev/null +++ b/docs/generic_webhook.md @@ -0,0 +1,40 @@ +## Generic webhook request + +If the configuration option `generic_webhook_urls` is set, then an asynchronous request to +each endpoint listed will be sent in parallel, 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. +You may need to provide a HTTP basic auth user/pass if configured on your rageshake server. diff --git a/main.go b/main.go index dcaeaf8..52ff5db 100644 --- a/main.go +++ b/main.go @@ -71,6 +71,8 @@ type config struct { SMTPUsername string `yaml:"smtp_username"` SMTPPassword string `yaml:"smtp_password"` + + GenericWebhookURLs []string `yaml:"generic_webhook_urls"` } func basicAuth(handler http.Handler, username, password, realm string) http.Handler { @@ -134,6 +136,8 @@ func main() { log.Fatal("Email address(es) specified but no smtp_server configured. Wrong configuration, aborting...") } + genericWebhookClient := configureGenericWebhookClient(cfg) + apiPrefix := cfg.APIPrefix if apiPrefix == "" { _, port, err := net.SplitHostPort(*bindAddr) @@ -148,7 +152,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) @@ -176,6 +180,17 @@ func main() { log.Fatal(http.ListenAndServe(*bindAddr, nil)) } +func configureGenericWebhookClient(cfg *config) (*http.Client) { + if len(cfg.GenericWebhookURLs) == 0 { + fmt.Println("No generic_webhook_urls configured.") + return nil + } + fmt.Println("Will forward metadata of all requests to ", cfg.GenericWebhookURLs) + return &http.Client{ + Timeout: time.Second * 300, + } +} + func loadConfig(configPath string) (*config, error) { contents, err := ioutil.ReadFile(configPath) if err != nil { diff --git a/rageshake.sample.yaml b/rageshake.sample.yaml index efcdb01..dcc1493 100644 --- a/rageshake.sample.yaml +++ b/rageshake.sample.yaml @@ -50,3 +50,9 @@ email_from: Rageshake smtp_server: localhost:25 smtp_username: myemailuser smtp_password: myemailpass + + +# a list of webhook URLs, (see docs/generic_webhook.md) +generic_webhook_urls: + - https://server.example.com/your-server/api + - http://another-server.com/api diff --git a/submit.go b/submit.go index 0554a4f..e151225 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,61 @@ 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 + } + genericHookPayload := genericWebhookPayload{ + parsedPayload: p, + ReportURL: reportURL, + ListingURL: listingURL, + } + for _, url := range s.cfg.GenericWebhookURLs { + // Enrich the parsedPayload with a reportURL and listingURL, to convert a single struct + // to JSON easily + + 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 { + log.Println("Unable to submit to URL ", url, " ", err) + return err + } + log.Println("Making generic webhook request to URL ", url) + 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