Monorepo for Tangled
0

Configure Feed

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

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