Webhook-to-SSE gateway with hierarchical topic routing and signature verification
1package main
2
3import (
4 "log"
5 "os"
6 "strings"
7
8 "github.com/fsnotify/fsnotify"
9 "gopkg.in/yaml.v3"
10)
11
12type Configuration struct {
13 Paths map[string]PathConfiguration `yaml:"paths"`
14}
15
16type PathConfiguration struct {
17 Verify string `yaml:"verify"`
18 Secret string `yaml:"secret"`
19 SignatureHeader string `yaml:"signature_header"`
20 SubscribeSecret string `yaml:"subscribe_secret"`
21}
22
23func LoadConfiguration(path string) (*Configuration, error) {
24 data, err := os.ReadFile(path)
25 if err != nil {
26 return nil, err
27 }
28 expanded := os.ExpandEnv(string(data))
29 var cfg Configuration
30 if err := yaml.Unmarshal([]byte(expanded), &cfg); err != nil {
31 return nil, err
32 }
33 return &cfg, nil
34}
35
36var newWatcher = fsnotify.NewWatcher
37
38func WatchConfiguration(path string, callback func(*Configuration)) (func(), error) {
39 watcher, err := newWatcher()
40 if err != nil {
41 return nil, err
42 }
43 if err := watcher.Add(path); err != nil {
44 watcher.Close()
45 return nil, err
46 }
47
48 done := make(chan struct{})
49 go func() {
50 defer watcher.Close()
51 for {
52 select {
53 case <-done:
54 return
55 case event := <-watcher.Events:
56 if event.Has(fsnotify.Write) || event.Has(fsnotify.Create) {
57 cfg, err := LoadConfiguration(path)
58 if err != nil {
59 log.Printf("reloading configuration: %v", err)
60 continue
61 }
62 callback(cfg)
63 }
64 }
65 }
66 }()
67
68 return func() { close(done) }, nil
69}
70
71func (c *Configuration) LookupSubscribeSecret(path string) string {
72 if c == nil {
73 return ""
74 }
75 for {
76 if pc, ok := c.Paths[path]; ok && pc.SubscribeSecret != "" {
77 return pc.SubscribeSecret
78 }
79 i := strings.LastIndex(path, "/")
80 if i < 0 {
81 break
82 }
83 path = path[:i]
84 }
85 return ""
86}
87
88func (c *Configuration) LookupVerification(path string) *PathConfiguration {
89 if c == nil {
90 return nil
91 }
92 for {
93 if pc, ok := c.Paths[path]; ok && pc.Verify != "" {
94 return &pc
95 }
96 i := strings.LastIndex(path, "/")
97 if i < 0 {
98 break
99 }
100 path = path[:i]
101 }
102 return nil
103}