package handlers import ( "sneak.berlin/go/webhooker/internal/delivery" ) // maxRenderedResponseBytes caps how many bytes of one stored // delivery response body reach the event log page. // // The bound is enforced here and in SQL, because this page's // memory profile must not depend on a constant in another // package staying where it is, and because rows predating the // delivery engine's own cap or restored from an archive are // not covered by it at all. // // It happens to equal that engine cap, so a row written by the // current engine reaches this one exactly and is never cut // twice. Nothing here may assume the two differ: see view. const maxRenderedResponseBytes = 4096 // deliveryResultColumns is the delivery attempt projection. // The casts to blob are load-bearing for the same reason they // are in eventLogColumns: they make substr and length count // bytes rather than characters, and they make SQLite do the // cut, so an oversized stored response never becomes a Go // string at all. const deliveryResultColumns = "delivery_id, attempt_num, success, " + "status_code, error, duration, " + "substr(cast(response_body as blob), 1, ?) AS response_body, " + "length(cast(response_body as blob)) AS response_bytes" // DeliveryResultView is the display-safe projection of one // delivery attempt for the event log page. It carries a // capped response body plus the true stored size, so the page // can mark a response as truncated without holding the whole // thing. // // Both Error and ResponseBody have been through the target's // Redactor. The engine already masks the URL out of the // errors it stores, so for errors this is a second line // covering rows written before it did; for response bodies it // is the only line, and its reach is what // delivery.Redactor documents. type DeliveryResultView struct { AttemptNum int Success bool // StatusCode is 0 when the attempt never got a response, // which is why the page asks HasStatusCode rather than // printing the number. StatusCode int // Error is the stored failure message, redacted. Error string // DurationMS is how long the attempt took. DurationMS int64 // ResponseBody holds at most maxRenderedResponseBytes // bytes of the stored response, redacted. It is remote // content and must only ever be rendered escaped. ResponseBody string // ResponseBytes is the size of the stored response body, // before the cut and before redaction. It is what the // remote sent only when ResponseSizeKnown is set. ResponseBytes int64 // ResponseShownBytes is how much of that the page is // showing. It is the size of the cut, taken before // redaction, so the truncation marker reports what SQLite // returned rather than how much the marker substitution // then changed the length. ResponseShownBytes int // ResponseTruncated reports that the body shown may be // incomplete, so the page owes the reader a marker. Every // body that reaches the cap counts, because one the // delivery engine cut at its own equal cap is // indistinguishable from a complete one. ResponseTruncated bool // ResponseSizeKnown reports that ResponseBytes is the whole // response the remote sent, so the marker may quote it. It // is false for a body that only reaches the cap, where how // much came after it was never recorded. ResponseSizeKnown bool } // HasStatusCode reports whether the attempt got as far as an // HTTP response. A transport failure stores no status code, // and rendering that as "0" would read as a real status. func (v DeliveryResultView) HasStatusCode() bool { return v.StatusCode != 0 } // deliveryResultRow is one row of the delivery attempt // projection. Its response body arrives already cut to the // cap by SQLite, with the true size beside it. type deliveryResultRow struct { DeliveryID string AttemptNum int Success bool StatusCode int Error string Duration int64 ResponseBody []byte ResponseBytes int64 } // view projects a loaded row for rendering, stripping the // target's own credential out of the two fields a remote peer // gets to influence. func (r *deliveryResultRow) view( redactor delivery.Redactor, ) DeliveryResultView { body := r.ResponseBody // Two different cuts can have shortened this body, and the // row records only one of them. SQLite cuts here, whenever // the stored value is larger than the cap. The delivery // engine cut earlier, whenever the remote sent more than // its own maxBodyLog — which is this same number, so such a // row stores the cut length as its whole length and nothing // in it separates a response that ended at the cap from one // severed there. // // So a body that reaches the cap is treated as cut either // way. Gating on ResponseBytes alone would assume the two // caps differ, and they do not: under the current engine // that gate never opens. cut := r.ResponseBytes > int64(len(body)) || len(body) >= maxRenderedResponseBytes // The row holds more than the page shows only in the first // of those cases. In the second the stored row is all there // is, and its size is a floor rather than the true one. sizeKnown := r.ResponseBytes > int64(len(body)) // Only a cut response can have been left mid-sequence, // exactly as with an event body. if cut { body = trimPartialRune(body) } // A cut body goes through RedactCut: the remote controls // the padding ahead of a credential it echoes, so it // controls where the cut falls inside that credential, and // the severed prefix left behind matches no secret whole. rendered := string(body) if cut { rendered = redactor.RedactCut(rendered) } else { rendered = redactor.Redact(rendered) } return DeliveryResultView{ AttemptNum: r.AttemptNum, Success: r.Success, StatusCode: r.StatusCode, Error: redactor.Redact(r.Error), DurationMS: r.Duration, ResponseBody: rendered, ResponseBytes: r.ResponseBytes, ResponseShownBytes: len(body), ResponseTruncated: cut, ResponseSizeKnown: sizeKnown, } }