webhook

package
v0.2.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 25, 2026 License: MIT Imports: 21 Imported by: 0

Documentation

Overview

Package webhook turns one delivery into a version; expressions reach it compiled, so one can build a string but never decide that a signature is valid.

Index

Constants

View Source
const Custom = "custom"

Custom is the provider whose scheme is the resource's own source.signature.

View Source
const DefaultMaxBody = 1 << 20

DefaultMaxBody is the body cap when a resource sets no max_body.

View Source
const Tolerance = 5 * time.Minute

Tolerance is how far a signed timestamp may sit from now, either way. Without it a captured delivery replays forever, which is the hole pocketci left by signing the timestamp and never reading it.

Variables

View Source
var (
	// ErrUnauthorized is a delivery whose signature did not verify, including every delivery to a resource whose secret is empty.
	ErrUnauthorized = errors.New("webhook: signature did not verify")
	// ErrTooLarge is a body over the resource's max_body.
	ErrTooLarge = errors.New("webhook: body too large")
	// ErrExpression is a filter:/id:/version: that failed to evaluate against this delivery: the pipeline's fault, not the sender's, so it is answered 500 and the delivery can be redelivered once the pipeline is fixed.
	ErrExpression = errors.New("webhook: expression failed")
)
View Source
var Providers = map[string]Provider{
	"github": {
		Verify: hmacBody("X-Hub-Signature-256", "sha256="),
		ID:     header("X-GitHub-Delivery"),
		Event:  header("X-GitHub-Event"),
		Signed: []string{"X-Hub-Signature-256", "X-Hub-Signature"},
	},
	"gitlab": {
		Verify: token("X-Gitlab-Token", ""),
		ID:     header("X-Gitlab-Event-UUID"),
		Event:  header("X-Gitlab-Event"),
		Signed: []string{"X-Gitlab-Token"},
	},
	"gitea": {
		Verify: hmacBody("X-Gitea-Signature", ""),
		ID:     header("X-Gitea-Delivery"),
		Event:  header("X-Gitea-Event"),
		Signed: []string{"X-Gitea-Signature", "X-Gogs-Signature", "X-Hub-Signature-256", "X-Hub-Signature"},
	},
	"forgejo": {
		Verify: hmacBody("X-Forgejo-Signature", ""),
		ID:     header("X-Forgejo-Delivery"),
		Event:  header("X-Forgejo-Event"),
		Signed: []string{"X-Forgejo-Signature", "X-Gitea-Signature", "X-Gogs-Signature", "X-Hub-Signature-256", "X-Hub-Signature"},
	},
	"bitbucket": {
		Verify: hmacBody("X-Hub-Signature", "sha256="),
		ID:     header("X-Request-UUID"),
		Event:  header("X-Event-Key"),
		Signed: []string{"X-Hub-Signature"},
	},
	"slack": {
		Verify:    slackVerify,
		ID:        jsonField("event_id"),
		Event:     slackEvent,
		Handshake: slackHandshake,
		Signed:    []string{"X-Slack-Signature"},
	},
	"stripe": {
		Verify: stripeVerify,
		ID:     jsonField("id"),
		Event:  jsonField("type"),
		Signed: []string{"Stripe-Signature"},
	},
	"sentry": {
		Verify: hmacBody("Sentry-Hook-Signature", ""),
		ID:     header("Request-ID"),
		Event:  header("Sentry-Hook-Resource"),
		Signed: []string{"Sentry-Hook-Signature"},
	},
	"linear": {
		Verify: hmacBody("Linear-Signature", ""),
		ID:     header("Linear-Delivery"),
		Event:  header("Linear-Event"),
		Signed: []string{"Linear-Signature"},
	},
	"pagerduty": {
		Verify: pagerdutyVerify,
		ID:     jsonField("event", "id"),
		Event:  jsonField("event", "event_type"),
		Signed: []string{"X-PagerDuty-Signature"},
	},
	"honeybadger": {
		Verify: token("Honeybadger-Token", ""),
		ID:     none,
		Event:  jsonField("event"),
		Signed: []string{"Honeybadger-Token"},
	},
	"standard-webhooks": {
		Verify: standardVerify,
		ID:     header("Webhook-Id"),
		Event:  jsonField("type"),
		Secret: standardSecret,
		Signed: []string{"Webhook-Signature"},
	},
	"token": {
		Verify: token("Authorization", "Bearer "),
		ID:     none,
		Event:  none,
		Signed: []string{"Authorization"},
	},
}

Providers is every signature scheme a resource may declare. A provider is declared by the pipeline, never detected from headers the sender controls.

Functions

func ProviderNames

func ProviderNames() []string

ProviderNames is every value source.provider may take, sorted: the table, and custom.

Types

type Delivery

type Delivery struct {
	Version map[string]any
	Body    []byte
	Headers map[string]string
}

Delivery is what gets recorded: the version, and the payload a get writes out.

type Input

type Input struct {
	Provider string
	Event    string
	Method   string
	Headers  map[string]string
	Query    map[string]string
	Body     string
	Payload  any
}

Input is what an expression sees: the delivery, already verified.

type Provider

type Provider struct {
	// Verify reports whether the delivery was signed with secret. now is for the schemes that sign a timestamp.
	Verify func(r Request, secret []byte, now time.Time) bool
	// ID is the sender's own delivery id, or "" when it sends none.
	ID func(r Request) string
	// Event is what the sender says happened, or "".
	Event func(r Request) string
	// Handshake, when set, answers a verified delivery that is the sender checking the URL rather than reporting anything.
	Handshake func(r Request) ([]byte, bool)
	// Secret, when set, turns the configured secret into the key; a nil Secret uses its bytes.
	Secret func(raw string) ([]byte, error)
	// Signed names the headers that carry the credential, which never reach headers.json; the first is the one Verify reads.
	Signed []string
}

Provider is one sender's signature scheme and where it says what happened.

type Receiver

type Receiver struct {
	Provider string
	// Scheme is provider: custom's signature, and nil for every other provider.
	Scheme *Scheme
	// SecretEnv names the variable holding the secret; the caller reads it, per delivery, so a rotated secret needs no reload.
	SecretEnv string
	MaxBody   int64
	// Filter, when set, drops a delivery it answers false for.
	Filter func(Input) (bool, error)
	// ID, when set, replaces the sender's delivery id.
	ID func(Input) (string, error)
	// Version projects extra fields into the version, by name.
	Version map[string]func(Input) (string, error)
}

Receiver is one webhook resource, compiled.

func (*Receiver) Accept

func (r *Receiver) Accept(request Request) (Result, error)

Accept runs a delivery that has already been verified through the pipeline's own expressions, in order: handshake, filter, id, version.

func (*Receiver) ReadBody

func (r *Receiver) ReadBody(w http.ResponseWriter, req *http.Request) ([]byte, error)

ReadBody reads a delivery's body under the cap, answering ErrTooLarge rather than a truncated body.

func (*Receiver) Verify

func (r *Receiver) Verify(req Request, secret string, now time.Time) error

Verify checks the delivery's signature. An empty secret verifies nothing.

type Request

type Request struct {
	Method string
	Header http.Header
	Query  url.Values
	Body   []byte
}

Request is one delivery as it arrived.

func ParseRequest

func ParseRequest(raw []byte) (Request, error)

ParseRequest reads a captured delivery: a request line, headers, a blank line, and the body — everything after the blank line, byte for byte, whatever Content-Length says, since a hand-written capture rarely carries one.

type Result

type Result struct {
	Delivery  Delivery
	Handshake []byte
	Filtered  bool
}

Result is what one delivery came to. At most one of Handshake and Filtered is set; with neither, Delivery is to be recorded.

type Scheme

type Scheme struct {
	Header    string
	Prefix    string
	Encoding  string
	Algorithm string
	// Signed builds the message the sender signed; nil means the raw body.
	Signed func(Input) (string, error)
	// Timestamp, when set, is the unix time the sender signed, refused outside Tolerance. It only protects anything when Signed includes it.
	Timestamp func(Input) (string, error)
}

Scheme is an HMAC carried in one header. It is what provider: custom compiles into and what hmacBody is at its defaults (hex, sha256, the raw body signed), so a described scheme and a built-in one are checked by the same code.

func (Scheme) Verify

func (s Scheme) Verify(r Request, secret []byte, now time.Time) bool

Verify is the Provider.Verify for this scheme. An expression that fails to evaluate verifies nothing.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL