feat(gateway): repair native Apollo provider path
This commit is contained in:
+792
-27
@@ -2,22 +2,34 @@ package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
protocol "git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol/gen/go/protocol"
|
||||
)
|
||||
|
||||
type nativeRoundTripFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (fn nativeRoundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
return fn(request)
|
||||
}
|
||||
|
||||
func TestNativeApolloManagementUsesSessionScopedMTLS(t *testing.T) {
|
||||
serverTLS, clientTLS := testTLS(t)
|
||||
server := httptest.NewUnstartedServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
||||
@@ -47,6 +59,7 @@ func TestNativeApolloManagementUsesSessionScopedMTLS(t *testing.T) {
|
||||
ClientCertificatePem: certificatePEM(t, clientTLS.Certificates[0]),
|
||||
ClientPrivateKeyPem: privateKeyPEM(t, clientTLS.Certificates[0]),
|
||||
ServerCertificatePem: certificatePEM(t, tls.Certificate{Certificate: [][]byte{serverTLS.Certificates[0].Certificate[1]}}),
|
||||
ClipboardPolicy: protocol.ClipboardPolicy{MaxTextBytes: 65536, MaxUpdatesPerMinute: 30},
|
||||
}
|
||||
data, err := NewNativeApolloBackend().Management(context.Background(), LaunchRequest{SessionID: "session-1", ProviderProfile: ProviderProfileApollo, ProviderWork: work})
|
||||
if err != nil {
|
||||
@@ -57,13 +70,28 @@ func TestNativeApolloManagementUsesSessionScopedMTLS(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNativeApolloSetupLaunchesInventoriedApplicationWithEncryptedRTSP(t *testing.T) {
|
||||
func TestNativeApolloSetupRequiresModernEncryptedRTSPOrder(t *testing.T) {
|
||||
serverTLS, clientTLS := testTLS(t)
|
||||
streamListener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer streamListener.Close()
|
||||
controlServer, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1")})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer controlServer.Close()
|
||||
audioServer, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1")})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer audioServer.Close()
|
||||
videoServer, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1")})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer videoServer.Close()
|
||||
streamHost, streamPortText, err := net.SplitHostPort(streamListener.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -72,40 +100,83 @@ func TestNativeApolloSetupLaunchesInventoriedApplicationWithEncryptedRTSP(t *tes
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
type fixtureKeyMaterial struct {
|
||||
key []byte
|
||||
keyID uint32
|
||||
}
|
||||
keyReady := make(chan []byte, 1)
|
||||
keyMaterialReady := make(chan fixtureKeyMaterial, 1)
|
||||
clipboardWrites := make(chan string, 1)
|
||||
cancelCalls := make(chan struct{}, 1)
|
||||
streamDone := make(chan error, 1)
|
||||
go func() {
|
||||
connection, acceptErr := streamListener.Accept()
|
||||
if acceptErr != nil {
|
||||
streamDone <- acceptErr
|
||||
return
|
||||
}
|
||||
defer connection.Close()
|
||||
key := <-keyReady
|
||||
header := make([]byte, encryptedRTSPHeaderSize)
|
||||
if _, readErr := io.ReadFull(connection, header); readErr != nil {
|
||||
streamDone <- readErr
|
||||
return
|
||||
}
|
||||
length := binary.BigEndian.Uint32(header[:4]) & 0x7fffffff
|
||||
frame := make([]byte, encryptedRTSPHeaderSize+int(length))
|
||||
copy(frame, header)
|
||||
if _, readErr := io.ReadFull(connection, frame[encryptedRTSPHeaderSize:]); readErr != nil {
|
||||
streamDone <- readErr
|
||||
return
|
||||
}
|
||||
codec, codecErr := newEncryptedRTSPCodec(key)
|
||||
if codecErr != nil {
|
||||
streamDone <- codecErr
|
||||
return
|
||||
}
|
||||
plaintext, decryptErr := codec.OpenClient(frame)
|
||||
if decryptErr != nil || string(plaintext) != "SETUP rtsp://"+streamHost+"/streamid=video/0/0 RTSP/1.0\r\nCSeq: 1\r\nTransport: RTP/AVP/TCP;interleaved=0-1\r\n\r\n" {
|
||||
streamDone <- ErrProviderMalformed
|
||||
return
|
||||
expectedMethods := []string{"OPTIONS", "DESCRIBE", "SETUP", "SETUP", "SETUP", "ANNOUNCE", "PLAY"}
|
||||
expectedTargets := []string{"rtspenc://" + streamListener.Addr().String(), "rtspenc://" + streamListener.Addr().String(), "streamid=audio/0/0", "streamid=video/0/0", "streamid=control/13/0", "streamid=control/13/0", "/"}
|
||||
describeBody := "a=x-ss-general.featureFlags:1\r\na=x-ss-general.encryptionSupported:7\r\na=x-ss-general.encryptionRequested:1\r\na=fmtp:97 surround-params=21101\r\n"
|
||||
responses := []string{
|
||||
"RTSP/1.0 200 OK\r\nCSeq: 1\r\n\r\n",
|
||||
fmt.Sprintf("RTSP/1.0 200 OK\r\nCSeq: 2\r\nContent-Type: application/sdp\r\nContent-Length: %d\r\n\r\n%s", len(describeBody), describeBody),
|
||||
fmt.Sprintf("RTSP/1.0 200 OK\r\nCSeq: 3\r\nSession: fixture-session;timeout=90\r\nTransport: unicast;server_port=%d\r\nX-SS-Ping-Payload: 0123456789abcdef\r\n\r\n", audioServer.LocalAddr().(*net.UDPAddr).Port),
|
||||
fmt.Sprintf("RTSP/1.0 200 OK\r\nCSeq: 4\r\nSession: fixture-session\r\nTransport: unicast;server_port=%d\r\nX-SS-Ping-Payload: fedcba9876543210\r\n\r\n", videoServer.LocalAddr().(*net.UDPAddr).Port),
|
||||
fmt.Sprintf("RTSP/1.0 200 OK\r\nCSeq: 5\r\nSession: fixture-session\r\nTransport: unicast;server_port=%d\r\nX-SS-Connect-Data: 305419896\r\n\r\n", controlServer.LocalAddr().(*net.UDPAddr).Port),
|
||||
"RTSP/1.0 200 OK\r\nCSeq: 6\r\nSession: fixture-session\r\n\r\n",
|
||||
"RTSP/1.0 200 OK\r\nCSeq: 7\r\nSession: fixture-session\r\n\r\n",
|
||||
}
|
||||
_, writeErr := connection.Write(hostEncryptedRTSPFrame(t, key, 1, []byte("RTSP/1.0 200 OK\r\nSession: host-session\r\nTransport: RTP/AVP/TCP;interleaved=0-1\r\n\r\n")))
|
||||
streamDone <- writeErr
|
||||
for index, method := range expectedMethods {
|
||||
connection, acceptErr := streamListener.Accept()
|
||||
if acceptErr != nil {
|
||||
streamDone <- acceptErr
|
||||
return
|
||||
}
|
||||
header := make([]byte, encryptedRTSPHeaderSize)
|
||||
if _, readErr := io.ReadFull(connection, header); readErr != nil {
|
||||
_ = connection.Close()
|
||||
streamDone <- readErr
|
||||
return
|
||||
}
|
||||
length := binary.BigEndian.Uint32(header[:4]) & 0x7fffffff
|
||||
frame := make([]byte, encryptedRTSPHeaderSize+int(length))
|
||||
copy(frame, header)
|
||||
if _, readErr := io.ReadFull(connection, frame[encryptedRTSPHeaderSize:]); readErr != nil {
|
||||
_ = connection.Close()
|
||||
streamDone <- readErr
|
||||
return
|
||||
}
|
||||
plaintext, decryptErr := codec.OpenClient(frame)
|
||||
firstLine, _, _ := strings.Cut(string(plaintext), "\r\n")
|
||||
if decryptErr != nil || firstLine != method+" "+expectedTargets[index]+" RTSP/1.0" || !strings.Contains(string(plaintext), "CSeq: "+strconv.Itoa(index+1)+"\r\n") {
|
||||
_, _ = connection.Write(hostEncryptedRTSPFrame(t, key, uint32(index+1), []byte("RTSP/1.0 400 Bad Request\r\nCSeq: "+strconv.Itoa(index+1)+"\r\n\r\n")))
|
||||
_ = connection.Close()
|
||||
streamDone <- ErrProviderMalformed
|
||||
return
|
||||
}
|
||||
if method == "ANNOUNCE" {
|
||||
for _, required := range []string{
|
||||
"a=x-nv-video[0].clientViewportWd:1920", "a=x-nv-video[0].clientViewportHt:1080", "a=x-nv-video[0].maxFPS:60",
|
||||
"a=x-nv-video[0].packetSize:1024", "a=x-nv-vqos[0].bw.maximumBitrateKbps:8000", "a=x-nv-audio.surround.numChannels:2",
|
||||
"a=x-nv-general.useReliableUdp:13", "a=x-ss-general.encryptionEnabled:7",
|
||||
} {
|
||||
if !strings.Contains(string(plaintext), required+"\r\n") {
|
||||
_ = connection.Close()
|
||||
streamDone <- ErrProviderMalformed
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
_, writeErr := connection.Write(hostEncryptedRTSPFrame(t, key, uint32(index+1), []byte(responses[index])))
|
||||
_ = connection.Close()
|
||||
if writeErr != nil {
|
||||
streamDone <- writeErr
|
||||
return
|
||||
}
|
||||
}
|
||||
streamDone <- nil
|
||||
}()
|
||||
management := httptest.NewUnstartedServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
||||
if request.TLS == nil || len(request.TLS.PeerCertificates) != 1 {
|
||||
@@ -113,6 +184,8 @@ func TestNativeApolloSetupLaunchesInventoriedApplicationWithEncryptedRTSP(t *tes
|
||||
return
|
||||
}
|
||||
switch request.URL.Path {
|
||||
case "/serverinfo":
|
||||
_, _ = response.Write([]byte("<root><uniqueid>apollo-server</uniqueid></root>"))
|
||||
case "/applist":
|
||||
if request.URL.Query().Get("uniqueid") != "paired-client" {
|
||||
http.Error(response, "wrong client", http.StatusBadRequest)
|
||||
@@ -130,8 +203,38 @@ func TestNativeApolloSetupLaunchesInventoriedApplicationWithEncryptedRTSP(t *tes
|
||||
http.Error(response, "bad key", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
keyID, keyIDErr := strconv.ParseUint(query.Get("rikeyid"), 10, 32)
|
||||
if keyIDErr != nil {
|
||||
http.Error(response, "bad key id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
keyReady <- key
|
||||
keyMaterialReady <- fixtureKeyMaterial{key: append([]byte(nil), key...), keyID: uint32(keyID)}
|
||||
_, _ = response.Write([]byte("<root status_code=\"200\"><sessionUrl0>rtspenc://" + streamListener.Addr().String() + "</sessionUrl0></root>"))
|
||||
case "/resume":
|
||||
_, _ = response.Write([]byte("<root status_code=\"200\"/>"))
|
||||
case "/cancel":
|
||||
if request.Method != http.MethodGet {
|
||||
http.Error(response, "cancel must be GET", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
cancelCalls <- struct{}{}
|
||||
_, _ = response.Write([]byte("<root status_code=\"200\"><cancel>1</cancel></root>"))
|
||||
case "/actions/clipboard":
|
||||
if request.URL.Query().Get("type") != "text" || (request.Method != http.MethodGet && request.Method != http.MethodPost) {
|
||||
http.Error(response, "bad clipboard request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if request.Method == http.MethodPost {
|
||||
body, readErr := io.ReadAll(request.Body)
|
||||
if readErr != nil || string(body) != "client clipboard" {
|
||||
http.Error(response, "bad clipboard body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
clipboardWrites <- string(body)
|
||||
return
|
||||
}
|
||||
_, _ = response.Write([]byte("fixture clipboard"))
|
||||
default:
|
||||
http.NotFound(response, request)
|
||||
}
|
||||
@@ -154,19 +257,681 @@ func TestNativeApolloSetupLaunchesInventoriedApplicationWithEncryptedRTSP(t *tes
|
||||
ProviderIdentity: "apollo-server#sha256:" + hex.EncodeToString(pinned[:]), PolicyVersionID: "policy-1", ApplicationID: "42", ClientID: "paired-client",
|
||||
ManagementHost: managementHost, ManagementPort: managementPort, StreamHost: streamHost, StreamPort: streamPort,
|
||||
ClientCertificatePem: certificatePEM(t, clientTLS.Certificates[0]), ClientPrivateKeyPem: privateKeyPEM(t, clientTLS.Certificates[0]),
|
||||
ServerCertificatePem: certificatePEM(t, tls.Certificate{Certificate: [][]byte{serverTLS.Certificates[0].Certificate[1]}}),
|
||||
ServerCertificatePem: certificatePEM(t, tls.Certificate{Certificate: [][]byte{serverTLS.Certificates[0].Certificate[1]}}),
|
||||
ClipboardPolicy: protocol.ClipboardPolicy{MaxTextBytes: 65536, MaxUpdatesPerMinute: 30},
|
||||
ProviderApplicationTerminationAllowed: true,
|
||||
}
|
||||
backend := NewNativeApolloBackend()
|
||||
response, err := backend.Setup(context.Background(), LaunchRequest{SessionID: "session-1", ProviderProfile: ProviderProfileApollo, ProviderWork: work})
|
||||
if err != nil {
|
||||
t.Fatalf("Setup() error = %v", err)
|
||||
}
|
||||
if parsed, err := ParseRTSPResponse(response); err != nil || parsed.Session != "host-session" {
|
||||
if parsed, err := ParseRTSPResponse(response); err != nil || parsed.Session != "fixture-session" {
|
||||
t.Fatalf("ParseRTSPResponse() = %+v, %v", parsed, err)
|
||||
}
|
||||
if err := <-streamDone; err != nil {
|
||||
t.Fatalf("stream transaction error = %v", err)
|
||||
}
|
||||
material := <-keyMaterialReady
|
||||
videoData, videoParity := sourceShapedEncryptedVideoFEC(t, material.key)
|
||||
audioPackets := make([][]byte, 0, apolloAudioDataShards)
|
||||
for index, payload := range [][]byte{{'A'}, {'B'}, {'C'}, {'D'}} {
|
||||
audioPackets = append(audioPackets, sourceShapedEncryptedAudioPacket(t, material.key, material.keyID, uint16(index), payload))
|
||||
}
|
||||
mediaDone := make(chan error, 2)
|
||||
serveMedia := func(server *net.UDPConn, ping string, packets [][]byte) {
|
||||
buffer := make([]byte, apolloMediaMaximumPacket)
|
||||
count, remote, readErr := server.ReadFromUDP(buffer)
|
||||
if readErr != nil || count != 20 || string(buffer[:16]) != ping {
|
||||
if readErr != nil {
|
||||
mediaDone <- readErr
|
||||
} else {
|
||||
mediaDone <- ErrProviderMalformed
|
||||
}
|
||||
return
|
||||
}
|
||||
for _, packet := range packets {
|
||||
if _, writeErr := server.WriteToUDP(packet, remote); writeErr != nil {
|
||||
mediaDone <- writeErr
|
||||
return
|
||||
}
|
||||
}
|
||||
mediaDone <- nil
|
||||
}
|
||||
go serveMedia(audioServer, "0123456789abcdef", audioPackets)
|
||||
go serveMedia(videoServer, "fedcba9876543210", [][]byte{videoData, videoParity})
|
||||
type observedControl struct {
|
||||
typeID uint16
|
||||
reliable bool
|
||||
}
|
||||
controls := make(chan observedControl, 16)
|
||||
controlRemote := make(chan *net.UDPAddr, 1)
|
||||
controlDone := make(chan error, 1)
|
||||
go func() {
|
||||
buffer := make([]byte, apolloENetMaximumPacket)
|
||||
count, remote, readErr := controlServer.ReadFromUDP(buffer)
|
||||
if readErr != nil {
|
||||
controlDone <- readErr
|
||||
return
|
||||
}
|
||||
connect := buffer[:count]
|
||||
if count != 52 || connect[4] != apolloENetConnect|apolloENetAcknowledged || connect[5] != 0xff || binary.BigEndian.Uint32(connect[20:24]) != apolloENetChannels || binary.BigEndian.Uint32(connect[48:52]) != 305419896 {
|
||||
controlDone <- ErrProviderMalformed
|
||||
return
|
||||
}
|
||||
verify := make([]byte, 48)
|
||||
apolloENetHeader(verify, 0, 0, time.Now())
|
||||
verify[4] = apolloENetVerifyConnect | apolloENetAcknowledged
|
||||
verify[5] = 0xff
|
||||
binary.BigEndian.PutUint16(verify[6:8], 1)
|
||||
binary.BigEndian.PutUint16(verify[8:10], 7)
|
||||
verify[10], verify[11] = 2, 3
|
||||
binary.BigEndian.PutUint32(verify[12:16], 1400)
|
||||
binary.BigEndian.PutUint32(verify[16:20], 32768)
|
||||
binary.BigEndian.PutUint32(verify[20:24], apolloENetChannels)
|
||||
binary.BigEndian.PutUint32(verify[44:48], binary.BigEndian.Uint32(connect[44:48]))
|
||||
if _, writeErr := controlServer.WriteToUDP(verify, remote); writeErr != nil {
|
||||
controlDone <- writeErr
|
||||
return
|
||||
}
|
||||
controlRemote <- remote
|
||||
for {
|
||||
count, _, readErr = controlServer.ReadFromUDP(buffer)
|
||||
if readErr != nil {
|
||||
controlDone <- readErr
|
||||
return
|
||||
}
|
||||
packet := buffer[:count]
|
||||
if len(packet) < 8 {
|
||||
controlDone <- ErrProviderMalformed
|
||||
return
|
||||
}
|
||||
command, channel := packet[4]&apolloENetCommandMask, packet[5]
|
||||
sequence := binary.BigEndian.Uint16(packet[6:8])
|
||||
switch command {
|
||||
case 1:
|
||||
continue
|
||||
case apolloENetSendReliable:
|
||||
if len(packet) < 10 || int(binary.BigEndian.Uint16(packet[8:10])) != len(packet)-10 {
|
||||
controlDone <- ErrProviderMalformed
|
||||
return
|
||||
}
|
||||
typeID, _ := sourceOpenClientControl(t, material.key, packet[10:])
|
||||
controls <- observedControl{typeID: typeID, reliable: true}
|
||||
if _, writeErr := controlServer.WriteToUDP(sourceShapedENetAcknowledgePacket(7, 2, channel, sequence), remote); writeErr != nil {
|
||||
controlDone <- writeErr
|
||||
return
|
||||
}
|
||||
case apolloENetSendUnsequenced:
|
||||
if len(packet) < 12 || int(binary.BigEndian.Uint16(packet[10:12])) != len(packet)-12 {
|
||||
controlDone <- ErrProviderMalformed
|
||||
return
|
||||
}
|
||||
typeID, _ := sourceOpenClientControl(t, material.key, packet[12:])
|
||||
controls <- observedControl{typeID: typeID}
|
||||
case apolloENetPing:
|
||||
if _, writeErr := controlServer.WriteToUDP(sourceShapedENetAcknowledgePacket(7, 2, channel, sequence), remote); writeErr != nil {
|
||||
controlDone <- writeErr
|
||||
return
|
||||
}
|
||||
case apolloENetDisconnect:
|
||||
_, writeErr := controlServer.WriteToUDP(sourceShapedENetAcknowledgePacket(7, 2, channel, sequence), remote)
|
||||
controlDone <- writeErr
|
||||
return
|
||||
default:
|
||||
controlDone <- ErrProviderMalformed
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
request := LaunchRequest{SessionID: "session-1", ProviderProfile: ProviderProfileApollo, ProviderWork: work}
|
||||
parsed, err := ParseRTSPResponse(response)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
session, err := backend.Open(context.Background(), request, parsed)
|
||||
if err != nil {
|
||||
t.Fatalf("Open() error = %v", err)
|
||||
}
|
||||
if err := session.Ready(context.Background()); err != nil {
|
||||
t.Fatalf("Ready() error = %v", err)
|
||||
}
|
||||
if clipboard, clipboardErr := session.ReadClipboard(context.Background()); clipboardErr != nil || clipboard != "fixture clipboard" {
|
||||
t.Fatalf("ReadClipboard() = %q, %v", clipboard, clipboardErr)
|
||||
}
|
||||
if err := session.WriteClipboard(context.Background(), "client clipboard"); err != nil {
|
||||
t.Fatalf("WriteClipboard() error = %v", err)
|
||||
}
|
||||
select {
|
||||
case <-clipboardWrites:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("provider did not receive authenticated clipboard write")
|
||||
}
|
||||
awaitControl := func(want uint16, reliable bool) {
|
||||
deadline := time.NewTimer(time.Second)
|
||||
defer deadline.Stop()
|
||||
for {
|
||||
select {
|
||||
case observed := <-controls:
|
||||
if observed.typeID == want && observed.reliable == reliable {
|
||||
return
|
||||
}
|
||||
case <-deadline.C:
|
||||
t.Fatalf("provider did not receive control %#x", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
awaitControl(apolloControlTypeIDR, true)
|
||||
awaitControl(apolloControlTypeStart, true)
|
||||
if err := session.Input(context.Background(), InputEvent{Device: "keyboard", Code: 7, Pressed: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
awaitControl(apolloControlTypeInput, true)
|
||||
if err := session.Feedback(context.Background(), Feedback{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}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
awaitControl(apolloControlTypeFEC, false)
|
||||
remote := <-controlRemote
|
||||
hostTermination := sourceSealHostControl(t, material.key, 0, apolloControlTypeTerm, []byte{1, 2, 3, 4})
|
||||
if _, err := controlServer.WriteToUDP(sourceShapedENetReliablePacketOn(7, 2, apolloChannelGeneric, 1, hostTermination), remote); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
select {
|
||||
case event := <-session.Events():
|
||||
if event.Kind != ProviderEventTerminated || string(event.Payload) != string([]byte{1, 2, 3, 4}) {
|
||||
t.Fatalf("provider termination event = %#v", event)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("encrypted host termination was not forwarded")
|
||||
}
|
||||
select {
|
||||
case payload := <-session.Video():
|
||||
if len(payload) != 1001 || payload[0] != 'A' || payload[1000] != 'B' {
|
||||
t.Fatalf("source-shaped video relay = %x", payload)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("source-shaped video was not relayed")
|
||||
}
|
||||
select {
|
||||
case payload := <-session.Audio():
|
||||
if string(payload) != "A" {
|
||||
t.Fatalf("source-shaped audio relay = %x", payload)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("source-shaped audio was not relayed")
|
||||
}
|
||||
terminateCtx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
if err := session.Terminate(terminateCtx); err != nil {
|
||||
t.Fatalf("Terminate() error = %v", err)
|
||||
}
|
||||
select {
|
||||
case <-cancelCalls:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("authorized provider application cancellation was not sent")
|
||||
}
|
||||
if err := <-controlDone; err != nil {
|
||||
t.Fatalf("control fake = %v", err)
|
||||
}
|
||||
for range 2 {
|
||||
if err := <-mediaDone; err != nil {
|
||||
t.Fatalf("media fake = %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateApolloDescribeRejectsUnknownLines(t *testing.T) {
|
||||
message := apolloRTSPMessage{
|
||||
headers: map[string]string{"content-type": "application/sdp"},
|
||||
body: []byte("a=x-ss-general.featureFlags:1\r\n" +
|
||||
"a=x-ss-general.encryptionSupported:7\r\n" +
|
||||
"a=x-ss-general.encryptionRequested:1\r\n" +
|
||||
"a=fmtp:97 surround-params=21101\r\n" +
|
||||
"unexpected-source-line\r\n"),
|
||||
}
|
||||
if err := validateApolloDescribe(message); err == nil {
|
||||
t.Fatal("validateApolloDescribe() accepted an unknown source line")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNativeApolloTerminateSkipsProviderCancelWithoutServerPolicy(t *testing.T) {
|
||||
session := newNativeApolloSession("session-1")
|
||||
close(session.readDone)
|
||||
session.managementClient = &http.Client{Transport: nativeRoundTripFunc(func(request *http.Request) (*http.Response, error) {
|
||||
t.Fatalf("unauthorized provider cancellation request: %s %s", request.Method, request.URL)
|
||||
return nil, fmt.Errorf("unexpected provider cancellation")
|
||||
})}
|
||||
session.managementHost = "provider.invalid"
|
||||
session.managementPort = 47984
|
||||
if err := session.Terminate(context.Background()); err != nil {
|
||||
t.Fatalf("Terminate() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNativeApolloTerminateRetainsCleanupPendingAfterFailure(t *testing.T) {
|
||||
session := newNativeApolloSession("session-1")
|
||||
failed, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
if err := session.Terminate(failed); !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("first Terminate() error = %v", err)
|
||||
}
|
||||
if state := session.State(); state.State != ProviderStateCleanup || !state.CleanupPending {
|
||||
t.Fatalf("failed cleanup state = %#v", state)
|
||||
}
|
||||
if err := session.Terminate(context.Background()); !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("second Terminate() error = %v", err)
|
||||
}
|
||||
if state := session.State(); state.State != ProviderStateCleanup || !state.CleanupPending {
|
||||
t.Fatalf("repeated cleanup state = %#v", state)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApolloReconnectUsesResumeWithFreshRIK(t *testing.T) {
|
||||
work := protocol.ProviderSessionWork{ReconnectSequence: 1, ApplicationID: "42", ClientID: "paired-client"}
|
||||
path, values := apolloSessionRequest(work, []byte("0123456789abcdef"), 0x01020304)
|
||||
if path != "/resume" || values.Get("rikey") != "30313233343536373839616263646566" || values.Get("rikeyid") != "16909060" || values.Get("localAudioPlayMode") != "0" {
|
||||
t.Fatalf("resume request = path %q values %#v", path, values)
|
||||
}
|
||||
if values.Get("appid") != "" || values.Get("uniqueid") != "" || values.Get("corever") != "" {
|
||||
t.Fatalf("resume request leaked launch-only values %#v", values)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNativeApolloSessionRelaysOnlyAuthenticatedEncodedUDPMedia(t *testing.T) {
|
||||
key := []byte("0123456789abcdef")
|
||||
audioServer, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1")})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer audioServer.Close()
|
||||
videoServer, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1")})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer videoServer.Close()
|
||||
audioClient, err := net.DialUDP("udp", nil, audioServer.LocalAddr().(*net.UDPAddr))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
videoClient, err := net.DialUDP("udp", nil, videoServer.LocalAddr().(*net.UDPAddr))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
session := newNativeApolloSession("session-media")
|
||||
const keyID = 0x01020304
|
||||
session.media, err = newApolloMediaCodec(key, keyID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
session.audioConn, session.videoConn = audioClient, videoClient
|
||||
go session.readUDPMedia()
|
||||
|
||||
videoPacket := sourceShapedEncryptedVideoPacket(t, key, []byte{0x01, 0x02, 0x03})
|
||||
if _, err := videoServer.WriteToUDP(videoPacket, videoClient.LocalAddr().(*net.UDPAddr)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantAudio := [][]byte{{0xf8, 0x08}, {0xf8, 0x09}, {0xf8, 0x0a}, {0xf8, 0x0b}}
|
||||
for index, payload := range wantAudio {
|
||||
packet := sourceShapedEncryptedAudioPacket(t, key, keyID, uint16(8+index), payload)
|
||||
if _, err := audioServer.WriteToUDP(packet, audioClient.LocalAddr().(*net.UDPAddr)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case payload := <-session.Video():
|
||||
if string(payload) != string([]byte{0x01, 0x02, 0x03}) {
|
||||
t.Fatalf("video relay = %x, want encoded payload", payload)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("encrypted video was not relayed")
|
||||
}
|
||||
for _, want := range wantAudio {
|
||||
select {
|
||||
case payload := <-session.Audio():
|
||||
if string(payload) != string(want) {
|
||||
t.Fatalf("audio relay = %x, want %x", payload, want)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("encrypted audio was not relayed")
|
||||
}
|
||||
}
|
||||
dataPacket, parityPacket := sourceShapedEncryptedVideoFEC(t, key)
|
||||
for _, packet := range [][]byte{dataPacket, parityPacket} {
|
||||
if _, err := videoServer.WriteToUDP(packet, videoClient.LocalAddr().(*net.UDPAddr)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
select {
|
||||
case payload := <-session.Video():
|
||||
if len(payload) != 1001 || payload[0] != 'A' || payload[999] != 'A' || payload[1000] != 'B' {
|
||||
t.Fatalf("FEC video relay = %x", payload)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("encrypted FEC video was not recovered")
|
||||
}
|
||||
for _, packet := range sourceShapedEncryptedAudioFEC(t, key, keyID) {
|
||||
if _, err := audioServer.WriteToUDP(packet, audioClient.LocalAddr().(*net.UDPAddr)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for _, want := range [][]byte{{'A'}, {'B'}, {'C'}, {'D'}} {
|
||||
select {
|
||||
case payload := <-session.Audio():
|
||||
if string(payload) != string(want) {
|
||||
t.Fatalf("FEC audio relay = %x, want %x", payload, want)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("encrypted FEC audio was not recovered")
|
||||
}
|
||||
}
|
||||
terminateCtx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
if err := session.Terminate(terminateCtx); err != nil {
|
||||
t.Fatalf("Terminate() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNativeApolloTerminateReleasesPressedProviderInput(t *testing.T) {
|
||||
server, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1")})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer server.Close()
|
||||
client, err := net.DialUDP("udp", nil, server.LocalAddr().(*net.UDPAddr))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
peer, err := newApolloENetPeer(client, time.Now)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
peer.state, peer.peerID, peer.outboundSession = apolloENetConnected, 1, 1
|
||||
codecKey := []byte("0123456789abcdef")
|
||||
codec, err := newApolloControlCodec(codecKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
session := newNativeApolloSession("session-terminate")
|
||||
session.enet, session.control = peer, codec
|
||||
close(session.readDone)
|
||||
packets := make(chan []byte, 3)
|
||||
go func() {
|
||||
defer close(packets)
|
||||
buffer := make([]byte, apolloENetMaximumPacket)
|
||||
for {
|
||||
_ = server.SetReadDeadline(time.Now().Add(time.Second))
|
||||
count, _, readErr := server.ReadFromUDP(buffer)
|
||||
if readErr != nil {
|
||||
return
|
||||
}
|
||||
packet := append([]byte(nil), buffer[:count]...)
|
||||
packets <- packet
|
||||
if len(packet) >= 5 && packet[4]&apolloENetCommandMask == apolloENetDisconnect {
|
||||
peer.close(nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
if err := session.Input(context.Background(), InputEvent{Device: "keyboard", Code: 7, Pressed: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
terminateCtx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
if err := session.Terminate(terminateCtx); err != nil {
|
||||
t.Fatalf("Terminate() error = %v", err)
|
||||
}
|
||||
var observed [][]byte
|
||||
for packet := range packets {
|
||||
observed = append(observed, packet)
|
||||
}
|
||||
if len(observed) != 3 {
|
||||
t.Fatalf("provider packets = %d, want pressed input, release, disconnect", len(observed))
|
||||
}
|
||||
typeID, payload := sourceOpenClientControl(t, codecKey, observed[1][10:])
|
||||
if typeID != apolloControlTypeInput || len(payload) < 8 || binary.LittleEndian.Uint32(payload[4:8]) != 4 {
|
||||
t.Fatalf("release packet = type %#x payload %x", typeID, payload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNativeApolloSessionForwardsEncryptedHostFeedback(t *testing.T) {
|
||||
key := []byte("0123456789abcdef")
|
||||
codec, err := newApolloControlCodec(key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
session := newNativeApolloSession("session-feedback")
|
||||
session.control = codec
|
||||
messages := []struct {
|
||||
typeID uint16
|
||||
payload []byte
|
||||
want ProviderEvent
|
||||
}{
|
||||
{apolloControlTypeRumble, []byte{0, 0, 0, 0, 1, 0, 0x34, 0x12, 0x78, 0x56}, ProviderEvent{Kind: ProviderEventRumble, Payload: []byte{1, 0x12, 0x34, 0x56, 0x78}}},
|
||||
{apolloControlTypeHDR, append([]byte{1}, make([]byte, 26)...), ProviderEvent{Kind: ProviderEventHDR, Payload: []byte{1}}},
|
||||
{apolloControlTypeTerm, []byte{1, 2, 3, 4}, ProviderEvent{Kind: ProviderEventTerminated, Payload: []byte{1, 2, 3, 4}}},
|
||||
}
|
||||
for sequence, message := range messages {
|
||||
session.handleApolloControlPayload(apolloChannelGeneric, true, sourceSealHostControl(t, key, uint32(sequence), message.typeID, message.payload))
|
||||
select {
|
||||
case event := <-session.Events():
|
||||
if event.Kind != message.want.Kind || string(event.Payload) != string(message.want.Payload) {
|
||||
t.Fatalf("provider event = %#v, want %#v", event, message.want)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatalf("host event %#x was not forwarded", message.typeID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPushLatestDropsExactlyOneOldPayload(t *testing.T) {
|
||||
queue := make(chan []byte, 1)
|
||||
if dropped := pushLatest(queue, []byte("old")); dropped {
|
||||
t.Fatal("first media payload dropped")
|
||||
}
|
||||
if dropped := pushLatest(queue, []byte("new")); !dropped {
|
||||
t.Fatal("bounded media queue did not report a drop")
|
||||
}
|
||||
if got := string(<-queue); got != "new" {
|
||||
t.Fatalf("bounded media queue payload = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func sourceShapedEncryptedVideoPacket(t *testing.T, key, encoded []byte) []byte {
|
||||
t.Helper()
|
||||
payload := make([]byte, apolloVideoShardPayloadSize)
|
||||
payload[0], payload[3] = 0x01, 0x01
|
||||
binary.LittleEndian.PutUint16(payload[4:6], uint16(8+len(encoded)))
|
||||
copy(payload[8:], encoded)
|
||||
plaintext := sourceShapedVideoRaw(42, 7, 1, 0x07, 1, 0, 0, payload)
|
||||
return sourceEncryptVideoRaw(t, key, plaintext, "0123456789aV")
|
||||
}
|
||||
|
||||
func sourceShapedEncryptedVideoFEC(t *testing.T, key []byte) ([]byte, []byte) {
|
||||
t.Helper()
|
||||
firstPayload := make([]byte, apolloVideoShardPayloadSize)
|
||||
firstPayload[0], firstPayload[3] = 0x01, 0x01
|
||||
binary.LittleEndian.PutUint16(firstPayload[4:6], 1)
|
||||
for index := 8; index < len(firstPayload); index++ {
|
||||
firstPayload[index] = 'A'
|
||||
}
|
||||
secondPayload := make([]byte, apolloVideoShardPayloadSize)
|
||||
secondPayload[0] = 'B'
|
||||
first := sourceShapedVideoRaw(43, 100, 100, 0x05, 2, 50, 0, firstPayload)
|
||||
second := sourceShapedVideoRaw(43, 101, 101, 0x03, 2, 50, 1, secondPayload)
|
||||
parity := make([]byte, len(first))
|
||||
for index := range parity {
|
||||
parity[index] = first[index] ^ sourceGFMultiply(second[index], 142)
|
||||
}
|
||||
sourceConfigureVideoShard(first, 43, 100, 0, 2, 50, 0)
|
||||
sourceConfigureVideoShard(second, 43, 101, 1, 2, 50, 1)
|
||||
sourceConfigureVideoShard(parity, 43, 102, 2, 2, 50, 2)
|
||||
return sourceEncryptVideoRaw(t, key, second, "0123456789bV"), sourceEncryptVideoRaw(t, key, parity, "0123456789cV")
|
||||
}
|
||||
|
||||
func sourceShapedVideoRaw(frame uint32, sequence uint16, streamIndex uint32, flags byte, dataShards, percentage, shardIndex int, payload []byte) []byte {
|
||||
raw := make([]byte, apolloVideoRawPacketSize)
|
||||
binary.LittleEndian.PutUint32(raw[16:20], streamIndex<<8)
|
||||
binary.LittleEndian.PutUint32(raw[20:24], frame)
|
||||
raw[24], raw[26] = flags, 0x10
|
||||
binary.LittleEndian.PutUint32(raw[28:32], uint32(shardIndex<<12|dataShards<<22|percentage<<4))
|
||||
copy(raw[32:], payload)
|
||||
sourceConfigureVideoShard(raw, frame, sequence, streamIndex, dataShards, percentage, shardIndex)
|
||||
return raw
|
||||
}
|
||||
|
||||
func sourceConfigureVideoShard(raw []byte, frame uint32, sequence uint16, streamIndex uint32, dataShards, percentage, shardIndex int) {
|
||||
raw[0] = 0x90
|
||||
binary.BigEndian.PutUint16(raw[2:4], sequence)
|
||||
binary.BigEndian.PutUint32(raw[4:8], 99)
|
||||
binary.LittleEndian.PutUint32(raw[20:24], frame)
|
||||
raw[27] = 0
|
||||
binary.LittleEndian.PutUint32(raw[28:32], uint32(shardIndex<<12|dataShards<<22|percentage<<4))
|
||||
}
|
||||
|
||||
func sourceEncryptVideoRaw(t *testing.T, key, plaintext []byte, iv string) []byte {
|
||||
t.Helper()
|
||||
if len(plaintext) != apolloVideoRawPacketSize || len(iv) != 12 {
|
||||
t.Fatal("invalid source video fixture")
|
||||
}
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
aead, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sealed := aead.Seal(nil, []byte(iv), plaintext, nil)
|
||||
packet := make([]byte, 32+len(plaintext))
|
||||
copy(packet[:12], iv)
|
||||
binary.LittleEndian.PutUint32(packet[12:16], binary.LittleEndian.Uint32(plaintext[20:24]))
|
||||
copy(packet[16:32], sealed[len(plaintext):])
|
||||
copy(packet[32:], sealed[:len(plaintext)])
|
||||
return packet
|
||||
}
|
||||
|
||||
func sourceOpenClientControl(t *testing.T, key, packet []byte) (uint16, []byte) {
|
||||
t.Helper()
|
||||
if len(packet) < apolloControlHeaderSize+apolloControlTagSize+apolloControlInnerSize || binary.LittleEndian.Uint16(packet[:2]) != apolloControlOuterType || int(binary.LittleEndian.Uint16(packet[2:4])) != len(packet)-4 {
|
||||
t.Fatalf("control packet = %x", packet)
|
||||
}
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
aead, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
nonce := make([]byte, 12)
|
||||
binary.LittleEndian.PutUint32(nonce, binary.LittleEndian.Uint32(packet[4:8]))
|
||||
nonce[10], nonce[11] = 'C', 'C'
|
||||
sealed := append([]byte(nil), packet[24:]...)
|
||||
sealed = append(sealed, packet[8:24]...)
|
||||
plaintext, err := aead.Open(nil, nonce, sealed, nil)
|
||||
if err != nil || len(plaintext) < apolloControlInnerSize || int(binary.LittleEndian.Uint16(plaintext[2:4])) != len(plaintext)-apolloControlInnerSize {
|
||||
t.Fatalf("control open = %x, %v", packet, err)
|
||||
}
|
||||
return binary.LittleEndian.Uint16(plaintext[:2]), append([]byte(nil), plaintext[4:]...)
|
||||
}
|
||||
|
||||
func sourceSealHostControl(t *testing.T, key []byte, sequence uint32, typeID uint16, payload []byte) []byte {
|
||||
t.Helper()
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
aead, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
inner := make([]byte, apolloControlInnerSize+len(payload))
|
||||
binary.LittleEndian.PutUint16(inner[:2], typeID)
|
||||
binary.LittleEndian.PutUint16(inner[2:4], uint16(len(payload)))
|
||||
copy(inner[4:], payload)
|
||||
nonce := make([]byte, 12)
|
||||
binary.LittleEndian.PutUint32(nonce, sequence)
|
||||
nonce[10], nonce[11] = 'H', 'C'
|
||||
sealed := aead.Seal(nil, nonce, inner, nil)
|
||||
packet := make([]byte, apolloControlHeaderSize+len(sealed))
|
||||
binary.LittleEndian.PutUint16(packet[:2], apolloControlOuterType)
|
||||
binary.LittleEndian.PutUint16(packet[2:4], uint16(4+len(sealed)))
|
||||
binary.LittleEndian.PutUint32(packet[4:8], sequence)
|
||||
copy(packet[8:24], sealed[len(inner):])
|
||||
copy(packet[24:], sealed[:len(inner)])
|
||||
return packet
|
||||
}
|
||||
|
||||
func sourceGFMultiply(first, second byte) byte {
|
||||
var product byte
|
||||
for second != 0 {
|
||||
if second&1 != 0 {
|
||||
product ^= first
|
||||
}
|
||||
high := first & 0x80
|
||||
first <<= 1
|
||||
if high != 0 {
|
||||
first ^= 0x1d
|
||||
}
|
||||
second >>= 1
|
||||
}
|
||||
return product
|
||||
}
|
||||
|
||||
func sourceShapedEncryptedAudioPacket(t *testing.T, key []byte, keyID uint32, sequence uint16, encoded []byte) []byte {
|
||||
return sourceShapedEncryptedAudioPacketWithHeaders(t, key, keyID, sequence, 99, 1, encoded)
|
||||
}
|
||||
|
||||
func sourceShapedEncryptedAudioPacketWithHeaders(t *testing.T, key []byte, keyID uint32, sequence uint16, timestamp, ssrc uint32, encoded []byte) []byte {
|
||||
t.Helper()
|
||||
packet := make([]byte, 12)
|
||||
packet[0], packet[1] = 0x80, 97
|
||||
binary.BigEndian.PutUint16(packet[2:4], sequence)
|
||||
binary.BigEndian.PutUint32(packet[4:8], timestamp)
|
||||
binary.BigEndian.PutUint32(packet[8:12], ssrc)
|
||||
padded := append([]byte(nil), encoded...)
|
||||
padding := aes.BlockSize - len(padded)%aes.BlockSize
|
||||
for range padding {
|
||||
padded = append(padded, byte(padding))
|
||||
}
|
||||
iv := make([]byte, aes.BlockSize)
|
||||
binary.BigEndian.PutUint32(iv, keyID+uint32(sequence))
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cipher.NewCBCEncrypter(block, iv).CryptBlocks(padded, padded)
|
||||
return append(packet, padded...)
|
||||
}
|
||||
|
||||
func sourceShapedEncryptedAudioFEC(t *testing.T, key []byte, keyID uint32) [][]byte {
|
||||
t.Helper()
|
||||
const base = uint16(12)
|
||||
const timestamp = uint32(100)
|
||||
data := make([][]byte, apolloAudioDataShards)
|
||||
for index, payload := range [][]byte{{'A'}, {'B'}, {'C'}, {'D'}} {
|
||||
data[index] = sourceShapedEncryptedAudioPacketWithHeaders(t, key, keyID, base+uint16(index), timestamp+uint32(index*5), 0, payload)
|
||||
}
|
||||
parity := make([]byte, len(data[0])-apolloRTPHeaderSize)
|
||||
for index, coefficient := range sourceAudioFECRow() {
|
||||
for offset, value := range data[index][apolloRTPHeaderSize:] {
|
||||
parity[offset] ^= sourceGFMultiply(value, coefficient)
|
||||
}
|
||||
}
|
||||
fec := make([]byte, apolloRTPHeaderSize+12+len(parity))
|
||||
fec[0], fec[1] = 0x80, 127
|
||||
binary.BigEndian.PutUint16(fec[2:4], base+apolloAudioDataShards)
|
||||
fec[apolloRTPHeaderSize+1] = 97
|
||||
binary.BigEndian.PutUint16(fec[apolloRTPHeaderSize+2:apolloRTPHeaderSize+4], base)
|
||||
binary.BigEndian.PutUint32(fec[apolloRTPHeaderSize+4:apolloRTPHeaderSize+8], timestamp)
|
||||
binary.BigEndian.PutUint32(fec[apolloRTPHeaderSize+8:apolloRTPHeaderSize+12], 0)
|
||||
copy(fec[apolloRTPHeaderSize+12:], parity)
|
||||
return [][]byte{data[1], data[2], data[3], fec}
|
||||
}
|
||||
|
||||
func sourceAudioFECRow() []byte {
|
||||
return []byte{0x77, 0x40, 0x38, 0x0e}
|
||||
}
|
||||
|
||||
func certificatePEM(t *testing.T, certificate tls.Certificate) string {
|
||||
|
||||
Reference in New Issue
Block a user