porting all github actions from bluesky-social/indigo to tangled CI
at main 7.3 kB view raw
1package identity 2 3import ( 4 "context" 5 "errors" 6 "fmt" 7 "io" 8 "log/slog" 9 "net" 10 "net/http" 11 "strings" 12 "time" 13 14 "github.com/bluesky-social/indigo/atproto/syntax" 15) 16 17func parseTXTResp(res []string) (syntax.DID, error) { 18 for _, s := range res { 19 if strings.HasPrefix(s, "did=") { 20 parts := strings.SplitN(s, "=", 2) 21 did, err := syntax.ParseDID(parts[1]) 22 if err != nil { 23 return "", fmt.Errorf("%w: invalid DID in handle DNS record: %w", ErrHandleResolutionFailed, err) 24 } 25 return did, nil 26 } 27 } 28 return "", ErrHandleNotFound 29} 30 31// Does not cross-verify, only does the handle resolution step. 32func (d *BaseDirectory) ResolveHandleDNS(ctx context.Context, handle syntax.Handle) (syntax.DID, error) { 33 res, err := d.Resolver.LookupTXT(ctx, "_atproto."+handle.String()) 34 // check for NXDOMAIN 35 var dnsErr *net.DNSError 36 if errors.As(err, &dnsErr) { 37 if dnsErr.IsNotFound { 38 return "", fmt.Errorf("%w: %s", ErrHandleNotFound, handle) 39 } 40 } 41 if err != nil { 42 return "", fmt.Errorf("%w: %w", ErrHandleResolutionFailed, err) 43 } 44 return parseTXTResp(res) 45} 46 47// this is a variant of ResolveHandleDNS which first does an authoritative nameserver lookup, then queries there 48func (d *BaseDirectory) ResolveHandleDNSAuthoritative(ctx context.Context, handle syntax.Handle) (syntax.DID, error) { 49 // lookup nameserver using configured resolver 50 resNS, err := d.Resolver.LookupNS(ctx, handle.String()) 51 // check for NXDOMAIN 52 var dnsErr *net.DNSError 53 if errors.As(err, &dnsErr) { 54 if dnsErr.IsNotFound { 55 return "", ErrHandleNotFound 56 } 57 } 58 if err != nil { 59 return "", fmt.Errorf("%w: DNS error: %w", ErrHandleResolutionFailed, err) 60 } 61 if len(resNS) == 0 { 62 return "", ErrHandleNotFound 63 } 64 ns := resNS[0].Host 65 if !strings.Contains(ns, ":") { 66 ns = ns + ":53" 67 } 68 69 // create a custom resolver to use the specific nameserver for TXT lookup 70 resolver := &net.Resolver{ 71 PreferGo: true, 72 Dial: func(ctx context.Context, network, address string) (net.Conn, error) { 73 rd := net.Dialer{ 74 Timeout: time.Second * 5, 75 } 76 return rd.DialContext(ctx, network, ns) 77 }, 78 } 79 res, err := resolver.LookupTXT(ctx, "_atproto."+handle.String()) 80 // check for NXDOMAIN 81 if errors.As(err, &dnsErr) { 82 if dnsErr.IsNotFound { 83 return "", ErrHandleNotFound 84 } 85 } 86 if err != nil { 87 return "", fmt.Errorf("%w: DNS resolution failed: %w", ErrHandleResolutionFailed, err) 88 } 89 return parseTXTResp(res) 90} 91 92// variant of ResolveHandleDNS which uses any configured fallback DNS servers 93func (d *BaseDirectory) ResolveHandleDNSFallback(ctx context.Context, handle syntax.Handle) (syntax.DID, error) { 94 retErr := fmt.Errorf("no fallback servers configured") 95 var dnsErr *net.DNSError 96 for _, ns := range d.FallbackDNSServers { 97 // create a custom resolver to use the specific nameserver for TXT lookup 98 resolver := &net.Resolver{ 99 PreferGo: true, 100 Dial: func(ctx context.Context, network, address string) (net.Conn, error) { 101 rd := net.Dialer{ 102 Timeout: time.Second * 5, 103 } 104 return rd.DialContext(ctx, network, ns) 105 }, 106 } 107 res, err := resolver.LookupTXT(ctx, "_atproto."+handle.String()) 108 // check for NXDOMAIN 109 if errors.As(err, &dnsErr) { 110 if dnsErr.IsNotFound { 111 retErr = ErrHandleNotFound 112 continue 113 } 114 } 115 if err != nil { 116 retErr = fmt.Errorf("%w: %w", ErrHandleResolutionFailed, err) 117 continue 118 } 119 ret, err := parseTXTResp(res) 120 if err != nil { 121 retErr = err 122 continue 123 } 124 return ret, nil 125 } 126 return "", retErr 127} 128 129func (d *BaseDirectory) ResolveHandleWellKnown(ctx context.Context, handle syntax.Handle) (syntax.DID, error) { 130 req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://%s/.well-known/atproto-did", handle), nil) 131 if err != nil { 132 return "", fmt.Errorf("constructing HTTP request for handle resolution: %w", err) 133 } 134 if d.UserAgent != "" { 135 req.Header.Set("User-Agent", d.UserAgent) 136 } 137 138 resp, err := d.HTTPClient.Do(req) 139 if err != nil { 140 // check for NXDOMAIN 141 var dnsErr *net.DNSError 142 if errors.As(err, &dnsErr) { 143 if dnsErr.IsNotFound { 144 return "", fmt.Errorf("%w: DNS NXDOMAIN for HTTP well-known resolution of %s", ErrHandleNotFound, handle) 145 } 146 } 147 return "", fmt.Errorf("%w: HTTP well-known request error: %w", ErrHandleResolutionFailed, err) 148 } 149 defer resp.Body.Close() 150 if resp.ContentLength > 2048 { 151 // NOTE: intentionally not draining body 152 return "", fmt.Errorf("%w: HTTP well-known body too large for %s", ErrHandleResolutionFailed, handle) 153 } 154 if resp.StatusCode == http.StatusNotFound { 155 io.Copy(io.Discard, resp.Body) 156 return "", fmt.Errorf("%w: HTTP 404 for %s", ErrHandleNotFound, handle) 157 } 158 if resp.StatusCode != http.StatusOK { 159 io.Copy(io.Discard, resp.Body) 160 return "", fmt.Errorf("%w: HTTP well-known status %d for %s", ErrHandleResolutionFailed, resp.StatusCode, handle) 161 } 162 163 b, err := io.ReadAll(io.LimitReader(resp.Body, 2048)) 164 if err != nil { 165 return "", fmt.Errorf("%w: HTTP well-known body read for %s: %w", ErrHandleResolutionFailed, handle, err) 166 } 167 line := strings.TrimSpace(string(b)) 168 outDid, err := syntax.ParseDID(line) 169 if err != nil { 170 return outDid, fmt.Errorf("%w: invalid DID in HTTP well-known for %s", ErrHandleResolutionFailed, handle) 171 } 172 return outDid, err 173} 174 175func (d *BaseDirectory) ResolveHandle(ctx context.Context, handle syntax.Handle) (syntax.DID, error) { 176 // TODO: *could* do resolution in parallel, but expecting that sequential is sufficient to start 177 var dnsErr error 178 var did syntax.DID 179 180 handle = handle.Normalize() 181 182 if handle.IsInvalidHandle() { 183 return "", fmt.Errorf("can not resolve handle: %w", ErrInvalidHandle) 184 } 185 186 if !handle.AllowedTLD() { 187 return "", ErrHandleReservedTLD 188 } 189 190 tryDNS := true 191 for _, suffix := range d.SkipDNSDomainSuffixes { 192 if strings.HasSuffix(handle.String(), suffix) { 193 tryDNS = false 194 break 195 } 196 } 197 198 if tryDNS { 199 start := time.Now() 200 triedAuthoritative := false 201 triedFallback := false 202 did, dnsErr = d.ResolveHandleDNS(ctx, handle) 203 if errors.Is(dnsErr, ErrHandleNotFound) && d.TryAuthoritativeDNS { 204 slog.Debug("attempting authoritative handle DNS resolution", "handle", handle) 205 triedAuthoritative = true 206 // try harder with authoritative lookup 207 did, dnsErr = d.ResolveHandleDNSAuthoritative(ctx, handle) 208 } 209 if errors.Is(dnsErr, ErrHandleNotFound) && len(d.FallbackDNSServers) > 0 { 210 slog.Debug("attempting fallback DNS resolution", "handle", handle) 211 triedFallback = true 212 // try harder with fallback lookup 213 did, dnsErr = d.ResolveHandleDNSFallback(ctx, handle) 214 } 215 elapsed := time.Since(start) 216 slog.Debug("resolve handle DNS", "handle", handle, "err", dnsErr, "did", did, "authoritative", triedAuthoritative, "fallback", triedFallback, "duration_ms", elapsed.Milliseconds()) 217 if nil == dnsErr { // if *not* an error 218 return did, nil 219 } 220 } 221 222 start := time.Now() 223 did, httpErr := d.ResolveHandleWellKnown(ctx, handle) 224 elapsed := time.Since(start) 225 slog.Debug("resolve handle HTTP well-known", "handle", handle, "err", httpErr, "did", did, "duration_ms", elapsed.Milliseconds()) 226 if nil == httpErr { // if *not* an error 227 return did, nil 228 } 229 230 // return the most specific/helpful error 231 if !errors.Is(dnsErr, ErrHandleNotFound) { 232 return "", dnsErr 233 } 234 if !errors.Is(httpErr, ErrHandleNotFound) { 235 return "", httpErr 236 } 237 return "", dnsErr 238}