Antonio Mika
·
2026-10-11
1package pico
2
3import (
4 "crypto/rand"
5 "encoding/hex"
6 "errors"
7 "fmt"
8 "net"
9 "slices"
10 "strings"
11
12 "github.com/picosh/pico/pkg/db"
13 "github.com/picosh/pico/pkg/shared/router"
14 "golang.org/x/crypto/ssh"
15)
16
17// customDomainService is a service that serves a domain whose TXT record
18// names it. check judges the records the way the service does and describes
19// the result.
20type customDomainService struct {
21 space string
22 check func(c *Cmd, records []string) string
23 wildcard bool
24}
25
26var customDomainServices = []customDomainService{
27 {space: "pgs", check: siteCheck(resolvePgsSite)},
28 {space: "prose", check: siteCheck(resolveProseSite)},
29 // tuns accepts a match under either prefix; _sish is what sish uses.
30 {space: "tuns", check: checkTunsRecords, wildcard: true},
31 {space: "sish", check: checkTunsRecords, wildcard: true},
32}
33
34// siteCheck checks a record whose value names a site. Like the services,
35// it reads only the first record.
36func siteCheck(resolve func(dbpool db.DB, value string) (*db.User, string, error)) func(c *Cmd, records []string) string {
37 return func(c *Cmd, records []string) string {
38 value := strings.TrimSpace(records[0])
39 user, site, err := resolve(c.Dbpool, value)
40 if err != nil {
41 return fmt.Sprintf("%q is invalid: %v", value, err)
42 }
43 line := fmt.Sprintf("%q is valid, serving %s", value, site)
44 if user.ID != c.User.ID {
45 line += " (not your account)"
46 }
47 return line
48 }
49}
50
51func resolvePgsSite(dbpool db.DB, value string) (*db.User, string, error) {
52 props, err := router.GetProjectFromSubdomain(value)
53 if err != nil {
54 return nil, "", err
55 }
56 user, err := dbpool.FindUserByName(props.Username)
57 if err != nil {
58 return nil, "", fmt.Errorf("user %q not found", props.Username)
59 }
60 if _, err := dbpool.FindProjectByName(user.ID, props.ProjectName); err != nil {
61 return user, "", fmt.Errorf("user %q has no project %q", user.Name, props.ProjectName)
62 }
63 return user, fmt.Sprintf("pgs project %q of %s", props.ProjectName, user.Name), nil
64}
65
66func resolveProseSite(dbpool db.DB, value string) (*db.User, string, error) {
67 user, err := dbpool.FindUserByName(value)
68 if err != nil {
69 return nil, "", fmt.Errorf("user %q not found", value)
70 }
71 return user, fmt.Sprintf("prose blog of %s", user.Name), nil
72}
73
74// checkTunsRecords looks for a record holding the user's name or the SHA256
75// fingerprint of one of their keys. tuns compares the whole record, so it
76// must match exactly.
77func checkTunsRecords(c *Cmd, records []string) string {
78 keys, err := c.Dbpool.FindKeysByUser(c.User)
79 if err != nil {
80 return fmt.Sprintf("could not load your keys: %v", err)
81 }
82 names := map[string]string{}
83 for _, k := range keys {
84 pk, _, _, _, err := ssh.ParseAuthorizedKey([]byte(k.Key))
85 if err != nil {
86 continue
87 }
88 names[ssh.FingerprintSHA256(pk)] = k.Name
89 }
90
91 for _, r := range records {
92 if r == c.User.Name {
93 return fmt.Sprintf("%q is valid, tunnels from any of your keys can use this domain", r)
94 }
95 if name, ok := names[r]; ok {
96 return fmt.Sprintf("%q is valid, tunnels from your key %q can use this domain", r, name)
97 }
98 }
99 for _, r := range records {
100 trimmed := strings.TrimSpace(r)
101 if _, ok := names[trimmed]; ok || trimmed == c.User.Name {
102 return fmt.Sprintf("%q is invalid: it has surrounding whitespace, and tuns needs an exact match", r)
103 }
104 }
105 quoted := make([]string, len(records))
106 for i, r := range records {
107 quoted[i] = fmt.Sprintf("%q", r)
108 }
109 return fmt.Sprintf("%s is invalid: no record is your username or the SHA256 fingerprint of one of your keys", strings.Join(quoted, ", "))
110}
111
112// lookupHost resolves host and returns its canonical name, or "" when it has
113// no CNAME.
114func lookupHost(host string) (string, []string, error) {
115 addrs, err := net.LookupHost(host)
116 if err != nil {
117 return "", nil, err
118 }
119 cname, err := net.LookupCNAME(host)
120 if err != nil {
121 return "", addrs, nil
122 }
123 cname = strings.TrimSuffix(cname, ".")
124 if strings.EqualFold(cname, host) {
125 cname = ""
126 }
127 return cname, addrs, nil
128}
129
130// checkPointer reports what host resolves to and which of the service
131// domains it points at. Addresses are compared so a CNAME to a regional
132// name or an apex A record both count.
133func (c *Cmd) checkPointer(host string, lookup func(string) (string, []string, error)) string {
134 cname, addrs, err := lookup(host)
135 var dnsErr *net.DNSError
136 if errors.As(err, &dnsErr) && dnsErr.IsNotFound {
137 return fmt.Sprintf("%s: no CNAME or A record", host)
138 }
139 if err != nil {
140 return fmt.Sprintf("%s: lookup failed: %v", host, err)
141 }
142
143 line := fmt.Sprintf("%s: resolves to %s", host, strings.Join(addrs, ", "))
144 if cname != "" {
145 line = fmt.Sprintf("%s: CNAME %s, resolves to %s", host, cname, strings.Join(addrs, ", "))
146 }
147 matches := []string{}
148 for _, target := range c.DomainTargets {
149 _, targetAddrs, err := lookup(target)
150 if err != nil {
151 continue
152 }
153 if slices.ContainsFunc(addrs, func(a string) bool { return slices.Contains(targetAddrs, a) }) {
154 matches = append(matches, target)
155 }
156 }
157 if len(matches) == 0 {
158 return fmt.Sprintf("%s, which is not %s", line, strings.Join(c.DomainTargets, ", "))
159 }
160 return fmt.Sprintf("%s, which is %s", line, strings.Join(matches, ", "))
161}
162
163// dnsCheck reports what domain points at and, for each service, whether it
164// has a TXT record the service would accept.
165func (c *Cmd) dnsCheck(domain string) error {
166 domain = strings.TrimSuffix(strings.ToLower(strings.TrimSpace(domain)), ".")
167 base, wildcard := strings.CutPrefix(domain, "*.")
168 if base == "" || strings.ContainsAny(base, "/:@ *") {
169 return fmt.Errorf("expected a domain name like example.com or *.example.com")
170 }
171 lookup := c.lookupTXT
172 if lookup == nil {
173 lookup = router.LookupTXTRecords
174 }
175
176 host := domain
177 if wildcard {
178 // tuns verifies a wildcard by looking up a random name under it.
179 label := make([]byte, 6)
180 _, _ = rand.Read(label)
181 host = hex.EncodeToString(label) + "." + base
182 }
183
184 resolve := c.lookupHost
185 if resolve == nil {
186 resolve = lookupHost
187 }
188 c.output(c.checkPointer(host, resolve))
189
190 for _, svc := range customDomainServices {
191 name := router.TXTRecordName(host, svc.space)
192 if wildcard && !svc.wildcard {
193 c.output(fmt.Sprintf("%s: wildcard domains are not supported", router.TXTRecordName(domain, svc.space)))
194 continue
195 }
196 records, err := lookup(host, svc.space)
197 switch {
198 case errors.Is(err, router.ErrNoTXTRecord):
199 c.output(fmt.Sprintf("%s: no TXT record", name))
200 case err != nil:
201 c.output(fmt.Sprintf("%s: lookup failed: %v", name, err))
202 default:
203 c.output(fmt.Sprintf("%s: %s", name, svc.check(c, records)))
204 }
205 }
206 c.output("pgs and prose remember a missing record for 30 seconds and a found record for 2 minutes. tuns checks when a tunnel connects.")
207 return nil
208}