139 lines
3.6 KiB
Go
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
|
|
}
|