This repository has no description
0

Configure Feed

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

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