repo_description: Open-source, self-hosted note-taking tool built for quick capture. Markdown-native, lightweight, and fully yours.
repo_topics: docker foss go markdown memo microblog note-taking notecard own-your-data react self-hosted social-network sqlite
code: package webhook
  
  import (
  	"net"
  	"net/url"
  
  	"google.golang.org/grpc/codes"
  	"google.golang.org/grpc/status"
  )
  
  // reservedCIDRs lists IP ranges that must never be targeted by outbound webhook requests.
  // Covers loopback, RFC-1918 private, link-local (including cloud IMDS at 169.254.169.254),
  // and their IPv6 equivalents.
  var reservedCIDRs = []string{
  	"127.0.0.0/8",    // IPv4 loopback
  	"10.0.0.0/8",     // RFC-1918 class A
  	"172.16.0.0/12",  // RFC-1918 class B
  	"192.168.0.0/16", // RFC-1918 class C
  	"169.254.0.0/16", // Link-local / cloud IMDS
  	"::1/128",        // IPv6 loopback
  	"fc00::/7",       // IPv6 unique local
  	"fe80::/10",      // IPv6 link-local
  }
  
  // reservedNetworks is the parsed form of reservedCIDRs, built once at startup.
  var reservedNetworks []*net.IPNet
  
  func init() {
  	for _, cidr := range reservedCIDRs {
  		_, network, err := net.ParseCIDR(cidr)
  		if err != nil {
  			panic("webhook: invalid reserved CIDR " + cidr + ": " + err.Error())
  		}
  		reservedNetworks = append(reservedNetworks, network)
  	}
  }
  
  // AllowPrivateIPs controls whether webhook URLs may resolve to reserved/private
  // IP addresses. When true, the SSRF protection is disabled. This is useful for
  // self-hosted deployments where webhooks target services on the local network.
  var AllowPrivateIPs bool
  
  // isReservedIP reports whether ip falls within any reserved/private range.
  func isReservedIP(ip net.IP) bool {
  	if AllowPrivateIPs {
  		return false
  	}
  	for _, network := range reservedNetworks {
  		if network.Contains(ip) {
  			return true
  		}
  	}
  	return false
  }
  
  // ValidateURL checks that rawURL:
  //  1. Parses as a valid absolute URL.
  //  2. Uses the http or https scheme.
  //  3. Does not resolve to a reserved/private IP address.
  //
  // It returns a gRPC InvalidArgument status error so callers can return it directly.
  func ValidateURL(rawURL string) error {
  	u, err := url.ParseRequestURI(rawURL)
  	if err != nil {
  		return status.Errorf(codes.InvalidArgument, "invalid webhook URL: %v", err)
  	}
  	if u.Scheme != "http" && u.Scheme != "https" {
  		return status.Errorf(codes.InvalidArgument, "webhook URL must use http or https scheme, got %q", u.Scheme)
  	}
  
  	ips, err := net.LookupHost(u.Hostname())
  	if err != nil {
  		return status.Errorf(codes.InvalidArgument, "webhook URL hostname could not be resolved: %v", err)
  	}
  
  	for _, ipStr := range ips {
  		ip := net.ParseIP(ipStr)
  		if ip != nil && isReservedIP(ip) {
  			return status.Errorf(codes.InvalidArgument, "webhook URL must not resolve to a reserved or private IP address")
  		}
  	}
  	return nil
  }
  