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,
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).

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"`
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 {

View file

@ -50,3 +50,9 @@ email_from: Rageshake <rageshake@matrix.org>
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

View file

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