This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / repoverify / verify.go
3.7 kB 140 lines
1package repoverify 2 3import ( 4 "context" 5 "errors" 6 "fmt" 7 "net" 8 "net/http" 9 "net/url" 10 "syscall" 11 "time" 12 13 "github.com/bluesky-social/indigo/atproto/identity" 14 "github.com/bluesky-social/indigo/atproto/syntax" 15 indigoxrpc "github.com/bluesky-social/indigo/xrpc" 16 "tangled.org/core/api/tangled" 17 "tangled.org/core/repoident" 18 "tangled.org/core/xrpc/xrpcclient" 19) 20 21func ParseKnotEndpoint(raw string, dev bool) (*url.URL, error) { 22 if raw == "" { 23 return nil, fmt.Errorf("empty knot URL") 24 } 25 u, err := url.Parse(raw) 26 if err != nil { 27 return nil, fmt.Errorf("invalid knot URL %q: %w", raw, err) 28 } 29 if u.Host == "" { 30 return nil, fmt.Errorf("knot URL %q has no host", raw) 31 } 32 switch u.Scheme { 33 case "https": 34 case "http": 35 if !dev { 36 return nil, fmt.Errorf("knot URL %q must use https outside dev mode", raw) 37 } 38 default: 39 return nil, fmt.Errorf("knot URL %q has unsupported scheme %q", raw, u.Scheme) 40 } 41 return u, nil 42} 43 44type Result struct { 45 RepoDid repoident.RepoDid 46 OwnerDid repoident.OwnerDid 47 KnotURL *url.URL 48 // Rkey of the sh.tangled.repo record tracked by the knot; empty when the 49 // knot does not support describeRepo. 50 Rkey string 51} 52 53type Verifier func(ctx context.Context, repoDid repoident.RepoDid) (Result, error) 54 55const verifyTimeout = 10 * time.Second 56 57func New(dir identity.Directory, dev bool) Verifier { 58 transport := &http.Transport{ 59 DialContext: safeDialer(dev).DialContext, 60 } 61 httpClient := &http.Client{ 62 Timeout: verifyTimeout, 63 Transport: transport, 64 } 65 66 return func(ctx context.Context, repoDid repoident.RepoDid) (Result, error) { 67 ctx, cancel := context.WithTimeout(ctx, verifyTimeout) 68 defer cancel() 69 return resolveAndDescribe(ctx, dir, httpClient, repoDid, dev) 70 } 71} 72 73func resolveAndDescribe( 74 ctx context.Context, 75 dir identity.Directory, 76 httpClient *http.Client, 77 repoDid repoident.RepoDid, 78 dev bool, 79) (Result, error) { 80 ident, err := dir.LookupDID(ctx, syntax.DID(repoDid)) 81 if err != nil { 82 return Result{}, fmt.Errorf("resolve repoDid %s: %w", repoDid, err) 83 } 84 85 knot, err := ParseKnotEndpoint(ident.GetServiceEndpoint("atproto_pds"), dev) 86 if err != nil { 87 return Result{}, fmt.Errorf("repoDid %s: %w", repoDid, err) 88 } 89 90 client := &indigoxrpc.Client{Host: knot.String(), Client: httpClient} 91 out, err := tangled.RepoDescribeRepo(ctx, client, repoDid.String()) 92 if xrpcErr := xrpcclient.HandleXrpcErr(err); xrpcErr != nil { 93 if errors.Is(xrpcErr, xrpcclient.ErrXrpcUnsupported) { 94 return Result{RepoDid: repoDid, KnotURL: knot}, nil 95 } 96 return Result{}, fmt.Errorf("describeRepo on %s: %w", knot, xrpcErr) 97 } 98 99 if out.RepoDid != repoDid.String() { 100 return Result{}, fmt.Errorf("knot %s returned mismatched repoDid: got %q, want %q", knot, out.RepoDid, repoDid) 101 } 102 103 ownerDid, err := repoident.NewOwnerDid(out.OwnerDid) 104 if err != nil { 105 return Result{}, fmt.Errorf("describeRepo on %s returned invalid ownerDid: %w", knot, err) 106 } 107 108 return Result{ 109 RepoDid: repoDid, 110 OwnerDid: ownerDid, 111 KnotURL: knot, 112 Rkey: out.Rkey, 113 }, nil 114} 115 116func safeDialer(dev bool) *net.Dialer { 117 d := &net.Dialer{ 118 Timeout: 5 * time.Second, 119 KeepAlive: 30 * time.Second, 120 } 121 if dev { 122 return d 123 } 124 d.Control = func(network, address string, _ syscall.RawConn) error { 125 host, _, err := net.SplitHostPort(address) 126 if err != nil { 127 return fmt.Errorf("invalid dial address %q: %w", address, err) 128 } 129 ip := net.ParseIP(host) 130 if ip == nil { 131 return fmt.Errorf("dial address %q did not resolve to IP", address) 132 } 133 if ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || 134 ip.IsLinkLocalMulticast() || ip.IsMulticast() || ip.IsUnspecified() { 135 return fmt.Errorf("refusing to dial %s: reserved or private address", ip) 136 } 137 return nil 138 } 139 return d 140}