feat(input): add absolute pointer and scroll
This commit is contained in:
@@ -16,7 +16,10 @@ linked, or embedded:
|
|||||||
|
|
||||||
- Apollo `adc5c5a0bd80831ce495434bb16aee2cd4175fb8`, GPLv3:
|
- Apollo `adc5c5a0bd80831ce495434bb16aee2cd4175fb8`, GPLv3:
|
||||||
`src/rtsp.cpp`, `src/stream.cpp`, `src/audio.cpp`, `src/audio.h`,
|
`src/rtsp.cpp`, `src/stream.cpp`, `src/audio.cpp`, `src/audio.h`,
|
||||||
`src/nvhttp.cpp`, `LICENSE`, and `NOTICE`.
|
`src/nvhttp.cpp`, `src/input.cpp`, `LICENSE`, and `NOTICE`.
|
||||||
|
- Apollo's moonlight-common-c pin
|
||||||
|
`c999436858471dfefa7617af3b7dc03ec1644ce4`, GPLv3: `src/Input.h`,
|
||||||
|
`src/InputStream.c`, and `LICENSE.txt`.
|
||||||
- Moonlight Qt `c0c4d6056569bba40ac4458a3c225c05ff86df6d` with common-c
|
- Moonlight Qt `c0c4d6056569bba40ac4458a3c225c05ff86df6d` with common-c
|
||||||
pin `2ea47752c3051d72a64bcca190024e8b354fa1ef`, GPLv3:
|
pin `2ea47752c3051d72a64bcca190024e8b354fa1ef`, GPLv3:
|
||||||
`src/ControlStream.c`, `src/Video.h`, `src/RtpAudioQueue.h`,
|
`src/ControlStream.c`, `src/Video.h`, `src/RtpAudioQueue.h`,
|
||||||
|
|||||||
@@ -57,11 +57,12 @@ func run() error {
|
|||||||
controlPlaneClient := gateway.NewControlPlaneClient(controlPlane, &http.Client{Transport: transport, Timeout: 5 * time.Second})
|
controlPlaneClient := gateway.NewControlPlaneClient(controlPlane, &http.Client{Transport: transport, Timeout: 5 * time.Second})
|
||||||
provider := gateway.NewApolloAdapter(gateway.NewNativeApolloBackend(), gateway.ProviderIdentity{})
|
provider := gateway.NewApolloAdapter(gateway.NewNativeApolloBackend(), gateway.ProviderIdentity{})
|
||||||
capabilities := gateway.DefaultCapabilities()
|
capabilities := gateway.DefaultCapabilities()
|
||||||
server, err := gateway.NewServer(gateway.ServerConfig{ListenAddress: listen, TLSConfig: serverTLS, GatewayID: gatewayID, Capabilities: capabilities, ProviderCapabilities: capabilities, Admission: controlPlaneClient, ProviderStateReporter: controlPlaneClient, ClipboardAuditReporter: controlPlaneClient, Provider: provider, PacerKbps: 100000})
|
features := gateway.DefaultFeatures()
|
||||||
|
server, err := gateway.NewServer(gateway.ServerConfig{ListenAddress: listen, TLSConfig: serverTLS, GatewayID: gatewayID, Features: features, Capabilities: capabilities, ProviderCapabilities: capabilities, Admission: controlPlaneClient, ProviderStateReporter: controlPlaneClient, ClipboardAuditReporter: controlPlaneClient, Provider: provider, PacerKbps: 100000})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
registration := protocol.GatewayRegistration{Version: "1", GatewayID: gatewayID, InstanceIdentity: instanceIdentity, CertificateIdentity: certificateIdentity, PublicIdentity: publicIdentity, Address: advertiseAddress, ProviderIdentity: "server-derived", ProtocolMinVersion: 1, ProtocolMaxVersion: 1, ConnectionCapacity: 8, BandwidthCapacityKbps: 100000, Features: []string{"quic-tls13", "datagram.media", "apollo"}, Capabilities: capabilities}
|
registration := protocol.GatewayRegistration{Version: "1", GatewayID: gatewayID, InstanceIdentity: instanceIdentity, CertificateIdentity: certificateIdentity, PublicIdentity: publicIdentity, Address: advertiseAddress, ProviderIdentity: "server-derived", ProtocolMinVersion: 1, ProtocolMaxVersion: 1, ConnectionCapacity: 8, BandwidthCapacityKbps: 100000, Features: features, Capabilities: capabilities}
|
||||||
if _, err := controlPlaneClient.Register(context.Background(), registration); err != nil {
|
if _, err := controlPlaneClient.Register(context.Background(), registration); err != nil {
|
||||||
_ = server.Close()
|
_ = server.Close()
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -328,13 +329,52 @@ func TestApolloControlWireVectorAndTagFailure(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestApolloKeyboardInputWireVector(t *testing.T) {
|
func TestApolloKeyboardInputWireVector(t *testing.T) {
|
||||||
packet, err := encodeApolloInputEvent(InputEvent{Device: "keyboard", Code: 30, Pressed: true, Payload: []byte{2}})
|
packets, err := encodeApolloInputEvent(InputEvent{Device: "keyboard", Code: 30, Pressed: true, Payload: []byte{2}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
const expected = "0000000a03000000001e00020000"
|
const expected = "0000000a03000000001e00020000"
|
||||||
if string(packet.payload) != string(mustDecodeHex(t, expected)) {
|
if len(packets) != 1 || string(packets[0].payload) != string(mustDecodeHex(t, expected)) {
|
||||||
t.Fatalf("keyboard packet = %x, want %s", packet.payload, expected)
|
t.Fatalf("keyboard packets = %#v, want %s", packets, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApolloAbsoluteAndScrollInputWireVectors(t *testing.T) {
|
||||||
|
// Independently implemented from the approved Apollo adc5c5a0 input.cpp
|
||||||
|
// consumer and its moonlight-common-c c999436 Input.h/InputStream.c pin.
|
||||||
|
absolute, err := encodeApolloInputEvent(InputEvent{Device: "mouse-absolute", Payload: []byte{0x04, 0xd2, 0x02, 0x37, 0x0a, 0x00, 0x05, 0xa0}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
const absoluteExpected = "0000000e0500000004d20237000009ff059f"
|
||||||
|
if len(absolute) != 1 || absolute[0].channel != apolloChannelMouse || string(absolute[0].payload) != string(mustDecodeHex(t, absoluteExpected)) {
|
||||||
|
t.Fatalf("absolute packets = %#v, want %s", absolute, absoluteExpected)
|
||||||
|
}
|
||||||
|
|
||||||
|
scroll, err := encodeApolloInputEvent(InputEvent{Device: "mouse-scroll", Payload: []byte{0xff, 0x88, 0x00, 0x78}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
const verticalExpected = "0000000a0a000000ff88ff880000"
|
||||||
|
const horizontalExpected = "00000006010000550078"
|
||||||
|
if len(scroll) != 2 || scroll[0].channel != apolloChannelMouse || scroll[1].channel != apolloChannelMouse ||
|
||||||
|
string(scroll[0].payload) != string(mustDecodeHex(t, verticalExpected)) || string(scroll[1].payload) != string(mustDecodeHex(t, horizontalExpected)) {
|
||||||
|
t.Fatalf("scroll packets = %#v", scroll)
|
||||||
|
}
|
||||||
|
|
||||||
|
zero, err := encodeApolloInputEvent(InputEvent{Device: "mouse-scroll", Payload: make([]byte, 4)})
|
||||||
|
if err != nil || len(zero) != 0 {
|
||||||
|
t.Fatalf("zero scroll packets = %#v, %v", zero, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, payload := range [][]byte{
|
||||||
|
{0, 0, 0, 0, 0, 1, 0, 2},
|
||||||
|
{0, 0, 0, 0, 0x80, 0, 0, 2},
|
||||||
|
{0, 0, 0, 0, 0, 2, 0x80, 0},
|
||||||
|
} {
|
||||||
|
if _, err := encodeApolloInputEvent(InputEvent{Device: "mouse-absolute", Payload: payload}); !errors.Is(err, ErrInputMalformed) {
|
||||||
|
t.Fatalf("Apollo accepted unrepresentable absolute payload %x: %v", payload, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+48
-12
@@ -19,11 +19,11 @@ type apolloInputPacket struct {
|
|||||||
payload []byte
|
payload []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func encodeApolloInputEvent(event InputEvent) (apolloInputPacket, error) {
|
func encodeApolloInputEvent(event InputEvent) ([]apolloInputPacket, error) {
|
||||||
switch event.Device {
|
switch event.Device {
|
||||||
case "keyboard":
|
case "keyboard":
|
||||||
if event.Code < 0 || event.Code > 0xffff || len(event.Payload) > 1 {
|
if event.Code < 0 || event.Code > 0xffff || len(event.Payload) > 1 {
|
||||||
return apolloInputPacket{}, ErrInputMalformed
|
return nil, ErrInputMalformed
|
||||||
}
|
}
|
||||||
packet := make([]byte, 14)
|
packet := make([]byte, 14)
|
||||||
binary.BigEndian.PutUint32(packet[:4], 10)
|
binary.BigEndian.PutUint32(packet[:4], 10)
|
||||||
@@ -36,10 +36,10 @@ func encodeApolloInputEvent(event InputEvent) (apolloInputPacket, error) {
|
|||||||
if len(event.Payload) == 1 {
|
if len(event.Payload) == 1 {
|
||||||
packet[11] = event.Payload[0]
|
packet[11] = event.Payload[0]
|
||||||
}
|
}
|
||||||
return apolloInputPacket{channel: apolloChannelKeyboard, payload: packet}, nil
|
return []apolloInputPacket{{channel: apolloChannelKeyboard, payload: packet}}, nil
|
||||||
case "mouse-button":
|
case "mouse-button":
|
||||||
if event.Code < 1 || event.Code > 8 || len(event.Payload) != 0 {
|
if event.Code < 1 || event.Code > 8 || len(event.Payload) != 0 {
|
||||||
return apolloInputPacket{}, ErrInputMalformed
|
return nil, ErrInputMalformed
|
||||||
}
|
}
|
||||||
packet := make([]byte, 9)
|
packet := make([]byte, 9)
|
||||||
binary.BigEndian.PutUint32(packet[:4], 5)
|
binary.BigEndian.PutUint32(packet[:4], 5)
|
||||||
@@ -49,28 +49,28 @@ func encodeApolloInputEvent(event InputEvent) (apolloInputPacket, error) {
|
|||||||
}
|
}
|
||||||
binary.LittleEndian.PutUint32(packet[4:8], magic)
|
binary.LittleEndian.PutUint32(packet[4:8], magic)
|
||||||
packet[8] = byte(event.Code)
|
packet[8] = byte(event.Code)
|
||||||
return apolloInputPacket{channel: apolloChannelMouse, payload: packet}, nil
|
return []apolloInputPacket{{channel: apolloChannelMouse, payload: packet}}, nil
|
||||||
case "mouse-relative":
|
case "mouse-relative":
|
||||||
if event.Pressed || len(event.Payload) != 4 {
|
if event.Pressed || len(event.Payload) != 4 {
|
||||||
return apolloInputPacket{}, ErrInputMalformed
|
return nil, ErrInputMalformed
|
||||||
}
|
}
|
||||||
packet := make([]byte, 12)
|
packet := make([]byte, 12)
|
||||||
binary.BigEndian.PutUint32(packet[:4], 8)
|
binary.BigEndian.PutUint32(packet[:4], 8)
|
||||||
binary.LittleEndian.PutUint32(packet[4:8], 7)
|
binary.LittleEndian.PutUint32(packet[4:8], 7)
|
||||||
copy(packet[8:], event.Payload)
|
copy(packet[8:], event.Payload)
|
||||||
return apolloInputPacket{channel: apolloChannelMouse, payload: packet}, nil
|
return []apolloInputPacket{{channel: apolloChannelMouse, payload: packet}}, nil
|
||||||
case "utf8":
|
case "utf8":
|
||||||
if event.Pressed || len(event.Payload) == 0 || len(event.Payload) > utf8.UTFMax || !utf8.Valid(event.Payload) || utf8.RuneCount(event.Payload) != 1 {
|
if event.Pressed || len(event.Payload) == 0 || len(event.Payload) > utf8.UTFMax || !utf8.Valid(event.Payload) || utf8.RuneCount(event.Payload) != 1 {
|
||||||
return apolloInputPacket{}, ErrInputMalformed
|
return nil, ErrInputMalformed
|
||||||
}
|
}
|
||||||
packet := make([]byte, 8+len(event.Payload))
|
packet := make([]byte, 8+len(event.Payload))
|
||||||
binary.BigEndian.PutUint32(packet[:4], uint32(4+len(event.Payload)))
|
binary.BigEndian.PutUint32(packet[:4], uint32(4+len(event.Payload)))
|
||||||
binary.LittleEndian.PutUint32(packet[4:8], 0x17)
|
binary.LittleEndian.PutUint32(packet[4:8], 0x17)
|
||||||
copy(packet[8:], event.Payload)
|
copy(packet[8:], event.Payload)
|
||||||
return apolloInputPacket{channel: apolloChannelUTF8, payload: packet}, nil
|
return []apolloInputPacket{{channel: apolloChannelUTF8, payload: packet}}, nil
|
||||||
case "controller":
|
case "controller":
|
||||||
if event.Code < 0 || event.Code > 15 || len(event.Payload) != 16 {
|
if event.Code < 0 || event.Code > 15 || len(event.Payload) != 16 {
|
||||||
return apolloInputPacket{}, ErrInputMalformed
|
return nil, ErrInputMalformed
|
||||||
}
|
}
|
||||||
packet := make([]byte, 34)
|
packet := make([]byte, 34)
|
||||||
binary.BigEndian.PutUint32(packet[:4], 30)
|
binary.BigEndian.PutUint32(packet[:4], 30)
|
||||||
@@ -83,8 +83,44 @@ func encodeApolloInputEvent(event InputEvent) (apolloInputPacket, error) {
|
|||||||
binary.LittleEndian.PutUint16(packet[28:30], 0x9c)
|
binary.LittleEndian.PutUint16(packet[28:30], 0x9c)
|
||||||
copy(packet[30:32], event.Payload[14:16])
|
copy(packet[30:32], event.Payload[14:16])
|
||||||
binary.LittleEndian.PutUint16(packet[32:34], 0x55)
|
binary.LittleEndian.PutUint16(packet[32:34], 0x55)
|
||||||
return apolloInputPacket{channel: apolloChannelGamepad + uint8(event.Code), payload: packet}, nil
|
return []apolloInputPacket{{channel: apolloChannelGamepad + uint8(event.Code), payload: packet}}, nil
|
||||||
|
case "mouse-absolute":
|
||||||
|
if event.Pressed || event.Code != 0 || !validAbsolutePayload(event.Payload) {
|
||||||
|
return nil, ErrInputMalformed
|
||||||
|
}
|
||||||
|
width, height := binary.BigEndian.Uint16(event.Payload[4:6]), binary.BigEndian.Uint16(event.Payload[6:8])
|
||||||
|
if width < 2 || height < 2 || width > 0x7fff || height > 0x7fff {
|
||||||
|
return nil, ErrInputMalformed
|
||||||
|
}
|
||||||
|
packet := make([]byte, 18)
|
||||||
|
binary.BigEndian.PutUint32(packet[:4], 14)
|
||||||
|
binary.LittleEndian.PutUint32(packet[4:8], 5)
|
||||||
|
copy(packet[8:12], event.Payload[:4])
|
||||||
|
binary.BigEndian.PutUint16(packet[14:16], width-1)
|
||||||
|
binary.BigEndian.PutUint16(packet[16:18], height-1)
|
||||||
|
return []apolloInputPacket{{channel: apolloChannelMouse, payload: packet}}, nil
|
||||||
|
case "mouse-scroll":
|
||||||
|
if event.Pressed || event.Code != 0 || len(event.Payload) != 4 {
|
||||||
|
return nil, ErrInputMalformed
|
||||||
|
}
|
||||||
|
packets := make([]apolloInputPacket, 0, 2)
|
||||||
|
if event.Payload[0] != 0 || event.Payload[1] != 0 {
|
||||||
|
packet := make([]byte, 14)
|
||||||
|
binary.BigEndian.PutUint32(packet[:4], 10)
|
||||||
|
binary.LittleEndian.PutUint32(packet[4:8], 10)
|
||||||
|
copy(packet[8:10], event.Payload[:2])
|
||||||
|
copy(packet[10:12], event.Payload[:2])
|
||||||
|
packets = append(packets, apolloInputPacket{channel: apolloChannelMouse, payload: packet})
|
||||||
|
}
|
||||||
|
if event.Payload[2] != 0 || event.Payload[3] != 0 {
|
||||||
|
packet := make([]byte, 10)
|
||||||
|
binary.BigEndian.PutUint32(packet[:4], 6)
|
||||||
|
binary.LittleEndian.PutUint32(packet[4:8], 0x55000001)
|
||||||
|
copy(packet[8:10], event.Payload[2:4])
|
||||||
|
packets = append(packets, apolloInputPacket{channel: apolloChannelMouse, payload: packet})
|
||||||
|
}
|
||||||
|
return packets, nil
|
||||||
default:
|
default:
|
||||||
return apolloInputPacket{}, ErrInputMalformed
|
return nil, ErrInputMalformed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -454,16 +454,18 @@ func (s *nativeApolloSession) Audio() <-chan ProviderMedia { return s.audio }
|
|||||||
func (s *nativeApolloSession) Events() <-chan ProviderEvent { return s.events }
|
func (s *nativeApolloSession) Events() <-chan ProviderEvent { return s.events }
|
||||||
|
|
||||||
func (s *nativeApolloSession) Input(ctx context.Context, event InputEvent) error {
|
func (s *nativeApolloSession) Input(ctx context.Context, event InputEvent) error {
|
||||||
packet, err := encodeApolloInputEvent(event)
|
packets, err := encodeApolloInputEvent(event)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
for _, packet := range packets {
|
||||||
if err := s.writeApolloControl(packet.channel, true, apolloControlTypeInput, packet.payload); err != nil {
|
if err := s.writeApolloControl(packet.channel, true, apolloControlTypeInput, packet.payload); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if event.Device == "keyboard" || event.Device == "mouse-button" || event.Device == "controller" {
|
if event.Device == "keyboard" || event.Device == "mouse-button" || event.Device == "controller" {
|
||||||
key := fmt.Sprintf("%s:%d", event.Device, event.Code)
|
key := fmt.Sprintf("%s:%d", event.Device, event.Code)
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ import (
|
|||||||
|
|
||||||
var ErrNoCapabilityOverlap = errors.New("no capability overlap")
|
var ErrNoCapabilityOverlap = errors.New("no capability overlap")
|
||||||
|
|
||||||
|
func DefaultFeatures() []string {
|
||||||
|
return []string{"quic-tls13", "datagram.media", "apollo", "display.request.v1", "input.absolute.v1", "input.scroll.v1"}
|
||||||
|
}
|
||||||
|
|
||||||
func DefaultCapabilities() protocol.CapabilityProfile {
|
func DefaultCapabilities() protocol.CapabilityProfile {
|
||||||
return protocol.CapabilityProfile{
|
return protocol.CapabilityProfile{
|
||||||
Transport: "quic-tls13",
|
Transport: "quic-tls13",
|
||||||
|
|||||||
+117
-2
@@ -67,6 +67,10 @@ func FuzzDecodeFrame(f *testing.F) {
|
|||||||
func FuzzDecodeInputEvent(f *testing.F) {
|
func FuzzDecodeInputEvent(f *testing.F) {
|
||||||
seed, _ := EncodeInputEvent(InputEvent{Sequence: 1, Device: "keyboard", Code: 7, Pressed: true})
|
seed, _ := EncodeInputEvent(InputEvent{Sequence: 1, Device: "keyboard", Code: 7, Pressed: true})
|
||||||
f.Add(seed)
|
f.Add(seed)
|
||||||
|
absolute, _ := EncodeInputEvent(InputEvent{Device: "mouse-absolute", Payload: []byte{0, 1, 0, 1, 0, 2, 0, 2}})
|
||||||
|
f.Add(absolute)
|
||||||
|
scroll, _ := EncodeInputEvent(InputEvent{Device: "mouse-scroll", Payload: []byte{0xff, 0x88, 0, 0x78}})
|
||||||
|
f.Add(scroll)
|
||||||
f.Add([]byte("VGI1"))
|
f.Add([]byte("VGI1"))
|
||||||
f.Fuzz(func(t *testing.T, data []byte) {
|
f.Fuzz(func(t *testing.T, data []byte) {
|
||||||
_, _ = DecodeInputEvent(data)
|
_, _ = DecodeInputEvent(data)
|
||||||
@@ -88,6 +92,107 @@ func TestInputEventUsesFixedProtocolVGI1Vector(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInputEventUsesFixedAbsoluteAndScrollVectors(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
event InputEvent
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "absolute",
|
||||||
|
event: InputEvent{Device: "mouse-absolute", Payload: []byte{0x04, 0xd2, 0x02, 0x37, 0x0a, 0x00, 0x05, 0xa0}},
|
||||||
|
expected: "56474931060804d202370a0005a0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "scroll",
|
||||||
|
event: InputEvent{Device: "mouse-scroll", Payload: []byte{0xff, 0x88, 0x00, 0x78}},
|
||||||
|
expected: "564749310704ff880078",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
encoded, err := EncodeInputEvent(test.event)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if hex.EncodeToString(encoded) != test.expected {
|
||||||
|
t.Fatalf("EncodeInputEvent() = %x, want %s", encoded, test.expected)
|
||||||
|
}
|
||||||
|
decoded, err := DecodeInputEvent(encoded)
|
||||||
|
if err != nil || decoded.Device != test.event.Device || decoded.Code != 0 || decoded.Pressed || !bytes.Equal(decoded.Payload, test.event.Payload) {
|
||||||
|
t.Fatalf("DecodeInputEvent() = %#v, %v", decoded, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInputEventRejectsMalformedAbsoluteAndScroll(t *testing.T) {
|
||||||
|
for _, event := range []InputEvent{
|
||||||
|
{Device: "mouse-absolute", Pressed: true, Payload: make([]byte, 8)},
|
||||||
|
{Device: "mouse-absolute", Payload: []byte{0, 0, 0, 0, 0, 0, 0, 1}},
|
||||||
|
{Device: "mouse-absolute", Payload: []byte{0, 2, 0, 0, 0, 2, 0, 1}},
|
||||||
|
{Device: "mouse-absolute", Payload: []byte{0, 0, 0, 1, 0, 2, 0, 1}},
|
||||||
|
{Device: "mouse-scroll", Pressed: true, Payload: make([]byte, 4)},
|
||||||
|
{Device: "mouse-scroll", Payload: make([]byte, 3)},
|
||||||
|
} {
|
||||||
|
if _, err := EncodeInputEvent(event); !errors.Is(err, ErrInputMalformed) {
|
||||||
|
t.Fatalf("EncodeInputEvent(%#v) = %v", event, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, value := range []string{
|
||||||
|
"56474931060800000000000005a0",
|
||||||
|
"5647493106080a0000000a0005a0",
|
||||||
|
"564749310608000005a00a0005a0",
|
||||||
|
"56474931060700000000010001",
|
||||||
|
"5647493107020000",
|
||||||
|
} {
|
||||||
|
raw, err := hex.DecodeString(value)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := DecodeInputEvent(raw); !errors.Is(err, ErrInputMalformed) {
|
||||||
|
t.Fatalf("DecodeInputEvent(%s) = %v", value, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGatewayRejectsUnadvertisedInputBeforeProviderTranslation(t *testing.T) {
|
||||||
|
provider := &fakeSession{
|
||||||
|
state: protocol.ProviderState{State: ProviderStateReady},
|
||||||
|
pressed: make(map[string]struct{}),
|
||||||
|
}
|
||||||
|
session := &gatewaySession{
|
||||||
|
server: &Server{config: ServerConfig{}},
|
||||||
|
provider: provider,
|
||||||
|
ctx: context.Background(),
|
||||||
|
pressed: make(map[string]struct{}),
|
||||||
|
}
|
||||||
|
absolute, err := EncodeInputEvent(InputEvent{Device: "mouse-absolute", Payload: []byte{0, 1, 0, 1, 0, 2, 0, 2}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := session.handleInput(absolute, 1); !errors.Is(err, ErrInputMalformed) {
|
||||||
|
t.Fatalf("unadvertised absolute input = %v", err)
|
||||||
|
}
|
||||||
|
if len(provider.inputs) != 0 {
|
||||||
|
t.Fatalf("unadvertised absolute input reached provider: %#v", provider.inputs)
|
||||||
|
}
|
||||||
|
session.server.config.Features = []string{"input.absolute.v1"}
|
||||||
|
if err := session.handleInput(absolute, 2); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
scroll, err := EncodeInputEvent(InputEvent{Device: "mouse-scroll", Payload: []byte{0, 1, 0, 1}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := session.handleInput(scroll, 3); !errors.Is(err, ErrInputMalformed) {
|
||||||
|
t.Fatalf("unadvertised scroll input = %v", err)
|
||||||
|
}
|
||||||
|
if len(provider.inputs) != 1 || len(session.pressed) != 0 {
|
||||||
|
t.Fatalf("provider inputs=%#v pressed=%#v", provider.inputs, session.pressed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestClientFeedbackUsesFixedProtocolVGFVector(t *testing.T) {
|
func TestClientFeedbackUsesFixedProtocolVGFVector(t *testing.T) {
|
||||||
feedback := Feedback{Sequence: 9, Kind: FeedbackFEC, Payload: []byte{0, 0, 0, 42, 0, 5, 0, 3, 0, 2, 0, 10, 0, 2, 0, 8, 0, 2, 20, 0, 1}}
|
feedback := Feedback{Sequence: 9, Kind: FeedbackFEC, Payload: []byte{0, 0, 0, 42, 0, 5, 0, 3, 0, 2, 0, 10, 0, 2, 0, 8, 0, 2, 20, 0, 1}}
|
||||||
encoded, err := EncodeClientFeedback(feedback)
|
encoded, err := EncodeClientFeedback(feedback)
|
||||||
@@ -581,6 +686,14 @@ func TestRegisteredChannelFramesTraversePublicTransport(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
absolute, err := EncodeInputEvent(InputEvent{Sequence: 10, Device: "mouse-absolute", Payload: []byte{0, 1, 0, 1, 0, 2, 0, 2}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
scroll, err := EncodeInputEvent(InputEvent{Sequence: 11, Device: "mouse-scroll", Payload: []byte{0, 1, 0, 1}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
feedback, err := EncodeClientFeedback(Feedback{Sequence: 8, Kind: FeedbackIDR})
|
feedback, err := EncodeClientFeedback(Feedback{Sequence: 8, Kind: FeedbackIDR})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -593,6 +706,8 @@ func TestRegisteredChannelFramesTraversePublicTransport(t *testing.T) {
|
|||||||
testChannelFrame("input.sequenced.v1", 7, input),
|
testChannelFrame("input.sequenced.v1", 7, input),
|
||||||
testChannelFrame("control.ack.v1", 8, feedback),
|
testChannelFrame("control.ack.v1", 8, feedback),
|
||||||
testChannelFrame("clipboard.text.v1", 9, clipboard),
|
testChannelFrame("clipboard.text.v1", 9, clipboard),
|
||||||
|
testChannelFrame("input.sequenced.v1", 10, absolute),
|
||||||
|
testChannelFrame("input.sequenced.v1", 11, scroll),
|
||||||
} {
|
} {
|
||||||
encoded, encodeErr := protocol.EncodeChannelFrame(frame)
|
encoded, encodeErr := protocol.EncodeChannelFrame(frame)
|
||||||
if encodeErr != nil {
|
if encodeErr != nil {
|
||||||
@@ -617,7 +732,7 @@ func TestRegisteredChannelFramesTraversePublicTransport(t *testing.T) {
|
|||||||
inputs := append([]InputEvent(nil), h.session.inputs...)
|
inputs := append([]InputEvent(nil), h.session.inputs...)
|
||||||
feedbacks := append([]Feedback(nil), h.session.feedback...)
|
feedbacks := append([]Feedback(nil), h.session.feedback...)
|
||||||
h.session.mu.Unlock()
|
h.session.mu.Unlock()
|
||||||
if len(inputs) == 1 && inputs[0].Sequence == 7 && len(feedbacks) == 1 && feedbacks[0].Sequence == 8 && feedbacks[0].Kind == FeedbackIDR {
|
if len(inputs) == 3 && inputs[0].Sequence == 7 && inputs[1].Sequence == 10 && inputs[1].Device == "mouse-absolute" && inputs[2].Sequence == 11 && inputs[2].Device == "mouse-scroll" && len(feedbacks) == 1 && feedbacks[0].Sequence == 8 && feedbacks[0].Kind == FeedbackIDR {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if time.Now().After(deadline) {
|
if time.Now().After(deadline) {
|
||||||
@@ -1320,7 +1435,7 @@ func newGatewayTransportHarnessWithClipboard(t *testing.T, clipboardEnabled bool
|
|||||||
authority := protocol.SessionAuthority{Version: "1", SessionID: "session-transport", GatewayID: "gateway-1", Audience: "versevdi-gateway", ReconnectSequence: 0, ExpiresAt: time.Now().Add(5 * time.Second).UTC().Format(time.RFC3339Nano), Capabilities: DefaultCapabilities(), ProviderProfile: ProviderProfileApollo, ProviderIdentity: fake.config.Identity.Key()}
|
authority := protocol.SessionAuthority{Version: "1", SessionID: "session-transport", GatewayID: "gateway-1", Audience: "versevdi-gateway", ReconnectSequence: 0, ExpiresAt: time.Now().Add(5 * time.Second).UTC().Format(time.RFC3339Nano), Capabilities: DefaultCapabilities(), ProviderProfile: ProviderProfileApollo, ProviderIdentity: fake.config.Identity.Key()}
|
||||||
admission := &oneTimeAdmission{authority: authority, released: make(chan struct{}), disableClipboard: !clipboardEnabled}
|
admission := &oneTimeAdmission{authority: authority, released: make(chan struct{}), disableClipboard: !clipboardEnabled}
|
||||||
reporter := &recordingProviderStateReporter{}
|
reporter := &recordingProviderStateReporter{}
|
||||||
server, err := NewServer(ServerConfig{ListenAddress: "127.0.0.1:0", TLSConfig: serverTLS, GatewayID: "gateway-1", Capabilities: DefaultCapabilities(), ProviderCapabilities: DefaultCapabilities(), Admission: admission, ProviderStateReporter: reporter, ClipboardAuditReporter: reporter, Provider: fake})
|
server, err := NewServer(ServerConfig{ListenAddress: "127.0.0.1:0", TLSConfig: serverTLS, GatewayID: "gateway-1", Features: DefaultFeatures(), Capabilities: DefaultCapabilities(), ProviderCapabilities: DefaultCapabilities(), Admission: admission, ProviderStateReporter: reporter, ClipboardAuditReporter: reporter, Provider: fake})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ const (
|
|||||||
gatewayInputRelative = 3
|
gatewayInputRelative = 3
|
||||||
gatewayInputUTF8 = 4
|
gatewayInputUTF8 = 4
|
||||||
gatewayInputController = 5
|
gatewayInputController = 5
|
||||||
|
gatewayInputAbsolute = 6
|
||||||
|
gatewayInputScroll = 7
|
||||||
)
|
)
|
||||||
|
|
||||||
func EncodeInputEvent(event InputEvent) ([]byte, error) {
|
func EncodeInputEvent(event InputEvent) ([]byte, error) {
|
||||||
@@ -76,6 +78,24 @@ func EncodeInputEvent(event InputEvent) ([]byte, error) {
|
|||||||
encoded[4], encoded[5], encoded[6] = gatewayInputController, 17, byte(event.Code)
|
encoded[4], encoded[5], encoded[6] = gatewayInputController, 17, byte(event.Code)
|
||||||
copy(encoded[7:], event.Payload)
|
copy(encoded[7:], event.Payload)
|
||||||
return encoded, nil
|
return encoded, nil
|
||||||
|
case "mouse-absolute":
|
||||||
|
if event.Pressed || event.Code != 0 || !validAbsolutePayload(event.Payload) {
|
||||||
|
return nil, ErrInputMalformed
|
||||||
|
}
|
||||||
|
encoded := make([]byte, gatewayInputHeaderSize+8)
|
||||||
|
copy(encoded, "VGI1")
|
||||||
|
encoded[4], encoded[5] = gatewayInputAbsolute, 8
|
||||||
|
copy(encoded[6:], event.Payload)
|
||||||
|
return encoded, nil
|
||||||
|
case "mouse-scroll":
|
||||||
|
if event.Pressed || event.Code != 0 || len(event.Payload) != 4 {
|
||||||
|
return nil, ErrInputMalformed
|
||||||
|
}
|
||||||
|
encoded := make([]byte, gatewayInputHeaderSize+4)
|
||||||
|
copy(encoded, "VGI1")
|
||||||
|
encoded[4], encoded[5] = gatewayInputScroll, 4
|
||||||
|
copy(encoded[6:], event.Payload)
|
||||||
|
return encoded, nil
|
||||||
default:
|
default:
|
||||||
return nil, ErrInputMalformed
|
return nil, ErrInputMalformed
|
||||||
}
|
}
|
||||||
@@ -117,11 +137,30 @@ func DecodeInputEvent(data []byte) (InputEvent, error) {
|
|||||||
return InputEvent{}, ErrInputMalformed
|
return InputEvent{}, ErrInputMalformed
|
||||||
}
|
}
|
||||||
return InputEvent{Device: "controller", Code: int32(body[0]), Pressed: active != 0, Payload: payload}, nil
|
return InputEvent{Device: "controller", Code: int32(body[0]), Pressed: active != 0, Payload: payload}, nil
|
||||||
|
case gatewayInputAbsolute:
|
||||||
|
if !validAbsolutePayload(body) {
|
||||||
|
return InputEvent{}, ErrInputMalformed
|
||||||
|
}
|
||||||
|
return InputEvent{Device: "mouse-absolute", Payload: append([]byte(nil), body...)}, nil
|
||||||
|
case gatewayInputScroll:
|
||||||
|
if len(body) != 4 {
|
||||||
|
return InputEvent{}, ErrInputMalformed
|
||||||
|
}
|
||||||
|
return InputEvent{Device: "mouse-scroll", Payload: append([]byte(nil), body...)}, nil
|
||||||
default:
|
default:
|
||||||
return InputEvent{}, ErrInputMalformed
|
return InputEvent{}, ErrInputMalformed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validAbsolutePayload(payload []byte) bool {
|
||||||
|
if len(payload) != 8 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
x, y := binary.BigEndian.Uint16(payload[:2]), binary.BigEndian.Uint16(payload[2:4])
|
||||||
|
width, height := binary.BigEndian.Uint16(payload[4:6]), binary.BigEndian.Uint16(payload[6:8])
|
||||||
|
return width != 0 && height != 0 && x < width && y < height
|
||||||
|
}
|
||||||
|
|
||||||
func anyNonzero(data []byte) bool {
|
func anyNonzero(data []byte) bool {
|
||||||
for _, value := range data {
|
for _, value := range data {
|
||||||
if value != 0 {
|
if value != 0 {
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ type ServerConfig struct {
|
|||||||
TLSConfig *tls.Config
|
TLSConfig *tls.Config
|
||||||
QUICConfig *quic.Config
|
QUICConfig *quic.Config
|
||||||
GatewayID string
|
GatewayID string
|
||||||
|
Features []string
|
||||||
Capabilities protocol.CapabilityProfile
|
Capabilities protocol.CapabilityProfile
|
||||||
ProviderCapabilities protocol.CapabilityProfile
|
ProviderCapabilities protocol.CapabilityProfile
|
||||||
Admission Admission
|
Admission Admission
|
||||||
@@ -866,6 +867,16 @@ func (s *gatewaySession) handleInput(payload []byte, sequence uint32) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
requiredFeature := ""
|
||||||
|
switch event.Device {
|
||||||
|
case "mouse-absolute":
|
||||||
|
requiredFeature = "input.absolute.v1"
|
||||||
|
case "mouse-scroll":
|
||||||
|
requiredFeature = "input.scroll.v1"
|
||||||
|
}
|
||||||
|
if requiredFeature != "" && !slices.Contains(s.server.config.Features, requiredFeature) {
|
||||||
|
return ErrInputMalformed
|
||||||
|
}
|
||||||
event.Sequence = sequence
|
event.Sequence = sequence
|
||||||
if err := s.provider.Input(s.ctx, event); err != nil {
|
if err := s.provider.Input(s.ctx, event); err != nil {
|
||||||
s.server.metrics.InputRejected.Add(1)
|
s.server.metrics.InputRejected.Add(1)
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
## 1. Contract Freeze
|
## 1. Contract Freeze
|
||||||
|
|
||||||
- [ ] 1.1 Add Protocol `DisplayMode`, negotiated display fields/features, VGI1 absolute/scroll grammar, strict valid/invalid cross-language fixtures, and optional-field omission regressions
|
- [x] 1.1 Add Protocol `DisplayMode`, negotiated display fields/features, VGI1 absolute/scroll grammar, strict valid/invalid cross-language fixtures, and optional-field omission regressions
|
||||||
- [ ] 1.2 Regenerate Go/Rust/Swift outputs twice, pass Protocol `make verify`, freeze a verified-unused immutable Phase 3D RC, and record its fixture/generated hashes
|
- [x] 1.2 Regenerate Go/Rust/Swift outputs twice, pass Protocol `make verify`, freeze a verified-unused immutable Phase 3D RC, and record its fixture/generated hashes
|
||||||
- [ ] 1.3 Update Server and Data Plane to the exact Protocol RC without a filesystem replacement and prove clean-cache module resolution before consumer implementation
|
- [x] 1.3 Update Server and Data Plane to the exact Protocol RC without a filesystem replacement and prove clean-cache module resolution before consumer implementation
|
||||||
|
|
||||||
## 2. Server Display Authority
|
## 2. Server Display Authority
|
||||||
|
|
||||||
- [ ] 2.1 Add the forward-only nullable requested/effective display and policy-version migration plus clean-install/upgrade/schema/grant tests
|
- [x] 2.1 Add the forward-only nullable requested/effective display and policy-version migration plus clean-install/upgrade/schema/grant tests
|
||||||
- [ ] 2.2 Add strict request validation and requested-mode idempotency identity, including waiting-session mismatch regressions
|
- [x] 2.2 Add strict request validation and requested-mode idempotency identity, including waiting-session mismatch regressions
|
||||||
- [ ] 2.3 Implement one proportional even-pixel/FPS clamp at allocation and atomically persist the immutable requested/effective decision
|
- [x] 2.3 Implement one proportional even-pixel/FPS clamp at allocation and atomically persist the immutable requested/effective decision
|
||||||
- [ ] 2.4 Disclose display-aware broker/manifest values only to negotiated clients, preserve legacy response shapes, and reuse the persisted mode on reconnect
|
- [x] 2.4 Disclose display-aware broker/manifest values only to negotiated clients, preserve legacy response shapes, and reuse the persisted mode on reconnect
|
||||||
- [ ] 2.5 Project persisted effective width/height/FPS through existing provider work with selected immutable codec/bitrate/audio and pass focused repository/E2E tests
|
- [x] 2.5 Project persisted effective width/height/FPS through existing provider work with selected immutable codec/bitrate/audio and pass focused repository/E2E tests
|
||||||
|
|
||||||
## 3. Gateway Input Translation
|
## 3. Gateway Input Translation
|
||||||
|
|
||||||
- [ ] 3.1 Add red VGI absolute/scroll encode/decode/bounds/fuzz/transport tests and prove unadvertised kinds fail before provider translation
|
- [x] 3.1 Add red VGI absolute/scroll encode/decode/bounds/fuzz/transport tests and prove unadvertised kinds fail before provider translation
|
||||||
- [ ] 3.2 Establish exact Apollo absolute-pointer and scroll vectors from the approved pinned source; hard-stop without that evidence
|
- [x] 3.2 Establish exact Apollo absolute-pointer and scroll vectors from the approved pinned source; hard-stop without that evidence
|
||||||
- [ ] 3.3 Implement the smallest provider-neutral VGI validation and Apollo adapter translation without adding pressed-state or direct-provider surfaces
|
- [x] 3.3 Implement the smallest provider-neutral VGI validation and Apollo adapter translation without adding pressed-state or direct-provider surfaces
|
||||||
- [ ] 3.4 Pass focused input vectors/fuzz/transport/fake-provider tests, then one affected Data Plane `make verify`
|
- [ ] 3.4 Pass focused input vectors/fuzz/transport/fake-provider tests, then one affected Data Plane `make verify`
|
||||||
|
|
||||||
## 4. Rust Core and Stable ABI
|
## 4. Rust Core and Stable ABI
|
||||||
|
|||||||
Reference in New Issue
Block a user