forked from
tangled.org/core
Monorepo for Tangled
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}