ci / rust (push) Canceled after 0s
ci / web (push) Canceled after 0s
ci / package-preview (push) Canceled after 0s
ci / package-installer (push) Canceled after 0s
ci / linux-agent (push) Canceled after 0s
ci / edge-service (push) Canceled after 0s
ci / coturn-pop (push) Canceled after 0s
ci / package-windows-host (push) Canceled after 0s
255 lines
9.0 KiB
Go
255 lines
9.0 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
agentControlLineMax = 16 * 1024
|
|
agentPipeTokenEnv = "REMOTEDESK_AGENT_PIPE_TOKEN"
|
|
agentPipeAuthDomain = "RemoteDesk Windows Agent pipe auth v1\x00"
|
|
)
|
|
|
|
type mediaRingDescriptor struct {
|
|
SchemaVersion uint16 `json:"schema_version"`
|
|
SessionID string `json:"session_id"`
|
|
Generation uint64 `json:"generation"`
|
|
OwnerPID uint32 `json:"owner_pid"`
|
|
RingPath string `json:"ring_path"`
|
|
SlotCount uint32 `json:"slot_count"`
|
|
SlotBytes uint32 `json:"slot_bytes"`
|
|
VideoTransport string `json:"video_transport"`
|
|
AudioTransport string `json:"audio_transport"`
|
|
AudioRing *audioRingDescriptor `json:"audio_ring,omitempty"`
|
|
}
|
|
|
|
type audioRingDescriptor struct {
|
|
SchemaVersion uint16 `json:"schema_version"`
|
|
SessionID string `json:"session_id"`
|
|
Generation uint64 `json:"generation"`
|
|
OwnerPID uint32 `json:"owner_pid"`
|
|
RingPath string `json:"ring_path"`
|
|
SlotCount uint32 `json:"slot_count"`
|
|
SlotBytes uint32 `json:"slot_bytes"`
|
|
AudioTransport string `json:"audio_transport"`
|
|
}
|
|
|
|
type encodedDesktopOpened struct {
|
|
Kind string `json:"kind"`
|
|
Error string `json:"error"`
|
|
VideoCodec string `json:"video_codec"`
|
|
MediaTransport string `json:"media_transport"`
|
|
Ring mediaRingDescriptor `json:"ring"`
|
|
}
|
|
|
|
type agentHello struct {
|
|
Kind string `json:"kind"`
|
|
Authenticated bool `json:"authenticated"`
|
|
}
|
|
|
|
type agentAuthChallenge struct {
|
|
Kind string `json:"kind"`
|
|
ProtocolVersion uint8 `json:"protocol_version"`
|
|
Nonce string `json:"nonce"`
|
|
}
|
|
|
|
func openWindowsEncodedSession(address, sessionID string, fps uint8, allowSoftwareFallback bool) (net.Conn, mediaRingDescriptor, error) {
|
|
if err := validateAgentControlAddress(address); err != nil {
|
|
return nil, mediaRingDescriptor{}, err
|
|
}
|
|
if !validSessionID(sessionID) || fps == 0 || fps > 120 {
|
|
return nil, mediaRingDescriptor{}, errors.New("Windows Agent encoded session parameters are invalid")
|
|
}
|
|
isPipe := isWindowsAgentPipe(address)
|
|
connection, err := dialWindowsAgentControl(address, 2*time.Second)
|
|
if err != nil {
|
|
return nil, mediaRingDescriptor{}, fmt.Errorf("connect Windows Agent control: %w", err)
|
|
}
|
|
closeOnError := true
|
|
defer func() {
|
|
if closeOnError {
|
|
_ = connection.Close()
|
|
}
|
|
}()
|
|
_ = connection.SetDeadline(time.Now().Add(10 * time.Second))
|
|
reader := bufio.NewReaderSize(connection, agentControlLineMax+1)
|
|
if isPipe {
|
|
if err := authenticateWindowsAgentPipe(reader, connection); err != nil {
|
|
return nil, mediaRingDescriptor{}, err
|
|
}
|
|
}
|
|
var hello agentHello
|
|
if err := readBoundedAgentJSON(reader, &hello); err != nil || hello.Kind != "windows_agent_hello" || (isPipe && !hello.Authenticated) {
|
|
return nil, mediaRingDescriptor{}, errors.New("Windows Agent control hello is invalid")
|
|
}
|
|
request := struct {
|
|
Kind string `json:"kind"`
|
|
SessionID string `json:"session_id"`
|
|
CaptureMode string `json:"capture_mode"`
|
|
VideoCodec string `json:"video_codec"`
|
|
FramesPerSecond uint8 `json:"frames_per_second"`
|
|
AllowSoftwareFallback bool `json:"allow_software_fallback"`
|
|
}{
|
|
Kind: "open_encoded_desktop", SessionID: sessionID,
|
|
CaptureMode: "compatibility", VideoCodec: "h264", FramesPerSecond: fps,
|
|
AllowSoftwareFallback: allowSoftwareFallback,
|
|
}
|
|
encoded, err := json.Marshal(request)
|
|
if err != nil {
|
|
return nil, mediaRingDescriptor{}, err
|
|
}
|
|
encoded = append(encoded, '\n')
|
|
if _, err := connection.Write(encoded); err != nil {
|
|
return nil, mediaRingDescriptor{}, fmt.Errorf("open Windows encoded desktop: %w", err)
|
|
}
|
|
var opened encodedDesktopOpened
|
|
if err := readBoundedAgentJSON(reader, &opened); err != nil {
|
|
return nil, mediaRingDescriptor{}, err
|
|
}
|
|
if opened.Kind != "encoded_desktop_opened" || opened.VideoCodec != "h264" ||
|
|
opened.MediaTransport != "shared_memory_encoded_access_units" {
|
|
return nil, mediaRingDescriptor{}, fmt.Errorf("Windows encoded desktop unavailable: %s", opened.Error)
|
|
}
|
|
if err := validateMediaRingDescriptor(opened.Ring, sessionID); err != nil {
|
|
return nil, mediaRingDescriptor{}, err
|
|
}
|
|
_ = connection.SetDeadline(time.Time{})
|
|
closeOnError = false
|
|
return connection, opened.Ring, nil
|
|
}
|
|
|
|
func dialWindowsAgentControl(address string, timeout time.Duration) (net.Conn, error) {
|
|
if isWindowsAgentPipe(address) {
|
|
return dialWindowsAgentPipe(address, timeout)
|
|
}
|
|
return net.DialTimeout("tcp", address, timeout)
|
|
}
|
|
|
|
func authenticateWindowsAgentPipe(reader *bufio.Reader, connection net.Conn) error {
|
|
var challenge agentAuthChallenge
|
|
if err := readBoundedAgentJSON(reader, &challenge); err != nil {
|
|
return err
|
|
}
|
|
if challenge.Kind != "agent_auth_challenge" || challenge.ProtocolVersion != 1 {
|
|
return errors.New("Windows Agent Pipe challenge is invalid")
|
|
}
|
|
nonce, err := base64.RawURLEncoding.DecodeString(challenge.Nonce)
|
|
if err != nil || len(nonce) != 32 {
|
|
return errors.New("Windows Agent Pipe nonce is invalid")
|
|
}
|
|
encodedToken, ok := os.LookupEnv(agentPipeTokenEnv)
|
|
if !ok {
|
|
return fmt.Errorf("%s is required for authenticated Windows Agent Pipe", agentPipeTokenEnv)
|
|
}
|
|
_ = os.Unsetenv(agentPipeTokenEnv)
|
|
key, err := base64.RawURLEncoding.DecodeString(encodedToken)
|
|
if err != nil || len(key) != 32 {
|
|
return errors.New("Windows Agent Pipe token must be 32-byte base64url")
|
|
}
|
|
mac := windowsAgentPipeMAC(key, nonce)
|
|
request := struct {
|
|
Kind string `json:"kind"`
|
|
ProtocolVersion uint8 `json:"protocol_version"`
|
|
MAC string `json:"mac"`
|
|
}{
|
|
Kind: "agent_auth",
|
|
ProtocolVersion: 1,
|
|
MAC: base64.RawURLEncoding.EncodeToString(mac),
|
|
}
|
|
encoded, err := json.Marshal(request)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
encoded = append(encoded, '\n')
|
|
if _, err := connection.Write(encoded); err != nil {
|
|
return fmt.Errorf("authenticate Windows Agent Pipe: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func windowsAgentPipeMAC(key, nonce []byte) []byte {
|
|
mac := hmac.New(sha256.New, key)
|
|
_, _ = mac.Write([]byte(agentPipeAuthDomain))
|
|
_, _ = mac.Write(nonce)
|
|
return mac.Sum(nil)
|
|
}
|
|
|
|
func readBoundedAgentJSON(reader *bufio.Reader, output any) error {
|
|
line, err := reader.ReadSlice('\n')
|
|
if err != nil {
|
|
return fmt.Errorf("read Windows Agent control: %w", err)
|
|
}
|
|
if len(line) == 0 || len(line) > agentControlLineMax || line[len(line)-1] != '\n' {
|
|
return errors.New("Windows Agent control response exceeds its bound")
|
|
}
|
|
if err := json.Unmarshal(line, output); err != nil {
|
|
return errors.New("Windows Agent control response is invalid JSON")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateAgentControlAddress(address string) error {
|
|
if isWindowsAgentPipe(address) {
|
|
if len(address) > 256 || !strings.HasPrefix(strings.ToLower(address), `\\.\pipe\remotedesk\`) || strings.Contains(address, "..") {
|
|
return errors.New("Windows Agent Pipe name is outside the RemoteDesk namespace")
|
|
}
|
|
return nil
|
|
}
|
|
host, port, err := net.SplitHostPort(address)
|
|
if err != nil || port == "" {
|
|
return errors.New("Windows Agent control must be an IP socket address")
|
|
}
|
|
ip := net.ParseIP(host)
|
|
if ip == nil || !ip.IsLoopback() {
|
|
return errors.New("Windows Agent control must use a loopback IP")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isWindowsAgentPipe(address string) bool {
|
|
return strings.HasPrefix(strings.ToLower(address), `\\.\pipe\`)
|
|
}
|
|
|
|
func validateMediaRingDescriptor(descriptor mediaRingDescriptor, sessionID string) error {
|
|
if descriptor.SchemaVersion != 1 || descriptor.SessionID != sessionID || descriptor.Generation == 0 || descriptor.OwnerPID == 0 ||
|
|
descriptor.VideoTransport != "shared_memory_encoded_access_units" || descriptor.AudioTransport != "separate_transport" ||
|
|
!filepath.IsAbs(descriptor.RingPath) || descriptor.SlotCount < 2 || descriptor.SlotCount > maxRingSlots ||
|
|
descriptor.SlotCount&(descriptor.SlotCount-1) != 0 || descriptor.SlotBytes == 0 || descriptor.SlotBytes > maxRingSlotBytes {
|
|
return errors.New("Windows Agent media ring descriptor is invalid")
|
|
}
|
|
if descriptor.AudioRing != nil {
|
|
audio := descriptor.AudioRing
|
|
if audio.SchemaVersion != 1 || audio.SessionID != sessionID || audio.Generation != descriptor.Generation ||
|
|
audio.OwnerPID != descriptor.OwnerPID || audio.AudioTransport != "shared_memory_opus_packets" ||
|
|
!filepath.IsAbs(audio.RingPath) || audio.SlotCount < 2 || audio.SlotCount > maxRingSlots ||
|
|
audio.SlotCount&(audio.SlotCount-1) != 0 || audio.SlotBytes == 0 || audio.SlotBytes > maxRingSlotBytes {
|
|
return errors.New("Windows Agent audio ring descriptor is invalid")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validSessionID(value string) bool {
|
|
if len(value) == 0 || len(value) > 128 {
|
|
return false
|
|
}
|
|
for _, character := range []byte(value) {
|
|
if !((character >= 'a' && character <= 'z') || (character >= 'A' && character <= 'Z') ||
|
|
(character >= '0' && character <= '9') || character == '-' || character == '_') {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|