1package identity
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "net"
8 "net/http"
9
10 "github.com/bluesky-social/indigo/atproto/syntax"
11 "golang.org/x/time/rate"
12)
13
14// The zero value ('BaseDirectory{}') is a usable Directory.
15type BaseDirectory struct {
16 // if non-empty, this string should have URL method, hostname, and optional port; it should not have a path or trailing slash
17 PLCURL string
18 // If not nil, this limiter will be used to rate-limit requests to the PLCURL
19 PLCLimiter *rate.Limiter
20 // If not nil, this function will be called inline with DID Web lookups, and can be used to limit the number of requests to a given hostname
21 DIDWebLimitFunc func(ctx context.Context, hostname string) error
22 // HTTP client used for did:web, did:plc, and HTTP (well-known) handle resolution
23 HTTPClient http.Client
24 // DNS resolver used for DNS handle resolution. Calling code can use a custom Dialer to query against a specific DNS server, or re-implement the interface for even more control over the resolution process
25 Resolver net.Resolver
26 // when doing DNS handle resolution, should this resolver attempt re-try against an authoritative nameserver if the first TXT lookup fails?
27 TryAuthoritativeDNS bool
28 // set of handle domain suffixes for for which DNS handle resolution will be skipped
29 SkipDNSDomainSuffixes []string
30 // set of fallback DNS servers (eg, domain registrars) to try as a fallback. each entry should be "ip:port", eg "8.8.8.8:53"
31 FallbackDNSServers []string
32 // skips bi-directional verification of handles when doing DID lookups (eg, `LookupDID`). Does not impact direct resolution (`ResolveHandle`) or handle-specific lookup (`LookupHandle`).
33 //
34 // The intended use-case for this flag is as an optimization for services which do not care about handles, but still want to use the `Directory` interface (instead of `ResolveDID`). For example, relay implementations, or services validating inter-service auth requests.
35 SkipHandleVerification bool
36 // User-Agent header for HTTP requests. Optional (ignored if empty string).
37 UserAgent string
38}
39
40var _ Directory = (*BaseDirectory)(nil)
41var _ Resolver = (*BaseDirectory)(nil)
42
43func (d *BaseDirectory) LookupHandle(ctx context.Context, h syntax.Handle) (*Identity, error) {
44 h = h.Normalize()
45 did, err := d.ResolveHandle(ctx, h)
46 if err != nil {
47 return nil, err
48 }
49 doc, err := d.ResolveDID(ctx, did)
50 if err != nil {
51 return nil, err
52 }
53 ident := ParseIdentity(doc)
54 declared, err := ident.DeclaredHandle()
55 if err != nil {
56 return nil, fmt.Errorf("could not verify handle/DID match: %w", err)
57 }
58 // NOTE: DeclaredHandle() returns a normalized handle, and we already normalized 'h' above
59 if declared != h {
60 return nil, fmt.Errorf("%w: %s != %s", ErrHandleMismatch, declared, h)
61 }
62 ident.Handle = declared
63
64 return &ident, nil
65}
66
67func (d *BaseDirectory) LookupDID(ctx context.Context, did syntax.DID) (*Identity, error) {
68 doc, err := d.ResolveDID(ctx, did)
69 if err != nil {
70 return nil, err
71 }
72 ident := ParseIdentity(doc)
73 if d.SkipHandleVerification {
74 ident.Handle = syntax.HandleInvalid
75 return &ident, nil
76 }
77 declared, err := ident.DeclaredHandle()
78 if errors.Is(err, ErrHandleNotDeclared) {
79 ident.Handle = syntax.HandleInvalid
80 } else if err != nil {
81 return nil, fmt.Errorf("could not parse handle from DID document: %w", err)
82 } else {
83 // if a handle was declared, resolve it
84 resolvedDID, err := d.ResolveHandle(ctx, declared)
85 if err != nil {
86 if errors.Is(err, ErrHandleNotFound) || errors.Is(err, ErrHandleResolutionFailed) {
87 ident.Handle = syntax.HandleInvalid
88 } else {
89 return nil, err
90 }
91 } else if resolvedDID != did {
92 ident.Handle = syntax.HandleInvalid
93 } else {
94 ident.Handle = declared
95 }
96 }
97
98 return &ident, nil
99}
100
101func (d *BaseDirectory) Lookup(ctx context.Context, a syntax.AtIdentifier) (*Identity, error) {
102 handle, err := a.AsHandle()
103 if nil == err { // if *not* an error
104 return d.LookupHandle(ctx, handle)
105 }
106 did, err := a.AsDID()
107 if nil == err { // if *not* an error
108 return d.LookupDID(ctx, did)
109 }
110 return nil, fmt.Errorf("at-identifier neither a Handle nor a DID")
111}
112
113func (d *BaseDirectory) Purge(ctx context.Context, atid syntax.AtIdentifier) error {
114 // BaseDirectory itself does not implement caching
115 return nil
116}