feat(gateway): repair native Apollo provider path
This commit is contained in:
+535
-118
@@ -9,15 +9,20 @@ import (
|
||||
"crypto/x509"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
protocol "git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol/gen/go/protocol"
|
||||
)
|
||||
@@ -29,11 +34,11 @@ type NativeApolloBackend struct {
|
||||
Dialer *net.Dialer
|
||||
|
||||
mu sync.Mutex
|
||||
pending map[string]net.Conn
|
||||
pending map[string]*apolloRTSPSetup
|
||||
}
|
||||
|
||||
func NewNativeApolloBackend() *NativeApolloBackend {
|
||||
return &NativeApolloBackend{Dialer: &net.Dialer{Timeout: 5 * time.Second}, pending: make(map[string]net.Conn)}
|
||||
return &NativeApolloBackend{Dialer: &net.Dialer{Timeout: 5 * time.Second}, pending: make(map[string]*apolloRTSPSetup)}
|
||||
}
|
||||
|
||||
func (b *NativeApolloBackend) Management(ctx context.Context, request LaunchRequest) ([]byte, error) {
|
||||
@@ -53,7 +58,11 @@ func newPinnedApolloHTTPClient(work protocol.ProviderSessionWork) (*http.Client,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}, Timeout: 5 * time.Second}, nil
|
||||
return &http.Client{
|
||||
Transport: &http.Transport{TLSClientConfig: tlsConfig},
|
||||
Timeout: 5 * time.Second,
|
||||
CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse },
|
||||
}, nil
|
||||
}
|
||||
|
||||
func apolloGet(ctx context.Context, client *http.Client, work protocol.ProviderSessionWork, path string, values url.Values) ([]byte, error) {
|
||||
@@ -74,6 +83,88 @@ func apolloGet(ctx context.Context, client *http.Client, work protocol.ProviderS
|
||||
return readBounded(response.Body, 64*1024)
|
||||
}
|
||||
|
||||
func apolloSessionRequest(work protocol.ProviderSessionWork, key []byte, keyID uint32) (string, url.Values) {
|
||||
values := url.Values{
|
||||
"rikey": {hex.EncodeToString(key)},
|
||||
"rikeyid": {strconv.FormatUint(uint64(keyID), 10)},
|
||||
"localAudioPlayMode": {"0"},
|
||||
}
|
||||
if work.ReconnectSequence > 0 {
|
||||
return "/resume", values
|
||||
}
|
||||
values.Set("uniqueid", work.ClientID)
|
||||
values.Set("appid", work.ApplicationID)
|
||||
values.Set("corever", "1")
|
||||
return "/launch", values
|
||||
}
|
||||
|
||||
func apolloClipboardRequest(ctx context.Context, client *http.Client, host string, port int64, method, text string) ([]byte, error) {
|
||||
if client == nil || host == "" || port < 1 || port > maxApolloRTSPPort || (method != http.MethodGet && method != http.MethodPost) {
|
||||
return nil, ErrProviderMalformed
|
||||
}
|
||||
endpoint := url.URL{Scheme: "https", Host: net.JoinHostPort(host, strconv.FormatInt(port, 10)), Path: "/actions/clipboard", RawQuery: url.Values{"type": {"text"}}.Encode()}
|
||||
var body io.Reader
|
||||
if method == http.MethodPost {
|
||||
if !utf8.ValidString(text) || len(text) > 65536 {
|
||||
return nil, ErrProviderMalformed
|
||||
}
|
||||
body = bytes.NewReader([]byte(text))
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, method, endpoint.String(), body)
|
||||
if err != nil {
|
||||
return nil, ErrProviderMalformed
|
||||
}
|
||||
if method == http.MethodPost {
|
||||
request.Header.Set("Content-Type", "text/plain; charset=utf-8")
|
||||
}
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("provider clipboard status %d", response.StatusCode)
|
||||
}
|
||||
return readBounded(response.Body, 65536)
|
||||
}
|
||||
|
||||
func apolloCancelRequest(ctx context.Context, client *http.Client, host string, port int64) error {
|
||||
if client == nil || host == "" || port < 1 || port > maxApolloRTSPPort {
|
||||
return ErrProviderMalformed
|
||||
}
|
||||
endpoint := url.URL{Scheme: "https", Host: net.JoinHostPort(host, strconv.FormatInt(port, 10)), Path: "/cancel"}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
|
||||
if err != nil {
|
||||
return ErrProviderMalformed
|
||||
}
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("provider cancel status %d", response.StatusCode)
|
||||
}
|
||||
body, err := readBounded(response.Body, 64*1024)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var result struct {
|
||||
XMLName xml.Name `xml:"root"`
|
||||
StatusCode int `xml:"status_code,attr"`
|
||||
Cancel int `xml:"cancel"`
|
||||
}
|
||||
decoder := xml.NewDecoder(bytes.NewReader(body))
|
||||
decoder.Strict = true
|
||||
if err := decoder.Decode(&result); err != nil || result.XMLName.Local != "root" || result.StatusCode != http.StatusOK || result.Cancel != 1 {
|
||||
return ErrProviderMalformed
|
||||
}
|
||||
if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
|
||||
return ErrProviderMalformed
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func pinnedApolloTLSConfig(work protocol.ProviderSessionWork) (*tls.Config, error) {
|
||||
identity, ok := providerIdentityFromKey(work.ProviderIdentity)
|
||||
if !ok || !strings.HasPrefix(identity.Fingerprint, "sha256:") {
|
||||
@@ -123,15 +214,13 @@ func (b *NativeApolloBackend) Setup(ctx context.Context, request LaunchRequest)
|
||||
if _, err := rand.Read(key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer zeroApolloSecret(key)
|
||||
var keyID [4]byte
|
||||
if _, err := rand.Read(keyID[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
launch, err := apolloGet(ctx, client, work, "/launch", url.Values{
|
||||
"uniqueid": {work.ClientID}, "appid": {work.ApplicationID}, "rikey": {hex.EncodeToString(key)},
|
||||
"rikeyid": {strconv.FormatUint(uint64(binary.BigEndian.Uint32(keyID[:])), 10)}, "localAudioPlayMode": {"0"},
|
||||
"corever": {"1"},
|
||||
})
|
||||
sessionPath, sessionValues := apolloSessionRequest(work, key, binary.BigEndian.Uint32(keyID[:]))
|
||||
launch, err := apolloGet(ctx, client, work, sessionPath, sessionValues)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -147,50 +236,25 @@ func (b *NativeApolloBackend) Setup(ctx context.Context, request LaunchRequest)
|
||||
if err != nil || streamPort != work.StreamPort {
|
||||
return nil, ErrProviderMalformed
|
||||
}
|
||||
conn, err := b.Dialer.DialContext(ctx, "tcp", net.JoinHostPort(work.StreamHost, strconv.FormatInt(work.StreamPort, 10)))
|
||||
setup, response, err := b.performRTSPHandshake(ctx, work, key, binary.BigEndian.Uint32(keyID[:]), streamURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
_ = conn.SetDeadline(deadline)
|
||||
}
|
||||
codec, err := newEncryptedRTSPCodec(key)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
requestText := "SETUP rtsp://" + work.StreamHost + "/streamid=video/0/0 RTSP/1.0\r\nCSeq: 1\r\nTransport: RTP/AVP/TCP;interleaved=0-1\r\n\r\n"
|
||||
encoded, err := codec.SealClient([]byte(requestText))
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
if _, err := conn.Write(encoded); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
response, err := readEncryptedRTSPHeaders(conn, codec)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
b.mu.Lock()
|
||||
b.pending[request.SessionID] = conn
|
||||
b.pending[request.SessionID] = setup
|
||||
b.mu.Unlock()
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (b *NativeApolloBackend) Open(_ context.Context, request LaunchRequest, _ RTSPResponse) (ProviderSession, error) {
|
||||
func (b *NativeApolloBackend) Open(ctx context.Context, request LaunchRequest, _ RTSPResponse) (ProviderSession, error) {
|
||||
b.mu.Lock()
|
||||
conn, ok := b.pending[request.SessionID]
|
||||
setup, ok := b.pending[request.SessionID]
|
||||
delete(b.pending, request.SessionID)
|
||||
b.mu.Unlock()
|
||||
if !ok || conn == nil {
|
||||
if !ok || setup == nil {
|
||||
return nil, ErrProviderDisconnected
|
||||
}
|
||||
session := newNativeApolloSession(conn, request.SessionID)
|
||||
go session.readMedia()
|
||||
return session, nil
|
||||
return newNativeApolloProviderSession(ctx, setup)
|
||||
}
|
||||
|
||||
func readBounded(reader io.Reader, max int) ([]byte, error) {
|
||||
@@ -204,152 +268,503 @@ func readBounded(reader io.Reader, max int) ([]byte, error) {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func readEncryptedRTSPHeaders(conn net.Conn, codec *encryptedRTSPCodec) ([]byte, error) {
|
||||
header := make([]byte, encryptedRTSPHeaderSize)
|
||||
if _, err := io.ReadFull(conn, header); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
length := binary.BigEndian.Uint32(header[:4]) & 0x7fffffff
|
||||
if length == 0 || length > encryptedRTSPMaxPayload {
|
||||
return nil, ErrProviderMalformed
|
||||
}
|
||||
frame := make([]byte, encryptedRTSPHeaderSize+int(length))
|
||||
copy(frame, header)
|
||||
if _, err := io.ReadFull(conn, frame[encryptedRTSPHeaderSize:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
plaintext, err := codec.OpenHost(frame)
|
||||
if err != nil || len(plaintext) > 16*1024 || !strings.HasSuffix(string(plaintext), "\r\n\r\n") {
|
||||
return nil, ErrProviderMalformed
|
||||
}
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
type nativeApolloSession struct {
|
||||
conn net.Conn
|
||||
sessionID string
|
||||
video chan []byte
|
||||
audio chan []byte
|
||||
mu sync.Mutex
|
||||
state protocol.ProviderState
|
||||
closeOnce sync.Once
|
||||
done chan struct{}
|
||||
readDone chan struct{}
|
||||
enet *apolloENetPeer
|
||||
control *apolloControlCodec
|
||||
audioConn *net.UDPConn
|
||||
videoConn *net.UDPConn
|
||||
media *apolloMediaCodec
|
||||
videoFEC apolloVideoAssembler
|
||||
audioFEC apolloAudioAssembler
|
||||
audioPing []byte
|
||||
videoPing []byte
|
||||
sessionID string
|
||||
video chan []byte
|
||||
audio chan []byte
|
||||
events chan ProviderEvent
|
||||
mu sync.Mutex
|
||||
controlMu sync.Mutex
|
||||
state protocol.ProviderState
|
||||
pressed map[string]InputEvent
|
||||
closeOnce sync.Once
|
||||
channelsOnce sync.Once
|
||||
done chan struct{}
|
||||
readDone chan struct{}
|
||||
managementClient *http.Client
|
||||
managementHost string
|
||||
managementPort int64
|
||||
allowApplicationTermination bool
|
||||
terminationErr error
|
||||
mediaDrops atomic.Uint64
|
||||
}
|
||||
|
||||
func newNativeApolloSession(conn net.Conn, sessionID string) *nativeApolloSession {
|
||||
return &nativeApolloSession{conn: conn, sessionID: sessionID, video: make(chan []byte, 16), audio: make(chan []byte, 16), state: protocol.ProviderState{Version: "1", SessionID: sessionID, State: ProviderStateStarting, Channels: []string{"video", "audio", "input", "feedback"}}, done: make(chan struct{}), readDone: make(chan struct{})}
|
||||
func newNativeApolloSession(sessionID string) *nativeApolloSession {
|
||||
return &nativeApolloSession{sessionID: sessionID, video: make(chan []byte, 16), audio: make(chan []byte, 16), events: make(chan ProviderEvent, 16), state: protocol.ProviderState{Version: "1", SessionID: sessionID, State: ProviderStateStarting, Channels: []string{"video", "audio", "input", "feedback"}}, pressed: make(map[string]InputEvent), done: make(chan struct{}), readDone: make(chan struct{})}
|
||||
}
|
||||
|
||||
func newNativeApolloProviderSession(ctx context.Context, setup *apolloRTSPSetup) (*nativeApolloSession, error) {
|
||||
if setup == nil || len(setup.streamKey) != 16 || setup.controlPort == 0 || setup.audioPort == 0 || setup.videoPort == 0 {
|
||||
return nil, ErrProviderMalformed
|
||||
}
|
||||
defer zeroApolloSecret(setup.streamKey)
|
||||
controlConn, err := dialApolloUDP(setup.streamHost, setup.controlPort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
peer, err := newApolloENetPeer(controlConn, time.Now)
|
||||
if err != nil {
|
||||
_ = controlConn.Close()
|
||||
return nil, err
|
||||
}
|
||||
codec, err := newApolloControlCodec(setup.streamKey)
|
||||
if err != nil {
|
||||
peer.close(err)
|
||||
return nil, err
|
||||
}
|
||||
media, err := newApolloMediaCodec(setup.streamKey, setup.streamKeyID)
|
||||
if err != nil {
|
||||
peer.close(err)
|
||||
return nil, err
|
||||
}
|
||||
session := newNativeApolloSession(setup.sessionID)
|
||||
managementClient, err := newPinnedApolloHTTPClient(setup.providerWork)
|
||||
if err != nil {
|
||||
peer.close(err)
|
||||
return nil, err
|
||||
}
|
||||
session.enet, session.control, session.media = peer, codec, media
|
||||
session.managementClient = managementClient
|
||||
session.managementHost, session.managementPort = setup.providerWork.ManagementHost, setup.providerWork.ManagementPort
|
||||
session.allowApplicationTermination = setup.providerWork.ProviderApplicationTerminationAllowed
|
||||
session.audioPing = append([]byte(nil), setup.audioPing...)
|
||||
session.videoPing = append([]byte(nil), setup.videoPing...)
|
||||
peer.onPayload = session.handleApolloControlPayload
|
||||
peer.onDisconnect = session.handleApolloDisconnect
|
||||
connectCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
if err := peer.Connect(connectCtx, setup.controlConnect); err != nil {
|
||||
peer.close(err)
|
||||
return nil, err
|
||||
}
|
||||
audioConn, err := dialApolloUDP(setup.streamHost, setup.audioPort)
|
||||
if err != nil {
|
||||
peer.close(err)
|
||||
return nil, err
|
||||
}
|
||||
videoConn, err := dialApolloUDP(setup.streamHost, setup.videoPort)
|
||||
if err != nil {
|
||||
_ = audioConn.Close()
|
||||
peer.close(err)
|
||||
return nil, err
|
||||
}
|
||||
if _, err := audioConn.Write(apolloMediaPing(setup.audioPing, 1)); err != nil {
|
||||
_ = audioConn.Close()
|
||||
_ = videoConn.Close()
|
||||
peer.close(err)
|
||||
return nil, err
|
||||
}
|
||||
if _, err := videoConn.Write(apolloMediaPing(setup.videoPing, 1)); err != nil {
|
||||
_ = audioConn.Close()
|
||||
_ = videoConn.Close()
|
||||
peer.close(err)
|
||||
return nil, err
|
||||
}
|
||||
session.audioConn, session.videoConn = audioConn, videoConn
|
||||
go session.readUDPMedia()
|
||||
go session.periodicApolloMediaPing()
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func dialApolloUDP(host string, port int) (*net.UDPConn, error) {
|
||||
if host == "" || port < 1 || port > maxApolloRTSPPort {
|
||||
return nil, ErrProviderMalformed
|
||||
}
|
||||
remote, err := net.ResolveUDPAddr("udp", net.JoinHostPort(host, strconv.Itoa(port)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return net.DialUDP("udp", nil, remote)
|
||||
}
|
||||
|
||||
func (s *nativeApolloSession) Ready(context.Context) error {
|
||||
if s.enet != nil {
|
||||
if err := s.writeApolloControl(apolloChannelGeneric, true, apolloControlTypeIDR, []byte{0, 0}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.writeApolloControl(apolloChannelGeneric, true, apolloControlTypeStart, []byte{0}); err != nil {
|
||||
return err
|
||||
}
|
||||
go s.periodicApolloPing()
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.state.State = ProviderStateReady
|
||||
s.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *nativeApolloSession) Video() <-chan []byte { return s.video }
|
||||
func (s *nativeApolloSession) Audio() <-chan []byte { return s.audio }
|
||||
func (s *nativeApolloSession) Video() <-chan []byte { return s.video }
|
||||
func (s *nativeApolloSession) Audio() <-chan []byte { return s.audio }
|
||||
func (s *nativeApolloSession) Events() <-chan ProviderEvent { return s.events }
|
||||
|
||||
func (s *nativeApolloSession) Input(ctx context.Context, event InputEvent) error {
|
||||
payload, err := EncodeInputEvent(event)
|
||||
packet, err := encodeApolloInputEvent(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.writeControl(ctx, ControlPacket{Kind: 1, Sequence: event.Sequence, Payload: payload})
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
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)
|
||||
s.mu.Lock()
|
||||
if event.Pressed {
|
||||
s.pressed[key] = event
|
||||
} else {
|
||||
delete(s.pressed, key)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *nativeApolloSession) Feedback(ctx context.Context, feedback Feedback) error {
|
||||
return s.writeControl(ctx, ControlPacket{Kind: 3, Sequence: feedback.Sequence, Payload: feedback.Payload})
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
switch feedback.Kind {
|
||||
case FeedbackIDR:
|
||||
if len(feedback.Payload) != 0 {
|
||||
return ErrProviderMalformed
|
||||
}
|
||||
return s.writeApolloControl(apolloChannelUrgent, true, apolloControlTypeIDR, []byte{0, 0})
|
||||
case FeedbackFEC:
|
||||
if !validGatewayFECStatus(feedback.Payload) {
|
||||
return ErrProviderMalformed
|
||||
}
|
||||
return s.writeApolloControl(apolloChannelGeneric, false, apolloControlTypeFEC, feedback.Payload)
|
||||
default:
|
||||
return ErrProviderMalformed
|
||||
}
|
||||
}
|
||||
|
||||
func (s *nativeApolloSession) Reconnect(ctx context.Context) error {
|
||||
return s.writeControl(ctx, ControlPacket{Kind: 4, Payload: []byte("RECN")})
|
||||
func (s *nativeApolloSession) ReadClipboard(ctx context.Context) (string, error) {
|
||||
response, err := apolloClipboardRequest(ctx, s.managementClient, s.managementHost, s.managementPort, http.MethodGet, "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !utf8.Valid(response) {
|
||||
return "", ErrProviderMalformed
|
||||
}
|
||||
return string(response), nil
|
||||
}
|
||||
|
||||
func (s *nativeApolloSession) WriteClipboard(ctx context.Context, text string) error {
|
||||
_, err := apolloClipboardRequest(ctx, s.managementClient, s.managementHost, s.managementPort, http.MethodPost, text)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *nativeApolloSession) ReleaseAll(ctx context.Context) error {
|
||||
return s.writeControl(ctx, ControlPacket{Kind: 2, Payload: []byte("RELEASE_ALL")})
|
||||
s.mu.Lock()
|
||||
pressed := make([]InputEvent, 0, len(s.pressed))
|
||||
for _, event := range s.pressed {
|
||||
pressed = append(pressed, event)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
sort.Slice(pressed, func(first, second int) bool {
|
||||
if pressed[first].Device != pressed[second].Device {
|
||||
return pressed[first].Device < pressed[second].Device
|
||||
}
|
||||
return pressed[first].Code < pressed[second].Code
|
||||
})
|
||||
for _, event := range pressed {
|
||||
event.Pressed = false
|
||||
if event.Device == "controller" {
|
||||
event.Payload = make([]byte, len(event.Payload))
|
||||
}
|
||||
if err := s.Input(ctx, event); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *nativeApolloSession) Terminate(ctx context.Context) error {
|
||||
_ = s.writeControl(ctx, ControlPacket{Kind: 5, Payload: []byte("TEAR")})
|
||||
var timedOut bool
|
||||
var cleanupErr error
|
||||
s.closeOnce.Do(func() {
|
||||
if err := s.ReleaseAll(ctx); err != nil {
|
||||
cleanupErr = err
|
||||
}
|
||||
close(s.done)
|
||||
_ = s.conn.Close()
|
||||
if s.enet != nil {
|
||||
if err := s.enet.Disconnect(ctx); err != nil && cleanupErr == nil {
|
||||
cleanupErr = err
|
||||
}
|
||||
}
|
||||
if s.audioConn != nil {
|
||||
_ = s.audioConn.Close()
|
||||
}
|
||||
if s.videoConn != nil {
|
||||
_ = s.videoConn.Close()
|
||||
}
|
||||
select {
|
||||
case <-s.readDone:
|
||||
case <-ctx.Done():
|
||||
timedOut = true
|
||||
if cleanupErr == nil {
|
||||
cleanupErr = ctx.Err()
|
||||
}
|
||||
}
|
||||
if !timedOut {
|
||||
close(s.video)
|
||||
close(s.audio)
|
||||
if cleanupErr == nil {
|
||||
s.closeMediaChannels()
|
||||
if s.allowApplicationTermination {
|
||||
if err := apolloCancelRequest(ctx, s.managementClient, s.managementHost, s.managementPort); err != nil {
|
||||
cleanupErr = err
|
||||
}
|
||||
}
|
||||
}
|
||||
if s.managementClient != nil {
|
||||
s.managementClient.CloseIdleConnections()
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.terminationErr = cleanupErr
|
||||
s.mu.Unlock()
|
||||
})
|
||||
s.mu.Lock()
|
||||
if timedOut {
|
||||
cleanupErr = s.terminationErr
|
||||
if cleanupErr != nil {
|
||||
s.state.State = ProviderStateCleanup
|
||||
s.state.CleanupPending = true
|
||||
} else {
|
||||
s.state.State = ProviderStateTerminated
|
||||
}
|
||||
s.mu.Unlock()
|
||||
if timedOut {
|
||||
return ctx.Err()
|
||||
if cleanupErr != nil {
|
||||
return cleanupErr
|
||||
}
|
||||
s.control, s.media = nil, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func zeroApolloSecret(secret []byte) {
|
||||
for index := range secret {
|
||||
secret[index] = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (s *nativeApolloSession) State() protocol.ProviderState {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.state
|
||||
}
|
||||
|
||||
func (s *nativeApolloSession) writeControl(ctx context.Context, packet ControlPacket) error {
|
||||
encoded, err := EncodeControlPacket(packet)
|
||||
func (s *nativeApolloSession) Telemetry() ProviderTelemetry {
|
||||
telemetry := s.enet.telemetry()
|
||||
telemetry.State = s.State().State
|
||||
telemetry.MediaDrops = s.mediaDrops.Load()
|
||||
return telemetry
|
||||
}
|
||||
|
||||
func (s *nativeApolloSession) writeApolloControl(channel uint8, reliable bool, typeID uint16, payload []byte) error {
|
||||
if s.enet == nil || s.control == nil {
|
||||
return ErrProviderDisconnected
|
||||
}
|
||||
s.controlMu.Lock()
|
||||
encoded, err := s.control.SealClient(typeID, payload)
|
||||
s.controlMu.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
_ = s.conn.SetWriteDeadline(deadline)
|
||||
if reliable {
|
||||
return s.enet.SendReliable(channel, encoded)
|
||||
}
|
||||
if _, err := s.conn.Write(encoded); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return s.enet.SendUnsequenced(channel, encoded)
|
||||
}
|
||||
|
||||
func (s *nativeApolloSession) readMedia() {
|
||||
defer close(s.readDone)
|
||||
header := make([]byte, 4)
|
||||
func (s *nativeApolloSession) periodicApolloPing() {
|
||||
ticker := time.NewTicker(100 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
if _, err := io.ReadFull(s.conn, header); err != nil {
|
||||
select {
|
||||
case <-s.done:
|
||||
return
|
||||
}
|
||||
if header[0] != '$' || (header[1] != 0 && header[1] != 1) {
|
||||
return
|
||||
}
|
||||
length := int(header[2])<<8 | int(header[3])
|
||||
if length > 65536 {
|
||||
return
|
||||
}
|
||||
payload := make([]byte, length)
|
||||
if _, err := io.ReadFull(s.conn, payload); err != nil {
|
||||
return
|
||||
}
|
||||
if header[1] == 0 {
|
||||
pushLatest(s.video, payload)
|
||||
} else {
|
||||
pushLatest(s.audio, payload)
|
||||
case <-ticker.C:
|
||||
if err := s.writeApolloControl(apolloChannelGeneric, true, apolloControlTypePing, []byte{4, 0, 0, 0, 0, 0, 0, 0}); err != nil {
|
||||
s.handleApolloDisconnect(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func pushLatest(channel chan []byte, payload []byte) {
|
||||
func (s *nativeApolloSession) periodicApolloMediaPing() {
|
||||
ticker := time.NewTicker(500 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
sequence := uint32(2)
|
||||
for {
|
||||
select {
|
||||
case <-s.done:
|
||||
return
|
||||
case <-ticker.C:
|
||||
if s.audioConn == nil || s.videoConn == nil {
|
||||
s.handleApolloDisconnect(ErrProviderDisconnected)
|
||||
return
|
||||
}
|
||||
if _, err := s.audioConn.Write(apolloMediaPing(s.audioPing, sequence)); err != nil {
|
||||
s.handleApolloDisconnect(err)
|
||||
return
|
||||
}
|
||||
if _, err := s.videoConn.Write(apolloMediaPing(s.videoPing, sequence)); err != nil {
|
||||
s.handleApolloDisconnect(err)
|
||||
return
|
||||
}
|
||||
sequence++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *nativeApolloSession) handleApolloControlPayload(_ uint8, _ bool, payload []byte) {
|
||||
s.controlMu.Lock()
|
||||
message, err := s.control.OpenHost(payload)
|
||||
s.controlMu.Unlock()
|
||||
if err != nil {
|
||||
s.handleApolloDisconnect(err)
|
||||
return
|
||||
}
|
||||
switch message.typeID {
|
||||
case apolloControlTypeTerm:
|
||||
if len(message.payload) != 4 {
|
||||
s.handleApolloDisconnect(ErrProviderMalformed)
|
||||
return
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.state.State = ProviderStateTerminated
|
||||
s.mu.Unlock()
|
||||
s.emitProviderEvent(ProviderEvent{Kind: ProviderEventTerminated, Payload: message.payload})
|
||||
case apolloControlTypeRumble:
|
||||
if len(message.payload) != 10 {
|
||||
s.handleApolloDisconnect(ErrProviderMalformed)
|
||||
return
|
||||
}
|
||||
controller := binary.LittleEndian.Uint16(message.payload[4:6])
|
||||
if controller > 15 {
|
||||
s.handleApolloDisconnect(ErrProviderMalformed)
|
||||
return
|
||||
}
|
||||
payload := make([]byte, 5)
|
||||
payload[0] = byte(controller)
|
||||
binary.BigEndian.PutUint16(payload[1:3], binary.LittleEndian.Uint16(message.payload[6:8]))
|
||||
binary.BigEndian.PutUint16(payload[3:5], binary.LittleEndian.Uint16(message.payload[8:10]))
|
||||
s.emitProviderEvent(ProviderEvent{Kind: ProviderEventRumble, Payload: payload})
|
||||
case apolloControlTypeHDR:
|
||||
if len(message.payload) != 27 || message.payload[0] > 1 {
|
||||
s.handleApolloDisconnect(ErrProviderMalformed)
|
||||
return
|
||||
}
|
||||
s.emitProviderEvent(ProviderEvent{Kind: ProviderEventHDR, Payload: []byte{message.payload[0]}})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *nativeApolloSession) emitProviderEvent(event ProviderEvent) {
|
||||
select {
|
||||
case s.events <- event:
|
||||
default:
|
||||
s.handleApolloDisconnect(ErrProviderMalformed)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *nativeApolloSession) handleApolloDisconnect(err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
s.mu.Lock()
|
||||
if s.state.State != ProviderStateTerminated {
|
||||
s.state.State = ProviderStateDisconnected
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *nativeApolloSession) closeMediaChannels() {
|
||||
s.channelsOnce.Do(func() {
|
||||
close(s.video)
|
||||
close(s.audio)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *nativeApolloSession) readUDPMedia() {
|
||||
if s.media == nil {
|
||||
close(s.readDone)
|
||||
s.closeMediaChannels()
|
||||
return
|
||||
}
|
||||
var readers sync.WaitGroup
|
||||
readers.Add(2)
|
||||
read := func(conn *net.UDPConn, output chan []byte, video bool) {
|
||||
defer readers.Done()
|
||||
buffer := make([]byte, apolloMediaMaximumPacket+1)
|
||||
for {
|
||||
if err := conn.SetReadDeadline(time.Now().Add(250 * time.Millisecond)); err != nil {
|
||||
return
|
||||
}
|
||||
count, err := conn.Read(buffer)
|
||||
if err != nil {
|
||||
if networkErr, ok := err.(net.Error); ok && networkErr.Timeout() {
|
||||
select {
|
||||
case <-s.done:
|
||||
return
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
if count > apolloMediaMaximumPacket {
|
||||
continue
|
||||
}
|
||||
var payloads [][]byte
|
||||
if video {
|
||||
shard, openErr := s.media.OpenVideo(buffer[:count])
|
||||
if openErr != nil {
|
||||
continue
|
||||
}
|
||||
payload, err := s.videoFEC.Add(shard)
|
||||
if err != nil || len(payload) == 0 {
|
||||
continue
|
||||
}
|
||||
payloads = [][]byte{payload}
|
||||
} else {
|
||||
shard, openErr := s.media.OpenAudio(buffer[:count])
|
||||
if openErr != nil {
|
||||
continue
|
||||
}
|
||||
payloads, err = s.audioFEC.Add(s.media, shard)
|
||||
}
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, payload := range payloads {
|
||||
if len(payload) != 0 {
|
||||
if pushLatest(output, payload) {
|
||||
s.mediaDrops.Add(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
go read(s.audioConn, s.audio, false)
|
||||
go read(s.videoConn, s.video, true)
|
||||
go func() {
|
||||
readers.Wait()
|
||||
close(s.readDone)
|
||||
s.closeMediaChannels()
|
||||
}()
|
||||
}
|
||||
|
||||
func pushLatest(channel chan []byte, payload []byte) bool {
|
||||
select {
|
||||
case channel <- payload:
|
||||
return false
|
||||
default:
|
||||
select {
|
||||
case <-channel:
|
||||
@@ -357,7 +772,9 @@ func pushLatest(channel chan []byte, payload []byte) {
|
||||
}
|
||||
select {
|
||||
case channel <- payload:
|
||||
return true
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user