56 lines
1.5 KiB
Go
56 lines
1.5 KiB
Go
// Webhook delivery proxy — pre-fix v3.2.0
|
|
// Internal infra service; receives webhook URLs from tenants and forwards
|
|
// the body after an "allowlist" check.
|
|
package webhookproxy
|
|
|
|
import (
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
)
|
|
|
|
var internalCIDRs = []string{"10.0.0.0/8", "192.168.0.0/16", "169.254.0.0/16", "127.0.0.0/8"}
|
|
|
|
func isInternalIP(addr string) bool {
|
|
ip := net.ParseIP(addr)
|
|
for _, c := range internalCIDRs {
|
|
_, n, _ := net.ParseCIDR(c)
|
|
if n.Contains(ip) { return true }
|
|
}
|
|
return false
|
|
}
|
|
|
|
// validateTarget resolves the hostname ONCE and checks against the deny list.
|
|
// Returns nil if safe.
|
|
func validateTarget(rawurl string) error {
|
|
u, err := url.Parse(rawurl)
|
|
if err != nil { return err }
|
|
if u.Scheme != "http" && u.Scheme != "https" {
|
|
return ErrBadScheme
|
|
}
|
|
host := u.Hostname()
|
|
ips, err := net.LookupIP(host)
|
|
if err != nil { return err }
|
|
for _, ip := range ips {
|
|
if isInternalIP(ip.String()) { return ErrInternalTarget }
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func DeliverWebhook(tenant string, rawurl string, body []byte) (int, error) {
|
|
if err := validateTarget(rawurl); err != nil {
|
|
return 0, err
|
|
}
|
|
// Build request and send. Note: net/http will perform ITS OWN DNS lookup
|
|
// when dialing the URL — independent of validateTarget's earlier lookup.
|
|
req, _ := http.NewRequest("POST", rawurl, strings.NewReader(string(body)))
|
|
req.Header.Set("X-Webhook-Tenant", tenant)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil { return 0, err }
|
|
defer resp.Body.Close()
|
|
io.Copy(io.Discard, resp.Body)
|
|
return resp.StatusCode, nil
|
|
}
|