This repository has no description
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}