Monorepo for Tangled tangled.org
5

Configure Feed

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

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/appview/xrpcclient" 17 "tangled.org/core/idresolver" 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} 72 73type Verifier func(ctx context.Context, repoDid RepoDid) (Result, error) 74 75const verifyTimeout = 10 * time.Second 76 77func New(resolver *idresolver.Resolver, dev bool) Verifier { 78 transport := &http.Transport{ 79 DialContext: safeDialer(dev).DialContext, 80 } 81 httpClient := &http.Client{ 82 Timeout: verifyTimeout, 83 Transport: transport, 84 } 85 86 return func(ctx context.Context, repoDid RepoDid) (Result, error) { 87 ctx, cancel := context.WithTimeout(ctx, verifyTimeout) 88 defer cancel() 89 return resolveAndDescribe(ctx, resolver, httpClient, repoDid, dev) 90 } 91} 92 93func resolveAndDescribe( 94 ctx context.Context, 95 resolver *idresolver.Resolver, 96 httpClient *http.Client, 97 repoDid RepoDid, 98 dev bool, 99) (Result, error) { 100 ident, err := resolver.ResolveIdent(ctx, repoDid.String()) 101 if err != nil { 102 return Result{}, fmt.Errorf("resolve repoDid %s: %w", repoDid, err) 103 } 104 105 knot, err := ParseKnotEndpoint(ident.GetServiceEndpoint("atproto_pds"), dev) 106 if err != nil { 107 return Result{}, fmt.Errorf("repoDid %s: %w", repoDid, err) 108 } 109 110 client := &indigoxrpc.Client{Host: knot.String(), Client: httpClient} 111 out, err := tangled.RepoDescribeRepo(ctx, client, repoDid.String()) 112 if xrpcErr := xrpcclient.HandleXrpcErr(err); xrpcErr != nil { 113 if errors.Is(xrpcErr, xrpcclient.ErrXrpcUnsupported) { 114 return Result{RepoDid: repoDid, KnotURL: knot}, nil 115 } 116 return Result{}, fmt.Errorf("describeRepo on %s: %w", knot, xrpcErr) 117 } 118 119 if out.RepoDid != repoDid.String() { 120 return Result{}, fmt.Errorf("knot %s returned mismatched repoDid: got %q, want %q", knot, out.RepoDid, repoDid) 121 } 122 123 ownerDid, err := NewOwnerDid(out.OwnerDid) 124 if err != nil { 125 return Result{}, fmt.Errorf("describeRepo on %s returned invalid ownerDid: %w", knot, err) 126 } 127 128 return Result{ 129 RepoDid: repoDid, 130 OwnerDid: ownerDid, 131 KnotURL: knot, 132 }, nil 133} 134 135func safeDialer(dev bool) *net.Dialer { 136 d := &net.Dialer{ 137 Timeout: 5 * time.Second, 138 KeepAlive: 30 * time.Second, 139 } 140 if dev { 141 return d 142 } 143 d.Control = func(network, address string, _ syscall.RawConn) error { 144 host, _, err := net.SplitHostPort(address) 145 if err != nil { 146 return fmt.Errorf("invalid dial address %q: %w", address, err) 147 } 148 ip := net.ParseIP(host) 149 if ip == nil { 150 return fmt.Errorf("dial address %q did not resolve to IP", address) 151 } 152 if ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || 153 ip.IsLinkLocalMulticast() || ip.IsMulticast() || ip.IsUnspecified() { 154 return fmt.Errorf("refusing to dial %s: reserved or private address", ip) 155 } 156 return nil 157 } 158 return d 159}