1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
package requester
import (
"cmp"
"context"
"encoding/json"
"errors"
"fmt"
"net"
"net/url"
"path"
"strings"
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/johnandersen777/socialweb-computer-kcp/api/v1alpha1"
"github.com/johnandersen777/socialweb-computer-kcp/internal/atproto/cloudinit"
"github.com/johnandersen777/socialweb-computer-kcp/internal/contract/vault"
)
const PublicPLCDirectoryURL = "https://plc.directory"
var errAgentWithoutSecrets = errors.New("an agent needs its token in spec.secretsRef: the secrets bundle writes the agent's envFile on the guest")
func LocalPdsSignsAtirc(plcURL, pdsURL string) (bool, string) {
if strings.TrimRight(strings.ToLower(strings.TrimSpace(plcURL)), "/") != PublicPLCDirectoryURL {
return false, fmt.Sprintf("the DID is on %q, not %s, so irc.fedfork.com cannot resolve it", plcURL, PublicPLCDirectoryURL)
}
parsed, err := url.Parse(strings.TrimSpace(pdsURL))
if err != nil || parsed.Scheme != "https" || parsed.Hostname() == "" {
return false, fmt.Sprintf("the Pds public URL %q is not https", pdsURL)
}
host := parsed.Hostname()
if ip := net.ParseIP(host); ip != nil {
if !ip.IsGlobalUnicast() || ip.IsPrivate() {
return false, fmt.Sprintf("the Pds public URL %q is a private address", pdsURL)
}
return true, ""
}
if h, err := syntax.ParseHandle(host); err != nil || !h.AllowedTLD() {
return false, fmt.Sprintf("the Pds public URL %q names no public host", pdsURL)
}
return true, ""
}
func (q *Requester) checkAgentHandle(ctx context.Context) error {
if q.cfg.Spec.AtircParentRef != nil {
return nil
}
handle := strings.ToLower(strings.TrimSpace(q.handle))
reason := ""
switch h, err := syntax.ParseHandle(handle); {
case handle == "":
reason = "the requester identity has no handle"
case err != nil:
reason = fmt.Sprintf("%q is not a handle", handle)
case !h.AllowedTLD():
reason = fmt.Sprintf("%s is a .%s handle that nobody resolves", handle, h.TLD())
default:
reason = q.handleMismatch(ctx, h)
}
if reason == "" {
return nil
}
return fmt.Errorf("the atirc agent needs a requester handle that resolves to %s: %s; start the requester with --atproto-handle <a domain you control> whose DNS TXT _atproto.<handle> or https://<handle>/.well-known/atproto-did names that DID", q.did, reason)
}
func (q *Requester) handleMismatch(ctx context.Context, h syntax.Handle) string {
if q.deps.Directory == nil {
return ""
}
ctx, cancel := context.WithTimeout(ctx, atircCallTimeout)
defer cancel()
ident, err := q.deps.Directory.LookupHandle(ctx, h)
switch {
case err != nil:
return fmt.Sprintf("%s does not resolve to it: %v", h, err)
case ident.DID.String() != q.did:
return fmt.Sprintf("%s resolves to %s", h, ident.DID)
}
return ""
}
func (r *contractRunner) ensureAgentBundle(ctx context.Context, contract v1alpha1.ComputeContract) error {
envFile, err := agentEnvFile(contract)
if err != nil || envFile == "" {
return nil
}
if r.q.deps.VaultWrite == nil {
return errors.New("the requester has no vault write handle for the agent's secrets bundle")
}
r.bundleMu.Lock()
defer r.bundleMu.Unlock()
ref := *contract.Spec.SecretsRef
kv, err := r.q.deps.VaultWrite(ctx, ref)
if err != nil {
return fmt.Errorf("the agent's secrets bundle %s/%s: %w", ref.Mount, ref.Path, err)
}
data, err := kv.Read(ctx, ref.Mount, ref.Path, 0)
if err != nil && !errors.Is(err, vault.ErrNotFound) {
return fmt.Errorf("reading the agent's secrets bundle %s/%s: %w", ref.Mount, ref.Path, err)
}
if text, _ := data["bundle"].(string); strings.TrimSpace(text) != "" {
return nil
}
raw, err := json.Marshal([]SecretEntry{{Path: envFile, Value: AtircTokenEnv + "=\n", Mode: "0600"}})
if err != nil {
return err
}
next := make(map[string]any, len(data)+1)
for k, v := range data {
next[k] = v
}
next["bundle"] = string(raw)
if err := kv.Write(ctx, ref.Mount, ref.Path, next); err != nil {
return fmt.Errorf("writing the agent's placeholder bundle %s/%s: %w", ref.Mount, ref.Path, err)
}
r.log().Info("requester_agent_bundle_placeholder", "mount", ref.Mount, "path", ref.Path, "envFile", envFile, "mode", "0600")
return nil
}
func agentEnvFile(contract v1alpha1.ComputeContract) (string, error) {
if contract.Spec.Agent == nil {
return "", nil
}
if contract.Spec.SecretsRef == nil {
return "", errAgentWithoutSecrets
}
if envFile := strings.TrimSpace(contract.Spec.Agent.EnvFile); envFile != "" {
return envFile, nil
}
return cloudinit.DefaultAgentEnvFile, nil
}
var ErrTokenPathConflict = errors.New("requester: a path names an atirc token file")
func CheckTokenPaths(contract v1alpha1.ComputeContract, bundlePaths []string) error {
tokens := map[string]string{cloudinit.GuestRequesterAtircEnvFile: "the guest requester's atirc token"}
if agent := contract.Spec.Agent; agent != nil {
envFile := path.Clean(cmp.Or(strings.TrimSpace(agent.EnvFile), cloudinit.DefaultAgentEnvFile))
if owner, ok := tokens[envFile]; ok {
return fmt.Errorf("%w: the agent envFile %s is %s", ErrTokenPathConflict, envFile, owner)
}
tokens[envFile] = "the agent's atirc token"
}
check := func(what, p string) error {
if owner, ok := tokens[path.Clean(p)]; ok {
return fmt.Errorf("%w: %s %s is %s", ErrTokenPathConflict, what, p, owner)
}
return nil
}
if contract.Spec.Association != nil {
if refFile, err := associationRefFile(contract); err == nil {
if err := check("spec.association.refFile", refFile); err != nil {
return err
}
}
if err := check("the association request", cloudinit.DefaultAssociationRequestFile); err != nil {
return err
}
}
for _, p := range bundlePaths {
if err := check("the secrets bundle path", p); err != nil {
return err
}
}
return nil
}
func tokenFile(contract v1alpha1.ComputeContract, p string) bool {
p = path.Clean(p)
if p == cloudinit.GuestRequesterAtircEnvFile {
return true
}
agent := contract.Spec.Agent
return agent != nil && path.Clean(cmp.Or(strings.TrimSpace(agent.EnvFile), cloudinit.DefaultAgentEnvFile)) == p
}