package handlers import ( "bytes" "encoding/json" "errors" "io" "unicode" "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 // maxIndentDepth is how deeply a JSON body's objects and arrays may // nest for it to be indented at all; a deeper one is shown as received. // Each level indents every line inside it two more spaces, so 10 KB of // nested brackets would indent to some 50 MB; within this depth a body // grows at most 35 times. const maxIndentDepth = 16 // A JSON body is shown pretty-printed only when that makes it at most // maxIndentGrowth times its size plus indentAllowance bytes, and // otherwise as received, so that indenting does not undo // maxRenderedBodyBytes. The allowance keeps a small nested body // pretty-printed. const ( maxIndentGrowth = 4 indentAllowance = 1 << 10 ) // 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 as replacement characters, // and a browser shows a control character other than tab, line // feed and carriage return as a box or not at all, so a body // holding either is not text. isControl := func(r rune) bool { return unicode.IsControl(r) && r != '\t' && r != '\n' && r != '\r' } if !utf8.Valid(body) || bytes.IndexFunc(body, isControl) >= 0 { v.Binary = true return v } // A cut JSON document is no longer valid JSON. if !v.Cut { body = indentJSON(body) } // The page shows a carriage return, a line feed, or the two // together as one line break. A final one ends the last line // rather than starting another. text := bytes.TrimSuffix(body, []byte("\n")) text = bytes.TrimSuffix(text, []byte("\r")) breaks := bytes.Count(text, []byte("\n")) + bytes.Count(text, []byte("\r")) - bytes.Count(text, []byte("\r\n")) lines := breaks + 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, nests deeper than maxIndentDepth, or // would grow past maxIndentGrowth times its size plus indentAllowance // bytes. 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 || out.Len() > maxIndentGrowth*len(body)+indentAllowance { return body } return out.Bytes() } // indentFits reports whether the objects and arrays of the JSON // document body nest at most maxIndentDepth deep. func indentFits(body []byte) bool { depth := 0 dec := json.NewDecoder(bytes.NewReader(body)) // A number too large for a float64 is still valid JSON. dec.UseNumber() for { 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++ if depth > maxIndentDepth { return false } case json.Delim('}'), json.Delim(']'): depth-- } } }