Add lease-bound Remote key IPC and SDK validation
This commit is contained in:
1 parent
9ac33560dc
commit
792cac5d0b
12 files changed
+807
-9
No files matched your search
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user