From 090a5797d0c195326e6a441f754788b9e9b5c1f0 Mon Sep 17 00:00:00 2001
From: Bolke de Bruin <bolke@xs4all.nl>
Date: Wed, 7 Sep 2022 10:52:20 +0200
Subject: [PATCH] Use rdp builder for generating the rdp file

---
 cmd/rdpgw/web/rdp.go      | 228 ++++++++++++++++++++++++++++++++++++++
 cmd/rdpgw/web/rdp_test.go |  40 +++++++
 cmd/rdpgw/web/web.go      |  30 +++--
 go.mod                    |   1 +
 4 files changed, 283 insertions(+), 16 deletions(-)
 create mode 100644 cmd/rdpgw/web/rdp.go
 create mode 100644 cmd/rdpgw/web/rdp_test.go

diff --git a/cmd/rdpgw/web/rdp.go b/cmd/rdpgw/web/rdp.go
new file mode 100644
index 0000000..179b62f
--- /dev/null
+++ b/cmd/rdpgw/web/rdp.go
@@ -0,0 +1,228 @@
+package web
+
+import (
+	"fmt"
+	"github.com/fatih/structs"
+	"log"
+	"reflect"
+	"strconv"
+	"strings"
+)
+
+const (
+	crlf = "\r\n"
+)
+
+const (
+	SourceNTLM int = iota
+	SourceSmartCard
+	SourceCurrent
+	SourceUserSelect
+	SourceCookie
+)
+
+type RdpConnection struct {
+	GatewayHostname          string `rdp:"gatewayhostname"`
+	FullAddress              string `rdp:"full address"`
+	AlternateFullAddress     string `rdp:"alternate full address"`
+	Username                 string `rdp:"username"`
+	Domain                   string `rdp:"domain"`
+	GatewayCredentialSource  int    `rdp:"gatewaycredentialsource" default:"0"`
+	GatewayCredentialMethode int    `rdp:"gatewayprofileusagemethod" default:"0"`
+	GatewayUsageMethod       int    `rdp:"gatewayusagemethod" default:"0"`
+	GatewayAccessToken       string `rdp:"gatewayaccesstoken"`
+	PromptCredentialsOnce    bool   `rdp:"promptcredentialonce" default:"true"`
+	AuthenticationLevel      int    `rdp:"authentication level" default:"3"`
+	EnableCredSSPSupport     bool   `rdp:"enablecredsspsupport" default:"true"`
+	EnableRdsAasAuth         bool   `rdp:"enablerdsaadauth" default:"false"`
+	DisableConnectionSharing bool   `rdp:"disableconnectionsharing" default:"false"`
+	AlternateShell           string `rdp:"alternate shell"`
+}
+
+type RdpSession struct {
+	AutoReconnectionEnabled bool `rdp:"autoreconnectionenabled" default:"true"`
+	BandwidthAutodetect     bool `rdp:"bandwidthautodetect" default:"true"`
+	NetworkAutodetect       bool `rdp:"networkautodetect" default:"true"`
+	Compression             bool `rdp:"compression" default:"true"`
+	VideoPlaybackMode       bool `rdp:"videoplaybackmode" default:"true"`
+	ConnectionType          int  `rdp:"connection type" default:"2"`
+}
+
+type RdpDeviceRedirect struct {
+	AudioCaptureMode                      bool   `rdp:"audiocapturemode" default:"false"`
+	EncodeRedirectedVideoCapture          bool   `rdp:"encode redirected video capture" default:"true"`
+	RedirectedVideoCaptureEncodingQuality int    `rdp:"redirected video capture encoding quality" default:"0"`
+	AudioMode                             int    `rdp:"audiomode" default:"0"`
+	CameraStoreRedirect                   string `rdp:"camerastoredirect" default:"false"`
+	DeviceStoreRedirect                   string `rdp:"devicestoredirect" default:"false"`
+	DriveStoreRedirect                    string `rdp:"drivestoredirect" default:"false"`
+	KeyboardHook                          int    `rdp:"keyboardhook" default:"2"`
+	RedirectClipboard                     bool   `rdp:"redirectclipboard" default:"true"`
+	RedirectComPorts                      bool   `rdp:"redirectcomports" default:"false"`
+	RedirectLocation                      bool   `rdp:"redirectlocation" default:"false"`
+	RedirectPrinters                      bool   `rdp:"redirectprinters" default:"true"`
+	RedirectSmartcards                    bool   `rdp:"redirectsmartcards" default:"true"`
+	RedirectWebAuthn                      bool   `rdp:"redirectwebauthn" default:"true"`
+	UsbDeviceStoRedirect                  string `rdp:"usbdevicestoredirect"`
+}
+
+type RdpDisplay struct {
+	UseMultimon               bool   `rdp:"use multimon" default:"false"`
+	SelectedMonitors          string `rdp:"selectedmonitors"`
+	MaximizeToCurrentDisplays bool   `rdp:"maximizetocurrentdisplays" default:"false"`
+	SingleMonInWindowedMode   bool   `rdp:"singlemoninwindowedmode" default:"0"`
+	ScreenModeId              int    `rdp:"screen mode id" default:"2"`
+	SmartSizing               bool   `rdp:"smart sizing" default:"false"`
+	DynamicResolution         bool   `rdp:"dynamic resolution" default:"true"`
+	DesktopSizeId             int    `rdp:"desktop size id"`
+	DesktopHeight             int    `rdp:"desktopheight"`
+	DesktopWidth              int    `rdp:"desktopwidth"`
+	DesktopScaleFactor        int    `rdp:"desktopscalefactor"`
+	BitmapCacheSize           int    `rdp:"bitmapcachesize" default:"1500"`
+}
+
+type RdpRemoteApp struct {
+	RemoteApplicationCmdLine  string `rdp:"remoteapplicationcmdline"`
+	RemoteAppExpandWorkingDir bool   `rdp:"remoteapplicationexpandworkingdir" default:"true"`
+	RemoteApplicationFile     string `rdp:"remoteapplicationfile" default:"true"`
+	RemoteApplicationIcon     string `rdp:"remoteapplicationicon"`
+	RemoteApplicationMode     bool   `rdp:"remoteapplicationmode" default:"true"`
+	RemoteApplicationName     string `rdp:"remoteapplicationname"`
+	RemoteApplicationProgram  string `rdp:"remoteapplicationprogram"`
+}
+
+type RdpBuilder struct {
+	Connection     RdpConnection
+	Session        RdpSession
+	DeviceRedirect RdpDeviceRedirect
+	Display        RdpDisplay
+	RemoteApp      RdpRemoteApp
+}
+
+func NewRdp() *RdpBuilder {
+	c := RdpConnection{}
+	s := RdpSession{}
+	dr := RdpDeviceRedirect{}
+	disp := RdpDisplay{}
+	ra := RdpRemoteApp{}
+
+	initStruct(&c)
+	initStruct(&s)
+	initStruct(&dr)
+	initStruct(&disp)
+	initStruct(&ra)
+
+	return &RdpBuilder{
+		Connection:     c,
+		Session:        s,
+		DeviceRedirect: dr,
+		Display:        disp,
+		RemoteApp:      ra,
+	}
+}
+
+func (rb *RdpBuilder) String() string {
+	var sb strings.Builder
+
+	addStructToString(rb.Connection, &sb)
+	addStructToString(rb.Session, &sb)
+	addStructToString(rb.DeviceRedirect, &sb)
+	addStructToString(rb.Display, &sb)
+	addStructToString(rb.RemoteApp, &sb)
+
+	return sb.String()
+}
+
+func addStructToString(st interface{}, sb *strings.Builder) {
+	s := structs.New(st)
+	for _, f := range s.Fields() {
+		if isZero(f) {
+			continue
+		}
+		sb.WriteString(f.Tag("rdp"))
+		sb.WriteString(":")
+
+		switch f.Kind() {
+		case reflect.String:
+			sb.WriteString("s:")
+			sb.WriteString(f.Value().(string))
+		case reflect.Int:
+			sb.WriteString("i:")
+			fmt.Fprintf(sb, "%d", f.Value())
+		case reflect.Bool:
+			sb.WriteString("i:")
+			if f.Value().(bool) {
+				sb.WriteString("1")
+			} else {
+				sb.WriteString("0")
+
+			}
+		}
+		sb.WriteString(crlf)
+	}
+}
+
+func isZero(f *structs.Field) bool {
+	t := f.Tag("default")
+	if t == "" {
+		return f.IsZero()
+	}
+
+	switch f.Kind() {
+	case reflect.String:
+		if f.Value().(string) != t {
+			return false
+		}
+		return true
+	case reflect.Int:
+		i, err := strconv.Atoi(t)
+		if err != nil {
+			log.Fatalf("runtime error: default %s is not an integer", t)
+		}
+		if f.Value().(int) != i {
+			return false
+		}
+		return true
+	case reflect.Bool:
+		b := false
+		if t == "true" || t == "1" {
+			b = true
+		}
+		if f.Value().(bool) != b {
+			return false
+		}
+		return true
+	}
+
+	return f.IsZero()
+}
+
+func initStruct(st interface{}) {
+	s := structs.New(st)
+	for _, f := range s.Fields() {
+		t := f.Tag("default")
+		if t == "" {
+			continue
+		}
+
+		switch f.Kind() {
+		case reflect.String:
+			f.Set(t)
+		case reflect.Int:
+			i, err := strconv.Atoi(t)
+			if err != nil {
+				log.Fatalf("runtime error: default %s is not an integer", t)
+			}
+			f.Set(i)
+		case reflect.Bool:
+			b := false
+			if t == "true" || t == "1" {
+				b = true
+			}
+			err := f.Set(b)
+			if err != nil {
+				log.Fatalf("Cannot set bool field")
+			}
+		}
+	}
+}
diff --git a/cmd/rdpgw/web/rdp_test.go b/cmd/rdpgw/web/rdp_test.go
new file mode 100644
index 0000000..5eff8b7
--- /dev/null
+++ b/cmd/rdpgw/web/rdp_test.go
@@ -0,0 +1,40 @@
+package web
+
+import (
+	"log"
+	"strings"
+	"testing"
+)
+
+const (
+	GatewayHostName = "my.yahoo.com"
+)
+
+func TestRdpBuilder(t *testing.T) {
+	builder := NewRdp()
+	builder.Connection.GatewayHostname = "my.yahoo.com"
+	builder.Session.AutoReconnectionEnabled = true
+	builder.Display.SmartSizing = true
+
+	s := builder.String()
+	if !strings.Contains(s, "gatewayhostname:s:"+GatewayHostName+crlf) {
+		t.Fatalf("%s does not contain `gatewayhostname:s:%s", s, GatewayHostName)
+	}
+	if strings.Contains(s, "autoreconnectionenabled") {
+		t.Fatalf("autoreconnectionenabled is in %s, but is default value", s)
+	}
+	if !strings.Contains(s, "smart sizing:i:1"+crlf) {
+		t.Fatalf("%s does not contain smart sizing:i:1", s)
+
+	}
+	log.Printf(builder.String())
+}
+
+func TestInitStruct(t *testing.T) {
+	conn := RdpConnection{}
+	initStruct(&conn)
+
+	if conn.PromptCredentialsOnce != true {
+		t.Fatalf("conn.PromptCredentialsOnce != true")
+	}
+}
diff --git a/cmd/rdpgw/web/web.go b/cmd/rdpgw/web/web.go
index 180fee9..e2b5988 100644
--- a/cmd/rdpgw/web/web.go
+++ b/cmd/rdpgw/web/web.go
@@ -10,7 +10,6 @@ import (
 	"math/rand"
 	"net/http"
 	"net/url"
-	"strconv"
 	"strings"
 	"time"
 )
@@ -197,19 +196,18 @@ func (h *Handler) HandleDownload(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Disposition", "attachment; filename="+fn)
 	w.Header().Set("Content-Type", "application/x-rdp")
 
-	data := "full address:s:" + host + "\r\n" +
-		"gatewayhostname:s:" + h.gatewayAddress.Host + "\r\n" +
-		"gatewaycredentialssource:i:5\r\n" +
-		"gatewayusagemethod:i:1\r\n" +
-		"gatewayprofileusagemethod:i:1\r\n" +
-		"gatewayaccesstoken:s:" + token + "\r\n" +
-		"networkautodetect:i:" + strconv.Itoa(opts.NetworkAutoDetect) + "\r\n" +
-		"bandwidthautodetect:i:" + strconv.Itoa(opts.BandwidthAutoDetect) + "\r\n" +
-		"connection type:i:" + strconv.Itoa(opts.ConnectionType) + "\r\n" +
-		"username:s:" + render + "\r\n" +
-		"domain:s:" + domain + "\r\n" +
-		"bitmapcachesize:i:32000\r\n" +
-		"smart sizing:i:1\r\n"
-
-	http.ServeContent(w, r, fn, time.Now(), strings.NewReader(data))
+	rdp := NewRdp()
+	rdp.Connection.Username = render
+	rdp.Connection.Domain = domain
+	rdp.Connection.FullAddress = host
+	rdp.Connection.GatewayHostname = h.gatewayAddress.Host
+	rdp.Connection.GatewayCredentialSource = SourceCookie
+	rdp.Connection.GatewayAccessToken = token
+	rdp.Session.NetworkAutodetect = opts.NetworkAutoDetect != 0
+	rdp.Session.BandwidthAutodetect = opts.BandwidthAutoDetect != 0
+	rdp.Session.ConnectionType = opts.ConnectionType
+	rdp.Display.SmartSizing = true
+	rdp.Display.BitmapCacheSize = 32000
+	
+	http.ServeContent(w, r, fn, time.Now(), strings.NewReader(rdp.String()))
 }
diff --git a/go.mod b/go.mod
index a0c35cd..bdf03cc 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,7 @@ go 1.19
 
 require (
 	github.com/coreos/go-oidc/v3 v3.2.0
+	github.com/fatih/structs v1.1.0
 	github.com/go-jose/go-jose/v3 v3.0.0
 	github.com/gorilla/sessions v1.2.1
 	github.com/gorilla/websocket v1.5.0
-- 
GitLab