feat(data-plane): implement phase3c gateway

This commit is contained in:
sechmachine
2026-07-29 05:52:53 +07:00
parent 8379c134ca
commit f639c1bcb6
25 changed files with 2573 additions and 10 deletions
+264
View File
@@ -0,0 +1,264 @@
package gateway
import (
"bufio"
"context"
"crypto/tls"
"fmt"
"io"
"net"
"net/http"
"strings"
"sync"
"time"
protocol "git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol/gen/go/protocol"
)
// NativeApolloBackend keeps provider sockets inside the gateway process. The
// RTSP endpoint is configuration owned by the gateway and is never serialized
// into a client manifest or authority.
type NativeApolloBackend struct {
ManagementURL string
RTSPAddress string
RTSPURL string
HTTPClient *http.Client
Dialer *net.Dialer
TLSConfig *tls.Config
mu sync.Mutex
pending map[string]net.Conn
}
func NewNativeApolloBackend(managementURL, rtspAddress, rtspURL string, client *http.Client) *NativeApolloBackend {
if client == nil {
client = &http.Client{Timeout: 5 * time.Second}
}
return &NativeApolloBackend{ManagementURL: managementURL, RTSPAddress: rtspAddress, RTSPURL: rtspURL, HTTPClient: client, Dialer: &net.Dialer{Timeout: 5 * time.Second}, pending: make(map[string]net.Conn)}
}
func (b *NativeApolloBackend) Management(ctx context.Context) ([]byte, error) {
if b.ManagementURL == "" {
return nil, ErrProviderMalformed
}
request, err := http.NewRequestWithContext(ctx, http.MethodGet, b.ManagementURL, nil)
if err != nil {
return nil, ErrProviderMalformed
}
response, err := b.HTTPClient.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 (b *NativeApolloBackend) Setup(ctx context.Context, request LaunchRequest) ([]byte, error) {
if b.RTSPAddress == "" || b.RTSPURL == "" || request.SessionID == "" {
return nil, ErrProviderMalformed
}
conn, err := b.Dialer.DialContext(ctx, "tcp", b.RTSPAddress)
if err != nil {
return nil, err
}
if deadline, ok := ctx.Deadline(); ok {
_ = conn.SetDeadline(deadline)
}
requestText := "SETUP " + b.RTSPURL + " RTSP/1.0\r\nCSeq: 1\r\nTransport: RTP/AVP/TCP;interleaved=0-1\r\nSession: " + request.SessionID + "\r\n\r\n"
if _, err := io.WriteString(conn, requestText); err != nil {
_ = conn.Close()
return nil, err
}
response, err := readRTSPHeaders(conn, 16*1024)
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 := b.pending[request.SessionID]
delete(b.pending, request.SessionID)
b.mu.Unlock()
if 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 readRTSPHeaders(conn net.Conn, max int) ([]byte, error) {
reader := bufio.NewReaderSize(conn, 4096)
var response []byte
for len(response) < max {
line, err := reader.ReadBytes('\n')
if err != nil {
return nil, err
}
response = append(response, line...)
if strings.HasSuffix(string(response), "\r\n\r\n") {
return response, nil
}
}
return nil, ErrProviderMalformed
}
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)