367 lines
11 KiB
Go
367 lines
11 KiB
Go
package gateway
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
protocol "git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol/gen/go/protocol"
|
|
)
|
|
|
|
// NativeApolloBackend keeps provider sockets inside the gateway process. The
|
|
// session-scoped Server work is the sole source of provider endpoint and mTLS
|
|
// material; it is never serialized into a client manifest or authority.
|
|
type NativeApolloBackend struct {
|
|
Dialer *net.Dialer
|
|
|
|
mu sync.Mutex
|
|
pending map[string]net.Conn
|
|
}
|
|
|
|
func NewNativeApolloBackend() *NativeApolloBackend {
|
|
return &NativeApolloBackend{Dialer: &net.Dialer{Timeout: 5 * time.Second}, pending: make(map[string]net.Conn)}
|
|
}
|
|
|
|
func (b *NativeApolloBackend) Management(ctx context.Context, request LaunchRequest) ([]byte, error) {
|
|
work := request.ProviderWork
|
|
if err := work.Validate(); err != nil || request.SessionID == "" || request.SessionID != work.SessionID || work.ProviderProfile != ProviderProfileApollo {
|
|
return nil, ErrProviderMalformed
|
|
}
|
|
client, err := newPinnedApolloHTTPClient(work)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return apolloGet(ctx, client, work, "/serverinfo", nil)
|
|
}
|
|
|
|
func newPinnedApolloHTTPClient(work protocol.ProviderSessionWork) (*http.Client, error) {
|
|
tlsConfig, err := pinnedApolloTLSConfig(work)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}, Timeout: 5 * time.Second}, nil
|
|
}
|
|
|
|
func apolloGet(ctx context.Context, client *http.Client, work protocol.ProviderSessionWork, path string, values url.Values) ([]byte, error) {
|
|
endpoint := url.URL{Scheme: "https", Host: net.JoinHostPort(work.ManagementHost, strconv.FormatInt(work.ManagementPort, 10)), Path: path}
|
|
endpoint.RawQuery = values.Encode()
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
|
|
if err != nil {
|
|
return nil, ErrProviderMalformed
|
|
}
|
|
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 management status %d", response.StatusCode)
|
|
}
|
|
return readBounded(response.Body, 64*1024)
|
|
}
|
|
|
|
func pinnedApolloTLSConfig(work protocol.ProviderSessionWork) (*tls.Config, error) {
|
|
identity, ok := providerIdentityFromKey(work.ProviderIdentity)
|
|
if !ok || !strings.HasPrefix(identity.Fingerprint, "sha256:") {
|
|
return nil, ErrProviderIdentity
|
|
}
|
|
pinned, err := hex.DecodeString(strings.TrimPrefix(identity.Fingerprint, "sha256:"))
|
|
if err != nil || len(pinned) != sha256.Size {
|
|
return nil, ErrProviderIdentity
|
|
}
|
|
certificate, err := tls.X509KeyPair([]byte(work.ClientCertificatePem), []byte(work.ClientPrivateKeyPem))
|
|
if err != nil {
|
|
return nil, ErrProviderIdentity
|
|
}
|
|
trust := x509.NewCertPool()
|
|
if !trust.AppendCertsFromPEM([]byte(work.ServerCertificatePem)) {
|
|
return nil, ErrProviderIdentity
|
|
}
|
|
return &tls.Config{
|
|
MinVersion: tls.VersionTLS13, Certificates: []tls.Certificate{certificate}, RootCAs: trust,
|
|
VerifyPeerCertificate: func(rawCertificates [][]byte, _ [][]*x509.Certificate) error {
|
|
if len(rawCertificates) == 0 {
|
|
return ErrProviderIdentity
|
|
}
|
|
digest := sha256.Sum256(rawCertificates[0])
|
|
if !bytes.Equal(digest[:], pinned) {
|
|
return ErrProviderIdentity
|
|
}
|
|
return nil
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (b *NativeApolloBackend) Setup(ctx context.Context, request LaunchRequest) ([]byte, error) {
|
|
work := request.ProviderWork
|
|
if err := work.Validate(); err != nil || request.SessionID == "" || request.SessionID != work.SessionID || work.ProviderProfile != ProviderProfileApollo {
|
|
return nil, ErrProviderMalformed
|
|
}
|
|
client, err := newPinnedApolloHTTPClient(work)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
inventory, err := apolloGet(ctx, client, work, "/applist", url.Values{"uniqueid": []string{work.ClientID}})
|
|
if err != nil || !apolloInventoryContains(inventory, work.ApplicationID) {
|
|
return nil, ErrProviderMalformed
|
|
}
|
|
key := make([]byte, 16)
|
|
if _, err := rand.Read(key); err != nil {
|
|
return nil, err
|
|
}
|
|
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"},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
launchResponse, err := parseApolloLaunchResponse(launch)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
streamURL, err := url.Parse(launchResponse.SessionURL)
|
|
if err != nil || streamURL.Scheme != "rtspenc" || streamURL.Hostname() != work.StreamHost {
|
|
return nil, ErrProviderMalformed
|
|
}
|
|
streamPort, err := strconv.ParseInt(streamURL.Port(), 10, 64)
|
|
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)))
|
|
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.mu.Unlock()
|
|
return response, nil
|
|
}
|
|
|
|
func (b *NativeApolloBackend) Open(_ context.Context, request LaunchRequest, _ RTSPResponse) (ProviderSession, error) {
|
|
b.mu.Lock()
|
|
conn, ok := b.pending[request.SessionID]
|
|
delete(b.pending, request.SessionID)
|
|
b.mu.Unlock()
|
|
if !ok || conn == nil {
|
|
return nil, ErrProviderDisconnected
|
|
}
|
|
session := newNativeApolloSession(conn, request.SessionID)
|
|
go session.readMedia()
|
|
return session, nil
|
|
}
|
|
|
|
func readBounded(reader io.Reader, max int) ([]byte, error) {
|
|
data, err := io.ReadAll(io.LimitReader(reader, int64(max)+1))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(data) > max {
|
|
return nil, ErrProviderMalformed
|
|
}
|
|
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{}
|
|
}
|
|
|
|
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 (s *nativeApolloSession) Ready(context.Context) error {
|
|
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) Input(ctx context.Context, event InputEvent) error {
|
|
payload, err := EncodeInputEvent(event)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.writeControl(ctx, ControlPacket{Kind: 1, Sequence: event.Sequence, Payload: payload})
|
|
}
|
|
|
|
func (s *nativeApolloSession) Feedback(ctx context.Context, feedback Feedback) error {
|
|
return s.writeControl(ctx, ControlPacket{Kind: 3, Sequence: feedback.Sequence, Payload: feedback.Payload})
|
|
}
|
|
|
|
func (s *nativeApolloSession) Reconnect(ctx context.Context) error {
|
|
return s.writeControl(ctx, ControlPacket{Kind: 4, Payload: []byte("RECN")})
|
|
}
|
|
|
|
func (s *nativeApolloSession) ReleaseAll(ctx context.Context) error {
|
|
return s.writeControl(ctx, ControlPacket{Kind: 2, Payload: []byte("RELEASE_ALL")})
|
|
}
|
|
|
|
func (s *nativeApolloSession) Terminate(ctx context.Context) error {
|
|
_ = s.writeControl(ctx, ControlPacket{Kind: 5, Payload: []byte("TEAR")})
|
|
var timedOut bool
|
|
s.closeOnce.Do(func() {
|
|
close(s.done)
|
|
_ = s.conn.Close()
|
|
select {
|
|
case <-s.readDone:
|
|
case <-ctx.Done():
|
|
timedOut = true
|
|
}
|
|
if !timedOut {
|
|
close(s.video)
|
|
close(s.audio)
|
|
}
|
|
})
|
|
s.mu.Lock()
|
|
if timedOut {
|
|
s.state.State = ProviderStateCleanup
|
|
s.state.CleanupPending = true
|
|
} else {
|
|
s.state.State = ProviderStateTerminated
|
|
}
|
|
s.mu.Unlock()
|
|
if timedOut {
|
|
return ctx.Err()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
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)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if deadline, ok := ctx.Deadline(); ok {
|
|
_ = s.conn.SetWriteDeadline(deadline)
|
|
}
|
|
if _, err := s.conn.Write(encoded); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *nativeApolloSession) readMedia() {
|
|
defer close(s.readDone)
|
|
header := make([]byte, 4)
|
|
for {
|
|
if _, err := io.ReadFull(s.conn, header); err != nil {
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
func pushLatest(channel chan []byte, payload []byte) {
|
|
select {
|
|
case channel <- payload:
|
|
default:
|
|
select {
|
|
case <-channel:
|
|
default:
|
|
}
|
|
select {
|
|
case channel <- payload:
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
|
|
var _ ApolloBackend = (*NativeApolloBackend)(nil)
|
|
var _ ProviderSession = (*nativeApolloSession)(nil)
|