Files

139 lines
3.6 KiB
Go

// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
// Package remoteinput implements private, bounded input-helper IPC. Possession
// of the inherited control pipe is not reader consent; only a future reader
// owner may launch it for an already authorized lease.
package remoteinput
import (
"encoding/binary"
"errors"
"git.stormux.org/storm/cthulhu/internal/input"
)
const (
Version = 1
OperationSize = 52
AckSize = 20
Heartbeat byte = 1
Down byte = 2
Up byte = 3
Close byte = 4
Ready byte = 1
Accepted byte = 2
Closed byte = 3
ReasonClose byte = 1
ReasonEOF byte = 2
ReasonExpired byte = 3
ReasonProtocol byte = 4
ReasonTarget byte = 5
ReasonOutput byte = 6
ReasonCancelled byte = 7
)
var ErrProtocol = errors.New("invalid private remote input protocol")
type Operation struct {
Kind byte
Epoch, Sequence uint64
Name string
}
func EncodeOperation(operation Operation) ([]byte, error) {
if operation.Epoch == 0 || operation.Sequence == 0 || len(operation.Name) > 32 {
return nil, ErrProtocol
}
switch operation.Kind {
case Down, Up:
if !input.ValidRemoteKeyName(operation.Name) {
return nil, ErrProtocol
}
case Heartbeat, Close:
if operation.Name != "" {
return nil, ErrProtocol
}
default:
return nil, ErrProtocol
}
frame := make([]byte, OperationSize)
frame[0], frame[1], frame[2] = Version, operation.Kind, byte(len(operation.Name))
binary.BigEndian.PutUint64(frame[4:12], operation.Epoch)
binary.BigEndian.PutUint64(frame[12:20], operation.Sequence)
copy(frame[20:], operation.Name)
return frame, nil
}
func DecodeOperation(frame []byte) (Operation, error) {
if len(frame) != OperationSize || frame[0] != Version || frame[3] != 0 || frame[2] > 32 {
return Operation{}, ErrProtocol
}
length := int(frame[2])
for _, padding := range frame[20+length:] {
if padding != 0 {
return Operation{}, ErrProtocol
}
}
operation := Operation{Kind: frame[1], Epoch: binary.BigEndian.Uint64(frame[4:12]),
Sequence: binary.BigEndian.Uint64(frame[12:20]), Name: string(frame[20 : 20+length])}
if _, err := EncodeOperation(operation); err != nil {
return Operation{}, err
}
return operation, nil
}
// Accepted acknowledges a completed target Apply, not application delivery.
// Only Closed with CleanupFailed=false confirms successful target cleanup.
type Ack struct {
Kind, Reason byte
CleanupFailed bool
Epoch, Sequence uint64
}
func EncodeAck(ack Ack) ([]byte, error) {
if ack.Epoch == 0 {
return nil, ErrProtocol
}
switch ack.Kind {
case Ready:
if ack.Sequence != 0 || ack.Reason != 0 || ack.CleanupFailed {
return nil, ErrProtocol
}
case Accepted:
if ack.Sequence == 0 || ack.Reason != 0 || ack.CleanupFailed {
return nil, ErrProtocol
}
case Closed:
if ack.Reason < ReasonClose || ack.Reason > ReasonCancelled {
return nil, ErrProtocol
}
default:
return nil, ErrProtocol
}
frame := make([]byte, AckSize)
frame[0], frame[1], frame[2] = Version, ack.Kind, ack.Reason
if ack.CleanupFailed {
frame[3] = 1
}
binary.BigEndian.PutUint64(frame[4:12], ack.Epoch)
binary.BigEndian.PutUint64(frame[12:20], ack.Sequence)
return frame, nil
}
func DecodeAck(frame []byte) (Ack, error) {
if len(frame) != AckSize || frame[0] != Version || frame[3] > 1 {
return Ack{}, ErrProtocol
}
ack := Ack{Kind: frame[1], Reason: frame[2], CleanupFailed: frame[3] != 0,
Epoch: binary.BigEndian.Uint64(frame[4:12]), Sequence: binary.BigEndian.Uint64(frame[12:20])}
if _, err := EncodeAck(ack); err != nil {
return Ack{}, err
}
return ack, nil
}