carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package maintenance
2
3import (
4 "context"
5 "crypto/ecdh"
6 "encoding/base64"
7 "errors"
8 "io"
9 "os"
10 "time"
11)
12
13const MaxResultBytes = 64 << 10
14const MaxExecutionDuration = 10 * time.Minute
15
16var (
17 ErrResult = errors.New("maintenance: private result unavailable")
18 ErrResultOverflow = errors.New("maintenance: private result exceeds limit")
19)
20
21// CaptureResult takes ownership of a dedicated stdout read pipe and closes it
22// on every return. It returns complete bytes only after EOF within the deadline.
23// Closing this pipe does not prove the command or its descendants have stopped;
24// the worker must separately check Wait and containment before publishing success.
25func CaptureResult(ctx context.Context, pipe *os.File) ([]byte, error) {
26 if pipe == nil {
27 return nil, ErrResult
28 }
29 defer pipe.Close()
30 if ctx == nil {
31 return nil, ErrResult
32 }
33 deadline, ok := ctx.Deadline()
34 remaining := time.Until(deadline)
35 if !ok || remaining <= 0 || remaining > MaxExecutionDuration || ctx.Err() != nil {
36 return nil, ErrResult
37 }
38 info, err := pipe.Stat()
39 if err != nil || info.Mode()&os.ModeNamedPipe == 0 {
40 return nil, ErrResult
41 }
42 stop := context.AfterFunc(ctx, func() { _ = pipe.Close() })
43 defer stop()
44 body, err := io.ReadAll(io.LimitReader(pipe, MaxResultBytes+1))
45 if err != nil || ctx.Err() != nil {
46 clear(body)
47 return nil, ErrResult
48 }
49 if len(body) > MaxResultBytes {
50 clear(body)
51 return nil, ErrResultOverflow
52 }
53 if err := pipe.Close(); err != nil || ctx.Err() != nil {
54 clear(body)
55 return nil, ErrResult
56 }
57 return body, nil
58}
59
60func SealResult(pub *ecdh.PublicKey, binding Binding, body []byte) (string, error) {
61 if binding.Direction != DirectionResult || len(body) > MaxResultBytes {
62 return "", ErrResult
63 }
64 sealed, err := Seal(pub, binding, body)
65 if err != nil {
66 return "", ErrResult
67 }
68 return sealed, nil
69}
70
71// OpenResult decrypts bytes for the recipient. Encryption alone does not prove
72// which worker produced them; callers must verify the authenticated operation
73// outcome before treating these bytes as execution or completion evidence.
74func OpenResult(priv *ecdh.PrivateKey, binding Binding, sealed string) ([]byte, error) {
75 const overhead = len(envelopePrefix) + ephemeralBytes + nonceBytes + tagBytes
76 if binding.Direction != DirectionResult || len(sealed) > base64.StdEncoding.EncodedLen(overhead+MaxResultBytes) {
77 return nil, ErrResult
78 }
79 body, err := Open(priv, binding, sealed)
80 if err != nil {
81 return nil, ErrResult
82 }
83 // The decoded bound is necessary even after the encoded-length check:
84 // base64 padding can give adjacent raw lengths the same encoded length.
85 if len(body) > MaxResultBytes {
86 clear(body)
87 return nil, ErrResult
88 }
89 return body, nil
90}
91