Add lease-bound Remote key IPC and SDK validation

This commit is contained in:
ak47 committed 2026-11-05 21:28:29 -05:00
1 parent 9ac33560dc
commit 792cac5d0b
12 files changed
+807 -9

No files matched your search

+15
View File
@@ -41,6 +41,8 @@ type remoteAccess struct {
speechBatch *remoteSpeechBatch
speechItems int
speechBytes int
keyItems int
keyBytes int
}
// AuthorizeRemote is the unrestricted protocol-fixture hook. Reader managers
@@ -178,6 +180,7 @@ func (session *Session) revoke_remote_locked(reason string, notify bool) {
access.cancel()
access.speechBatch = nil
access.speechItems, access.speechBytes = 0, 0
access.keyItems, access.keyBytes = 0, 0
if session.remoteSpeech != nil {
drain:
for {
@@ -188,6 +191,16 @@ func (session *Session) revoke_remote_locked(reason string, notify bool) {
}
}
}
if session.remoteKeys != nil {
drainKeys:
for {
select {
case <-session.remoteKeys:
default:
break drainKeys
}
}
}
if access.stopPolicy != nil {
access.stopPolicy()
}
@@ -265,6 +278,8 @@ func (session *Session) remote_call_locked(method string, params json.RawMessage
switch method {
case "remote.present":
return session.remote_present_locked(params)
case "remote.key":
return session.remote_key_locked(params)
case "remote.open":
var request tentaclewire.RemoteOpen
if decode_strict(params, &request) != nil || len(request.Action) != 32 || request.Epoch == 0 || !tentaclewire.ValidRoleGrants(request.Role, request.Grants) {
+85
View File
@@ -0,0 +1,85 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package tentacle
import (
"context"
"encoding/json"
"sync"
"git.stormux.org/storm/cthulhu/internal/tentaclewire"
)
// RemoteKeyDelivery is accepted work, not evidence of native injection. A
// reader input owner must recheck Context, RemoteContext(keyboard-inject),
// focus/security policy and its own key ledger immediately before delivery.
// Revocation must independently release native-owned keys, without relying on
// this queue or a final key-up from the network child.
type RemoteKeyDelivery struct {
Context context.Context
Lease tentaclewire.RemoteLease
Sequence uint64
Key tentaclewire.RemoteKey
// Release returns retained capacity after delivery/discard. Dequeuing does
// not free capacity. It is idempotent and scoped to the original lease.
Release func()
}
// EnableRemoteKeys attaches one reader-owned mailbox. It performs no native
// setup and grants no authority. No production owner is attached yet, so
// remote.key remains unavailable in ordinary reader sessions.
func (session *Session) EnableRemoteKeys() <-chan RemoteKeyDelivery {
session.mutex.Lock()
defer session.mutex.Unlock()
if session.remoteKeys == nil {
session.remoteKeys = make(chan RemoteKeyDelivery, tentaclewire.RemoteKeyQueueSize)
}
return session.remoteKeys
}
func (session *Session) remote_key_locked(params json.RawMessage) (any, *RPCError) {
var request tentaclewire.RemoteKeyRequest
if decode_strict(params, &request) != nil {
return nil, &RPCError{Code: -32602, Message: "Invalid session parameters"}
}
lease := tentaclewire.RemoteLease{Lease: request.Lease, Epoch: request.Epoch}
access := session.remote_access_locked(lease, "keyboard-inject")
if access == nil || access.role != "controlled" || request.Sequence <= access.sequence {
return nil, &RPCError{Code: -32001, Message: "Session permission denied"}
}
if session.remoteKeys == nil {
return nil, &RPCError{Code: -32601, Message: "Remote keyboard owner unavailable"}
}
// Account canonical full request bytes, including the lease/sequence. Both
// admission counts include queued and in-flight work until Release.
data, _ := json.Marshal(request)
bytes := len(data)
if access.keyItems == tentaclewire.RemoteKeyQueueSize || access.keyBytes+bytes > tentaclewire.RemoteKeyQueueBytes {
session.revoke_remote_locked("local", true)
return nil, &RPCError{Code: -32002, Message: "Remote keyboard queue full"}
}
access.keyItems++
access.keyBytes += bytes
access.sequence = request.Sequence
var released sync.Once
delivery := RemoteKeyDelivery{Context: access.ctx, Lease: lease, Sequence: request.Sequence, Key: request.Key, Release: func() {
released.Do(func() {
session.mutex.Lock()
defer session.mutex.Unlock()
if session.remote == access {
access.keyItems--
access.keyBytes -= bytes
}
})
}}
select {
case session.remoteKeys <- delivery:
return struct{}{}, nil
default:
// Losing a release must never leave the input lease active.
session.revoke_remote_locked("local", true)
return nil, &RPCError{Code: -32002, Message: "Remote keyboard queue full"}
}
}
+270
View File
@@ -0,0 +1,270 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package tentacle
import (
"encoding/json"
"errors"
"strings"
"sync"
"testing"
"time"
"git.stormux.org/storm/cthulhu/internal/tentaclewire"
)
func keyRequest(lease tentaclewire.RemoteLease, sequence uint64, pressed bool) tentaclewire.RemoteKeyRequest {
return tentaclewire.RemoteKeyRequest{RemoteOperation: tentaclewire.RemoteOperation{
Lease: lease.Lease, Epoch: lease.Epoch, Sequence: sequence},
Key: tentaclewire.RemoteKey{Name: "Control_R", Pressed: pressed}}
}
func TestRemoteKeySDKMailboxAndSequence(t *testing.T) {
for _, language := range []string{"go", "python"} {
t.Run(language, func(t *testing.T) {
manager := test_manager(t, DefaultLimits(), nil)
session := startSessionSDK(t, manager, language, "keys", "keyboard-inject")
lease := speech_fixture_lease(t, session, "controlled", []string{"keyboard-inject"})
request := keyRequest(lease, 1, true)
if _, code := remoteCall(t, session, "remote.key", request); code != -32601 {
t.Fatal("accepted keys without reader input owner", code)
}
deliveries := session.EnableRemoteKeys()
if session.EnableRemoteKeys() != deliveries {
t.Fatal("attachment created another owner mailbox")
}
if _, code := remoteCall(t, session, "remote.key", request); code != 0 {
t.Fatal("valid key was not admitted", code)
}
first := <-deliveries
if first.Key != request.Key || first.Sequence != 1 || first.Lease != lease || first.Context.Err() != nil {
t.Fatal("delivery changed key identity, sequence or lifetime")
}
if _, err := session.RemoteContext(first.Lease, "keyboard-inject"); err != nil {
t.Fatal("delivery lost original consent", err)
}
if _, code := remoteCall(t, session, "remote.key", request); code != -32001 {
t.Fatal("replayed key was accepted", code)
}
request.Sequence++
if _, code := remoteCall(t, session, "remote.heartbeat", request.RemoteOperation); code != 0 {
t.Fatal("heartbeat did not share the operation sequence", code)
}
if _, code := remoteCall(t, session, "remote.key", request); code != -32001 {
t.Fatal("key reused heartbeat sequence", code)
}
request.Sequence++
request.Key.Pressed = false
if _, code := remoteCall(t, session, "remote.key", request); code != 0 {
t.Fatal("key release was not admitted", code)
}
second := <-deliveries
if second.Key != request.Key || second.Sequence != request.Sequence {
t.Fatal("lost release ordering")
}
firstBytes, _ := json.Marshal(keyRequest(lease, 1, true))
secondBytes, _ := json.Marshal(request)
session.mutex.Lock()
items, bytes := session.remote.keyItems, session.remote.keyBytes
session.mutex.Unlock()
if items != 2 || bytes != len(firstBytes)+len(secondBytes) {
t.Fatal("in-flight accounting omitted request metadata", items, bytes)
}
var releases sync.WaitGroup
for range 8 {
releases.Add(1)
go func() { defer releases.Done(); first.Release(); second.Release() }()
}
releases.Wait()
session.mutex.Lock()
items, bytes = session.remote.keyItems, session.remote.keyBytes
session.mutex.Unlock()
if items != 0 || bytes != 0 {
t.Fatal("idempotent concurrent release retained capacity", items, bytes)
}
})
}
}
func TestRemoteKeySDKPermissionAndParameters(t *testing.T) {
for _, language := range []string{"go", "python"} {
t.Run(language, func(t *testing.T) {
manager := test_manager(t, DefaultLimits(), nil)
session := startSessionSDK(t, manager, language, "keys", "keyboard-inject")
deliveries := session.EnableRemoteKeys()
lease := speech_fixture_lease(t, session, "controller", []string{"remote-present"})
if _, code := remoteCall(t, session, "remote.key", keyRequest(lease, 1, true)); code != -32001 {
t.Fatal("controller role injected input", code)
}
lease = speech_fixture_lease(t, session, "controlled", []string{"output-observe"})
if _, code := remoteCall(t, session, "remote.key", keyRequest(lease, 1, true)); code != -32001 {
t.Fatal("output grant allowed input", code)
}
lease = speech_fixture_lease(t, session, "controlled", []string{"keyboard-inject"})
request := keyRequest(lease, 1, true)
for _, foreign := range []tentaclewire.RemoteKeyRequest{
keyRequest(tentaclewire.RemoteLease{Lease: strings.Repeat("f", 32), Epoch: lease.Epoch}, 1, true),
keyRequest(tentaclewire.RemoteLease{Lease: lease.Lease, Epoch: lease.Epoch + 1}, 1, true),
} {
if _, code := remoteCall(t, session, "remote.key", foreign); code != -32001 {
t.Fatal("foreign lease/epoch admitted input", code)
}
}
other := startSessionSDK(t, manager, language, "other-keys", "keyboard-inject")
other.EnableRemoteKeys()
if _, code := remoteCall(t, other, "remote.key", request); code != -32001 {
t.Fatal("foreign process used another session's lease", code)
}
data, _ := json.Marshal(request)
for _, raw := range []string{
strings.Replace(string(data), `"pressed":true`, `"pressed":null`, 1),
strings.Replace(string(data), `"name":"Control_R"`, `"name":"unknown private value"`, 1),
strings.Replace(string(data), `"sequence":1`, `"sequence":1,"origin":"local"`, 1),
strings.Replace(string(data), `"sequence":1`, `"sequence":0`, 1),
} {
if _, code := remoteCall(t, session, "remote.key", json.RawMessage(raw)); code != -32602 {
t.Fatal("invalid key parameters were admitted", code)
}
}
if len(deliveries) != 0 {
t.Fatal("denied or invalid input reached the owner")
}
if _, code := remoteCall(t, session, "remote.key", request); code != 0 {
t.Fatal("denied requests consumed the live operation sequence", code)
}
(<-deliveries).Release()
})
}
}
func TestRemoteKeySDKOverflowRetiresInFlightWork(t *testing.T) {
for _, language := range []string{"go", "python"} {
t.Run(language, func(t *testing.T) {
session := startSessionSDK(t, test_manager(t, DefaultLimits(), nil), language, "keys", "keyboard-inject")
lease := speech_fixture_lease(t, session, "controlled", []string{"keyboard-inject"})
deliveries := session.EnableRemoteKeys()
request := keyRequest(lease, 1, true)
if _, code := remoteCall(t, session, "remote.key", request); code != 0 {
t.Fatal(code)
}
inFlight := <-deliveries // Dequeue must not free input capacity.
session.mutex.Lock()
old := session.remote
session.mutex.Unlock()
for index := 2; index <= tentaclewire.RemoteKeyQueueSize; index++ {
request.Sequence++
if index%32 == 0 {
if _, code := remoteCall(t, session, "remote.heartbeat", request.RemoteOperation); code != 0 {
t.Fatal(code)
}
request.Sequence++
}
if _, code := remoteCall(t, session, "remote.key", request); code != 0 {
t.Fatal("input capacity ended early", index, code)
}
}
request.Sequence++
request.Key.Pressed = false
if _, code := remoteCall(t, session, "remote.key", request); code != -32002 {
t.Fatal("full queue silently lost a release", code)
}
if inFlight.Context.Err() == nil {
t.Fatal("overflow retained in-flight authority")
}
if _, err := session.RemoteContext(lease, "keyboard-inject"); !errors.Is(err, ErrPermission) {
t.Fatal("overflow retained input consent", err)
}
inFlight.Release()
inFlight.Release()
session.mutex.Lock()
defer session.mutex.Unlock()
if session.remote != nil || old.keyItems != 0 || old.keyBytes != 0 || len(deliveries) != 0 {
t.Fatal("overflow retained queued input or accounting")
}
})
}
}
func TestRemoteKeySDKRetirementAndReplacement(t *testing.T) {
for _, language := range []string{"go", "python"} {
for _, reason := range []string{"local", "close", "replace", "expire", "disable", "failure", "policy"} {
t.Run(language+"/"+reason, func(t *testing.T) {
manager := test_manager(t, DefaultLimits(), nil)
policy := NewRemotePolicy()
if err := manager.UseRemotePolicy(policy); err != nil {
t.Fatal(err)
}
session := startSessionSDK(t, manager, language, "keys", "keyboard-inject")
lease := speech_fixture_lease(t, session, "controlled", []string{"keyboard-inject"})
deliveries := session.EnableRemoteKeys()
for sequence := uint64(1); sequence <= 2; sequence++ {
if _, code := remoteCall(t, session, "remote.key", keyRequest(lease, sequence, true)); code != 0 {
t.Fatal(code)
}
}
inFlight := <-deliveries
session.mutex.Lock()
old := session.remote
session.mutex.Unlock()
switch reason {
case "local":
session.RevokeRemote()
case "close":
if _, code := remoteCall(t, session, "remote.close", keyRequest(lease, 3, false).RemoteOperation); code != 0 {
t.Fatal(code)
}
case "replace":
fresh := speech_fixture_lease(t, session, "controlled", []string{"keyboard-inject"})
if _, code := remoteCall(t, session, "remote.key", keyRequest(fresh, 1, true)); code != 0 {
t.Fatal(code)
}
replacement := <-deliveries
inFlight.Release() // Must not release the replacement budget.
session.mutex.Lock()
count := session.remote.keyItems
session.mutex.Unlock()
if count != 1 || replacement.Context.Err() != nil {
t.Fatal("stale release affected replacement work")
}
replacement.Release()
case "expire", "policy":
session.mutex.Lock()
if reason == "expire" {
session.refresh_remote_locked(time.Millisecond)
} else {
policy.SetAvailable(false)
if inFlight.Context.Err() == nil {
session.mutex.Unlock()
t.Fatal("policy cancellation waited for a session worker")
}
policy.SetAvailable(true)
}
session.mutex.Unlock()
var event tentaclewire.SessionEvent
if json.Unmarshal(invokeSessionSDK(t, session, "event", nil), &event) != nil || event.Kind != "remote.revoked" || event.Epoch != lease.Epoch {
t.Fatal("missing original-epoch retirement notification")
}
case "disable":
session.Disable()
case "failure":
session.fail(ErrProtocol)
}
if inFlight.Context.Err() == nil {
t.Fatal("retired input survived")
}
if _, err := session.RemoteContext(lease, "keyboard-inject"); !errors.Is(err, ErrPermission) {
t.Fatal("retired input passed delivery-time consent check")
}
inFlight.Release()
inFlight.Release()
session.mutex.Lock()
defer session.mutex.Unlock()
if old.keyItems != 0 || old.keyBytes != 0 || len(deliveries) != 0 {
t.Fatal("retirement retained pending input/accounting")
}
})
}
}
}
+8 -4
View File
@@ -79,9 +79,10 @@ func sessionSDKProgram(t *testing.T, language string) []string {
return []string{python, "-B", peer}
}
func startSessionSDK(t *testing.T, manager *Manager, language, id string) *Session {
func startSessionSDK(t *testing.T, manager *Manager, language, id string, extraGrants ...string) *Session {
t.Helper()
session, err := manager.Start(t.Context(), Spec{ID: id, Program: sessionSDKProgram(t, language), SessionEvents: true, RemoteGrants: []string{"output-observe", "remote-present", "clipboard-exchange"}})
grants := append([]string{"output-observe", "remote-present", "clipboard-exchange"}, extraGrants...)
session, err := manager.Start(t.Context(), Spec{ID: id, Program: sessionSDKProgram(t, language), SessionEvents: true, RemoteGrants: grants})
if err != nil {
t.Fatal(err)
}
@@ -214,8 +215,11 @@ func TestSessionSDKLifecycleConformance(t *testing.T) {
if _, err := session.RemoteContext(lease, "clipboard-exchange"); !errors.Is(err, ErrPermission) {
t.Fatal("install grant became session grant")
}
if _, code := remoteCall(t, session, "remote.key", map[string]any{}); code != -32601 {
t.Fatal("unimplemented desktop operation accepted")
if _, code := remoteCall(t, session, "remote.key", map[string]any{}); code != -32602 {
t.Fatal("malformed typed key operation accepted", code)
}
if _, code := remoteCall(t, session, "remote.braille", map[string]any{}); code != -32601 {
t.Fatal("unimplemented desktop operation accepted", code)
}
// One blocked handler cannot block the endpoint receive loop or lifecycle calls.
waiting, err := session.Invoke(t.Context(), "wait", nil)
+1
View File
@@ -77,6 +77,7 @@ type Session struct {
remote *remoteAccess
remoteApproval *RemoteApproval
remoteSpeech chan RemoteSpeechDelivery
remoteKeys chan RemoteKeyDelivery
}
func (session *Session) Ready() <-chan struct{} { return session.ready }
+91
View File
@@ -0,0 +1,91 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package tentaclewire
import (
"encoding/json"
"strconv"
)
const RemoteKeyQueueSize = 256
const RemoteKeyQueueBytes = 256 * 1024
// RemoteKey identifies one logical, unshifted key, never an X keycode, Windows
// VK, scan code, text to type or command to execute. Native ownership and
// destination-layout resolution belong to a reader input owner.
type RemoteKey struct {
Name string `json:"name"`
Pressed bool `json:"pressed"`
}
type RemoteKeyRequest struct {
RemoteOperation
Key RemoteKey `json:"key"`
}
func ValidRemoteKey(key RemoteKey) bool {
name := key.Name
if len(name) == 1 && (name[0] >= 'a' && name[0] <= 'z' || name[0] >= '0' && name[0] <= '9') {
return true
}
if len(name) >= 2 && len(name) <= 3 && name[0] == 'F' {
number, err := strconv.Atoi(name[1:])
return err == nil && number >= 1 && number <= 24 && strconv.Itoa(number) == name[1:]
}
if len(name) == 4 && name[:3] == "KP_" && name[3] >= '0' && name[3] <= '9' {
return true
}
switch name {
case "BackSpace", "Tab", "Return", "Escape", "space", "Pause", "Print", "Menu",
"Caps_Lock", "Num_Lock", "Scroll_Lock", "Shift_L", "Shift_R", "Control_L", "Control_R",
"Alt_L", "Alt_R", "Super_L", "Super_R", "Page_Up", "Page_Down", "End", "Home",
"Left", "Up", "Right", "Down", "Insert", "Delete",
"KP_Begin", "KP_Enter", "KP_Page_Up", "KP_Page_Down", "KP_End", "KP_Home",
"KP_Left", "KP_Up", "KP_Right", "KP_Down", "KP_Insert", "KP_Delete",
"KP_Multiply", "KP_Add", "KP_Separator", "KP_Subtract", "KP_Decimal", "KP_Divide",
"semicolon", "equal", "comma", "minus", "period", "slash", "grave", "bracketleft",
"backslash", "bracketright", "apostrophe":
return true
}
return false
}
func ValidRemoteKeyRequest(request RemoteKeyRequest) bool {
return len(request.Lease) == 32 && request.Epoch != 0 && request.Sequence != 0 && ValidRemoteKey(request.Key)
}
func (key *RemoteKey) UnmarshalJSON(data []byte) error {
var fields map[string]json.RawMessage
if DecodeStrict(data, &fields) != nil || len(fields) != 2 || fields["name"] == nil || fields["pressed"] == nil {
return ErrProtocol
}
var value struct {
Name string `json:"name"`
Pressed *bool `json:"pressed"`
}
if DecodeStrict(data, &value) != nil || value.Pressed == nil {
return ErrProtocol
}
result := RemoteKey{Name: value.Name, Pressed: *value.Pressed}
if !ValidRemoteKey(result) {
return ErrProtocol
}
*key = result
return nil
}
func (request *RemoteKeyRequest) UnmarshalJSON(data []byte) error {
var fields map[string]json.RawMessage
if DecodeStrict(data, &fields) != nil || len(fields) != 4 || fields["lease"] == nil || fields["epoch"] == nil || fields["sequence"] == nil || fields["key"] == nil {
return ErrProtocol
}
type plain RemoteKeyRequest
var value plain
if DecodeStrict(data, &value) != nil || !ValidRemoteKeyRequest(RemoteKeyRequest(value)) {
return ErrProtocol
}
*request = RemoteKeyRequest(value)
return nil
}
+152
View File
@@ -0,0 +1,152 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package tentaclewire
import (
"encoding/json"
"fmt"
"os/exec"
"path/filepath"
"strings"
"testing"
)
func TestRemoteKeySDKValidation(t *testing.T) {
cases := []struct {
data string
valid bool
}{
{`{"name":"a","pressed":true}`, true},
{`{"name":"Control_R","pressed":false}`, true},
{`{"name":"KP_Enter","pressed":true}`, true},
{`{"name":"semicolon","pressed":false}`, true},
{`{"name":"Prior","pressed":true}`, false},
{`{"name":"A","pressed":true}`, false},
{`{"name":"F01","pressed":true}`, false},
{`{"name":"F+1","pressed":true}`, false},
{`{"name":"F25","pressed":true}`, false},
{`{"name":"F1","pressed":true}`, false},
{`{"name":"KP_01","pressed":true}`, false},
{`{"name":"ISO_Level3_Shift","pressed":true}`, false},
{`{"name":"dead_acute","pressed":true}`, false},
{`{"name":"U20AC","pressed":true}`, false},
{`{"name":"../anything","pressed":true}`, false},
{`{"name":"a"}`, false},
{`{"name":"a","pressed":null}`, false},
{`{"name":"a","pressed":0}`, false},
{`{"name":"a","pressed":"false"}`, false},
{`{"Name":"a","pressed":true}`, false},
{`{"name":"a","Pressed":true}`, false},
{`{"name":"a","pressed":false,"origin":"local"}`, false},
{`{"name":"a","pressed":true,"scan_code":38}`, false},
{`{"pressed":false}`, false},
{`{"name":null,"pressed":false}`, false},
{`{"name":"","pressed":false}`, false},
{`{"name":1,"pressed":false}`, false},
{`{"name":"a","name":"b","pressed":false}`, false},
{`null`, false}, {`[]`, false}, {`{}`, false},
}
for _, group := range []struct {
first, last int
name func(int) string
}{
{'a', 'z', func(code int) string { return string(rune(code)) }},
{'0', '9', func(code int) string { return string(rune(code)) }},
{1, 24, func(code int) string { return fmt.Sprintf("F%d", code) }},
{0, 9, func(code int) string { return fmt.Sprintf("KP_%d", code) }},
} {
for code := group.first; code <= group.last; code++ {
for _, pressed := range []bool{false, true} {
data, _ := json.Marshal(RemoteKey{Name: group.name(code), Pressed: pressed})
cases = append(cases, struct {
data string
valid bool
}{string(data), true})
}
}
}
var probes []any
for _, test := range cases {
var key RemoteKey
if valid := DecodeStrict([]byte(test.data), &key) == nil && ValidRemoteKey(key); valid != test.valid {
t.Fatalf("Go key validation %s: %v", test.data, valid)
}
probes = append(probes, map[string]any{"raw": test.data, "valid": test.valid})
}
checkRemoteKeyPython(t, "valid_remote_key", probes)
}
func TestRemoteKeyRequestSDKValidation(t *testing.T) {
base := `{"lease":"` + strings.Repeat("a", 32) + `","epoch":1,"sequence":1,"key":{"name":"a","pressed":false}}`
cases := []struct {
data string
valid bool
}{
{base, true},
{strings.Replace(base, `"sequence":1`, `"sequence":18446744073709551615`, 1), true},
{strings.Replace(base, `"epoch":1`, `"epoch":18446744073709551615`, 1), true},
}
for _, field := range []string{"epoch", "sequence"} {
for _, invalid := range []string{"0", "-1", "1.0", "true", "null", "18446744073709551616"} {
cases = append(cases, struct {
data string
valid bool
}{strings.Replace(base, `"`+field+`":1`, `"`+field+`":`+invalid, 1), false})
}
}
for _, data := range []string{
strings.Replace(base, `"lease":"`+strings.Repeat("a", 32)+`"`, `"lease":null`, 1),
strings.Replace(base, strings.Repeat("a", 32), "short", 1),
strings.Replace(base, `"key":{"name":"a","pressed":false}`, `"key":null`, 1),
strings.Replace(base, `"key":{"name":"a","pressed":false}`, `"key":{}`, 1),
strings.Replace(base, `"epoch":1,`, "", 1),
strings.Replace(base, `,"key":{"name":"a","pressed":false}`, "", 1),
strings.Replace(base, `"epoch":1`, `"epoch":1,"epoch":2`, 1),
strings.Replace(base, `"epoch":1`, `"epoch":1,"origin":"local"`, 1),
strings.Replace(base, `"epoch":1`, `"epoch":1,"keys":[]`, 1),
strings.Replace(base, `"epoch":1`, `"Epoch":1`, 1),
`null`, `[]`, `{}`,
} {
cases = append(cases, struct {
data string
valid bool
}{data, false})
}
var probes []any
for _, test := range cases {
var request RemoteKeyRequest
if valid := DecodeStrict([]byte(test.data), &request) == nil && ValidRemoteKeyRequest(request); valid != test.valid {
t.Fatalf("Go key request validation %s: %v", test.data, valid)
}
probes = append(probes, map[string]any{"raw": test.data, "valid": test.valid})
}
checkRemoteKeyPython(t, "valid_remote_key_request", probes)
}
func checkRemoteKeyPython(t *testing.T, validator string, probes []any) {
t.Helper()
path, err := filepath.Abs("../../sdk/python")
if err != nil {
t.Fatal(err)
}
script := `import json, sys
sys.path.insert(0, sys.argv[1])
from cthulhu_tentacle import _object, ProtocolError
from cthulhu_tentacle import session
validate = getattr(session, sys.argv[2])
for case in json.load(sys.stdin):
try:
actual = validate(json.loads(case['raw'], object_pairs_hook=_object))
except (ProtocolError, ValueError):
actual = False
assert actual == case['valid'], str(case)[:180]
`
data, _ := json.Marshal(probes)
command := exec.Command("python3", "-B", "-c", script, path, validator)
command.Stdin = strings.NewReader(string(data))
if output, err := command.CombinedOutput(); err != nil {
t.Fatalf("Python key validation: %v: %s", err, output)
}
}