Merge pull request #50 from matrix-org/michaelk/proxy_rageshake_requests

A generic webhook for notifications about reports.
This commit is contained in:
Michael Kaye 2022-02-01 14:02:14 +00:00 committed by GitHub
commit a8c57f2eb9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 141 additions and 9 deletions

View file

@ -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, 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 through a Slack webhook or by email, cf sample config file for how to
configure them. 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).

1
changelog.d/50.feature Normal file
View file

@ -0,0 +1 @@
Allow forwarding of a request to a webhook endpoint.

40
docs/generic_webhook.md Normal file
View file

@ -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.

17
main.go
View file

@ -71,6 +71,8 @@ type config struct {
SMTPUsername string `yaml:"smtp_username"` SMTPUsername string `yaml:"smtp_username"`
SMTPPassword string `yaml:"smtp_password"` SMTPPassword string `yaml:"smtp_password"`
GenericWebhookURLs []string `yaml:"generic_webhook_urls"`
} }
func basicAuth(handler http.Handler, username, password, realm string) http.Handler { 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...") log.Fatal("Email address(es) specified but no smtp_server configured. Wrong configuration, aborting...")
} }
genericWebhookClient := configureGenericWebhookClient(cfg)
apiPrefix := cfg.APIPrefix apiPrefix := cfg.APIPrefix
if apiPrefix == "" { if apiPrefix == "" {
_, port, err := net.SplitHostPort(*bindAddr) _, port, err := net.SplitHostPort(*bindAddr)
@ -148,7 +152,7 @@ func main() {
log.Printf("Using %s/listing as public URI", apiPrefix) log.Printf("Using %s/listing as public URI", apiPrefix)
rand.Seed(time.Now().UnixNano()) 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 // Make sure bugs directory exists
_ = os.Mkdir("bugs", os.ModePerm) _ = os.Mkdir("bugs", os.ModePerm)
@ -176,6 +180,17 @@ func main() {
log.Fatal(http.ListenAndServe(*bindAddr, nil)) 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) { func loadConfig(configPath string) (*config, error) {
contents, err := ioutil.ReadFile(configPath) contents, err := ioutil.ReadFile(configPath)
if err != nil { if err != nil {

View file

@ -50,3 +50,9 @@ email_from: Rageshake <rageshake@matrix.org>
smtp_server: localhost:25 smtp_server: localhost:25
smtp_username: myemailuser smtp_username: myemailuser
smtp_password: myemailpass 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

View file

@ -57,6 +57,7 @@ type submitServer struct {
slack *slackClient slack *slackClient
genericWebhookClient *http.Client
cfg *config cfg *config
} }
@ -76,16 +77,23 @@ type jsonLogEntry struct {
Lines string `json:"lines"` Lines string `json:"lines"`
} }
type genericWebhookPayload struct {
parsedPayload
ReportURL string `json:"report_url"`
ListingURL string `json:"listing_url"`
}
// the payload after parsing // the payload after parsing
type parsedPayload struct { type parsedPayload struct {
UserText string UserText string `json:"user_text"`
AppName string AppName string `json:"app"`
Data map[string]string Data map[string]string `json:"data"`
Labels []string Labels []string `json:"labels"`
Logs []string Logs []string `json:"logs"`
LogErrors []string LogErrors []string `json:"logErrors"`
Files []string Files []string `json:"files"`
FileErrors []string FileErrors []string `json:"fileErrors"`
} }
func (p parsedPayload) WriteTo(out io.Writer) { func (p parsedPayload) WriteTo(out io.Writer) {
@ -492,9 +500,61 @@ func (s *submitServer) saveReport(ctx context.Context, p parsedPayload, reportDi
return nil, err return nil, err
} }
if err := s.submitGenericWebhook(p, listingURL, resp.ReportURL); err != nil {
return nil, err
}
return &resp, nil 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 { func (s *submitServer) submitGithubIssue(ctx context.Context, p parsedPayload, listingURL string, resp *submitResponse) error {
if s.ghClient == nil { if s.ghClient == nil {
return nil return nil