Webhook-to-SSE gateway with hierarchical topic routing and signature verification
1package main
2
3import (
4 "fmt"
5 "net/url"
6 "strings"
7)
8
9type Filter struct {
10 Path string
11 Value string
12}
13
14func ParseFilters(query url.Values) []Filter {
15 raw := query["filter"]
16 if len(raw) == 0 {
17 return nil
18 }
19 filters := make([]Filter, 0, len(raw))
20 for _, f := range raw {
21 path, value, ok := strings.Cut(f, ":")
22 if !ok {
23 continue
24 }
25 filters = append(filters, Filter{Path: path, Value: value})
26 }
27 return filters
28}
29
30func MatchAll(filters []Filter, event *Event) bool {
31 for _, f := range filters {
32 if !matchOne(f, event) {
33 return false
34 }
35 }
36 return true
37}
38
39func matchOne(f Filter, event *Event) bool {
40 val, ok := resolveField(f.Path, event)
41 if !ok {
42 return false
43 }
44 return val == f.Value
45}
46
47func resolveField(path string, event *Event) (string, bool) {
48 parts := strings.Split(path, ".")
49
50 switch parts[0] {
51 case "id":
52 return event.ID, len(parts) == 1
53 case "path":
54 return event.Path, len(parts) == 1
55 case "timestamp":
56 return event.Timestamp.Format("2006-01-02T15:04:05Z07:00"), len(parts) == 1
57 case "headers":
58 if len(parts) != 2 {
59 return "", false
60 }
61 val, ok := event.Headers[parts[1]]
62 return val, ok
63 case "payload":
64 return navigateJSON(event.Payload, parts[1:])
65 default:
66 return "", false
67 }
68}
69
70func navigateJSON(val any, parts []string) (string, bool) {
71 if len(parts) == 0 {
72 return fmt.Sprintf("%v", val), true
73 }
74
75 m, ok := val.(map[string]any)
76 if !ok {
77 return "", false
78 }
79 next, ok := m[parts[0]]
80 if !ok {
81 return "", false
82 }
83 return navigateJSON(next, parts[1:])
84}