This repository has no description
0

Configure Feed

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

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