mirror of
https://github.com/bolkedebruin/rdpgw.git
synced 2025-08-12 03:49:19 +02:00
Add templating option for RDP files
This commit is contained in:
parent
769abae3ba
commit
cdc497f365
10 changed files with 424 additions and 266 deletions
|
@ -89,12 +89,11 @@ type SecurityConfig struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClientConfig struct {
|
type ClientConfig struct {
|
||||||
NetworkAutoDetect int `koanf:"networkautodetect"`
|
Defaults string `koanf:"defaults"`
|
||||||
BandwidthAutoDetect int `koanf:"bandwidthautodetect"`
|
// kept for backwards compatibility
|
||||||
ConnectionType int `koanf:"connectiontype"`
|
UsernameTemplate string `koanf:"usernametemplate"`
|
||||||
UsernameTemplate string `koanf:"usernametemplate"`
|
SplitUserDomain bool `koanf:"splituserdomain"`
|
||||||
SplitUserDomain bool `koanf:"splituserdomain"`
|
DefaultDomain string `koanf:"defaultdomain"`
|
||||||
DefaultDomain string `koanf:"defaultdomain"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToCamel(s string) string {
|
func ToCamel(s string) string {
|
||||||
|
|
83
cmd/rdpgw/config/parsers/rdp.go
Normal file
83
cmd/rdpgw/config/parsers/rdp.go
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
package parsers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RDP struct{}
|
||||||
|
|
||||||
|
func Parser() *RDP {
|
||||||
|
return &RDP{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *RDP) Unmarshal(b []byte) (map[string]interface{}, error) {
|
||||||
|
r := bytes.NewReader(b)
|
||||||
|
scanner := bufio.NewScanner(r)
|
||||||
|
mp := make(map[string]interface{})
|
||||||
|
|
||||||
|
c := 0
|
||||||
|
for scanner.Scan() {
|
||||||
|
c++
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fields := strings.SplitN(line, ":", 3)
|
||||||
|
if len(fields) != 3 {
|
||||||
|
return nil, fmt.Errorf("malformed line %d: %q", c, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
key := strings.TrimSpace(fields[0])
|
||||||
|
t := strings.TrimSpace(fields[1])
|
||||||
|
val := strings.TrimSpace(fields[2])
|
||||||
|
|
||||||
|
switch t {
|
||||||
|
case "i":
|
||||||
|
intValue, err := strconv.Atoi(val)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot parse integer at line %d: %s", c, line)
|
||||||
|
}
|
||||||
|
mp[key] = intValue
|
||||||
|
case "s":
|
||||||
|
mp[key] = val
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("malformed line %d: %s", c, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *RDP) Marshal(o map[string]interface{}) ([]byte, error) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
|
||||||
|
keys := make([]string, 0, len(o))
|
||||||
|
for k := range o {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
for _, key := range keys {
|
||||||
|
v := o[key]
|
||||||
|
switch v.(type) {
|
||||||
|
case bool:
|
||||||
|
if v == true {
|
||||||
|
fmt.Fprintf(&b, "%s:i:1", key)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(&b, "%s:i:0", key)
|
||||||
|
}
|
||||||
|
case int:
|
||||||
|
fmt.Fprintf(&b, "%s:i:%d", key, v)
|
||||||
|
case string:
|
||||||
|
fmt.Fprintf(&b, "%s:s:%s", key, v)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("error marshalling")
|
||||||
|
}
|
||||||
|
fmt.Fprint(&b, "\r\n")
|
||||||
|
}
|
||||||
|
return b.Bytes(), nil
|
||||||
|
}
|
85
cmd/rdpgw/config/parsers/rdp_test.go
Normal file
85
cmd/rdpgw/config/parsers/rdp_test.go
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
package parsers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUnmarshalRDPFile(t *testing.T) {
|
||||||
|
rdp := Parser()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
cfg []byte
|
||||||
|
expOutput map[string]interface{}
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
expOutput: map[string]interface{}{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "string",
|
||||||
|
cfg: []byte(`username:s:user1`),
|
||||||
|
expOutput: map[string]interface{}{
|
||||||
|
"username": "user1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "integer",
|
||||||
|
cfg: []byte(`session bpp:i:32`),
|
||||||
|
expOutput: map[string]interface{}{
|
||||||
|
"session bpp": 32,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multi",
|
||||||
|
cfg: []byte("compression:i:1\r\nusername:s:user2\r\n"),
|
||||||
|
expOutput: map[string]interface{}{
|
||||||
|
"compression": 1,
|
||||||
|
"username": "user2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
outMap, err := rdp.Unmarshal(tc.cfg)
|
||||||
|
assert.Equal(t, tc.err, err)
|
||||||
|
assert.Equal(t, tc.expOutput, outMap)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRDP_Marshal(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
input map[string]interface{}
|
||||||
|
output []byte
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Empty RDP",
|
||||||
|
input: map[string]interface{}{},
|
||||||
|
output: []byte(nil),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid RDP all types",
|
||||||
|
input: map[string]interface{}{
|
||||||
|
"compression": 1,
|
||||||
|
"session bpp": 32,
|
||||||
|
"username": "user1",
|
||||||
|
},
|
||||||
|
output: []byte("compression:i:1\r\nsession bpp:i:32\r\nusername:s:user1\r\n"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
rdp := Parser()
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
out, err := rdp.Marshal(tc.input)
|
||||||
|
assert.Equal(t, tc.output, out)
|
||||||
|
assert.Equal(t, tc.err, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -108,14 +108,12 @@ func main() {
|
||||||
Hosts: conf.Server.Hosts,
|
Hosts: conf.Server.Hosts,
|
||||||
HostSelection: conf.Server.HostSelection,
|
HostSelection: conf.Server.HostSelection,
|
||||||
RdpOpts: web.RdpOpts{
|
RdpOpts: web.RdpOpts{
|
||||||
UsernameTemplate: conf.Client.UsernameTemplate,
|
UsernameTemplate: conf.Client.UsernameTemplate,
|
||||||
SplitUserDomain: conf.Client.SplitUserDomain,
|
SplitUserDomain: conf.Client.SplitUserDomain,
|
||||||
DefaultDomain: conf.Client.DefaultDomain,
|
DefaultDomain: conf.Client.DefaultDomain,
|
||||||
NetworkAutoDetect: conf.Client.NetworkAutoDetect,
|
|
||||||
BandwidthAutoDetect: conf.Client.BandwidthAutoDetect,
|
|
||||||
ConnectionType: conf.Client.ConnectionType,
|
|
||||||
},
|
},
|
||||||
GatewayAddress: url,
|
GatewayAddress: url,
|
||||||
|
TemplateFile: conf.Client.Defaults,
|
||||||
}
|
}
|
||||||
|
|
||||||
if conf.Caps.TokenAuth {
|
if conf.Caps.TokenAuth {
|
||||||
|
|
204
cmd/rdpgw/rdp/rdp.go
Normal file
204
cmd/rdpgw/rdp/rdp.go
Normal file
|
@ -0,0 +1,204 @@
|
||||||
|
package rdp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/fatih/structs"
|
||||||
|
"log"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
CRLF = "\r\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SourceNTLM int = iota
|
||||||
|
SourceSmartCard
|
||||||
|
SourceCurrent
|
||||||
|
SourceBasic
|
||||||
|
SourceUserSelect
|
||||||
|
SourceCookie
|
||||||
|
)
|
||||||
|
|
||||||
|
type RdpSettings struct {
|
||||||
|
GatewayHostname string `rdp:"gatewayhostname"`
|
||||||
|
FullAddress string `rdp:"full address"`
|
||||||
|
AlternateFullAddress string `rdp:"alternate full address"`
|
||||||
|
Username string `rdp:"username"`
|
||||||
|
Domain string `rdp:"domain"`
|
||||||
|
GatewayCredentialsSource int `rdp:"gatewaycredentialssource" default:"0"`
|
||||||
|
GatewayCredentialMethod 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"`
|
||||||
|
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"`
|
||||||
|
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"`
|
||||||
|
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"`
|
||||||
|
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 {
|
||||||
|
Settings RdpSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRdp() *RdpBuilder {
|
||||||
|
c := RdpSettings{}
|
||||||
|
|
||||||
|
initStruct(&c)
|
||||||
|
|
||||||
|
return &RdpBuilder{
|
||||||
|
Settings: c,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rb *RdpBuilder) String() string {
|
||||||
|
var sb strings.Builder
|
||||||
|
|
||||||
|
addStructToString(rb.Settings, &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
|
||||||
|
}
|
||||||
|
|
||||||
|
err := setVariable(f, t)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("cannot init rdp struct: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setVariable(f *structs.Field, v string) error {
|
||||||
|
switch f.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
return f.Set(v)
|
||||||
|
case reflect.Int:
|
||||||
|
i, err := strconv.Atoi(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return f.Set(i)
|
||||||
|
case reflect.Bool:
|
||||||
|
b := false
|
||||||
|
if v == "true" || v == "1" {
|
||||||
|
b = true
|
||||||
|
}
|
||||||
|
return f.Set(b)
|
||||||
|
default:
|
||||||
|
return errors.New("invalid field type")
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package web
|
package rdp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
@ -12,18 +12,18 @@ const (
|
||||||
|
|
||||||
func TestRdpBuilder(t *testing.T) {
|
func TestRdpBuilder(t *testing.T) {
|
||||||
builder := NewRdp()
|
builder := NewRdp()
|
||||||
builder.Connection.GatewayHostname = "my.yahoo.com"
|
builder.Settings.GatewayHostname = "my.yahoo.com"
|
||||||
builder.Session.AutoReconnectionEnabled = true
|
builder.Settings.AutoReconnectionEnabled = true
|
||||||
builder.Display.SmartSizing = true
|
builder.Settings.SmartSizing = true
|
||||||
|
|
||||||
s := builder.String()
|
s := builder.String()
|
||||||
if !strings.Contains(s, "gatewayhostname:s:"+GatewayHostName+crlf) {
|
if !strings.Contains(s, "gatewayhostname:s:"+GatewayHostName+CRLF) {
|
||||||
t.Fatalf("%s does not contain `gatewayhostname:s:%s", s, GatewayHostName)
|
t.Fatalf("%s does not contain `gatewayhostname:s:%s", s, GatewayHostName)
|
||||||
}
|
}
|
||||||
if strings.Contains(s, "autoreconnectionenabled") {
|
if strings.Contains(s, "autoreconnectionenabled") {
|
||||||
t.Fatalf("autoreconnectionenabled is in %s, but is default value", s)
|
t.Fatalf("autoreconnectionenabled is in %s, but is default value", s)
|
||||||
}
|
}
|
||||||
if !strings.Contains(s, "smart sizing:i:1"+crlf) {
|
if !strings.Contains(s, "smart sizing:i:1"+CRLF) {
|
||||||
t.Fatalf("%s does not contain smart sizing:i:1", s)
|
t.Fatalf("%s does not contain smart sizing:i:1", s)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,7 @@ func TestRdpBuilder(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInitStruct(t *testing.T) {
|
func TestInitStruct(t *testing.T) {
|
||||||
conn := RdpConnection{}
|
conn := RdpSettings{}
|
||||||
initStruct(&conn)
|
initStruct(&conn)
|
||||||
|
|
||||||
if conn.PromptCredentialsOnce != true {
|
if conn.PromptCredentialsOnce != true {
|
|
@ -1,229 +0,0 @@
|
||||||
package web
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/fatih/structs"
|
|
||||||
"log"
|
|
||||||
"reflect"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
crlf = "\r\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
SourceNTLM int = iota
|
|
||||||
SourceSmartCard
|
|
||||||
SourceCurrent
|
|
||||||
SourceBasic
|
|
||||||
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"`
|
|
||||||
GatewayCredentialsSource int `rdp:"gatewaycredentialssource" default:"0"`
|
|
||||||
GatewayCredentialMethod 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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -5,7 +5,12 @@ import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/config/parsers"
|
||||||
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity"
|
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity"
|
||||||
|
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/rdp"
|
||||||
|
"github.com/knadh/koanf/providers/file"
|
||||||
|
"github.com/knadh/koanf/v2"
|
||||||
|
"hash/maphash"
|
||||||
"log"
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -28,6 +33,7 @@ type Config struct {
|
||||||
HostSelection string
|
HostSelection string
|
||||||
GatewayAddress *url.URL
|
GatewayAddress *url.URL
|
||||||
RdpOpts RdpOpts
|
RdpOpts RdpOpts
|
||||||
|
TemplateFile string
|
||||||
}
|
}
|
||||||
|
|
||||||
type RdpOpts struct {
|
type RdpOpts struct {
|
||||||
|
@ -49,12 +55,14 @@ type Handler struct {
|
||||||
hosts []string
|
hosts []string
|
||||||
hostSelection string
|
hostSelection string
|
||||||
rdpOpts RdpOpts
|
rdpOpts RdpOpts
|
||||||
|
rdpDefaults string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) NewHandler() *Handler {
|
func (c *Config) NewHandler() *Handler {
|
||||||
if len(c.Hosts) < 1 {
|
if len(c.Hosts) < 1 {
|
||||||
log.Fatal("Not enough hosts to connect to specified")
|
log.Fatal("Not enough hosts to connect to specified")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Handler{
|
return &Handler{
|
||||||
paaTokenGenerator: c.PAATokenGenerator,
|
paaTokenGenerator: c.PAATokenGenerator,
|
||||||
enableUserToken: c.EnableUserToken,
|
enableUserToken: c.EnableUserToken,
|
||||||
|
@ -65,12 +73,13 @@ func (c *Config) NewHandler() *Handler {
|
||||||
hosts: c.Hosts,
|
hosts: c.Hosts,
|
||||||
hostSelection: c.HostSelection,
|
hostSelection: c.HostSelection,
|
||||||
rdpOpts: c.RdpOpts,
|
rdpOpts: c.RdpOpts,
|
||||||
|
rdpDefaults: c.TemplateFile,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) selectRandomHost() string {
|
func (h *Handler) selectRandomHost() string {
|
||||||
rand.Seed(time.Now().Unix())
|
r := rand.New(rand.NewSource(int64(new(maphash.Hash).Sum64())))
|
||||||
host := h.hosts[rand.Intn(len(h.hosts))]
|
host := h.hosts[r.Intn(len(h.hosts))]
|
||||||
return host
|
return host
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,20 +197,25 @@ func (h *Handler) HandleDownload(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Disposition", "attachment; filename="+fn)
|
w.Header().Set("Content-Disposition", "attachment; filename="+fn)
|
||||||
w.Header().Set("Content-Type", "application/x-rdp")
|
w.Header().Set("Content-Type", "application/x-rdp")
|
||||||
|
|
||||||
rdp := NewRdp()
|
d := rdp.NewRdp()
|
||||||
rdp.Connection.Username = render
|
|
||||||
rdp.Connection.Domain = domain
|
|
||||||
rdp.Connection.FullAddress = host
|
|
||||||
rdp.Connection.GatewayHostname = h.gatewayAddress.Host
|
|
||||||
rdp.Connection.GatewayCredentialsSource = SourceCookie
|
|
||||||
rdp.Connection.GatewayAccessToken = token
|
|
||||||
rdp.Connection.GatewayCredentialMethod = 1
|
|
||||||
rdp.Connection.GatewayUsageMethod = 1
|
|
||||||
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()))
|
if h.rdpDefaults != "" {
|
||||||
|
var k = koanf.New(".")
|
||||||
|
if err := k.Load(file.Provider(h.rdpDefaults), parsers.Parser()); err != nil {
|
||||||
|
log.Fatalf("cannot load rdp template file from %s", h.rdpDefaults)
|
||||||
|
}
|
||||||
|
tag := koanf.UnmarshalConf{Tag: "rdp"}
|
||||||
|
k.UnmarshalWithConf("", &d.Settings, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Settings.Username = render
|
||||||
|
d.Settings.Domain = domain
|
||||||
|
d.Settings.FullAddress = host
|
||||||
|
d.Settings.GatewayHostname = h.gatewayAddress.Host
|
||||||
|
d.Settings.GatewayCredentialsSource = rdp.SourceCookie
|
||||||
|
d.Settings.GatewayAccessToken = token
|
||||||
|
d.Settings.GatewayCredentialMethod = 1
|
||||||
|
d.Settings.GatewayUsageMethod = 1
|
||||||
|
|
||||||
|
http.ServeContent(w, r, fn, time.Now(), strings.NewReader(d.String()))
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package web
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity"
|
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity"
|
||||||
|
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/rdp"
|
||||||
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/security"
|
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/security"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
@ -149,7 +150,7 @@ func TestHandler_HandleDownload(t *testing.T) {
|
||||||
t.Errorf("content disposition is nil")
|
t.Errorf("content disposition is nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
data := rdpToMap(strings.Split(rr.Body.String(), crlf))
|
data := rdpToMap(strings.Split(rr.Body.String(), rdp.CRLF))
|
||||||
if data["username"] != testuser {
|
if data["username"] != testuser {
|
||||||
t.Errorf("username key in rdp does not match: got %v want %v", data["username"], testuser)
|
t.Errorf("username key in rdp does not match: got %v want %v", data["username"], testuser)
|
||||||
}
|
}
|
||||||
|
|
3
go.mod
3
go.mod
|
@ -21,6 +21,7 @@ require (
|
||||||
github.com/msteinert/pam v1.0.0
|
github.com/msteinert/pam v1.0.0
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||||
github.com/prometheus/client_golang v1.15.0
|
github.com/prometheus/client_golang v1.15.0
|
||||||
|
github.com/stretchr/testify v1.8.1
|
||||||
github.com/thought-machine/go-flags v1.6.2
|
github.com/thought-machine/go-flags v1.6.2
|
||||||
golang.org/x/crypto v0.8.0
|
golang.org/x/crypto v0.8.0
|
||||||
golang.org/x/oauth2 v0.7.0
|
golang.org/x/oauth2 v0.7.0
|
||||||
|
@ -31,6 +32,7 @@ require (
|
||||||
require (
|
require (
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||||
github.com/golang/protobuf v1.5.3 // indirect
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||||
|
@ -43,6 +45,7 @@ require (
|
||||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_model v0.3.0 // indirect
|
github.com/prometheus/client_model v0.3.0 // indirect
|
||||||
github.com/prometheus/common v0.42.0 // indirect
|
github.com/prometheus/common v0.42.0 // indirect
|
||||||
github.com/prometheus/procfs v0.9.0 // indirect
|
github.com/prometheus/procfs v0.9.0 // indirect
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue