check / check (push) Waiting to run
Several comments justified code by what an earlier build would have left in a database or on disk. Each now says what the code does: reserveSQLiteFile sets the mode on every open whoever created the file, and expired treats a session lacking a deadline's timestamp as expired. The comments on retention, resubmitted events, archive pruning, target headers, the redactor and the delivery result view drop their mentions of older rows. Comments only. Model: opus-5-5
179 lines
6.2 KiB
Go
179 lines
6.2 KiB
Go
package handlers
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/dustin/go-humanize"
|
|
"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, or on every stored row having
|
|
// been written by the delivery engine under its own cap.
|
|
//
|
|
// It happens to equal that engine cap, so a row written by the
|
|
// 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, created_at, " +
|
|
"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; for
|
|
// response bodies it is the only line, and its reach is what
|
|
// delivery.Redactor documents.
|
|
type DeliveryResultView struct {
|
|
AttemptNum int
|
|
Success bool
|
|
|
|
// Ran is how long ago the attempt was recorded, and RanUTC the
|
|
// full timestamp the page shows on hover.
|
|
Ran string
|
|
RanUTC string
|
|
|
|
// 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
|
|
CreatedAt time.Time
|
|
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,
|
|
Ran: humanize.Time(r.CreatedAt),
|
|
RanUTC: r.CreatedAt.UTC().Format(time.DateTime) + " UTC",
|
|
StatusCode: r.StatusCode,
|
|
Error: redactor.Redact(r.Error),
|
|
DurationMS: r.Duration,
|
|
ResponseBody: rendered,
|
|
ResponseBytes: r.ResponseBytes,
|
|
ResponseShownBytes: len(body),
|
|
ResponseTruncated: cut,
|
|
ResponseSizeKnown: sizeKnown,
|
|
}
|
|
}
|