Files
2026-08-30 01:41:57 +01:00

188 lines
5.3 KiB
Go

package agenthelper
import (
"bytes"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
"math"
)
const (
ProtocolVersion = 1
MaxRequestBytes uint32 = 64 * 1024
MaxResponseBytes uint32 = 10 * 1024 * 1024
)
const (
ErrorInvalidFrame = "invalid_frame"
ErrorInvalidRequest = "invalid_request"
ErrorUnsupportedProtocol = "unsupported_protocol"
ErrorUnknownOperation = "unknown_operation"
ErrorUnsupportedOperation = "unsupported_operation_version"
ErrorUnauthorizedPeer = "unauthorized_peer"
ErrorDeadlineExceeded = "deadline_exceeded"
ErrorProviderUnavailable = "provider_unavailable"
ErrorArtifactInvalid = "artifact_invalid"
ErrorStateConflict = "state_conflict"
ErrorResponseTooLarge = "response_too_large"
ErrorInternal = "internal_error"
)
const (
OperationHealth = "helper.health"
OperationCapabilities = "helper.capabilities"
OperationSMARTSnapshot = "smart.snapshot"
OperationProxmoxLXCFilesystems = "proxmox.lxc_filesystems"
OperationContainerInventory = "container.inventory"
OperationAgentUpdateStage = "agent_update.stage"
OperationAgentUpdateActivate = "agent_update.activate"
OperationAgentUpdateCommit = "agent_update.commit"
OperationAgentUpdateRollback = "agent_update.rollback"
OperationVersion1 = 1
)
// Request is the common envelope for one local helper operation. Payload is
// decoded again against the selected operation's exact schema.
type Request struct {
ProtocolVersion int `json:"protocolVersion"`
RequestID string `json:"requestId"`
Operation string `json:"operation"`
OperationVersion int `json:"operationVersion"`
DeadlineMillis int64 `json:"deadlineMillis"`
Payload json.RawMessage `json:"payload,omitempty"`
}
type Response struct {
ProtocolVersion int `json:"protocolVersion"`
RequestID string `json:"requestId,omitempty"`
Operation string `json:"operation,omitempty"`
OperationVersion int `json:"operationVersion,omitempty"`
Success bool `json:"success"`
Result json.RawMessage `json:"result,omitempty"`
Error *ResponseError `json:"error,omitempty"`
}
type ResponseError struct {
Code string `json:"code"`
Message string `json:"message"`
Retryable bool `json:"retryable"`
}
type frameError struct {
message string
}
func (e *frameError) Error() string { return e.message }
const frameHeaderBytes = 4
func checkedFrameSize(payloadBytes int) (int, error) {
if payloadBytes < 0 || payloadBytes > math.MaxInt-frameHeaderBytes {
return 0, &frameError{message: "frame payload size overflows allocation"}
}
return frameHeaderBytes + payloadBytes, nil
}
func readFrame(r io.Reader, limit uint32) ([]byte, error) {
var header [frameHeaderBytes]byte
if _, err := io.ReadFull(r, header[:]); err != nil {
return nil, &frameError{message: fmt.Sprintf("read frame length: %v", err)}
}
length := binary.BigEndian.Uint32(header[:])
if length == 0 {
return nil, &frameError{message: "frame payload is empty"}
}
if length > limit {
return nil, &frameError{message: fmt.Sprintf("frame payload exceeds %d bytes", limit)}
}
payload := make([]byte, length)
if _, err := io.ReadFull(r, payload); err != nil {
return nil, &frameError{message: fmt.Sprintf("read frame payload: %v", err)}
}
return payload, nil
}
func marshalFrame(value any, limit uint32) ([]byte, error) {
payload, err := json.Marshal(value)
if err != nil {
return nil, fmt.Errorf("marshal frame: %w", err)
}
if len(payload) == 0 || uint64(len(payload)) > uint64(limit) {
return nil, &frameError{message: fmt.Sprintf("frame payload exceeds %d bytes", limit)}
}
frameSize, err := checkedFrameSize(len(payload))
if err != nil {
return nil, err
}
framed := make([]byte, frameSize)
binary.BigEndian.PutUint32(framed[:frameHeaderBytes], uint32(len(payload)))
copy(framed[frameHeaderBytes:], payload)
return framed, nil
}
func writeFrame(w io.Writer, value any, limit uint32) (int, error) {
framed, err := marshalFrame(value, limit)
if err != nil {
return 0, err
}
written, err := writeAll(w, framed)
if err != nil {
return written, fmt.Errorf("write frame: %w", err)
}
return written, nil
}
func writeAll(w io.Writer, payload []byte) (int, error) {
total := 0
for total < len(payload) {
written, err := w.Write(payload[total:])
total += written
if err != nil {
return total, err
}
if written == 0 {
return total, io.ErrShortWrite
}
}
return total, nil
}
func decodeStrict(data []byte, target any) error {
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
if err := decoder.Decode(target); err != nil {
return err
}
var trailing any
err := decoder.Decode(&trailing)
if !errors.Is(err, io.EOF) {
if err == nil {
return errors.New("trailing JSON value is not allowed")
}
return fmt.Errorf("decode trailing JSON: %w", err)
}
return nil
}
func decodeRequest(data []byte) (Request, error) {
var request Request
if err := decodeStrict(data, &request); err != nil {
return Request{}, err
}
return request, nil
}
func decodePayload(raw json.RawMessage, target any) error {
if len(raw) == 0 {
raw = json.RawMessage(`{}`)
}
return decodeStrict(raw, target)
}