1package identity
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "io"
8 "net"
9 "net/http"
10 "strings"
11
12 "github.com/bluesky-social/indigo/atproto/syntax"
13 "github.com/bluesky-social/indigo/util"
14)
15
16func ResolveHandleFromTXT(ctx context.Context, handle string) (string, error) {
17 name := fmt.Sprintf("_atproto.%s", handle)
18 recs, err := net.LookupTXT(name)
19 if err != nil {
20 return "", fmt.Errorf("handle could not be resolved via txt: %w", err)
21 }
22
23 for _, rec := range recs {
24 if strings.HasPrefix(rec, "did=") {
25 maybeDid := strings.Split(rec, "did=")[1]
26 if _, err := syntax.ParseDID(maybeDid); err == nil {
27 return maybeDid, nil
28 }
29 }
30 }
31
32 return "", fmt.Errorf("handle could not be resolved via txt: no record found")
33}
34
35func ResolveHandleFromWellKnown(ctx context.Context, cli *http.Client, handle string) (string, error) {
36 ustr := fmt.Sprintf("https://%s/.well-known/atproto-did", handle)
37 req, err := http.NewRequestWithContext(
38 ctx,
39 "GET",
40 ustr,
41 nil,
42 )
43 if err != nil {
44 return "", fmt.Errorf("handle could not be resolved via web: %w", err)
45 }
46
47 resp, err := cli.Do(req)
48 if err != nil {
49 return "", fmt.Errorf("handle could not be resolved via web: %w", err)
50 }
51 defer resp.Body.Close()
52
53 b, err := io.ReadAll(resp.Body)
54 if err != nil {
55 return "", fmt.Errorf("handle could not be resolved via web: %w", err)
56 }
57
58 if resp.StatusCode != http.StatusOK {
59 return "", fmt.Errorf("handle could not be resolved via web: invalid status code %d", resp.StatusCode)
60 }
61
62 maybeDid := string(b)
63
64 if _, err := syntax.ParseDID(maybeDid); err != nil {
65 return "", fmt.Errorf("handle could not be resolved via web: invalid did in document")
66 }
67
68 return maybeDid, nil
69}
70
71func ResolveHandle(ctx context.Context, cli *http.Client, handle string) (string, error) {
72 if cli == nil {
73 cli = util.RobustHTTPClient()
74 }
75
76 _, err := syntax.ParseHandle(handle)
77 if err != nil {
78 return "", err
79 }
80
81 if maybeDidFromTxt, err := ResolveHandleFromTXT(ctx, handle); err == nil {
82 return maybeDidFromTxt, nil
83 }
84
85 if maybeDidFromWeb, err := ResolveHandleFromWellKnown(ctx, cli, handle); err == nil {
86 return maybeDidFromWeb, nil
87 }
88
89 return "", fmt.Errorf("handle could not be resolved")
90}
91
92func DidToDocUrl(did string) (string, error) {
93 if strings.HasPrefix(did, "did:plc:") {
94 return fmt.Sprintf("https://plc.directory/%s", did), nil
95 } else if after, ok := strings.CutPrefix(did, "did:web:"); ok {
96 return fmt.Sprintf("https://%s/.well-known/did.json", after), nil
97 } else {
98 return "", fmt.Errorf("did was not a supported did type")
99 }
100}
101
102func FetchDidDoc(ctx context.Context, cli *http.Client, did string) (*DidDoc, error) {
103 if cli == nil {
104 cli = util.RobustHTTPClient()
105 }
106
107 ustr, err := DidToDocUrl(did)
108 if err != nil {
109 return nil, err
110 }
111
112 req, err := http.NewRequestWithContext(ctx, "GET", ustr, nil)
113 if err != nil {
114 return nil, err
115 }
116
117 resp, err := cli.Do(req)
118 if err != nil {
119 return nil, err
120 }
121 defer resp.Body.Close()
122
123 if resp.StatusCode != 200 {
124 io.Copy(io.Discard, resp.Body)
125 return nil, fmt.Errorf("unable to find did doc at url. did: %s. url: %s", did, ustr)
126 }
127
128 var diddoc DidDoc
129 if err := json.NewDecoder(resp.Body).Decode(&diddoc); err != nil {
130 return nil, err
131 }
132
133 return &diddoc, nil
134}
135
136func FetchDidData(ctx context.Context, cli *http.Client, did string) (*DidData, error) {
137 if cli == nil {
138 cli = util.RobustHTTPClient()
139 }
140
141 var ustr string
142 ustr = fmt.Sprintf("https://plc.directory/%s/data", did)
143
144 req, err := http.NewRequestWithContext(ctx, "GET", ustr, nil)
145 if err != nil {
146 return nil, err
147 }
148
149 resp, err := cli.Do(req)
150 if err != nil {
151 return nil, err
152 }
153 defer resp.Body.Close()
154
155 if resp.StatusCode != 200 {
156 io.Copy(io.Discard, resp.Body)
157 return nil, fmt.Errorf("could not find identity in plc registry")
158 }
159
160 var diddata DidData
161 if err := json.NewDecoder(resp.Body).Decode(&diddata); err != nil {
162 return nil, err
163 }
164
165 return &diddata, nil
166}
167
168func FetchDidAuditLog(ctx context.Context, cli *http.Client, did string) (DidAuditLog, error) {
169 if cli == nil {
170 cli = util.RobustHTTPClient()
171 }
172
173 var ustr string
174 ustr = fmt.Sprintf("https://plc.directory/%s/log/audit", did)
175
176 req, err := http.NewRequestWithContext(ctx, "GET", ustr, nil)
177 if err != nil {
178 return nil, err
179 }
180
181 resp, err := http.DefaultClient.Do(req)
182 if err != nil {
183 return nil, err
184 }
185 defer resp.Body.Close()
186
187 if resp.StatusCode != 200 {
188 io.Copy(io.Discard, resp.Body)
189 return nil, fmt.Errorf("could not find identity in plc registry")
190 }
191
192 var didlog DidAuditLog
193 if err := json.NewDecoder(resp.Body).Decode(&didlog); err != nil {
194 return nil, err
195 }
196
197 return didlog, nil
198}
199
200func ResolveService(ctx context.Context, cli *http.Client, did string) (string, error) {
201 if cli == nil {
202 cli = util.RobustHTTPClient()
203 }
204
205 diddoc, err := FetchDidDoc(ctx, cli, did)
206 if err != nil {
207 return "", err
208 }
209
210 var service string
211 for _, svc := range diddoc.Service {
212 if svc.Id == "#atproto_pds" {
213 service = svc.ServiceEndpoint
214 }
215 }
216
217 if service == "" {
218 return "", fmt.Errorf("could not find atproto_pds service in identity services")
219 }
220
221 return service, nil
222}