Files
VerseVDI-Data-Plane/gateway/apollo_rtsp_handshake.go
T

535 lines
18 KiB
Go

package gateway
import (
"context"
"fmt"
"io"
"net"
"net/url"
"strconv"
"strings"
"time"
protocol "git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol/gen/go/protocol"
)
const (
maxApolloRTSPHeaders = 16 << 10
maxApolloRTSPBody = 48 << 10
maxApolloRTSPPort = 65535
apolloEncryptionAll = 0x07
)
type apolloRTSPMessage struct {
status int
cseq uint32
headers map[string]string
body []byte
raw []byte
}
type apolloRTSPSetup struct {
sessionID string
audioPort int
videoPort int
controlPort int
audioPing []byte
videoPing []byte
controlConnect uint32
streamHost string
streamPort int64
providerWork protocol.ProviderSessionWork
streamKey []byte
streamKeyID uint32
}
func (b *NativeApolloBackend) performRTSPHandshake(ctx context.Context, work protocol.ProviderSessionWork, key []byte, keyID uint32, streamURL *url.URL) (*apolloRTSPSetup, []byte, error) {
if streamURL == nil || streamURL.Scheme != "rtspenc" || streamURL.Hostname() != work.StreamHost || streamURL.Port() != strconv.FormatInt(work.StreamPort, 10) || streamURL.User != nil || streamURL.RawQuery != "" || streamURL.Fragment != "" {
return nil, nil, ErrProviderMalformed
}
codec, err := newEncryptedRTSPCodec(key)
if err != nil {
return nil, nil, err
}
request := func(method, target, session string, headers []apolloRTSPHeader, body []byte, sequence uint32) (apolloRTSPMessage, error) {
return b.encryptedRTSPRequest(ctx, work, codec, method, target, session, headers, body, sequence)
}
options, err := request("OPTIONS", streamURL.String(), "", nil, nil, 1)
if err != nil {
return nil, nil, err
}
describe, err := request("DESCRIBE", streamURL.String(), "", []apolloRTSPHeader{{"Accept", "application/sdp"}, {"If-Modified-Since", "Thu, 01 Jan 1970 00:00:00 GMT"}}, nil, 2)
if err != nil {
return nil, nil, err
}
if err := validateApolloDescribe(describe); err != nil {
return nil, nil, err
}
setupHeaders := []apolloRTSPHeader{{"Transport", "unicast;X-GS-ClientPort=50000-50001"}, {"If-Modified-Since", "Thu, 01 Jan 1970 00:00:00 GMT"}}
audio, err := request("SETUP", "streamid=audio/0/0", "", setupHeaders, nil, 3)
if err != nil {
return nil, nil, err
}
sessionID, err := apolloRTSPSession(audio)
if err != nil {
return nil, nil, err
}
audioPort, err := apolloRTSPServerPort(audio)
if err != nil {
return nil, nil, err
}
audioPing, err := apolloRTSPPingPayload(audio)
if err != nil {
return nil, nil, err
}
video, err := request("SETUP", "streamid=video/0/0", sessionID, setupHeaders, nil, 4)
if err != nil {
return nil, nil, err
}
if err := apolloRTSPMatchSession(video, sessionID); err != nil {
return nil, nil, err
}
videoPort, err := apolloRTSPServerPort(video)
if err != nil {
return nil, nil, err
}
videoPing, err := apolloRTSPPingPayload(video)
if err != nil {
return nil, nil, err
}
control, err := request("SETUP", "streamid=control/13/0", sessionID, setupHeaders, nil, 5)
if err != nil {
return nil, nil, err
}
if err := apolloRTSPMatchSession(control, sessionID); err != nil {
return nil, nil, err
}
controlPort, err := apolloRTSPServerPort(control)
if err != nil {
return nil, nil, err
}
connectData, err := apolloRTSPConnectData(control)
if err != nil {
return nil, nil, err
}
announceBody, err := apolloAnnounceProfile(work.StreamPolicy)
if err != nil {
return nil, nil, err
}
announce, err := request("ANNOUNCE", "streamid=control/13/0", sessionID, []apolloRTSPHeader{{"Content-Type", "application/sdp"}}, announceBody, 6)
if err != nil {
return nil, nil, err
}
if err := apolloRTSPMatchSession(announce, sessionID); err != nil {
return nil, nil, err
}
play, err := request("PLAY", "/", sessionID, nil, nil, 7)
if err != nil {
return nil, nil, err
}
if err := apolloRTSPMatchSession(play, sessionID); err != nil {
return nil, nil, err
}
_ = options
setup := &apolloRTSPSetup{
sessionID: sessionID, audioPort: audioPort, videoPort: videoPort, controlPort: controlPort,
audioPing: audioPing, videoPing: videoPing, controlConnect: connectData, streamHost: work.StreamHost,
streamPort: work.StreamPort, providerWork: work, streamKey: append([]byte(nil), key...), streamKeyID: keyID,
}
return setup, append([]byte(nil), control.raw...), nil
}
type apolloRTSPHeader struct{ key, value string }
func (b *NativeApolloBackend) encryptedRTSPRequest(ctx context.Context, work protocol.ProviderSessionWork, codec *encryptedRTSPCodec, method, target, session string, headers []apolloRTSPHeader, body []byte, sequence uint32) (apolloRTSPMessage, error) {
if b == nil || b.Dialer == nil || codec == nil || sequence == 0 || len(body) > maxApolloRTSPBody || method == "" || target == "" {
return apolloRTSPMessage{}, ErrProviderMalformed
}
conn, err := b.Dialer.DialContext(ctx, "tcp", net.JoinHostPort(work.StreamHost, strconv.FormatInt(work.StreamPort, 10)))
if err != nil {
return apolloRTSPMessage{}, err
}
defer conn.Close()
deadline := time.Now().Add(5 * time.Second)
if contextDeadline, ok := ctx.Deadline(); ok && contextDeadline.Before(deadline) {
deadline = contextDeadline
}
if err := conn.SetDeadline(deadline); err != nil {
return apolloRTSPMessage{}, err
}
plaintext, err := buildApolloRTSPRequest(method, target, session, headers, body, sequence)
if err != nil {
return apolloRTSPMessage{}, err
}
frame, err := codec.SealClient(plaintext)
if err != nil {
return apolloRTSPMessage{}, err
}
if _, err := conn.Write(frame); err != nil {
return apolloRTSPMessage{}, err
}
response, err := readEncryptedRTSPMessage(conn, codec)
if err != nil {
return apolloRTSPMessage{}, err
}
if response.status != 200 || response.cseq != sequence {
return apolloRTSPMessage{}, ErrProviderMalformed
}
return response, nil
}
func buildApolloRTSPRequest(method, target, session string, headers []apolloRTSPHeader, body []byte, sequence uint32) ([]byte, error) {
if method == "" || target == "" || strings.ContainsAny(method, "\r\n ") || strings.ContainsAny(target, "\r\n") || sequence == 0 {
return nil, ErrProviderMalformed
}
var builder strings.Builder
builder.Grow(256 + len(body))
fmt.Fprintf(&builder, "%s %s RTSP/1.0\r\nCSeq: %d\r\n", method, target, sequence)
if session != "" {
if !validApolloRTSPToken(session) {
return nil, ErrProviderMalformed
}
fmt.Fprintf(&builder, "Session: %s\r\n", session)
}
seen := map[string]struct{}{"cseq": {}, "session": {}}
for _, header := range headers {
key := strings.ToLower(header.key)
if !validApolloRTSPToken(header.key) || header.value == "" || len(header.value) > 1024 || strings.ContainsAny(header.value, "\r\n") {
return nil, ErrProviderMalformed
}
if _, ok := seen[key]; ok {
return nil, ErrProviderMalformed
}
seen[key] = struct{}{}
fmt.Fprintf(&builder, "%s: %s\r\n", header.key, header.value)
}
if len(body) != 0 {
if _, ok := seen["content-length"]; ok {
return nil, ErrProviderMalformed
}
fmt.Fprintf(&builder, "Content-Length: %d\r\n", len(body))
}
builder.WriteString("\r\n")
builder.Write(body)
return []byte(builder.String()), nil
}
func readEncryptedRTSPMessage(conn net.Conn, codec *encryptedRTSPCodec) (apolloRTSPMessage, error) {
header := make([]byte, encryptedRTSPHeaderSize)
if _, err := io.ReadFull(conn, header); err != nil {
return apolloRTSPMessage{}, err
}
length := uint64(header[0]&0x7f)<<24 | uint64(header[1])<<16 | uint64(header[2])<<8 | uint64(header[3])
if header[0]&0x80 == 0 || length == 0 || length > encryptedRTSPMaxPayload {
return apolloRTSPMessage{}, ErrProviderMalformed
}
frame := make([]byte, encryptedRTSPHeaderSize+int(length))
copy(frame, header)
if _, err := io.ReadFull(conn, frame[encryptedRTSPHeaderSize:]); err != nil {
return apolloRTSPMessage{}, err
}
plaintext, err := codec.OpenHost(frame)
if err != nil {
return apolloRTSPMessage{}, ErrProviderMalformed
}
return parseApolloRTSPMessage(plaintext)
}
func parseApolloRTSPMessage(data []byte) (apolloRTSPMessage, error) {
if len(data) == 0 || len(data) > encryptedRTSPMaxPayload {
return apolloRTSPMessage{}, ErrProviderMalformed
}
headerEnd := strings.Index(string(data), "\r\n\r\n")
if headerEnd < 0 || headerEnd+4 > maxApolloRTSPHeaders {
return apolloRTSPMessage{}, ErrProviderMalformed
}
lines := strings.Split(string(data[:headerEnd]), "\r\n")
if len(lines) < 1 {
return apolloRTSPMessage{}, ErrProviderMalformed
}
parts := strings.SplitN(lines[0], " ", 3)
if len(parts) != 3 || parts[0] != "RTSP/1.0" || len(parts[2]) == 0 || len(parts[2]) > 128 {
return apolloRTSPMessage{}, ErrProviderMalformed
}
status, err := strconv.Atoi(parts[1])
if err != nil || status < 100 || status > 599 {
return apolloRTSPMessage{}, ErrProviderMalformed
}
message := apolloRTSPMessage{status: status, headers: make(map[string]string), raw: append([]byte(nil), data...)}
for _, line := range lines[1:] {
key, value, ok := strings.Cut(line, ":")
key = strings.ToLower(strings.TrimSpace(key))
value = strings.TrimSpace(value)
if !ok || !validApolloRTSPToken(key) || value == "" || len(value) > 1024 {
return apolloRTSPMessage{}, ErrProviderMalformed
}
if _, duplicate := message.headers[key]; duplicate {
return apolloRTSPMessage{}, ErrProviderMalformed
}
message.headers[key] = value
}
cseq, ok := message.headers["cseq"]
if !ok {
return apolloRTSPMessage{}, ErrProviderMalformed
}
parsedCSeq, err := strconv.ParseUint(cseq, 10, 32)
if err != nil || parsedCSeq == 0 {
return apolloRTSPMessage{}, ErrProviderMalformed
}
message.cseq = uint32(parsedCSeq)
message.body = append([]byte(nil), data[headerEnd+4:]...)
if len(message.body) > maxApolloRTSPBody {
return apolloRTSPMessage{}, ErrProviderMalformed
}
if length, hasLength := message.headers["content-length"]; hasLength {
declared, err := strconv.ParseUint(length, 10, 16)
if err != nil || int(declared) != len(message.body) {
return apolloRTSPMessage{}, ErrProviderMalformed
}
} else if len(message.body) != 0 {
return apolloRTSPMessage{}, ErrProviderMalformed
}
return message, nil
}
func validateApolloDescribe(message apolloRTSPMessage) error {
if message.headers["content-type"] != "application/sdp" || len(message.body) == 0 {
return ErrProviderMalformed
}
body := string(message.body)
lineEnding := "\n"
if strings.Contains(body, "\r\n") {
if strings.Contains(strings.ReplaceAll(body, "\r\n", ""), "\n") {
return ErrProviderMalformed
}
lineEnding = "\r\n"
}
if !strings.HasSuffix(body, lineEnding) {
return ErrProviderMalformed
}
attributes := map[string]string{}
seen := map[string]struct{}{}
stage := 0
stereo := false
for _, line := range strings.Split(strings.TrimSuffix(body, lineEnding), lineEnding) {
if line == "" {
return ErrProviderMalformed
}
if !strings.HasPrefix(line, "a=") {
if line == "sprop-parameter-sets=AAAAAU" && stage >= 3 && stage <= 4 {
stage = 5
continue
}
return ErrProviderMalformed
}
key, value, ok := strings.Cut(strings.TrimPrefix(line, "a="), ":")
if !ok || key == "" || value == "" || len(key) > 128 || len(value) > 256 {
return ErrProviderMalformed
}
if key == "fmtp" {
if stage < 3 || !validApolloSurroundParameters(value) {
return ErrProviderMalformed
}
if _, duplicate := seen["fmtp:"+value]; duplicate {
return ErrProviderMalformed
}
seen["fmtp:"+value] = struct{}{}
if value == "97 surround-params=21101" {
stereo = true
}
stage = 6
continue
}
if key == "rtpmap" && value == "98 AV1/90000" && stage >= 3 && stage <= 5 {
if _, duplicate := seen[key]; duplicate {
return ErrProviderMalformed
}
seen[key] = struct{}{}
stage = 6
continue
}
if key == "x-nv-video[0].refPicInvalidation" && value == "1" && stage == 3 {
if _, duplicate := seen[key]; duplicate {
return ErrProviderMalformed
}
seen[key] = struct{}{}
stage = 4
continue
}
if key != "x-ss-general.featureFlags" && key != "x-ss-general.encryptionSupported" && key != "x-ss-general.encryptionRequested" {
return ErrProviderMalformed
}
if _, duplicate := attributes[key]; duplicate || (key == "x-ss-general.featureFlags" && stage != 0) || (key == "x-ss-general.encryptionSupported" && stage != 1) || (key == "x-ss-general.encryptionRequested" && stage != 2) {
return ErrProviderMalformed
}
attributes[key] = value
stage++
}
featureFlags, featureFlagsOK := attributes["x-ss-general.featureFlags"]
supported, supportedOK := attributes["x-ss-general.encryptionSupported"]
requested, requestedOK := attributes["x-ss-general.encryptionRequested"]
if !featureFlagsOK || !supportedOK || !requestedOK || !stereo {
return ErrProviderMalformed
}
if _, err := strconv.ParseUint(featureFlags, 10, 32); err != nil {
return ErrProviderMalformed
}
supportedFlags, err := strconv.ParseUint(supported, 10, 32)
if err != nil || supportedFlags&apolloEncryptionAll != apolloEncryptionAll {
return ErrProviderMalformed
}
requestedFlags, err := strconv.ParseUint(requested, 10, 32)
if err != nil || requestedFlags&^supportedFlags != 0 || requestedFlags&1 == 0 {
return ErrProviderMalformed
}
return nil
}
func validApolloSurroundParameters(value string) bool {
const prefix = "97 surround-params="
if !strings.HasPrefix(value, prefix) {
return false
}
parameters := strings.TrimPrefix(value, prefix)
if len(parameters) < 5 || len(parameters) > 11 {
return false
}
channels := int(parameters[0] - '0')
streams := int(parameters[1] - '0')
coupled := int(parameters[2] - '0')
if (channels != 2 && channels != 6 && channels != 8) || len(parameters) != channels+3 || streams+coupled != channels || streams == 0 {
return false
}
used := [8]bool{}
for _, character := range parameters[3:] {
if character < '0' || int(character-'0') >= channels || used[character-'0'] {
return false
}
used[character-'0'] = true
}
return true
}
func apolloRTSPSession(message apolloRTSPMessage) (string, error) {
value, ok := message.headers["session"]
if !ok {
return "", ErrProviderMalformed
}
token, _, _ := strings.Cut(value, ";")
token = strings.TrimSpace(token)
if !validApolloRTSPToken(token) || len(token) > 256 {
return "", ErrProviderMalformed
}
return token, nil
}
func apolloRTSPMatchSession(message apolloRTSPMessage, expected string) error {
actual, err := apolloRTSPSession(message)
if err != nil || actual != expected {
return ErrProviderMalformed
}
return nil
}
func apolloRTSPServerPort(message apolloRTSPMessage) (int, error) {
transport, ok := message.headers["transport"]
if !ok {
return 0, ErrProviderMalformed
}
parts := strings.Split(transport, ";")
if len(parts) != 2 || parts[0] != "unicast" {
return 0, ErrProviderMalformed
}
key, value, ok := strings.Cut(parts[1], "=")
port, err := strconv.Atoi(value)
if !ok || key != "server_port" || err != nil || port < 1 || port > maxApolloRTSPPort {
return 0, ErrProviderMalformed
}
return port, nil
}
func apolloRTSPPingPayload(message apolloRTSPMessage) ([]byte, error) {
payload, ok := message.headers["x-ss-ping-payload"]
if !ok || len(payload) != 16 || !validApolloRTSPToken(payload) {
return nil, ErrProviderMalformed
}
return []byte(payload), nil
}
func apolloRTSPConnectData(message apolloRTSPMessage) (uint32, error) {
value, ok := message.headers["x-ss-connect-data"]
if !ok {
return 0, ErrProviderMalformed
}
if value == "" || strings.Trim(value, "0123456789") != "" {
return 0, ErrProviderMalformed
}
parsed, err := strconv.ParseUint(value, 10, 32)
if err != nil || parsed == 0 {
return 0, ErrProviderMalformed
}
return uint32(parsed), nil
}
func apolloAnnounceProfile(policy protocol.ProviderStreamPolicy) ([]byte, error) {
if err := validateApolloStreamPolicy(policy); err != nil {
return nil, err
}
format, supportsHEVC := int64(0), int64(0)
if policy.Codec == "HEVC" {
format, supportsHEVC = 1, 1
}
maximumBitrate := policy.BitrateKbps * 80 / 100
if maximumBitrate > 100000 {
maximumBitrate = 100000
}
return []byte("v=0\r\n" +
"o=android 0 0 IN IP4 0.0.0.0\r\n" +
"s=NVIDIA Streaming Client\r\n" +
fmt.Sprintf("a=x-nv-video[0].clientViewportWd:%d\r\n", policy.ResolutionWidth) +
fmt.Sprintf("a=x-nv-video[0].clientViewportHt:%d\r\n", policy.ResolutionHeight) +
fmt.Sprintf("a=x-nv-video[0].maxFPS:%d\r\n", policy.Fps) +
"a=x-nv-video[0].packetSize:1024\r\n" +
"a=x-nv-video[0].videoEncoderSlicesPerFrame:1\r\n" +
"a=x-nv-video[0].maxNumReferenceFrames:0\r\n" +
fmt.Sprintf("a=x-nv-clientSupportHevc:%d\r\n", supportsHEVC) +
fmt.Sprintf("a=x-nv-vqos[0].bitStreamFormat:%d\r\n", format) +
fmt.Sprintf("a=x-nv-video[0].initialBitrateKbps:%d\r\n", maximumBitrate) +
fmt.Sprintf("a=x-nv-video[0].initialPeakBitrateKbps:%d\r\n", maximumBitrate) +
fmt.Sprintf("a=x-nv-vqos[0].bw.minimumBitrateKbps:%d\r\n", maximumBitrate) +
fmt.Sprintf("a=x-nv-vqos[0].bw.maximumBitrateKbps:%d\r\n", maximumBitrate) +
"a=x-nv-vqos[0].fec.minRequiredFecPackets:2\r\n" +
"a=x-nv-vqos[0].qosTrafficType:5\r\n" +
"a=x-nv-audio.surround.numChannels:2\r\n" +
"a=x-nv-audio.surround.channelMask:3\r\n" +
"a=x-nv-audio.surround.AudioQuality:0\r\n" +
"a=x-nv-aqos.packetDuration:5\r\n" +
"a=x-nv-aqos.qosTrafficType:4\r\n" +
"a=x-nv-general.useReliableUdp:13\r\n" +
"a=x-nv-general.featureFlags:167\r\n" +
"a=x-ml-general.featureFlags:0\r\n" +
fmt.Sprintf("a=x-ml-video.configuredBitrateKbps:%d\r\n", policy.BitrateKbps) +
"a=x-ss-general.encryptionEnabled:7\r\n" +
"a=x-ss-video[0].chromaSamplingType:0\r\n" +
"a=x-ss-video[0].intraRefresh:0\r\n"), nil
}
func validateApolloStreamPolicy(policy protocol.ProviderStreamPolicy) error {
if err := policy.Validate(); err != nil || !policy.AudioEnabled || (policy.Codec != "H264" && policy.Codec != "HEVC") {
return ErrProviderMalformed
}
return nil
}
func validApolloRTSPToken(value string) bool {
if value == "" || len(value) > 128 {
return false
}
for _, character := range value {
if character <= 0x20 || character >= 0x7f || strings.ContainsRune("()<>@,;:\\\"/[]?={}", character) {
return false
}
}
return true
}