~james/core

core/idresolver/resolver.go -rw-r--r-- 4.5 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 14 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
package idresolver

import (
	"context"
	"fmt"
	"net"
	"net/http"
	"slices"
	"strings"
	"sync"
	"time"

	"os"

	"tangled.org/core/netutil"

	"github.com/bluesky-social/indigo/atproto/identity"
	"github.com/bluesky-social/indigo/atproto/identity/redisdir"
	"github.com/bluesky-social/indigo/atproto/syntax"
	"github.com/carlmjohnson/versioninfo"
)

type Resolver struct {
	directory identity.Directory
	base      *identity.BaseDirectory
}

func BaseDirectory(plcUrl string) *identity.BaseDirectory {
	base := identity.BaseDirectory{
		PLCURL: plcUrl,
		HTTPClient: http.Client{
			Timeout: time.Second * 10,
			Transport: &http.Transport{
				// did documents are fetched for caller-supplied dids, so the transport refuses
				// non-public addresses; IDRESOLVER_DEV=1 lifts the guard for local plc instances
				DialContext: netutil.SSRFDialer(os.Getenv("IDRESOLVER_DEV") == "1").DialContext,
				// would want this around 100ms for services doing lots of handle resolution. Impacts PLC connections as well, but not too bad.
				IdleConnTimeout: time.Millisecond * 1000,
				MaxIdleConns:    100,
			},
		},
		Resolver: net.Resolver{
			Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
				d := net.Dialer{Timeout: time.Second * 3}
				return d.DialContext(ctx, network, address)
			},
		},
		TryAuthoritativeDNS: true,
		// primary Bluesky PDS instance only supports HTTP resolution method
		SkipDNSDomainSuffixes: []string{".bsky.social"},
		UserAgent:             "indigo-identity/" + versioninfo.Short(),
	}
	return &base
}

func DefaultResolver(plcUrl string) *Resolver {
	base := BaseDirectory(plcUrl)
	cached := identity.NewCacheDirectory(base, 250_000, time.Hour*24, time.Minute*2, time.Minute*5)
	return &Resolver{
		directory: cached,
		base:      base,
	}
}

func RedisDirectory(base *identity.BaseDirectory, url string) (identity.Directory, error) {
	hitTTL := time.Hour * 24
	errTTL := time.Second * 30
	invalidHandleTTL := time.Minute * 5
	return redisdir.NewRedisDirectory(base, url, hitTTL, errTTL, invalidHandleTTL, 10000)
}

func RedisResolver(redisUrl, plcUrl string) (*Resolver, error) {
	base := BaseDirectory(plcUrl)
	directory, err := RedisDirectory(base, redisUrl)
	if err != nil {
		return nil, err
	}
	return &Resolver{
		directory: directory,
		base:      base,
	}, nil
}

type handleResolver interface {
	ResolveHandle(ctx context.Context, h syntax.Handle) (syntax.DID, error)
}

func (r *Resolver) ResolveHandle(ctx context.Context, handle syntax.Handle) (syntax.DID, error) {
	if hr, ok := r.directory.(handleResolver); ok {
		return hr.ResolveHandle(ctx, handle)
	}
	return r.base.ResolveHandle(ctx, handle)
}

func (r *Resolver) ResolveAtIdentifier(ctx context.Context, input string) (*identity.Identity, error) {
	if did, err := syntax.ParseDID(input); err == nil {
		return r.directory.LookupDID(ctx, did)
	}
	handle, err := syntax.ParseHandle(input)
	if err != nil {
		return nil, fmt.Errorf("not a did or handle: %w", err)
	}
	handle = handle.Normalize()
	did, err := r.ResolveHandle(ctx, handle)
	if err != nil {
		return nil, fmt.Errorf("resolve handle %q: %w", handle, err)
	}
	ident, err := r.directory.LookupDID(ctx, did)
	if err != nil {
		return nil, fmt.Errorf("lookup did for %q: %w", handle, err)
	}
	aka := "at://" + handle.String()
	if !slices.ContainsFunc(ident.AlsoKnownAs, func(s string) bool { return strings.EqualFold(s, aka) }) {
		return nil, fmt.Errorf("handle %q not declared in alsoKnownAs for %s", handle, did)
	}
	return ident, nil
}

func (r *Resolver) ResolveIdent(ctx context.Context, arg string) (*identity.Identity, error) {
	id, err := syntax.ParseAtIdentifier(arg)
	if err != nil {
		return nil, err
	}

	return r.directory.Lookup(ctx, id)
}

func (r *Resolver) ResolveIdents(ctx context.Context, idents []string) []*identity.Identity {
	results := make([]*identity.Identity, len(idents))
	var wg sync.WaitGroup

	done := make(chan struct{})
	defer close(done)

	for idx, ident := range idents {
		wg.Add(1)
		go func(index int, id string) {
			defer wg.Done()

			select {
			case <-ctx.Done():
				results[index] = nil
			case <-done:
				results[index] = nil
			default:
				identity, _ := r.ResolveIdent(ctx, id)
				results[index] = identity
			}
		}(idx, ident)
	}

	wg.Wait()
	return results
}

func (r *Resolver) InvalidateIdent(ctx context.Context, arg string) error {
	id, err := syntax.ParseAtIdentifier(arg)
	if err != nil {
		return err
	}

	return r.directory.Purge(ctx, id)
}

func (r *Resolver) Directory() identity.Directory {
	return r.directory
}