127 lines
3.5 KiB
Go
127 lines
3.5 KiB
Go
// Package windowstray contains the small, versioned protocol between the
|
|
// per-session notification-area process and the machine-wide service. The
|
|
// tray never receives command payloads or opens the client spool.
|
|
package windowstray
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
const (
|
|
protocolVersion uint16 = 1
|
|
maxFrameBytes = 64 << 10
|
|
maxPayloadBytes = 4 << 10
|
|
)
|
|
|
|
var (
|
|
ErrInvalidFrame = errors.New("invalid tray protocol frame")
|
|
ErrFrameTooLarge = errors.New("tray protocol frame is too large")
|
|
ErrUnauthorized = errors.New("tray peer is not authorized for this action")
|
|
ErrInvalidPeer = errors.New("tray peer identity is not verified")
|
|
)
|
|
|
|
type Action uint16
|
|
|
|
const (
|
|
ActionStatus Action = iota + 1
|
|
ActionOpenConfig
|
|
ActionOpenLog
|
|
ActionStartService
|
|
ActionStopService
|
|
ActionRestartService
|
|
ActionExitTray
|
|
)
|
|
|
|
func (action Action) valid() bool { return action >= ActionStatus && action <= ActionExitTray }
|
|
|
|
// Frame is deliberately not an RPC envelope. Payloads are bounded display
|
|
// text only (status/detail); service mutations use an enum and are rechecked
|
|
// by the service under the caller's token.
|
|
type Frame struct {
|
|
Action Action
|
|
Payload []byte
|
|
}
|
|
|
|
func Encode(frame Frame) ([]byte, error) {
|
|
if !frame.Action.valid() || len(frame.Payload) > maxPayloadBytes || !utf8.Valid(frame.Payload) {
|
|
return nil, ErrInvalidFrame
|
|
}
|
|
if frame.Action != ActionStatus && len(frame.Payload) != 0 {
|
|
return nil, ErrInvalidFrame
|
|
}
|
|
total := 4 + 2 + 2 + 4 + len(frame.Payload)
|
|
if total > maxFrameBytes {
|
|
return nil, ErrFrameTooLarge
|
|
}
|
|
encoded := make([]byte, total)
|
|
copy(encoded[:4], []byte("RVTY"))
|
|
binary.BigEndian.PutUint16(encoded[4:6], protocolVersion)
|
|
binary.BigEndian.PutUint16(encoded[6:8], uint16(frame.Action))
|
|
binary.BigEndian.PutUint32(encoded[8:12], uint32(len(frame.Payload)))
|
|
copy(encoded[12:], frame.Payload)
|
|
return encoded, nil
|
|
}
|
|
|
|
func Decode(encoded []byte) (Frame, error) {
|
|
if len(encoded) > maxFrameBytes {
|
|
return Frame{}, ErrFrameTooLarge
|
|
}
|
|
if len(encoded) < 12 || !bytes.Equal(encoded[:4], []byte("RVTY")) || binary.BigEndian.Uint16(encoded[4:6]) != protocolVersion {
|
|
return Frame{}, ErrInvalidFrame
|
|
}
|
|
action := Action(binary.BigEndian.Uint16(encoded[6:8]))
|
|
length := binary.BigEndian.Uint32(encoded[8:12])
|
|
if !action.valid() || length > maxPayloadBytes || uint64(length)+12 != uint64(len(encoded)) {
|
|
return Frame{}, ErrInvalidFrame
|
|
}
|
|
payload := bytes.Clone(encoded[12:])
|
|
if !utf8.Valid(payload) || action != ActionStatus && len(payload) != 0 {
|
|
return Frame{}, ErrInvalidFrame
|
|
}
|
|
return Frame{Action: action, Payload: payload}, nil
|
|
}
|
|
|
|
type Peer struct {
|
|
PID uint32
|
|
SessionID uint32
|
|
SID string
|
|
TokenVerified bool
|
|
Interactive bool
|
|
Administrator bool
|
|
System bool
|
|
}
|
|
|
|
func (peer Peer) Validate() error {
|
|
if peer.PID == 0 || peer.SessionID == ^uint32(0) || peer.SID == "" || !peer.TokenVerified {
|
|
return ErrInvalidPeer
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func Authorize(peer Peer, action Action) error {
|
|
if !action.valid() {
|
|
return ErrInvalidFrame
|
|
}
|
|
if err := peer.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if !peer.Interactive {
|
|
return fmt.Errorf("%w: tray peer is not interactive", ErrUnauthorized)
|
|
}
|
|
switch action {
|
|
case ActionStatus, ActionOpenConfig, ActionOpenLog, ActionExitTray:
|
|
return nil
|
|
case ActionStartService, ActionStopService, ActionRestartService:
|
|
if peer.Administrator || peer.System {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("%w: service mutation requires administrator authorization", ErrUnauthorized)
|
|
default:
|
|
return ErrInvalidFrame
|
|
}
|
|
}
|