Caddy module to require at-proto authentication and restrict routes to DIDs
1package resolver
2
3import (
4 "context"
5 "fmt"
6 "net"
7 "net/http"
8 "strings"
9 "time"
10
11 "github.com/bluesky-social/indigo/atproto/identity"
12 "github.com/bluesky-social/indigo/atproto/syntax"
13)
14
15// Resolver handles AT Protocol identity resolution (Handles, DID:PLC, DID:Web).
16type Resolver struct {
17 dir identity.Directory
18}
19
20// New initializes a new identity resolver using the default Bluesky directory.
21// This handles DNS TXT, HTTPS well-known, PLC, and DID:Web lookups with caching.
22func New() *Resolver {
23 return &Resolver{
24 dir: identity.DefaultDirectory(),
25 }
26}
27
28// NewWithAllowedCIDRs initializes a new identity resolver with a custom HTTP client
29// that allows connections to the specified private CIDRs.
30func NewWithAllowedCIDRs(allowedCIDRs []net.IPNet) *Resolver {
31 base := identity.BaseDirectory{
32 HTTPClient: createCustomHTTPClient(allowedCIDRs),
33 }
34
35 cached := identity.NewCacheDirectory(&base, 250000, time.Hour*24, time.Minute*2, time.Minute*5)
36
37 return &Resolver{
38 dir: cached,
39 }
40}
41
42func createCustomHTTPClient(allowedCIDRs []net.IPNet) http.Client {
43 if len(allowedCIDRs) == 0 {
44 return http.Client{}
45 }
46
47 transport := http.DefaultTransport.(*http.Transport).Clone()
48 originalDial := transport.DialContext
49
50 transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
51 host, _, err := net.SplitHostPort(addr)
52 if err != nil {
53 host = addr
54 }
55
56 ip := net.ParseIP(host)
57 if ip == nil {
58 addrs, err := net.DefaultResolver.LookupIP(ctx, "ip", host)
59 if err != nil {
60 return nil, fmt.Errorf("failed to resolve %s: %w", host, err)
61 }
62 for _, a := range addrs {
63 if isPublicIP(a) {
64 return originalDial(ctx, network, addr)
65 }
66 for _, cidr := range allowedCIDRs {
67 if cidr.Contains(a) {
68 return originalDial(ctx, network, addr)
69 }
70 }
71 }
72 return nil, fmt.Errorf("resolved IP for %s is not public and not in allowed private CIDRs", host)
73 }
74
75 if isPublicIP(ip) {
76 return originalDial(ctx, network, addr)
77 }
78
79 for _, cidr := range allowedCIDRs {
80 if cidr.Contains(ip) {
81 return originalDial(ctx, network, addr)
82 }
83 }
84
85 return nil, fmt.Errorf("address %s is not public and not in allowed private CIDRs", addr)
86 }
87
88 return http.Client{
89 Transport: transport,
90 Timeout: 10 * time.Second,
91 }
92}
93
94func isPublicIP(ip net.IP) bool {
95 if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsUnspecified() {
96 return false
97 }
98 if ip4 := ip.To4(); ip4 != nil {
99 return (ip4[0] != 10) && (ip4[0] != 172 || ip4[1] < 16 || ip4[1] > 31) && (ip4[0] != 192 || ip4[1] != 168)
100 }
101 _, ipv6private, _ := net.ParseCIDR("fc00::/7")
102 return ipv6private == nil || !ipv6private.Contains(ip)
103}
104
105// ResolveIdentifier converts either a handle (e.g., @user.com) or a DID into
106// a verified DID, validating it against the directory.
107func (r *Resolver) ResolveIdentifier(ctx context.Context, ident string) (string, error) {
108 ident = strings.TrimPrefix(ident, "@")
109
110 atID, err := syntax.ParseAtIdentifier(ident)
111 if err != nil {
112 return "", fmt.Errorf("invalid identifier '%s': %w", ident, err)
113 }
114
115 id, err := r.dir.Lookup(ctx, atID)
116 if err != nil {
117 return "", fmt.Errorf("failed to resolve identity '%s': %w", ident, err)
118 }
119
120 if id == nil {
121 return "", fmt.Errorf("identity '%s' not found", ident)
122 }
123
124 return id.DID.String(), nil
125}
126
127// GetPDSEndpoint returns the Personal Data Server (PDS) URL for a given DID or handle.
128func (r *Resolver) GetPDSEndpoint(ctx context.Context, ident string) (string, error) {
129 ident = strings.TrimPrefix(ident, "@")
130
131 atID, err := syntax.ParseAtIdentifier(ident)
132 if err != nil {
133 return "", fmt.Errorf("invalid identifier '%s': %w", ident, err)
134 }
135
136 id, err := r.dir.Lookup(ctx, atID)
137 if err != nil {
138 return "", fmt.Errorf("failed to resolve identity '%s': %w", ident, err)
139 }
140
141 if id == nil {
142 return "", fmt.Errorf("identity '%s' not found", ident)
143 }
144
145 pds := id.PDSEndpoint()
146 if pds == "" {
147 return "", fmt.Errorf("no PDS endpoint found for identity '%s'", ident)
148 }
149
150 return pds, nil
151}