feat(input): add absolute pointer and scroll
This commit is contained in:
@@ -16,7 +16,10 @@ linked, or embedded:
|
||||
|
||||
- Apollo `adc5c5a0bd80831ce495434bb16aee2cd4175fb8`, GPLv3:
|
||||
`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
|
||||
pin `2ea47752c3051d72a64bcca190024e8b354fa1ef`, GPLv3:
|
||||
`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})
|
||||
provider := gateway.NewApolloAdapter(gateway.NewNativeApolloBackend(), gateway.ProviderIdentity{})
|
||||
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 {
|
||||
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 {
|
||||
_ = server.Close()
|
||||
return err
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -328,13 +329,52 @@ func TestApolloControlWireVectorAndTagFailure(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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
const expected = "0000000a03000000001e00020000"
|
||||
if string(packet.payload) != string(mustDecodeHex(t, expected)) {
|
||||
t.Fatalf("keyboard packet = %x, want %s", packet.payload, expected)
|
||||
if len(packets) != 1 || string(packets[0].payload) != string(mustDecodeHex(t, 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
|
||||
}
|
||||
|
||||
func encodeApolloInputEvent(event InputEvent) (apolloInputPacket, error) {
|
||||
func encodeApolloInputEvent(event InputEvent) ([]apolloInputPacket, error) {
|
||||
switch event.Device {
|
||||
case "keyboard":
|
||||
if event.Code < 0 || event.Code > 0xffff || len(event.Payload) > 1 {
|
||||
return apolloInputPacket{}, ErrInputMalformed
|
||||
return nil, ErrInputMalformed
|
||||
}
|
||||
packet := make([]byte, 14)
|
||||
binary.BigEndian.PutUint32(packet[:4], 10)
|
||||
@@ -36,10 +36,10 @@ func encodeApolloInputEvent(event InputEvent) (apolloInputPacket, error) {
|
||||
if len(event.Payload) == 1 {
|
||||
packet[11] = event.Payload[0]
|
||||
}
|
||||
return apolloInputPacket{channel: apolloChannelKeyboard, payload: packet}, nil
|
||||
return []apolloInputPacket{{channel: apolloChannelKeyboard, payload: packet}}, nil
|
||||
case "mouse-button":
|
||||
if event.Code < 1 || event.Code > 8 || len(event.Payload) != 0 {
|
||||
return apolloInputPacket{}, ErrInputMalformed
|
||||
return nil, ErrInputMalformed
|
||||
}
|
||||
packet := make([]byte, 9)
|
||||
binary.BigEndian.PutUint32(packet[:4], 5)
|
||||
@@ -49,28 +49,28 @@ func encodeApolloInputEvent(event InputEvent) (apolloInputPacket, error) {
|
||||
}
|
||||
binary.LittleEndian.PutUint32(packet[4:8], magic)
|
||||
packet[8] = byte(event.Code)
|
||||
return apolloInputPacket{channel: apolloChannelMouse, payload: packet}, nil
|
||||
return []apolloInputPacket{{channel: apolloChannelMouse, payload: packet}}, nil
|
||||
case "mouse-relative":
|
||||
if event.Pressed || len(event.Payload) != 4 {
|
||||
return apolloInputPacket{}, ErrInputMalformed
|
||||
return nil, ErrInputMalformed
|
||||
}
|
||||
packet := make([]byte, 12)
|
||||
binary.BigEndian.PutUint32(packet[:4], 8)
|
||||
binary.LittleEndian.PutUint32(packet[4:8], 7)
|
||||
copy(packet[8:], event.Payload)
|
||||
return apolloInputPacket{channel: apolloChannelMouse, payload: packet}, nil
|
||||
return []apolloInputPacket{{channel: apolloChannelMouse, payload: packet}}, nil
|
||||
case "utf8":
|
||||
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))
|
||||
binary.BigEndian.PutUint32(packet[:4], uint32(4+len(event.Payload)))
|
||||
binary.LittleEndian.PutUint32(packet[4:8], 0x17)
|
||||
copy(packet[8:], event.Payload)
|
||||
return apolloInputPacket{channel: apolloChannelUTF8, payload: packet}, nil
|
||||
return []apolloInputPacket{{channel: apolloChannelUTF8, payload: packet}}, nil
|
||||
case "controller":
|
||||
if event.Code < 0 || event.Code > 15 || len(event.Payload) != 16 {
|
||||
return apolloInputPacket{}, ErrInputMalformed
|
||||
return nil, ErrInputMalformed
|
||||
}
|
||||
packet := make([]byte, 34)
|
||||
binary.BigEndian.PutUint32(packet[:4], 30)
|
||||
@@ -83,8 +83,44 @@ func encodeApolloInputEvent(event InputEvent) (apolloInputPacket, error) {
|
||||
binary.LittleEndian.PutUint16(packet[28:30], 0x9c)
|
||||
copy(packet[30:32], event.Payload[14:16])
|
||||
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:
|
||||
return apolloInputPacket{}, ErrInputMalformed
|
||||
return nil, ErrInputMalformed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -454,15 +454,17 @@ func (s *nativeApolloSession) Audio() <-chan ProviderMedia { return s.audio }
|
||||
func (s *nativeApolloSession) Events() <-chan ProviderEvent { return s.events }
|
||||
|
||||
func (s *nativeApolloSession) Input(ctx context.Context, event InputEvent) error {
|
||||
packet, err := encodeApolloInputEvent(event)
|
||||
packets, err := encodeApolloInputEvent(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.writeApolloControl(packet.channel, true, apolloControlTypeInput, packet.payload); err != nil {
|
||||
return err
|
||||
for _, packet := range packets {
|
||||
if err := s.writeApolloControl(packet.channel, true, apolloControlTypeInput, packet.payload); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if event.Device == "keyboard" || event.Device == "mouse-button" || event.Device == "controller" {
|
||||
key := fmt.Sprintf("%s:%d", event.Device, event.Code)
|
||||
|
||||
@@ -8,6 +8,10 @@ import (
|
||||
|
||||
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 {
|
||||
return protocol.CapabilityProfile{
|
||||
Transport: "quic-tls13",
|
||||
|
||||
+117
-2
@@ -67,6 +67,10 @@ func FuzzDecodeFrame(f *testing.F) {
|
||||
func FuzzDecodeInputEvent(f *testing.F) {
|
||||
seed, _ := EncodeInputEvent(InputEvent{Sequence: 1, Device: "keyboard", Code: 7, Pressed: true})
|
||||
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.Fuzz(func(t *testing.T, data []byte) {
|
||||
_, _ = 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) {
|
||||
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)
|
||||
@@ -581,6 +686,14 @@ func TestRegisteredChannelFramesTraversePublicTransport(t *testing.T) {
|
||||
if err != nil {
|
||||
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})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -593,6 +706,8 @@ func TestRegisteredChannelFramesTraversePublicTransport(t *testing.T) {
|
||||
testChannelFrame("input.sequenced.v1", 7, input),
|
||||
testChannelFrame("control.ack.v1", 8, feedback),
|
||||
testChannelFrame("clipboard.text.v1", 9, clipboard),
|
||||
testChannelFrame("input.sequenced.v1", 10, absolute),
|
||||
testChannelFrame("input.sequenced.v1", 11, scroll),
|
||||
} {
|
||||
encoded, encodeErr := protocol.EncodeChannelFrame(frame)
|
||||
if encodeErr != nil {
|
||||
@@ -617,7 +732,7 @@ func TestRegisteredChannelFramesTraversePublicTransport(t *testing.T) {
|
||||
inputs := append([]InputEvent(nil), h.session.inputs...)
|
||||
feedbacks := append([]Feedback(nil), h.session.feedback...)
|
||||
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
|
||||
}
|
||||
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()}
|
||||
admission := &oneTimeAdmission{authority: authority, released: make(chan struct{}), disableClipboard: !clipboardEnabled}
|
||||
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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ const (
|
||||
gatewayInputRelative = 3
|
||||
gatewayInputUTF8 = 4
|
||||
gatewayInputController = 5
|
||||
gatewayInputAbsolute = 6
|
||||
gatewayInputScroll = 7
|
||||
)
|
||||
|
||||
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)
|
||||
copy(encoded[7:], event.Payload)
|
||||
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:
|
||||
return nil, ErrInputMalformed
|
||||
}
|
||||
@@ -117,11 +137,30 @@ func DecodeInputEvent(data []byte) (InputEvent, error) {
|
||||
return InputEvent{}, ErrInputMalformed
|
||||
}
|
||||
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:
|
||||
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 {
|
||||
for _, value := range data {
|
||||
if value != 0 {
|
||||
|
||||
@@ -68,6 +68,7 @@ type ServerConfig struct {
|
||||
TLSConfig *tls.Config
|
||||
QUICConfig *quic.Config
|
||||
GatewayID string
|
||||
Features []string
|
||||
Capabilities protocol.CapabilityProfile
|
||||
ProviderCapabilities protocol.CapabilityProfile
|
||||
Admission Admission
|
||||
@@ -866,6 +867,16 @@ func (s *gatewaySession) handleInput(payload []byte, sequence uint32) error {
|
||||
if err != nil {
|
||||
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
|
||||
if err := s.provider.Input(s.ctx, event); err != nil {
|
||||
s.server.metrics.InputRejected.Add(1)
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
## 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
|
||||
- [ ] 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.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.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.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.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
|
||||
- [ ] 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
|
||||
- [ ] 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.1 Add the forward-only nullable requested/effective display and policy-version migration plus clean-install/upgrade/schema/grant tests
|
||||
- [x] 2.2 Add strict request validation and requested-mode idempotency identity, including waiting-session mismatch regressions
|
||||
- [x] 2.3 Implement one proportional even-pixel/FPS clamp at allocation and atomically persist the immutable requested/effective decision
|
||||
- [x] 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.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.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
|
||||
- [ ] 3.3 Implement the smallest provider-neutral VGI validation and Apollo adapter translation without adding pressed-state or direct-provider surfaces
|
||||
- [x] 3.1 Add red VGI absolute/scroll encode/decode/bounds/fuzz/transport tests and prove unadvertised kinds fail before provider translation
|
||||
- [x] 3.2 Establish exact Apollo absolute-pointer and scroll vectors from the approved pinned source; hard-stop without that evidence
|
||||
- [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`
|
||||
|
||||
## 4. Rust Core and Stable ABI
|
||||
|
||||
Reference in New Issue
Block a user