Skip to content

Commit d883369

Browse files
committed
Add config setting to limit number of log lines in a rageshake
1 parent df95e3b commit d883369

5 files changed

Lines changed: 137 additions & 28 deletions

File tree

changelog.d/107.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add new config setting `max_log_lines` which allows the admin to impose a limit on the number of log lines in a rageshake.

main.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ type config struct {
7171
// List of rejection conditions
7272
RejectionConditions []RejectionCondition `yaml:"rejection_conditions"`
7373

74+
// The maximum number of log lines that may be included in a submission. Submissions above this size will be rejected.
75+
// `nil` (i.e. unset in the YAML) means no limit.
76+
MaxLogLines *int `yaml:"max_log_lines"`
77+
7478
// A GitHub personal access token, to create a GitHub issue for each report.
7579
GithubToken string `yaml:"github_token"`
7680

rageshake.sample.yaml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
# username/password pair which will be required to access the bug report
22
# listings at `/api/listing`, via HTTP basic auth. If omitted, there will be
33
# *no* authentication on this access!
4+
listings_auth_user: alice
5+
listings_auth_pass: secret
46

57
# the external URL at which /api is accessible; it is used to add a link to the
68
# report to the GitHub issue. If unspecified, based on the listen address.
7-
# api_prefix: https://riot.im/bugreports
9+
api_prefix: https://riot.im/bugreports
810

911
# List of approved AppNames we accept. Names not in the list or missing an application name will be rejected.
1012
# An empty or missing list will retain legacy behaviour and permit reports from any application name.
@@ -29,6 +31,10 @@ rejection_conditions:
2931
reason: "it matches a recovery key and recovery keys are private"
3032
errorcode: "REJECTED_UNEXPECTED_RECOVERY_KEY"
3133

34+
# The maximum number of log lines that may be included in a submission. Submissions above this size will be rejected.
35+
# If omitted, there is no limit on the number of log lines.
36+
max_log_lines: 1000000
37+
3238
# a GitHub personal access token (https://github.com/settings/tokens), which
3339
# will be used to create a GitHub issue for each report. It requires
3440
# `public_repo` scope. If omitted, no issues will be created.

submit.go

Lines changed: 88 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"io"
2828
"io/ioutil"
2929
"log"
30+
"math"
3031
"math/rand"
3132
"mime"
3233
"mime/multipart"
@@ -219,7 +220,11 @@ func (s *submitServer) handleSubmission(w http.ResponseWriter, req *http.Request
219220
listingURL := s.apiPrefix + "/listing/" + prefix
220221
log.Println("Handling report submission; listing URI will be", listingURL)
221222

222-
p := parseRequest(w, req, reportDir)
223+
maxLogLines := math.MaxInt
224+
if s.cfg.MaxLogLines != nil {
225+
maxLogLines = *s.cfg.MaxLogLines
226+
}
227+
p := parseRequest(w, req, reportDir, maxLogLines)
223228
if p == nil {
224229
// parseRequest already wrote an error, but now let's delete the
225230
// useless report dir
@@ -279,7 +284,7 @@ func writeError(w http.ResponseWriter, status int, response submitErrorResponse)
279284

280285
// parseRequest attempts to parse a received request as a bug report. If
281286
// the request cannot be parsed, it responds with an error and returns nil.
282-
func parseRequest(w http.ResponseWriter, req *http.Request, reportDir string) *payload {
287+
func parseRequest(w http.ResponseWriter, req *http.Request, reportDir string, maxloglines int) *payload {
283288
length, err := strconv.Atoi(req.Header.Get("Content-Length"))
284289
if err != nil {
285290
log.Println("Couldn't parse content-length", err)
@@ -296,7 +301,7 @@ func parseRequest(w http.ResponseWriter, req *http.Request, reportDir string) *p
296301
if contentType != "" {
297302
d, _, _ := mime.ParseMediaType(contentType)
298303
if d == "multipart/form-data" {
299-
p, err1 := parseMultipartRequest(w, req, reportDir)
304+
p, err1 := parseMultipartRequest(w, req, reportDir, maxloglines)
300305
if err1 != nil {
301306
log.Println("Error parsing multipart data:", err1)
302307
writeError(w, 400, submitErrorResponse{Error: "Bad multipart data", ErrorCode: ErrCodeBadContent})
@@ -306,7 +311,7 @@ func parseRequest(w http.ResponseWriter, req *http.Request, reportDir string) *p
306311
}
307312
}
308313

309-
p, err := parseJSONRequest(w, req, reportDir)
314+
p, err := parseJSONRequest(w, req, reportDir, maxloglines)
310315
if err != nil {
311316
log.Println("Error parsing JSON body", err)
312317
writeError(w, 400, submitErrorResponse{Error: fmt.Sprintf("Could not decode payload: %s", err.Error()), ErrorCode: ErrCodeBadContent})
@@ -323,7 +328,7 @@ func parseUserAgent(userAgent string) string {
323328
return fmt.Sprintf(`%s on %s running on %s device`, client.UserAgent.ToString(), client.Os.ToString(), client.Device.ToString())
324329
}
325330

326-
func parseJSONRequest(_ http.ResponseWriter, req *http.Request, reportDir string) (*payload, error) {
331+
func parseJSONRequest(_ http.ResponseWriter, req *http.Request, reportDir string, maxloglines int) (*payload, error) {
327332
var p jsonPayload
328333
if err := json.NewDecoder(req.Body).Decode(&p); err != nil {
329334
return nil, err
@@ -339,14 +344,20 @@ func parseJSONRequest(_ http.ResponseWriter, req *http.Request, reportDir string
339344
parsed.Data = p.Data
340345
}
341346

347+
loglinesWritten := 0
342348
for i, logfile := range p.Logs {
343349
buf := bytes.NewBufferString(logfile.Lines)
344-
leafName, err := saveLogPart(i, logfile.ID, buf, reportDir)
350+
leafName, loglines, err := saveLogPart(i, logfile.ID, buf, reportDir, maxloglines-loglinesWritten)
345351
if err != nil {
346352
log.Printf("Error saving log %s: %v", leafName, err)
347353
parsed.LogErrors = append(parsed.LogErrors, fmt.Sprintf("Error saving log %s: %v", leafName, err))
348354
} else {
349355
parsed.Logs = append(parsed.Logs, leafName)
356+
loglinesWritten += loglines
357+
}
358+
359+
if loglinesWritten >= maxloglines {
360+
return nil, fmt.Errorf("too many log lines in rageshake")
350361
}
351362
}
352363

@@ -363,7 +374,7 @@ func parseJSONRequest(_ http.ResponseWriter, req *http.Request, reportDir string
363374
return &parsed, nil
364375
}
365376

366-
func parseMultipartRequest(_ http.ResponseWriter, req *http.Request, reportDir string) (*payload, error) {
377+
func parseMultipartRequest(_ http.ResponseWriter, req *http.Request, reportDir string, maxloglines int) (*payload, error) {
367378
rdr, err := req.MultipartReader()
368379
if err != nil {
369380
return nil, err
@@ -373,6 +384,7 @@ func parseMultipartRequest(_ http.ResponseWriter, req *http.Request, reportDir s
373384
Data: make(map[string]string),
374385
}
375386

387+
loglinesWritten := 0
376388
for true {
377389
part, err := rdr.NextPart()
378390
if err == io.EOF {
@@ -381,14 +393,25 @@ func parseMultipartRequest(_ http.ResponseWriter, req *http.Request, reportDir s
381393
return nil, err
382394
}
383395

384-
if err = parseFormPart(part, &p, reportDir); err != nil {
396+
loglines, err := parseFormPart(part, &p, reportDir, maxloglines-loglinesWritten)
397+
if err != nil {
385398
return nil, err
386399
}
400+
log.Printf("Part %s: %d lines\n", part.FormName(), loglines)
401+
402+
loglinesWritten += loglines
403+
if loglinesWritten >= maxloglines {
404+
return nil, fmt.Errorf("too many log lines in rageshake")
405+
}
387406
}
388407
return &p, nil
389408
}
390409

391-
func parseFormPart(part *multipart.Part, p *payload, reportDir string) error {
410+
// parseFormPart handles a single part of a multipart form. If the part is a log, the number of lines copied is limited
411+
// to `maxloglines`.
412+
//
413+
// It returns the number of log lines saved, or 0 if the part is not a log.
414+
func parseFormPart(part *multipart.Part, p *payload, reportDir string, maxloglines int) (int, error) {
392415
defer part.Close()
393416
field := part.FormName()
394417
partName := part.FileName()
@@ -407,7 +430,7 @@ func parseFormPart(part *multipart.Part, p *payload, reportDir string) error {
407430
log.Printf("Error unzipping %s: %v", partName, err)
408431

409432
p.LogErrors = append(p.LogErrors, fmt.Sprintf("Error unzipping %s: %v", partName, err))
410-
return nil
433+
return 0, nil
411434
}
412435
defer zrdr.Close()
413436
partReader = zrdr
@@ -424,27 +447,27 @@ func parseFormPart(part *multipart.Part, p *payload, reportDir string) error {
424447
} else {
425448
p.Files = append(p.Files, leafName)
426449
}
427-
return nil
450+
return 0, nil
428451
}
429452

430453
if field == "log" || field == "compressed-log" {
431-
leafName, err := saveLogPart(len(p.Logs), partName, partReader, reportDir)
454+
leafName, lines, err := saveLogPart(len(p.Logs), partName, partReader, reportDir, maxloglines)
432455
if err != nil {
433456
log.Printf("Error saving %s %s: %v", field, partName, err)
434457
p.LogErrors = append(p.LogErrors, fmt.Sprintf("Error saving %s: %v", partName, err))
435458
} else {
436459
p.Logs = append(p.Logs, leafName)
437460
}
438-
return nil
461+
return lines, nil
439462
}
440463

441464
b, err := ioutil.ReadAll(partReader)
442465
if err != nil {
443-
return err
466+
return 0, err
444467
}
445468
data := string(b)
446469
formPartToPayload(field, data, p)
447-
return nil
470+
return 0, nil
448471
}
449472

450473
// formPartToPayload updates the relevant part of *p from a name/value pair
@@ -509,10 +532,10 @@ func saveFormPart(leafName string, reader io.Reader, reportDir string) (string,
509532
// '.'
510533
var logRegexp = regexp.MustCompile(`^[a-zA-Z0-9_-][a-zA-Z0-9_.-]*\.(log|txt)(\.gz)?$`)
511534

512-
// saveLogPart saves a log upload to the report directory.
535+
// saveLogPart saves a log upload to the report directory, up to the given maximum number of lines.
513536
//
514-
// Returns the leafname of the saved file.
515-
func saveLogPart(logNum int, filename string, reader io.Reader, reportDir string) (string, error) {
537+
// Returns the leafname of the saved file and the number of lines saved.
538+
func saveLogPart(logNum int, filename string, reader io.Reader, reportDir string, maxlines int) (string, int, error) {
516539
// pick a name to save the log file with.
517540
//
518541
// some clients use sensible names (foo.N.log), which we preserve. For
@@ -536,19 +559,62 @@ func saveLogPart(logNum int, filename string, reader io.Reader, reportDir string
536559

537560
f, err := os.Create(fullname)
538561
if err != nil {
539-
return "", err
562+
return "", 0, err
540563
}
541564
defer f.Close()
542565

543566
gz := gzip.NewWriter(f)
544567
defer gz.Close()
545568

546-
_, err = io.Copy(gz, reader)
569+
lines, err := copyLines(gz, reader, maxlines)
547570
if err != nil {
548-
return "", err
571+
return "", 0, err
549572
}
550573

551-
return leafName, nil
574+
return leafName, lines, nil
575+
}
576+
577+
// copyLines copies lines of text from src to dst, up to maxlines.
578+
//
579+
// It returns the number of lines copied.
580+
func copyLines(dst io.Writer, src io.Reader, maxlines int) (int, error) {
581+
count := 0
582+
scanLines := func(data []byte, atEOF bool) (advance int, token []byte, err error) {
583+
if atEOF && len(data) == 0 {
584+
return 0, nil, nil
585+
}
586+
if i := bytes.IndexByte(data, '\n'); i >= 0 {
587+
// We have a full newline-terminated line.
588+
return i + 1, data[0 : i+1], nil
589+
}
590+
// If we're at EOF, we have a final, non-terminated line. Return it.
591+
if atEOF {
592+
return len(data), data, nil
593+
}
594+
// Request more data.
595+
return 0, nil, nil
596+
}
597+
598+
scanner := bufio.NewScanner(src)
599+
scanner.Split(scanLines)
600+
for scanner.Scan() {
601+
if count >= maxlines {
602+
break
603+
}
604+
line := scanner.Bytes()
605+
m, err := dst.Write(line)
606+
if err != nil {
607+
return 0, err
608+
}
609+
if m < len(line) {
610+
return 0, io.ErrShortWrite
611+
}
612+
count += 1
613+
}
614+
if err := scanner.Err(); err != nil {
615+
return 0, fmt.Errorf("error reading log submission: %w", err)
616+
}
617+
return count, nil
552618
}
553619

554620
func (s *submitServer) saveReport(ctx context.Context, p payload, reportDir, listingURL string) (*submitResponse, error) {

0 commit comments

Comments
 (0)