feat: webhooks support (#818)

This commit is contained in:
James Read 2026-01-06 23:33:01 +00:00 committed by GitHub
commit 15c4546150
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 709 additions and 0 deletions

View File

@ -10,6 +10,7 @@ require (
connectrpc.com/connect v1.19.1
github.com/Masterminds/semver v1.5.0
github.com/MicahParks/keyfunc/v3 v3.7.0
github.com/PaesslerAG/jsonpath v0.1.1
github.com/alexedwards/argon2id v1.0.0
github.com/bufbuild/buf v1.61.0
github.com/fsnotify/fsnotify v1.9.0
@ -55,6 +56,7 @@ require (
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/MicahParks/jwkset v0.11.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/PaesslerAG/gval v1.0.0 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bufbuild/protocompile v0.14.2-0.20251223142729-db46c1b9d34e // indirect

View File

@ -42,6 +42,11 @@ github.com/MicahParks/keyfunc/v3 v3.7.0 h1:pdafUNyq+p3ZlvjJX1HWFP7MA3+cLpDtg69U3
github.com/MicahParks/keyfunc/v3 v3.7.0/go.mod h1:z66bkCviwqfg2YUp+Jcc/xRE9IXLcMq6DrgV/+Htru0=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/PaesslerAG/gval v1.0.0 h1:GEKnRwkWDdf9dOmKcNrar9EA1bz1z9DqPIO1+iLzhd8=
github.com/PaesslerAG/gval v1.0.0/go.mod h1:y/nm5yEyTeX6av0OfKJNp9rBNj2XrGhAf5+v24IBN1I=
github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8=
github.com/PaesslerAG/jsonpath v0.1.1 h1:c1/AToHQMVsduPAa4Vh6xp2U0evy4t8SWp8imEsylIk=
github.com/PaesslerAG/jsonpath v0.1.1/go.mod h1:lVboNxFGal/VwW6d9JzIy56bUsYAP6tH/x80vjnCseY=
github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=
github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=

View File

@ -22,12 +22,14 @@ type Action struct {
ExecOnFileCreatedInDir []string `koanf:"execOnFileCreatedInDir"`
ExecOnFileChangedInDir []string `koanf:"execOnFileChangedInDir"`
ExecOnCalendarFile string `koanf:"execOnCalendarFile"`
ExecOnWebhook []WebhookConfig `koanf:"execOnWebhook"`
Triggers []string `koanf:"triggers"`
MaxConcurrent int `koanf:"maxConcurrent"`
MaxRate []RateSpec `koanf:"maxRate"`
Arguments []ActionArgument `koanf:"arguments"`
PopupOnStart string `koanf:"popupOnStart"`
SaveLogs SaveLogsConfig `koanf:"saveLogs"`
EnabledExpression string `koanf:"enabledExpression"`
}
// ActionArgument objects appear on Actions.
@ -55,6 +57,18 @@ type RateSpec struct {
Duration string `koanf:"duration"`
}
// WebhookConfig defines configuration for generic webhook triggers.
type WebhookConfig struct {
Secret string `koanf:"secret"` // Optional: secret for signature verification
AuthType string `koanf:"authType"` // Optional: "hmac-sha256", "hmac-sha1", "bearer", "basic", "none"
AuthHeader string `koanf:"authHeader"` // Optional: custom header name for auth (default: "X-Webhook-Signature")
MatchHeaders map[string]string `koanf:"matchHeaders"` // Match HTTP headers
MatchPath string `koanf:"matchPath"` // JSONPath expression to match in request body (format: "jsonpath=value" or just "jsonpath")
MatchQuery map[string]string `koanf:"matchQuery"` // Match URL query parameters
Extract map[string]string `koanf:"extract"` // Map action argument names to JSONPath expressions
Template string `koanf:"template"` // Optional: template name (e.g., "github-push", "github-pr")
}
// Entity represents a "thing" that can have multiple actions associated with it.
// for example, a media player with a start and stop action.
type EntityFile struct {

View File

@ -19,6 +19,7 @@ import (
"github.com/OliveTin/OliveTin/internal/auth/otoauth2"
config "github.com/OliveTin/OliveTin/internal/config"
"github.com/OliveTin/OliveTin/internal/executor"
"github.com/OliveTin/OliveTin/internal/webhooks"
log "github.com/sirupsen/logrus"
)
@ -72,6 +73,10 @@ func StartFrontendMux(cfg *config.Config, ex *executor.Executor) {
mux.HandleFunc("/readyz", handleReadyz)
webhookHandler := webhooks.NewWebhookHandler(cfg, ex)
mux.HandleFunc("/webhooks", webhookHandler.HandleWebhook)
mux.HandleFunc("/webhooks/", webhookHandler.HandleWebhook)
webuiServer := NewWebUIServer(cfg)
mux.HandleFunc("/theme.css", webuiServer.generateThemeCss)

View File

@ -0,0 +1,169 @@
package webhooks
import (
"crypto/hmac"
"crypto/sha1"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"net/http"
"strings"
"github.com/OliveTin/OliveTin/internal/config"
log "github.com/sirupsen/logrus"
)
type AuthVerifier struct {
config config.WebhookConfig
}
func NewAuthVerifier(cfg config.WebhookConfig) *AuthVerifier {
return &AuthVerifier{config: cfg}
}
func (v *AuthVerifier) Verify(r *http.Request, payload []byte) bool {
verifier := v.getVerifier()
if verifier == nil {
return v.handleUnknownAuthType()
}
return verifier(r, payload)
}
type authVerifierFunc func(*http.Request, []byte) bool
func (v *AuthVerifier) getVerifier() authVerifierFunc {
if v.config.AuthType == "" || v.config.AuthType == "none" {
return func(_ *http.Request, _ []byte) bool {
return true
}
}
verifierMap := v.buildVerifierMap()
if verifier, ok := verifierMap[v.config.AuthType]; ok {
return verifier
}
return nil
}
func (v *AuthVerifier) buildVerifierMap() map[string]authVerifierFunc {
return map[string]authVerifierFunc{
"hmac-sha256": v.verifyHMAC256,
"hmac-sha1": v.verifyHMAC1,
"bearer": func(r *http.Request, _ []byte) bool {
return v.verifyBearer(r)
},
"basic": func(r *http.Request, _ []byte) bool {
return v.verifyBasic(r)
},
}
}
func (v *AuthVerifier) handleUnknownAuthType() bool {
log.WithFields(log.Fields{
"authType": v.config.AuthType,
}).Warnf("Unknown auth type, rejecting")
return false
}
func (v *AuthVerifier) verifyHMAC256(r *http.Request, payload []byte) bool {
if v.config.Secret == "" {
log.Warnf("HMAC-SHA256 auth requires secret")
return false
}
headerName := v.config.AuthHeader
if headerName == "" {
headerName = "X-Webhook-Signature"
}
signature := r.Header.Get(headerName)
if signature == "" {
log.Debugf("Missing signature header: %s", headerName)
return false
}
expectedSig := strings.TrimPrefix(signature, "sha256=")
mac := hmac.New(sha256.New, []byte(v.config.Secret))
mac.Write(payload)
computedSig := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expectedSig), []byte(computedSig))
}
func (v *AuthVerifier) verifyHMAC1(r *http.Request, payload []byte) bool {
if v.config.Secret == "" {
log.Warnf("HMAC-SHA1 auth requires secret")
return false
}
headerName := v.config.AuthHeader
if headerName == "" {
headerName = "X-Webhook-Signature"
}
signature := r.Header.Get(headerName)
if signature == "" {
log.Debugf("Missing signature header: %s", headerName)
return false
}
expectedSig := strings.TrimPrefix(signature, "sha1=")
mac := hmac.New(sha1.New, []byte(v.config.Secret))
mac.Write(payload)
computedSig := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expectedSig), []byte(computedSig))
}
func (v *AuthVerifier) verifyBearer(r *http.Request) bool {
if v.config.Secret == "" {
log.Warnf("Bearer auth requires secret")
return false
}
authHeader := r.Header.Get("Authorization")
if !strings.HasPrefix(authHeader, "Bearer ") {
log.Debugf("Missing or invalid Bearer token")
return false
}
token := strings.TrimPrefix(authHeader, "Bearer ")
tokenBytes := []byte(token)
secretBytes := []byte(v.config.Secret)
return len(tokenBytes) == len(secretBytes) && subtle.ConstantTimeCompare(tokenBytes, secretBytes) == 1
}
func (v *AuthVerifier) verifyBasic(r *http.Request) bool {
if v.config.Secret == "" {
log.Warnf("Basic auth requires secret")
return false
}
username, password, ok := r.BasicAuth()
if !ok {
log.Debugf("Missing Basic auth header")
return false
}
return v.verifyBasicCredentials(username, password)
}
func (v *AuthVerifier) verifyBasicCredentials(username, password string) bool {
parts := strings.SplitN(v.config.Secret, ":", 2)
if len(parts) == 2 {
return v.verifyBasicWithUsername(username, password, parts[0], parts[1])
}
return v.verifyBasicPasswordOnly(password)
}
func (v *AuthVerifier) verifyBasicWithUsername(username, password, expectedUsername, expectedPassword string) bool {
usernameMatch := subtle.ConstantTimeCompare([]byte(username), []byte(expectedUsername))
passwordMatch := subtle.ConstantTimeCompare([]byte(password), []byte(expectedPassword))
return usernameMatch == 1 && passwordMatch == 1
}
func (v *AuthVerifier) verifyBasicPasswordOnly(password string) bool {
return subtle.ConstantTimeCompare([]byte(password), []byte(v.config.Secret)) == 1
}

View File

@ -0,0 +1,112 @@
package webhooks
import (
"github.com/OliveTin/OliveTin/internal/config"
)
// ApplyGitHubTemplate applies GitHub-specific template configurations
// This allows users to use simple template names instead of configuring everything manually
func ApplyGitHubTemplate(cfg *config.WebhookConfig, template string) {
applier := getTemplateApplier(template)
if applier != nil {
applier(cfg)
}
}
type templateApplier func(*config.WebhookConfig)
func getTemplateApplier(template string) templateApplier {
templateMap := map[string]templateApplier{
"github-push": applyGitHubPushTemplate,
"github-pr": applyGitHubPRTemplate,
"github-pull-request": applyGitHubPRTemplate,
"github-release": applyGitHubReleaseTemplate,
"github-workflow": applyGitHubWorkflowTemplate,
}
return templateMap[template]
}
func setDefaultAuth(cfg *config.WebhookConfig) {
if cfg.AuthHeader == "" {
cfg.AuthHeader = "X-Hub-Signature-256"
}
if cfg.AuthType == "" {
cfg.AuthType = "hmac-sha256"
}
}
func ensureMatchHeaders(cfg *config.WebhookConfig) {
if len(cfg.MatchHeaders) == 0 {
cfg.MatchHeaders = make(map[string]string)
}
}
func ensureExtract(cfg *config.WebhookConfig) {
if len(cfg.Extract) == 0 {
cfg.Extract = make(map[string]string)
}
}
func setExtractIfMissing(cfg *config.WebhookConfig, key, value string) {
if _, exists := cfg.Extract[key]; !exists {
cfg.Extract[key] = value
}
}
func setMatchHeaderIfMissing(cfg *config.WebhookConfig, key, value string) {
if _, exists := cfg.MatchHeaders[key]; !exists {
cfg.MatchHeaders[key] = value
}
}
func applyGitHubPushTemplate(cfg *config.WebhookConfig) {
setDefaultAuth(cfg)
ensureMatchHeaders(cfg)
setMatchHeaderIfMissing(cfg, "X-GitHub-Event", "push")
ensureExtract(cfg)
setExtractIfMissing(cfg, "git_repository", "$.repository.full_name")
setExtractIfMissing(cfg, "git_ref", "$.ref")
setExtractIfMissing(cfg, "git_commit", "$.head_commit.id")
setExtractIfMissing(cfg, "git_branch", "$.ref")
setExtractIfMissing(cfg, "git_message", "$.head_commit.message")
setExtractIfMissing(cfg, "git_author", "$.head_commit.author.name")
}
func applyGitHubPRTemplate(cfg *config.WebhookConfig) {
setDefaultAuth(cfg)
ensureMatchHeaders(cfg)
setMatchHeaderIfMissing(cfg, "X-GitHub-Event", "pull_request")
ensureExtract(cfg)
setExtractIfMissing(cfg, "pr_number", "$.number")
setExtractIfMissing(cfg, "pr_title", "$.pull_request.title")
setExtractIfMissing(cfg, "pr_author", "$.pull_request.user.login")
setExtractIfMissing(cfg, "pr_action", "$.action")
setExtractIfMissing(cfg, "git_repository", "$.repository.full_name")
setExtractIfMissing(cfg, "pr_state", "$.pull_request.state")
setExtractIfMissing(cfg, "pr_head_sha", "$.pull_request.head.sha")
}
func applyGitHubReleaseTemplate(cfg *config.WebhookConfig) {
setDefaultAuth(cfg)
ensureMatchHeaders(cfg)
setMatchHeaderIfMissing(cfg, "X-GitHub-Event", "release")
ensureExtract(cfg)
setExtractIfMissing(cfg, "release_action", "$.action")
setExtractIfMissing(cfg, "release_tag", "$.release.tag_name")
setExtractIfMissing(cfg, "release_name", "$.release.name")
setExtractIfMissing(cfg, "git_repository", "$.repository.full_name")
setExtractIfMissing(cfg, "release_author", "$.release.author.login")
}
func applyGitHubWorkflowTemplate(cfg *config.WebhookConfig) {
setDefaultAuth(cfg)
ensureMatchHeaders(cfg)
setMatchHeaderIfMissing(cfg, "X-GitHub-Event", "workflow_run")
ensureExtract(cfg)
setExtractIfMissing(cfg, "workflow_name", "$.workflow_run.name")
setExtractIfMissing(cfg, "workflow_status", "$.workflow_run.status")
setExtractIfMissing(cfg, "workflow_conclusion", "$.workflow_run.conclusion")
setExtractIfMissing(cfg, "git_repository", "$.repository.full_name")
setExtractIfMissing(cfg, "git_commit", "$.workflow_run.head_sha")
setExtractIfMissing(cfg, "git_branch", "$.workflow_run.head_branch")
}

View File

@ -0,0 +1,162 @@
package webhooks
import (
"io"
"net/http"
"github.com/OliveTin/OliveTin/internal/auth"
"github.com/OliveTin/OliveTin/internal/config"
"github.com/OliveTin/OliveTin/internal/executor"
log "github.com/sirupsen/logrus"
)
type ActionWebhookConfig struct {
Action *config.Action
Config config.WebhookConfig
}
type WebhookHandler struct {
cfg *config.Config
executor *executor.Executor
}
func NewWebhookHandler(cfg *config.Config, ex *executor.Executor) *WebhookHandler {
return &WebhookHandler{
cfg: cfg,
executor: ex,
}
}
func (h *WebhookHandler) HandleWebhook(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
payload, err := h.readPayload(r)
if err != nil {
http.Error(w, "Failed to read payload", http.StatusBadRequest)
return
}
matchingActions := h.findMatchingActions(r, payload)
if len(matchingActions) == 0 {
h.writeOKResponse(w, "no matching webhook actions")
return
}
processed := h.processMatchingActions(matchingActions, r, payload)
log.WithFields(log.Fields{
"matched": len(matchingActions),
"processed": processed,
}).Infof("Webhook processed")
h.writeOKResponse(w, "webhook actions")
}
func (h *WebhookHandler) readPayload(r *http.Request) ([]byte, error) {
maxSize := int64(1024 * 1024)
payload, err := io.ReadAll(io.LimitReader(r.Body, maxSize))
if err != nil {
log.WithFields(log.Fields{
"error": err,
}).Warnf("Failed to read webhook payload")
return nil, err
}
return payload, nil
}
func (h *WebhookHandler) writeOKResponse(w http.ResponseWriter, context string) {
w.WriteHeader(http.StatusOK)
if _, err := w.Write([]byte("OK")); err != nil {
log.WithError(err).Warnf("Failed to write response for %s", context)
}
}
func (h *WebhookHandler) processMatchingActions(matchingActions []ActionWebhookConfig, r *http.Request, payload []byte) int {
processed := 0
for _, actionConfig := range matchingActions {
if h.processWebhook(actionConfig, r, payload) {
processed++
}
}
return processed
}
func (h *WebhookHandler) findMatchingActions(r *http.Request, payload []byte) []ActionWebhookConfig {
var matches []ActionWebhookConfig
for _, action := range h.cfg.Actions {
matches = append(matches, h.findMatchingWebhooksForAction(action, r, payload)...)
}
return matches
}
func (h *WebhookHandler) findMatchingWebhooksForAction(action *config.Action, r *http.Request, payload []byte) []ActionWebhookConfig {
var matches []ActionWebhookConfig
for _, webhookConfig := range action.ExecOnWebhook {
webhookConfigCopy := webhookConfig
if webhookConfigCopy.Template != "" {
ApplyGitHubTemplate(&webhookConfigCopy, webhookConfigCopy.Template)
}
matcher := NewWebhookMatcher(webhookConfigCopy, r, payload)
if matcher.Matches() {
matches = append(matches, ActionWebhookConfig{
Action: action,
Config: webhookConfigCopy,
})
}
}
return matches
}
func (h *WebhookHandler) processWebhook(actionConfig ActionWebhookConfig, r *http.Request, payload []byte) bool {
verifier := NewAuthVerifier(actionConfig.Config)
if !verifier.Verify(r, payload) {
log.WithFields(log.Fields{
"actionTitle": actionConfig.Action.Title,
"authType": actionConfig.Config.AuthType,
}).Warnf("Webhook authentication failed")
return false
}
matcher := NewWebhookMatcher(actionConfig.Config, r, payload)
args, err := matcher.ExtractArguments()
if err != nil {
log.WithFields(log.Fields{
"actionTitle": actionConfig.Action.Title,
"error": err,
}).Warnf("Failed to extract webhook arguments")
return false
}
h.executeAction(actionConfig.Action, args)
return true
}
func (h *WebhookHandler) executeAction(action *config.Action, args map[string]string) {
binding := h.executor.FindBindingWithNoEntity(action)
if binding == nil {
log.WithFields(log.Fields{
"actionTitle": action.Title,
}).Warnf("Action binding not found, skipping execution")
return
}
req := &executor.ExecutionRequest{
Binding: binding,
Cfg: h.cfg,
Tags: []string{"webhook"},
Arguments: args,
AuthenticatedUser: auth.UserFromSystem(h.cfg, "webhook"),
}
h.executor.ExecRequest(req)
}

View File

@ -0,0 +1,55 @@
package webhooks
import (
"encoding/json"
"fmt"
"github.com/PaesslerAG/jsonpath"
)
type JSONMatcher struct {
payload interface{}
}
func NewJSONMatcher(payload []byte) (*JSONMatcher, error) {
var data interface{}
if err := json.Unmarshal(payload, &data); err != nil {
return nil, err
}
return &JSONMatcher{payload: data}, nil
}
func (m *JSONMatcher) MatchPath(pathExpr string, expectedValue string) (bool, error) {
value, err := jsonpath.Get(pathExpr, m.payload)
if err != nil {
return false, err
}
// Marshal to JSON for consistent string representation
jsonBytes, err := json.Marshal(value)
if err != nil {
return false, fmt.Errorf("failed to marshal extracted value: %w", err)
}
valueStr := string(jsonBytes)
return valueStr == expectedValue, nil
}
func (m *JSONMatcher) ExtractValue(pathExpr string) (string, error) {
value, err := jsonpath.Get(pathExpr, m.payload)
if err != nil {
return "", err
}
// Marshal to JSON for consistent string representation
jsonBytes, err := json.Marshal(value)
if err != nil {
return "", fmt.Errorf("failed to marshal extracted value: %w", err)
}
return string(jsonBytes), nil
}
func (m *JSONMatcher) GetPayload() interface{} {
return m.payload
}

View File

@ -0,0 +1,185 @@
package webhooks
import (
"net/http"
"regexp"
"strings"
"github.com/OliveTin/OliveTin/internal/config"
log "github.com/sirupsen/logrus"
)
type WebhookMatcher struct {
config config.WebhookConfig
req *http.Request
bodyBytes []byte
}
func NewWebhookMatcher(cfg config.WebhookConfig, r *http.Request, bodyBytes []byte) *WebhookMatcher {
return &WebhookMatcher{
config: cfg,
req: r,
bodyBytes: bodyBytes,
}
}
func (m *WebhookMatcher) Matches() bool {
if !m.matchHeaders() {
return false
}
if !m.matchQuery() {
return false
}
if !m.matchPath() {
return false
}
return true
}
func (m *WebhookMatcher) matchHeaders() bool {
if len(m.config.MatchHeaders) == 0 {
return true
}
for key, expectedValue := range m.config.MatchHeaders {
actualValue := m.req.Header.Get(key)
if !m.compareValues(actualValue, expectedValue) {
log.WithFields(log.Fields{
"header": key,
"expected": expectedValue,
"actual": actualValue,
}).Debugf("Header mismatch")
return false
}
}
return true
}
func (m *WebhookMatcher) matchQuery() bool {
if len(m.config.MatchQuery) == 0 {
return true
}
query := m.req.URL.Query()
for key, expectedValue := range m.config.MatchQuery {
actualValue := query.Get(key)
if !m.compareValues(actualValue, expectedValue) {
log.WithFields(log.Fields{
"query": key,
"expected": expectedValue,
"actual": actualValue,
}).Debugf("Query parameter mismatch")
return false
}
}
return true
}
func (m *WebhookMatcher) matchPath() bool {
if m.config.MatchPath == "" {
return true
}
jsonPath, expectedValue := m.parseMatchPath()
matcher, err := NewJSONMatcher(m.bodyBytes)
if err != nil {
log.WithFields(log.Fields{
"error": err,
}).Debugf("Failed to create JSON matcher")
return false
}
return m.matchPathValue(matcher, jsonPath, expectedValue)
}
func (m *WebhookMatcher) parseMatchPath() (string, string) {
parts := strings.SplitN(m.config.MatchPath, "=", 2)
jsonPath := parts[0]
expectedValue := ""
if len(parts) == 2 {
expectedValue = parts[1]
}
return jsonPath, expectedValue
}
func (m *WebhookMatcher) matchPathValue(matcher *JSONMatcher, jsonPath, expectedValue string) bool {
if expectedValue == "" {
_, err := matcher.ExtractValue(jsonPath)
return err == nil
}
matches, err := matcher.MatchPath(jsonPath, expectedValue)
if err != nil {
log.WithFields(log.Fields{
"jsonPath": jsonPath,
"error": err,
}).Debugf("Failed to match JSONPath")
return false
}
return matches
}
func (m *WebhookMatcher) compareValues(actual, expected string) bool {
if strings.HasPrefix(expected, "regex:") {
pattern := strings.TrimPrefix(expected, "regex:")
matched, err := regexp.MatchString(pattern, actual)
if err != nil {
log.WithFields(log.Fields{
"pattern": pattern,
"error": err,
}).Warnf("Invalid regex pattern")
return false
}
return matched
}
return actual == expected
}
func (m *WebhookMatcher) ExtractArguments() (map[string]string, error) {
matcher, err := NewJSONMatcher(m.bodyBytes)
if err != nil {
return nil, err
}
args := m.extractJSONPathValues(matcher)
m.addWebhookMetadata(args)
m.addWebhookHeaders(args)
return args, nil
}
func (m *WebhookMatcher) extractJSONPathValues(matcher *JSONMatcher) map[string]string {
args := make(map[string]string)
for argName, jsonPath := range m.config.Extract {
value, err := matcher.ExtractValue(jsonPath)
if err != nil {
log.WithFields(log.Fields{
"argName": argName,
"jsonPath": jsonPath,
"error": err,
}).Debugf("Failed to extract value")
continue
}
args[argName] = value
}
return args
}
func (m *WebhookMatcher) addWebhookMetadata(args map[string]string) {
args["webhook_method"] = m.req.Method
args["webhook_path"] = m.req.URL.Path
args["webhook_query"] = m.req.URL.RawQuery
}
func (m *WebhookMatcher) addWebhookHeaders(args map[string]string) {
for key, values := range m.req.Header {
if len(values) > 0 {
args["webhook_header_"+strings.ToLower(key)] = values[0]
}
}
}