package handlers import ( "bytes" "encoding/json" "errors" "io" "unicode/utf8" ) // maxRenderedBodyBytes is the most of one event's body that the // recent events on a webhook's page and the event log show; a larger // body is cut there and shown whole only on the event's own page. // Bodies come from the unauthenticated receiver under its 1 MB cap, // and renderTemplate buffers a whole page before writing it, so a list // of events cannot show every body whole. const maxRenderedBodyBytes = 32 << 10 // maxInlineBodyLines is the most lines a body is shown at its full // height with. A body with more lines, or larger than // maxRenderedBodyBytes, is shown in a box of fixed height that // scrolls, so that it does not make the page huge. const maxInlineBodyLines = 200 // maxIndentGrowth is how many times its size a JSON body may grow // when it is pretty-printed; past that it is shown as received. // Indenting grows with nesting depth as well as with size: 20 KB of // nested brackets indents to some 200 MB. const maxIndentGrowth = 4 // jsonIndent is the indent of a pretty-printed JSON body. const jsonIndent = " " // BodyView is an event's body as the pages show it. newBodyView // decides it and templates/event_body.html shows it, the same way in // the recent events on a webhook's page, in the event log and on the // event's own page. type BodyView struct { // EventURL is the event's own page. The stored body downloads // from EventURL/body. EventURL string // Text is the body as shown, pretty-printed when it is JSON. Text string // Size is the stored body's size in bytes, and ShownBytes how // many of them Text holds when Cut. Size int64 ShownBytes int // Cut reports that Text is only the start of the body. Cut bool // Binary reports a body that is not text. It is not shown. Binary bool // Scroll reports a body to show in a box that scrolls. Scroll bool } // newBodyView decides how to show an event's body. body is the // stored body, or its first maxRenderedBodyBytes when only those were // read, and size is the stored body's size. func newBodyView(eventURL string, body []byte, size int64) BodyView { v := BodyView{EventURL: eventURL, Size: size} if size > int64(len(body)) { v.Cut = true body = trimPartialRune(body) v.ShownBytes = len(body) } // html/template shows invalid UTF-8 and NUL bytes as replacement // characters, so a body holding either is not text. if !utf8.Valid(body) || bytes.IndexByte(body, 0) >= 0 { v.Binary = true return v } // A cut JSON document is no longer valid JSON. if !v.Cut { body = indentJSON(body) } lines := bytes.Count(body, []byte("\n")) + 1 v.Text = string(body) v.Scroll = lines > maxInlineBodyLines || size > maxRenderedBodyBytes return v } // indentJSON returns body pretty-printed when it is a JSON document, // and unchanged when it is not or would grow more than // maxIndentGrowth times. func indentJSON(body []byte) []byte { if !json.Valid(body) || !indentFits(body) { return body } var out bytes.Buffer err := json.Indent(&out, body, "", jsonIndent) if err != nil { return body } return out.Bytes() } // indentFits reports whether pretty-printing the JSON document body // keeps it within maxIndentGrowth times its size. It adds up an upper // bound instead of indenting: each token starts at most one line, // indented once per enclosing object or array, and a key gains the // space after its colon. func indentFits(body []byte) bool { limit := maxIndentGrowth * len(body) size, depth := len(body), 0 dec := json.NewDecoder(bytes.NewReader(body)) // A number too large for a float64 is still valid JSON. dec.UseNumber() for size <= limit { tok, err := dec.Token() if errors.Is(err, io.EOF) { return true } if err != nil { return false } switch tok { case json.Delim('{'), json.Delim('['): depth++ case json.Delim('}'), json.Delim(']'): depth-- } size += len("\n") + depth*len(jsonIndent) + len(" ") } return false }