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