Compare commits
40 commits
Author | SHA1 | Date | |
---|---|---|---|
bd93487c2d | |||
eaa54e466c | |||
a536481248 | |||
befb3780cc | |||
69e36736ca | |||
5ab35a58a9 | |||
7b72715bda | |||
6975c3faa1 | |||
|
85b721070a | ||
|
02237888c9 | ||
|
2ac7af0c18 | ||
|
79454de54f | ||
|
62e52e880d | ||
|
8cdcc4bba8 | ||
|
7cb7309097 | ||
|
1d008a0aad | ||
|
eb19aca921 | ||
|
b01b5a5863 | ||
|
1137cb2c04 | ||
|
ba8725a3aa | ||
|
77e66be90f | ||
|
714cc44807 | ||
|
7b2d70a3c9 | ||
|
a2caf1c546 | ||
|
f318399536 | ||
|
2a4434281c | ||
|
78060556a2 | ||
|
b7ffb434e9 | ||
|
cc2374e431 | ||
|
a7724b7bc8 | ||
|
53e8947cb9 | ||
|
18c8a83173 | ||
|
589e9254e7 | ||
|
a8c57f2eb9 | ||
|
095b55e640 | ||
|
4e3eeec92c | ||
|
8e001408d8 | ||
|
d8a5acd2e2 | ||
|
adc43f50ec | ||
|
81865b0193 |
11 changed files with 362 additions and 52 deletions
15
.woodpecker.yml
Normal file
15
.woodpecker.yml
Normal file
|
@ -0,0 +1,15 @@
|
|||
pipeline:
|
||||
build-test:
|
||||
image: golang
|
||||
commands:
|
||||
- go build
|
||||
- go test
|
||||
|
||||
publish:
|
||||
image: plugins/docker
|
||||
settings:
|
||||
username: realaravinth
|
||||
password:
|
||||
from_secret: DOCKER_TOKEN
|
||||
repo: realaravinth/rageshake
|
||||
tags: latest
|
36
CHANGES.md
36
CHANGES.md
|
@ -1,3 +1,39 @@
|
|||
1.7 (2022-04-14)
|
||||
================
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- Pass the prefix as a unique ID for the rageshake to the generic webhook mechanism. ([\#54](https://github.com/matrix-org/rageshake/issues/54))
|
||||
|
||||
|
||||
1.6 (2022-02-22)
|
||||
================
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- Provide ?format=tar.gz option on directory listings to download tarball. ([\#53](https://github.com/matrix-org/rageshake/issues/53))
|
||||
|
||||
|
||||
1.5 (2022-02-08)
|
||||
================
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- Allow upload of Files with a .json postfix. ([\#52](https://github.com/matrix-org/rageshake/issues/52))
|
||||
|
||||
|
||||
1.4 (2022-02-01)
|
||||
================
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- Allow forwarding of a request to a webhook endpoint. ([\#50](https://github.com/matrix-org/rageshake/issues/50))
|
||||
|
||||
|
||||
1.3 (2022-01-25)
|
||||
================
|
||||
|
||||
|
|
24
Dockerfile
24
Dockerfile
|
@ -1,27 +1,23 @@
|
|||
ARG GO_VERSION=1.17
|
||||
ARG DEBIAN_VERSION=11
|
||||
ARG DEBIAN_VERSION_NAME=bullseye
|
||||
|
||||
## Build stage ##
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:${GO_VERSION}-${DEBIAN_VERSION_NAME} AS builder
|
||||
|
||||
FROM golang as builder
|
||||
WORKDIR /build
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o rageshake
|
||||
RUN go build -o rageshake
|
||||
|
||||
## Runtime stage, debug variant ##
|
||||
FROM --platform=${TARGETPLATFORM} gcr.io/distroless/static-debian${DEBIAN_VERSION}:debug-nonroot AS debug
|
||||
COPY --from=builder /build/rageshake /rageshake
|
||||
FROM debian:bullseye as debug
|
||||
COPY --from=builder /build/rageshake /rageshake/
|
||||
WORKDIR /
|
||||
EXPOSE 9110
|
||||
ENTRYPOINT ["/rageshake"]
|
||||
ENTRYPOINT ["/rageshake/rageshake"]
|
||||
|
||||
## Runtime stage ##
|
||||
FROM --platform=${TARGETPLATFORM} gcr.io/distroless/static-debian${DEBIAN_VERSION}:nonroot
|
||||
COPY --from=builder /build/rageshake /rageshake
|
||||
FROM debian:bullseye as rageshake
|
||||
LABEL org.opencontainers.image.source https://git.batsense.net/mystiq/rageshake
|
||||
RUN apt-get update && apt-get install -y ca-certificates
|
||||
WORKDIR /
|
||||
COPY --from=builder /build/rageshake /rageshake/
|
||||
EXPOSE 9110
|
||||
ENTRYPOINT ["/rageshake"]
|
||||
ENTRYPOINT ["/rageshake/rageshake"]
|
||||
|
|
13
README.md
13
README.md
|
@ -1,3 +1,4 @@
|
|||
WOODPECKER: [![status-badge](https://ci.batsense.net/api/badges/mystiq/rageshake/status.svg)](https://ci.batsense.net/mystiq/rageshake)
|
||||
# rageshake [![Build status](https://badge.buildkite.com/76a4362a20b12dcd589f9308a905ffcc537278b9c363c0b5f1.svg?branch=master)](https://buildkite.com/matrix-dot-org/rageshake)
|
||||
|
||||
Web service which collects and serves bug reports.
|
||||
|
@ -28,6 +29,8 @@ Serves submitted bug reports. Protected by basic HTTP auth using the
|
|||
username/password provided in the environment. A browsable list, collated by
|
||||
report submission date and time.
|
||||
|
||||
A whole directory can be downloaded as a tarball by appending the parameter `?format=tar.gz` to the end of the URL path
|
||||
|
||||
### POST `/api/submit`
|
||||
|
||||
Submission endpoint: this is where applications should send their reports.
|
||||
|
@ -105,3 +108,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).
|
||||
|
||||
|
|
40
docs/generic_webhook.md
Normal file
40
docs/generic_webhook.md
Normal 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.
|
119
logserver.go
119
logserver.go
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"log"
|
||||
|
@ -79,10 +80,9 @@ func serveFile(w http.ResponseWriter, r *http.Request, path string) {
|
|||
// for anti-XSS belt-and-braces, set a very restrictive CSP
|
||||
w.Header().Set("Content-Security-Policy", "default-src: none")
|
||||
|
||||
// if it's a directory, serve a listing
|
||||
// if it's a directory, serve a listing or a tarball
|
||||
if d.IsDir() {
|
||||
log.Println("Serving", path)
|
||||
http.ServeFile(w, r, path)
|
||||
serveDirectory(w, r, path)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -119,9 +119,122 @@ func extensionToMimeType(path string) string {
|
|||
return "image/jpeg"
|
||||
}
|
||||
|
||||
if strings.HasSuffix(path, ".json") {
|
||||
return "application/json"
|
||||
}
|
||||
return "application/octet-stream"
|
||||
}
|
||||
|
||||
// Chooses to serve either a directory listing or tarball based on the 'format' parameter.
|
||||
func serveDirectory(w http.ResponseWriter, r *http.Request, path string) {
|
||||
format, _ := r.URL.Query()["format"]
|
||||
if len(format) == 1 && format[0] == "tar.gz" {
|
||||
log.Println("Serving tarball of", path)
|
||||
err := serveTarball(w, r, path)
|
||||
if err != nil {
|
||||
msg, code := toHTTPError(err)
|
||||
http.Error(w, msg, code)
|
||||
log.Println("Error", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
log.Println("Serving directory listing of", path)
|
||||
http.ServeFile(w, r, path)
|
||||
}
|
||||
|
||||
// Streams a dynamically created tar.gz file with the contents of the given directory
|
||||
// Will serve a partial, corrupted response if there is a error partway through the
|
||||
// operation as we stream the response.
|
||||
//
|
||||
// The resultant tarball will contain a single directory containing all the files
|
||||
// so it can unpack cleanly without overwriting other files.
|
||||
//
|
||||
// Errors are only returned if generated before the tarball has started being
|
||||
// written to the ResponseWriter
|
||||
func serveTarball(w http.ResponseWriter, r *http.Request, dir string) error {
|
||||
directory, err := os.Open(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Creates a "disposition filename"
|
||||
// Take a URL.path like `/2022-01-10/184843-BZZXEGYH/`
|
||||
// and removes leading and trailing `/` and replaces internal `/` with `_`
|
||||
// to form a suitable filename for use in the content-disposition header
|
||||
// dfilename would turn into `2022-01-10_184843-BZZXEGYH`
|
||||
dfilename := strings.Trim(r.URL.Path, "/")
|
||||
dfilename = strings.Replace(dfilename, "/", "_", -1)
|
||||
|
||||
// There is no application/tgz or similar; return a gzip file as best option.
|
||||
// This tends to trigger archive type tools, which will then use the filename to
|
||||
// identify the contents correctly.
|
||||
w.Header().Set("Content-Type", "application/gzip")
|
||||
w.Header().Set("Content-Disposition", "attachment; filename="+dfilename+".tar.gz")
|
||||
|
||||
files, err := directory.Readdir(-1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gzip := gzip.NewWriter(w)
|
||||
defer gzip.Close()
|
||||
targz := tar.NewWriter(gzip)
|
||||
defer targz.Close()
|
||||
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
// We avoid including nested directories
|
||||
// This will result in requests for directories with only directories in
|
||||
// to return an empty tarball instead of recursively including directories.
|
||||
// This helps the server remain performant as a download of 'everything' would be slow
|
||||
continue
|
||||
}
|
||||
path := dir + "/" + file.Name()
|
||||
// We use the existing disposition filename to create a base directory structure for the files
|
||||
// so when they are unpacked, they are grouped in a unique folder on disk
|
||||
err := addToArchive(targz, dfilename, path)
|
||||
if err != nil {
|
||||
// From this point we assume that data may have been sent to the client already.
|
||||
// We therefore do not http.Error() after this point, instead closing the stream and
|
||||
// allowing the client to deal with a partial file as if there was a network issue.
|
||||
log.Println("Error streaming tarball", err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add a single file into the archive.
|
||||
func addToArchive(targz *tar.Writer, dfilename string, filename string) error {
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
info, err := file.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
header, err := tar.FileInfoHeader(info, info.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
header.Name = dfilename + "/" + info.Name()
|
||||
|
||||
err = targz.WriteHeader(header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(targz, file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func serveGzippedFile(w http.ResponseWriter, r *http.Request, path string, size int64) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
|
||||
|
|
17
main.go
17
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 {
|
||||
|
|
BIN
rageshake
Executable file
BIN
rageshake
Executable file
Binary file not shown.
|
@ -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
|
||||
|
|
136
submit.go
136
submit.go
|
@ -57,7 +57,8 @@ type submitServer struct {
|
|||
|
||||
slack *slackClient
|
||||
|
||||
cfg *config
|
||||
genericWebhookClient *http.Client
|
||||
cfg *config
|
||||
}
|
||||
|
||||
// the type of payload which can be uploaded as JSON to the submit endpoint
|
||||
|
@ -76,19 +77,38 @@ type jsonLogEntry struct {
|
|||
Lines string `json:"lines"`
|
||||
}
|
||||
|
||||
// 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
|
||||
// Stores additional information created during processing of a payload
|
||||
type genericWebhookPayload struct {
|
||||
payload
|
||||
// If a github/gitlab report is generated, this is set.
|
||||
ReportURL string `json:"report_url"`
|
||||
// Complete link to the listing URL that contains all uploaded logs
|
||||
ListingURL string `json:"listing_url"`
|
||||
}
|
||||
|
||||
func (p parsedPayload) WriteTo(out io.Writer) {
|
||||
// Stores information about a request made to this server
|
||||
type payload struct {
|
||||
// A unique ID for this payload, generated within this server
|
||||
ID string `json:"id"`
|
||||
// A multi-line string containing the user description of the fault.
|
||||
UserText string `json:"user_text"`
|
||||
// A short slug to identify the app making the report
|
||||
AppName string `json:"app"`
|
||||
// Arbitrary data to annotate the report
|
||||
Data map[string]string `json:"data"`
|
||||
// Short labels to group reports
|
||||
Labels []string `json:"labels"`
|
||||
// A list of names of logs recognised by the server
|
||||
Logs []string `json:"logs"`
|
||||
// Set if there are log parsing errors
|
||||
LogErrors []string `json:"logErrors"`
|
||||
// A list of other files (not logs) uploaded as part of the rageshake
|
||||
Files []string `json:"files"`
|
||||
// Set if there are file parsing errors
|
||||
FileErrors []string `json:"fileErrors"`
|
||||
}
|
||||
|
||||
func (p payload) WriteTo(out io.Writer) {
|
||||
fmt.Fprintf(
|
||||
out,
|
||||
"%s\n\nNumber of logs: %d\nApplication: %s\n",
|
||||
|
@ -171,6 +191,11 @@ func (s *submitServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
// We use this prefix (eg, 2022-05-01/125223-abcde) as a unique identifier for this rageshake.
|
||||
// This is going to be used to uniquely identify rageshakes, even if they are not submitted to
|
||||
// an issue tracker for instance with automatic rageshakes that can be plentiful
|
||||
p.ID = prefix
|
||||
|
||||
resp, err := s.saveReport(req.Context(), *p, reportDir, listingURL)
|
||||
if err != nil {
|
||||
log.Println("Error handling report submission:", err)
|
||||
|
@ -185,7 +210,7 @@ func (s *submitServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||
|
||||
// parseRequest attempts to parse a received request as a bug report. If
|
||||
// the request cannot be parsed, it responds with an error and returns nil.
|
||||
func parseRequest(w http.ResponseWriter, req *http.Request, reportDir string) *parsedPayload {
|
||||
func parseRequest(w http.ResponseWriter, req *http.Request, reportDir string) *payload {
|
||||
length, err := strconv.Atoi(req.Header.Get("Content-Length"))
|
||||
if err != nil {
|
||||
log.Println("Couldn't parse content-length", err)
|
||||
|
@ -221,13 +246,13 @@ func parseRequest(w http.ResponseWriter, req *http.Request, reportDir string) *p
|
|||
return p
|
||||
}
|
||||
|
||||
func parseJSONRequest(w http.ResponseWriter, req *http.Request, reportDir string) (*parsedPayload, error) {
|
||||
func parseJSONRequest(w http.ResponseWriter, req *http.Request, reportDir string) (*payload, error) {
|
||||
var p jsonPayload
|
||||
if err := json.NewDecoder(req.Body).Decode(&p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parsed := parsedPayload{
|
||||
parsed := payload{
|
||||
UserText: strings.TrimSpace(p.Text),
|
||||
Data: make(map[string]string),
|
||||
Labels: p.Labels,
|
||||
|
@ -282,13 +307,13 @@ func parseJSONRequest(w http.ResponseWriter, req *http.Request, reportDir string
|
|||
return &parsed, nil
|
||||
}
|
||||
|
||||
func parseMultipartRequest(w http.ResponseWriter, req *http.Request, reportDir string) (*parsedPayload, error) {
|
||||
func parseMultipartRequest(w http.ResponseWriter, req *http.Request, reportDir string) (*payload, error) {
|
||||
rdr, err := req.MultipartReader()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p := parsedPayload{
|
||||
p := payload{
|
||||
Data: make(map[string]string),
|
||||
}
|
||||
|
||||
|
@ -307,7 +332,7 @@ func parseMultipartRequest(w http.ResponseWriter, req *http.Request, reportDir s
|
|||
return &p, nil
|
||||
}
|
||||
|
||||
func parseFormPart(part *multipart.Part, p *parsedPayload, reportDir string) error {
|
||||
func parseFormPart(part *multipart.Part, p *payload, reportDir string) error {
|
||||
defer part.Close()
|
||||
field := part.FormName()
|
||||
partName := part.FileName()
|
||||
|
@ -368,7 +393,7 @@ func parseFormPart(part *multipart.Part, p *parsedPayload, reportDir string) err
|
|||
|
||||
// formPartToPayload updates the relevant part of *p from a name/value pair
|
||||
// read from the form data.
|
||||
func formPartToPayload(field, data string, p *parsedPayload) {
|
||||
func formPartToPayload(field, data string, p *payload) {
|
||||
if field == "text" {
|
||||
p.UserText = data
|
||||
} else if field == "app" {
|
||||
|
@ -394,7 +419,7 @@ func formPartToPayload(field, data string, p *parsedPayload) {
|
|||
// * no silly characters (/, ctrl chars, etc)
|
||||
//
|
||||
// * nothing starting with '.'
|
||||
var filenameRegexp = regexp.MustCompile(`^[a-zA-Z0-9_-]+\.(jpg|png|txt)$`)
|
||||
var filenameRegexp = regexp.MustCompile(`^[a-zA-Z0-9_-]+\.(jpg|png|txt|json)$`)
|
||||
|
||||
// saveFormPart saves a file upload to the report directory.
|
||||
//
|
||||
|
@ -468,7 +493,7 @@ func saveLogPart(logNum int, filename string, reader io.Reader, reportDir string
|
|||
return leafName, nil
|
||||
}
|
||||
|
||||
func (s *submitServer) saveReport(ctx context.Context, p parsedPayload, reportDir, listingURL string) (*submitResponse, error) {
|
||||
func (s *submitServer) saveReport(ctx context.Context, p payload, reportDir, listingURL string) (*submitResponse, error) {
|
||||
var summaryBuf bytes.Buffer
|
||||
resp := submitResponse{}
|
||||
p.WriteTo(&summaryBuf)
|
||||
|
@ -492,10 +517,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
|
||||
}
|
||||
|
||||
func (s *submitServer) submitGithubIssue(ctx context.Context, p parsedPayload, listingURL string, resp *submitResponse) error {
|
||||
// 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 payload,
|
||||
// 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 payload, listingURL string, reportURL string) error {
|
||||
if s.genericWebhookClient == nil {
|
||||
return nil
|
||||
}
|
||||
genericHookPayload := genericWebhookPayload{
|
||||
payload: p,
|
||||
ReportURL: reportURL,
|
||||
ListingURL: listingURL,
|
||||
}
|
||||
for _, url := range s.cfg.GenericWebhookURLs {
|
||||
// Enrich the payload 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 payload, listingURL string, resp *submitResponse) error {
|
||||
if s.ghClient == nil {
|
||||
return nil
|
||||
}
|
||||
|
@ -526,7 +602,7 @@ func (s *submitServer) submitGithubIssue(ctx context.Context, p parsedPayload, l
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *submitServer) submitGitlabIssue(p parsedPayload, listingURL string, resp *submitResponse) error {
|
||||
func (s *submitServer) submitGitlabIssue(p payload, listingURL string, resp *submitResponse) error {
|
||||
if s.glClient == nil {
|
||||
return nil
|
||||
}
|
||||
|
@ -549,7 +625,7 @@ func (s *submitServer) submitGitlabIssue(p parsedPayload, listingURL string, res
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *submitServer) submitSlackNotification(p parsedPayload, listingURL string) error {
|
||||
func (s *submitServer) submitSlackNotification(p payload, listingURL string) error {
|
||||
if s.slack == nil {
|
||||
return nil
|
||||
}
|
||||
|
@ -567,7 +643,7 @@ func (s *submitServer) submitSlackNotification(p parsedPayload, listingURL strin
|
|||
return nil
|
||||
}
|
||||
|
||||
func buildReportTitle(p parsedPayload) string {
|
||||
func buildReportTitle(p payload) string {
|
||||
// set the title to the first (non-empty) line of the user's report, if any
|
||||
trimmedUserText := strings.TrimSpace(p.UserText)
|
||||
if trimmedUserText == "" {
|
||||
|
@ -581,7 +657,7 @@ func buildReportTitle(p parsedPayload) string {
|
|||
return trimmedUserText
|
||||
}
|
||||
|
||||
func buildReportBody(p parsedPayload, newline, quoteChar string) *bytes.Buffer {
|
||||
func buildReportBody(p payload, newline, quoteChar string) *bytes.Buffer {
|
||||
var bodyBuf bytes.Buffer
|
||||
fmt.Fprintf(&bodyBuf, "User message:\n\n%s\n\n", p.UserText)
|
||||
var dataKeys []string
|
||||
|
@ -597,7 +673,7 @@ func buildReportBody(p parsedPayload, newline, quoteChar string) *bytes.Buffer {
|
|||
return &bodyBuf
|
||||
}
|
||||
|
||||
func buildGenericIssueRequest(p parsedPayload, listingURL string) (title, body string) {
|
||||
func buildGenericIssueRequest(p payload, listingURL string) (title, body string) {
|
||||
bodyBuf := buildReportBody(p, " \n", "`")
|
||||
|
||||
// Add log links to the body
|
||||
|
@ -619,7 +695,7 @@ func buildGenericIssueRequest(p parsedPayload, listingURL string) (title, body s
|
|||
return
|
||||
}
|
||||
|
||||
func buildGithubIssueRequest(p parsedPayload, listingURL string) github.IssueRequest {
|
||||
func buildGithubIssueRequest(p payload, listingURL string) github.IssueRequest {
|
||||
title, body := buildGenericIssueRequest(p, listingURL)
|
||||
|
||||
labels := p.Labels
|
||||
|
@ -634,7 +710,7 @@ func buildGithubIssueRequest(p parsedPayload, listingURL string) github.IssueReq
|
|||
}
|
||||
}
|
||||
|
||||
func buildGitlabIssueRequest(p parsedPayload, listingURL string, labels []string, confidential bool) *gitlab.CreateIssueOptions {
|
||||
func buildGitlabIssueRequest(p payload, listingURL string, labels []string, confidential bool) *gitlab.CreateIssueOptions {
|
||||
title, body := buildGenericIssueRequest(p, listingURL)
|
||||
|
||||
if p.Labels != nil {
|
||||
|
@ -649,7 +725,7 @@ func buildGitlabIssueRequest(p parsedPayload, listingURL string, labels []string
|
|||
}
|
||||
}
|
||||
|
||||
func (s *submitServer) sendEmail(p parsedPayload, reportDir string) error {
|
||||
func (s *submitServer) sendEmail(p payload, reportDir string) error {
|
||||
if len(s.cfg.EmailAddresses) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ import (
|
|||
//
|
||||
// if tempDir is empty, a new temp dir is created, and deleted when the test
|
||||
// completes.
|
||||
func testParsePayload(t *testing.T, body, contentType string, tempDir string) (*parsedPayload, *http.Response) {
|
||||
func testParsePayload(t *testing.T, body, contentType string, tempDir string) (*payload, *http.Response) {
|
||||
req, err := http.NewRequest("POST", "/api/submit", strings.NewReader(body))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -232,7 +232,7 @@ Content-Type: application/octet-stream
|
|||
return
|
||||
}
|
||||
|
||||
func checkParsedMultipartUpload(t *testing.T, p *parsedPayload) {
|
||||
func checkParsedMultipartUpload(t *testing.T, p *payload) {
|
||||
wanted := "test words."
|
||||
if p.UserText != wanted {
|
||||
t.Errorf("User text: got %s, want %s", p.UserText, wanted)
|
||||
|
@ -478,7 +478,7 @@ user_id: id
|
|||
}
|
||||
var buf bytes.Buffer
|
||||
for _, v := range sample {
|
||||
p := parsedPayload{Data: v.data}
|
||||
p := payload{Data: v.data}
|
||||
buf.Reset()
|
||||
p.WriteTo(&buf)
|
||||
got := strings.TrimSpace(buf.String())
|
||||
|
@ -488,7 +488,7 @@ user_id: id
|
|||
}
|
||||
|
||||
for k, v := range sample {
|
||||
p := parsedPayload{Data: v.data}
|
||||
p := payload{Data: v.data}
|
||||
res := buildGithubIssueRequest(p, "")
|
||||
got := *res.Body
|
||||
if k == 0 {
|
||||
|
|
Reference in a new issue