webhooks

package
v0.0.0-...-d83ce93 Latest Latest
Warning

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

Go to latest
Published: Sep 2, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

README

Package cloudeng.io/webapp/webhooks

import cloudeng.io/webapp/webhooks

Constants

DefaultQueueSize, DefaultPayloadLimit
DefaultQueueSize = 100
DefaultPayloadLimit = 1024 * 1024 // 1MB


Variables

DefaultForwardedHeaders
DefaultForwardedHeaders = []string{
	"X-GitHub-Event",
	"X-GitHub-Delivery",
	"X-GitHub-Hook-ID",
}

DefaultForwardedHeaders are the request headers copied from an incoming webhook delivery onto the long-poll response when WithForwardedHeaders is not used. They carry the GitHub event type and delivery metadata a client needs to dispatch events. The X-Hub-Signature-256 header is deliberately omitted: the relay has already verified it, and the client has no secret to re-check.

ErrWrongServiceSpecificConfig
ErrWrongServiceSpecificConfig = fmt.Errorf("missing service specific config")

Functions

Func NoopValidator
func NoopValidator(req *http.Request) ([]byte, int)
Func ParseSpecific
func ParseSpecific[T any](c Config) (T, error)
Func SHA256SignatureFromHeader
func SHA256SignatureFromHeader(headerName string) func(req *http.Request) ([]byte, int)

SHA256SignatureFromHeader returns a function that extracts and decodes the HMAC SHA256 signature from the specified header in the HTTP request.

Func SignHTTPRequest
func SignHTTPRequest(header http.Header, payload []byte, secret []byte, headerName string) error

SignHTTPRequest signs the given payload using the provided secret and sets the signature in the specified header of the HTTP request.

Types

Type Config
type Config struct {
	DeliveryPath    string            `yaml:"delivery_path" doc:"path to receive webhooks on"`
	RelayPath       string            `yaml:"relay_path" doc:"path to read relay payloads from"`
	MaxPayloadSize  cmdyaml.ByteSize  `yaml:"max_payload_size" doc:"maximum allowed payload size for incoming webhook requests in bytes, e.g. 1048576 for 1MB"`
	MaxQueueSize    int               `yaml:"max_queue_size" doc:"maximum number of payloads to hold in the queue for processing, leave empty for default"`
	ExclusiveReads  bool              `yaml:"exclusive_reads" doc:"if true, at most one long poll reader is admitted at a time and concurrent readers are rejected with 409 Conflict"`
	Service         string            `yaml:"service" doc:"type of webhook to serve, e.g. github, etc."`
	ServiceSpecific *cmdyaml.Deferred `yaml:"service_specific" doc:"additional details specific to the type of webhook being served, leave empty for default"`
}

Config represents the configuration for a webhook server.

Methods
func (c Config) MarshalYAML() (any, error)
func (c Config) Options() []Option
func (c *Config) UnmarshalYAML(node *yaml.Node) error
Type Option
type Option func(*options)

Option is a function that configures the Relay.

Functions
func WithCounters(deniedCounter, relayedCounter, readCounter webapp.CounterInc) Option

WithCounters sets the counters for the Relay. If any of the counters are nil, they will be set to a no-op counter that does nothing when called. deniedCounter is incremented when a request is denied because the payload fails validation, e.g. due to an invalid signature. relayedCounter is incremented when a payload is successfully relayed to the FIFO. readCounter is incremented when a payload is successfully read from the FIFO and sent to a client.

func WithExclusiveReads(exclusive bool) Option

WithExclusiveReads configures the long-poll endpoint to admit at most one reader at a time. While a reader is waiting for, or receiving, a delivery any additional long-poll request is rejected immediately with 409 Conflict rather than competing for deliveries. The default is to allow any number of concurrent readers, with each delivery going to exactly one of them.

func WithExpiry(ttl, scanInterval time.Duration) Option

WithExpiry configures the relay to drop queued deliveries that have waited longer than ttl without being read by a client, guarding against unbounded staleness when no client is polling. The queue is scanned every scanInterval; if scanInterval is <= 0 it defaults to ttl. Expiry is disabled (the default) when ttl is <= 0, in which case deliveries are only dropped by the queue's drop-oldest behaviour when it is full.

func WithForwardedHeaders(names ...string) Option

WithForwardedHeaders sets the request header names that are copied from an incoming webhook delivery onto the long-poll response sent to clients. It replaces DefaultForwardedHeaders; pass no names to forward none. Header matching is case-insensitive and absent headers are skipped.

func WithLogger(logger *slog.Logger) Option

WithLogger sets the logger for the Relay.

func WithMaxPayloadSize(size int64) Option

WithMaxPayloadSize sets the maximum allowed payload size for incoming webhook requests.

func WithQueueSize(size int64) Option

WithQueueSize sets the size of the internal buffer for relaying payloads. When the buffer is full the oldest payload is dropped.

Type Relay
type Relay struct {
	// contains filtered or unexported fields
}

Relay is an HTTP handler that receives JSON payloads and relays them over a channel for subsequent processing. It is designed to be used in a webhook server to receive webhook payloads and relay them to another http handler that is used as a long polling endpoint for a client to receive the payloads. The Webhook endpoint will accept POST requests with JSON payloads and the Wait endpoint will accept GET requests and will block until a payload is received. When the internal buffer is full the oldest webhook is dropped to make room for the new one.

Functions
func NewRelay(ctx context.Context, validator Validator, opts ...Option) *Relay

NewRelay creates a new Relay with the provided Validator and options. ctx governs the lifetime of the internal FIFO goroutine; cancel it or call Stop to shut down cleanly.

Methods
func (r *Relay) DeliveryHandler() http.Handler

DeliveryHandler returns an http.Handler that serves the webhook endpoint for receiving payloads.

func (r *Relay) Handler(deliveryPath, relayPath string) func(w http.ResponseWriter, req *http.Request)

Handler returns an http.HandlerFunc that routes requests to the appropriate handler based on the URL path. It expects the webhook endpoint to be at deliveryPath and the wait endpoint to be at relayPath. Requests to other paths will receive a 404 Not Found response.

func (r *Relay) PollingHandler() http.Handler

PollingHandler returns an http.Handler that serves the wait endpoint for long polling clients to receive payloads.

func (r *Relay) ServeWebhook(w http.ResponseWriter, req *http.Request)

ServeWebhook handles incoming webhook requests, validates them using the provided Validator, and relays the payload to the FIFO for processing. If the internal buffer is full the oldest payload is dropped to make room. It responds with appropriate HTTP status codes based on the validation outcome.

func (r *Relay) Stop(ctx context.Context)

Stop shuts down the internal FIFO goroutine. It blocks until the goroutine exits or ctx is cancelled.

func (r *Relay) WaitForWebhook(w http.ResponseWriter, req *http.Request)

WaitForWebhook waits for a payload to be received on the FIFO and responds with the payload as JSON. It is intended to support long polling by blocking until a webhook payload is available. If the request context is cancelled while waiting, it logs the cancellation and returns without responding. When exclusive reads are configured (see WithExclusiveReads) and another reader is already waiting, it responds immediately with 409 Conflict.

Type SecretsConfig
type SecretsConfig struct {
	Secrets     map[string][]string `yaml:"-" doc:"map of users to lists of secret IDs, where users are service specific (e.g. GitHub usernames or email addresses) and secret IDs identify entries in the key store"`
	SecretSpecs []keys.KeySpec      `yaml:"-" doc:"-"`
}

SecretsConfig represents the secrets used to validate incoming webhooks. Keys are users (e.g. a GitHub username or email address) and values are lists of secret IDs that identify entries in the key store. SecretSpecs is populated automatically during unmarshal and must not be set directly.

YAML format (the node itself is the map — no wrapper key):

alice@example.com:
  - secret-id-1
  - secret-id-2
bob@example.com:
  - other-secret

Note that SecretsConfig cannot be inlined by a parent YAML struct, it must always be a named field.

Methods
func (sc SecretsConfig) MarshalYAML() (any, error)
func (sc SecretsConfig) TokensFromContext(ctx context.Context) ([]keys.Token, error)
func (sc *SecretsConfig) UnmarshalYAML(node *yaml.Node) error
Type Validator
type Validator func(r *http.Request) ([]byte, int)

Validator is called to validate and extract the webhook payload from an incoming request. It should return the payload as a byte slice and an error if validation fails.

Functions
func SignatureValidator(getSignature func(req *http.Request) ([]byte, int), getTokens func(ctx context.Context) ([]keys.Token, error)) (Validator, error)

SignatureValidator returns a Validator that verifies webhook payloads using one of possibly multiple Tokens returned by the getTokens function. The token value is a byte slice that the validator uses to compute the HMAC SHA256 signature of the payload and compare it to the signature provided in the request header as returned by the getSignature function. If a match is found, the payload is considered valid and returned; if none of the returned tokens' secrets match the signature, the payload is rejected and an appropriate HTTP status code is returned to indicate the error. It is the responsibility of the getTokens function to retrieve the tokens from the appropriate source, such as a file or a key store.

Documentation

Index

Constants

View Source
const (
	DefaultQueueSize    = 100
	DefaultPayloadLimit = 1024 * 1024 // 1MB
)

Variables

View Source
var DefaultForwardedHeaders = []string{
	"X-GitHub-Event",
	"X-GitHub-Delivery",
	"X-GitHub-Hook-ID",
}

DefaultForwardedHeaders are the request headers copied from an incoming webhook delivery onto the long-poll response when WithForwardedHeaders is not used. They carry the GitHub event type and delivery metadata a client needs to dispatch events. The X-Hub-Signature-256 header is deliberately omitted: the relay has already verified it, and the client has no secret to re-check.

View Source
var (
	ErrWrongServiceSpecificConfig = fmt.Errorf("missing service specific config")
)

Functions

func NoopValidator

func NoopValidator(req *http.Request) ([]byte, int)

func ParseSpecific

func ParseSpecific[T any](c Config) (T, error)

func SHA256SignatureFromHeader

func SHA256SignatureFromHeader(headerName string) func(req *http.Request) ([]byte, int)

SHA256SignatureFromHeader returns a function that extracts and decodes the HMAC SHA256 signature from the specified header in the HTTP request.

func SignHTTPRequest

func SignHTTPRequest(header http.Header, payload []byte, secret []byte, headerName string) error

SignHTTPRequest signs the given payload using the provided secret and sets the signature in the specified header of the HTTP request.

Types

type Config

type Config struct {
	DeliveryPath    string            `yaml:"delivery_path" doc:"path to receive webhooks on"`
	RelayPath       string            `yaml:"relay_path" doc:"path to read relay payloads from"`
	MaxPayloadSize  cmdyaml.ByteSize  `yaml:"max_payload_size" doc:"maximum allowed payload size for incoming webhook requests in bytes, e.g. 1048576 for 1MB"`
	MaxQueueSize    int               `yaml:"max_queue_size" doc:"maximum number of payloads to hold in the queue for processing, leave empty for default"`
	ExclusiveReads  bool              `` /* 142-byte string literal not displayed */
	Service         string            `yaml:"service" doc:"type of webhook to serve, e.g. github, etc."`
	ServiceSpecific *cmdyaml.Deferred `yaml:"service_specific" doc:"additional details specific to the type of webhook being served, leave empty for default"`
}

Config represents the configuration for a webhook server.

func (Config) MarshalYAML

func (c Config) MarshalYAML() (any, error)

func (Config) Options

func (c Config) Options() []Option

func (*Config) UnmarshalYAML

func (c *Config) UnmarshalYAML(node *yaml.Node) error

type Option

type Option func(*options)

Option is a function that configures the Relay.

func WithCounters

func WithCounters(deniedCounter, relayedCounter, readCounter webapp.CounterInc) Option

WithCounters sets the counters for the Relay. If any of the counters are nil, they will be set to a no-op counter that does nothing when called. deniedCounter is incremented when a request is denied because the payload fails validation, e.g. due to an invalid signature. relayedCounter is incremented when a payload is successfully relayed to the FIFO. readCounter is incremented when a payload is successfully read from the FIFO and sent to a client.

func WithExclusiveReads

func WithExclusiveReads(exclusive bool) Option

WithExclusiveReads configures the long-poll endpoint to admit at most one reader at a time. While a reader is waiting for, or receiving, a delivery any additional long-poll request is rejected immediately with 409 Conflict rather than competing for deliveries. The default is to allow any number of concurrent readers, with each delivery going to exactly one of them.

func WithExpiry

func WithExpiry(ttl, scanInterval time.Duration) Option

WithExpiry configures the relay to drop queued deliveries that have waited longer than ttl without being read by a client, guarding against unbounded staleness when no client is polling. The queue is scanned every scanInterval; if scanInterval is <= 0 it defaults to ttl. Expiry is disabled (the default) when ttl is <= 0, in which case deliveries are only dropped by the queue's drop-oldest behaviour when it is full.

func WithForwardedHeaders

func WithForwardedHeaders(names ...string) Option

WithForwardedHeaders sets the request header names that are copied from an incoming webhook delivery onto the long-poll response sent to clients. It replaces DefaultForwardedHeaders; pass no names to forward none. Header matching is case-insensitive and absent headers are skipped.

func WithLogger

func WithLogger(logger *slog.Logger) Option

WithLogger sets the logger for the Relay.

func WithMaxPayloadSize

func WithMaxPayloadSize(size int64) Option

WithMaxPayloadSize sets the maximum allowed payload size for incoming webhook requests.

func WithQueueSize

func WithQueueSize(size int64) Option

WithQueueSize sets the size of the internal buffer for relaying payloads. When the buffer is full the oldest payload is dropped.

type Relay

type Relay struct {
	// contains filtered or unexported fields
}

Relay is an HTTP handler that receives JSON payloads and relays them over a channel for subsequent processing. It is designed to be used in a webhook server to receive webhook payloads and relay them to another http handler that is used as a long polling endpoint for a client to receive the payloads. The Webhook endpoint will accept POST requests with JSON payloads and the Wait endpoint will accept GET requests and will block until a payload is received. When the internal buffer is full the oldest webhook is dropped to make room for the new one.

func NewRelay

func NewRelay(ctx context.Context, validator Validator, opts ...Option) *Relay

NewRelay creates a new Relay with the provided Validator and options. ctx governs the lifetime of the internal FIFO goroutine; cancel it or call Stop to shut down cleanly.

func (*Relay) DeliveryHandler

func (r *Relay) DeliveryHandler() http.Handler

DeliveryHandler returns an http.Handler that serves the webhook endpoint for receiving payloads.

func (*Relay) Handler

func (r *Relay) Handler(deliveryPath, relayPath string) func(w http.ResponseWriter, req *http.Request)

Handler returns an http.HandlerFunc that routes requests to the appropriate handler based on the URL path. It expects the webhook endpoint to be at deliveryPath and the wait endpoint to be at relayPath. Requests to other paths will receive a 404 Not Found response.

func (*Relay) PollingHandler

func (r *Relay) PollingHandler() http.Handler

PollingHandler returns an http.Handler that serves the wait endpoint for long polling clients to receive payloads.

func (*Relay) ServeWebhook

func (r *Relay) ServeWebhook(w http.ResponseWriter, req *http.Request)

ServeWebhook handles incoming webhook requests, validates them using the provided Validator, and relays the payload to the FIFO for processing. If the internal buffer is full the oldest payload is dropped to make room. It responds with appropriate HTTP status codes based on the validation outcome.

func (*Relay) Stop

func (r *Relay) Stop(ctx context.Context)

Stop shuts down the internal FIFO goroutine. It blocks until the goroutine exits or ctx is cancelled.

func (*Relay) WaitForWebhook

func (r *Relay) WaitForWebhook(w http.ResponseWriter, req *http.Request)

WaitForWebhook waits for a payload to be received on the FIFO and responds with the payload as JSON. It is intended to support long polling by blocking until a webhook payload is available. If the request context is cancelled while waiting, it logs the cancellation and returns without responding. When exclusive reads are configured (see WithExclusiveReads) and another reader is already waiting, it responds immediately with 409 Conflict.

type SecretsConfig

type SecretsConfig struct {
	Secrets     map[string][]string `` /* 176-byte string literal not displayed */
	SecretSpecs []keys.KeySpec      `yaml:"-" doc:"-"`
}

SecretsConfig represents the secrets used to validate incoming webhooks. Keys are users (e.g. a GitHub username or email address) and values are lists of secret IDs that identify entries in the key store. SecretSpecs is populated automatically during unmarshal and must not be set directly.

YAML format (the node itself is the map — no wrapper key):

alice@example.com:
  - secret-id-1
  - secret-id-2
bob@example.com:
  - other-secret

Note that SecretsConfig cannot be inlined by a parent YAML struct, it must always be a named field.

func (SecretsConfig) MarshalYAML

func (sc SecretsConfig) MarshalYAML() (any, error)

func (SecretsConfig) TokensFromContext

func (sc SecretsConfig) TokensFromContext(ctx context.Context) ([]keys.Token, error)

func (*SecretsConfig) UnmarshalYAML

func (sc *SecretsConfig) UnmarshalYAML(node *yaml.Node) error

type Validator

type Validator func(r *http.Request) ([]byte, int)

Validator is called to validate and extract the webhook payload from an incoming request. It should return the payload as a byte slice and an error if validation fails.

func SignatureValidator

func SignatureValidator(getSignature func(req *http.Request) ([]byte, int), getTokens func(ctx context.Context) ([]keys.Token, error)) (Validator, error)

SignatureValidator returns a Validator that verifies webhook payloads using one of possibly multiple Tokens returned by the getTokens function. The token value is a byte slice that the validator uses to compute the HMAC SHA256 signature of the payload and compare it to the signature provided in the request header as returned by the getSignature function. If a match is found, the payload is considered valid and returned; if none of the returned tokens' secrets match the signature, the payload is rejected and an appropriate HTTP status code is returned to indicate the error. It is the responsibility of the getTokens function to retrieve the tokens from the appropriate source, such as a file or a key store.

Jump to

Keyboard shortcuts

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