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. // // It matches the cap the delivery engine applies when it // records a result, so nothing written by the current engine // is cut twice. The bound is enforced here anyway, and in // SQL: this page's memory profile must not depend on a // constant in another package staying where it is, and rows // predating that cap or restored from an archive are not // covered by it at all. 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 true size of the stored response // body, before the cut and before redaction. 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 stored response was // larger than the cap, so the page owes the reader a // marker. ResponseTruncated 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 truncated := r.ResponseBytes > int64(len(body)) // Only a cut response can have been left mid-sequence by // this query, exactly as with an event body. if truncated { 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 truncated { 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: truncated, } }