diff --git a/Makefile b/Makefile index d1e05eb..596d935 100644 --- a/Makefile +++ b/Makefile @@ -50,7 +50,7 @@ install: build .PHONY: mod mod: - go mod tidy -compat=1.19 + go mod tidy -compat=1.20 # ------------------------------------------------------------------------------ # test diff --git a/UPGRADING.md b/UPGRADING.md new file mode 100644 index 0000000..74d7f4a --- /dev/null +++ b/UPGRADING.md @@ -0,0 +1,15 @@ +# Upgrading from 1.X to 2.0 + +In 2.0 the options for configuring client side RDP settings have been removed in favor of template file. +The template file is a RDP file that is used as a template for the connection. The template file is parsed +and a few settings are replaced to ensure the client can connect to the server and the correct domain is used. + +The format of the template file is as follows: + +``` +# :: +domain:s:testdomain +connection type:i:2 +``` + +The filename is set under `client > defaults`. diff --git a/cmd/rdpgw/config/configuration.go b/cmd/rdpgw/config/configuration.go index c0fb9cc..525158b 100644 --- a/cmd/rdpgw/config/configuration.go +++ b/cmd/rdpgw/config/configuration.go @@ -93,7 +93,6 @@ type ClientConfig struct { // kept for backwards compatibility UsernameTemplate string `koanf:"usernametemplate"` SplitUserDomain bool `koanf:"splituserdomain"` - DefaultDomain string `koanf:"defaultdomain"` } func ToCamel(s string) string { diff --git a/cmd/rdpgw/main.go b/cmd/rdpgw/main.go index 6aed44c..bf44b7b 100644 --- a/cmd/rdpgw/main.go +++ b/cmd/rdpgw/main.go @@ -110,7 +110,6 @@ func main() { RdpOpts: web.RdpOpts{ UsernameTemplate: conf.Client.UsernameTemplate, SplitUserDomain: conf.Client.SplitUserDomain, - DefaultDomain: conf.Client.DefaultDomain, }, GatewayAddress: url, TemplateFile: conf.Client.Defaults, diff --git a/cmd/rdpgw/config/parsers/rdp.go b/cmd/rdpgw/rdp/koanf/parsers/rdp/rdp.go similarity index 98% rename from cmd/rdpgw/config/parsers/rdp.go rename to cmd/rdpgw/rdp/koanf/parsers/rdp/rdp.go index 5f6d947..c902004 100644 --- a/cmd/rdpgw/config/parsers/rdp.go +++ b/cmd/rdpgw/rdp/koanf/parsers/rdp/rdp.go @@ -1,4 +1,4 @@ -package parsers +package rdp import ( "bufio" diff --git a/cmd/rdpgw/config/parsers/rdp_test.go b/cmd/rdpgw/rdp/koanf/parsers/rdp/rdp_test.go similarity index 99% rename from cmd/rdpgw/config/parsers/rdp_test.go rename to cmd/rdpgw/rdp/koanf/parsers/rdp/rdp_test.go index 73d3fe0..c73563e 100644 --- a/cmd/rdpgw/config/parsers/rdp_test.go +++ b/cmd/rdpgw/rdp/koanf/parsers/rdp/rdp_test.go @@ -1,4 +1,4 @@ -package parsers +package rdp import ( "github.com/stretchr/testify/assert" diff --git a/cmd/rdpgw/rdp/rdp.go b/cmd/rdpgw/rdp/rdp.go index fe0f29d..3b5a6a4 100644 --- a/cmd/rdpgw/rdp/rdp.go +++ b/cmd/rdpgw/rdp/rdp.go @@ -3,7 +3,10 @@ package rdp import ( "errors" "fmt" + "github.com/bolkedebruin/rdpgw/cmd/rdpgw/rdp/koanf/parsers/rdp" "github.com/fatih/structs" + "github.com/knadh/koanf/providers/file" + "github.com/knadh/koanf/v2" "log" "reflect" "strconv" @@ -81,21 +84,38 @@ type RdpSettings struct { RemoteApplicationProgram string `rdp:"remoteapplicationprogram"` } -type RdpBuilder struct { +type Builder struct { Settings RdpSettings } -func NewRdp() *RdpBuilder { +func NewBuilder() *Builder { c := RdpSettings{} initStruct(&c) - return &RdpBuilder{ + return &Builder{ Settings: c, } } -func (rb *RdpBuilder) String() string { +func NewBuilderFromFile(filename string) (*Builder, error) { + c := RdpSettings{} + initStruct(&c) + + var k = koanf.New(".") + if err := k.Load(file.Provider(filename), rdp.Parser()); err != nil { + return nil, err + } + t := koanf.UnmarshalConf{Tag: "rdp"} + if err := k.UnmarshalWithConf("", &c, t); err != nil { + return nil, err + } + return &Builder{ + Settings: c, + }, nil +} + +func (rb *Builder) String() string { var sb strings.Builder addStructToString(rb.Settings, &sb) diff --git a/cmd/rdpgw/rdp/rdp_test.go b/cmd/rdpgw/rdp/rdp_test.go index fc29bcb..8efb70e 100644 --- a/cmd/rdpgw/rdp/rdp_test.go +++ b/cmd/rdpgw/rdp/rdp_test.go @@ -11,7 +11,7 @@ const ( ) func TestRdpBuilder(t *testing.T) { - builder := NewRdp() + builder := NewBuilder() builder.Settings.GatewayHostname = "my.yahoo.com" builder.Settings.AutoReconnectionEnabled = true builder.Settings.SmartSizing = true diff --git a/cmd/rdpgw/transport/legacy.go b/cmd/rdpgw/transport/legacy.go index cfce517..4277e3f 100644 --- a/cmd/rdpgw/transport/legacy.go +++ b/cmd/rdpgw/transport/legacy.go @@ -2,9 +2,9 @@ package transport import ( "bufio" + "crypto/rand" "errors" "io" - "math/rand" "net" "net/http" "net/http/httputil" @@ -12,14 +12,14 @@ import ( ) const ( - crlf = "\r\n" + crlf = "\r\n" HttpOK = "HTTP/1.1 200 OK\r\n" ) type LegacyPKT struct { - Conn net.Conn + Conn net.Conn ChunkedReader io.Reader - Writer *bufio.Writer + Writer *bufio.Writer } func NewLegacy(w http.ResponseWriter) (*LegacyPKT, error) { @@ -27,9 +27,9 @@ func NewLegacy(w http.ResponseWriter) (*LegacyPKT, error) { if ok { conn, rw, err := hj.Hijack() l := &LegacyPKT{ - Conn: conn, + Conn: conn, ChunkedReader: httputil.NewChunkedReader(rw.Reader), - Writer: rw.Writer, + Writer: rw.Writer, } return l, err } @@ -37,7 +37,7 @@ func NewLegacy(w http.ResponseWriter) (*LegacyPKT, error) { return nil, errors.New("cannot hijack connection") } -func (t *LegacyPKT) ReadPacket() (n int, p []byte, err error){ +func (t *LegacyPKT) ReadPacket() (n int, p []byte, err error) { buf := make([]byte, 4096) // bufio.defaultBufSize n, err = t.ChunkedReader.Read(buf) p = make([]byte, n) diff --git a/cmd/rdpgw/web/oidc.go b/cmd/rdpgw/web/oidc.go index 03cece1..8458341 100644 --- a/cmd/rdpgw/web/oidc.go +++ b/cmd/rdpgw/web/oidc.go @@ -1,13 +1,13 @@ package web import ( + "crypto/rand" "encoding/hex" "encoding/json" "github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity" "github.com/coreos/go-oidc/v3/oidc" "github.com/patrickmn/go-cache" "golang.org/x/oauth2" - "math/rand" "net/http" "time" ) @@ -116,7 +116,11 @@ func (h *OIDC) Authenticated(next http.Handler) http.Handler { if !id.Authenticated() { seed := make([]byte, 16) - rand.Read(seed) + _, err := rand.Read(seed) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } state := hex.EncodeToString(seed) h.stateStore.Set(state, r.RequestURI, cache.DefaultExpiration) http.Redirect(w, r, h.oAuth2Config.AuthCodeURL(state), http.StatusFound) diff --git a/cmd/rdpgw/web/web.go b/cmd/rdpgw/web/web.go index 4b8fe78..97c6bcb 100644 --- a/cmd/rdpgw/web/web.go +++ b/cmd/rdpgw/web/web.go @@ -2,17 +2,15 @@ package web import ( "context" + "crypto/rand" "encoding/hex" "errors" "fmt" - "github.com/bolkedebruin/rdpgw/cmd/rdpgw/config/parsers" "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" - "math/rand" + rnd "math/rand" "net/http" "net/url" "strings" @@ -37,12 +35,8 @@ type Config struct { } type RdpOpts struct { - UsernameTemplate string - SplitUserDomain bool - DefaultDomain string - NetworkAutoDetect int - BandwidthAutoDetect int - ConnectionType int + UsernameTemplate string + SplitUserDomain bool } type Handler struct { @@ -78,7 +72,7 @@ func (c *Config) NewHandler() *Handler { } func (h *Handler) selectRandomHost() string { - r := rand.New(rand.NewSource(int64(new(maphash.Hash).Sum64()))) + r := rnd.New(rnd.NewSource(int64(new(maphash.Hash).Sum64()))) host := h.hosts[r.Intn(len(h.hosts))] return host } @@ -154,7 +148,7 @@ func (h *Handler) HandleDownload(w http.ResponseWriter, r *http.Request) { // split the username into user and domain var user = id.UserName() - var domain = opts.DefaultDomain + var domain = "" if opts.SplitUserDomain { creds := strings.SplitN(id.UserName(), "@", 2) user = creds[0] @@ -178,6 +172,7 @@ func (h *Handler) HandleDownload(w http.ResponseWriter, r *http.Request) { if err != nil { log.Printf("Cannot generate PAA token for user %s due to %s", user, err) http.Error(w, errors.New("unable to generate gateway credentials").Error(), http.StatusInternalServerError) + return } if h.enableUserToken { @@ -185,31 +180,40 @@ func (h *Handler) HandleDownload(w http.ResponseWriter, r *http.Request) { if err != nil { log.Printf("Cannot generate token for user %s due to %s", user, err) http.Error(w, errors.New("unable to generate gateway credentials").Error(), http.StatusInternalServerError) + return } render = strings.Replace(render, "{{ token }}", userToken, 1) } // authenticated seed := make([]byte, 16) - rand.Read(seed) + _, err = rand.Read(seed) + if err != nil { + log.Printf("Cannot generate random seed due to %s", err) + http.Error(w, errors.New("unable to generate random sequence").Error(), http.StatusInternalServerError) + return + } fn := hex.EncodeToString(seed) + ".rdp" w.Header().Set("Content-Disposition", "attachment; filename="+fn) w.Header().Set("Content-Type", "application/x-rdp") - d := rdp.NewRdp() - - 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) + var d *rdp.Builder + if h.rdpDefaults == "" { + d = rdp.NewBuilder() + } else { + d, err = rdp.NewBuilderFromFile(h.rdpDefaults) + if err != nil { + log.Printf("Cannot load RDP template file %s due to %s", h.rdpDefaults, err) + http.Error(w, errors.New("unable to load RDP template").Error(), http.StatusInternalServerError) + return } - tag := koanf.UnmarshalConf{Tag: "rdp"} - k.UnmarshalWithConf("", &d.Settings, tag) } d.Settings.Username = render - d.Settings.Domain = domain + if domain != "" { + d.Settings.Domain = domain + } d.Settings.FullAddress = host d.Settings.GatewayHostname = h.gatewayAddress.Host d.Settings.GatewayCredentialsSource = rdp.SourceCookie diff --git a/cmd/rdpgw/web/web_test.go b/cmd/rdpgw/web/web_test.go index f57cebf..6c53ca0 100644 --- a/cmd/rdpgw/web/web_test.go +++ b/cmd/rdpgw/web/web_test.go @@ -8,6 +8,7 @@ import ( "net/http" "net/http/httptest" "net/url" + "os" "strings" "testing" ) @@ -171,6 +172,51 @@ func TestHandler_HandleDownload(t *testing.T) { } +func TestHandler_HandleDownloadWithRdpTemplate(t *testing.T) { + f, err := os.CreateTemp("", "rdp") + if err != nil { + t.Fatal(err) + } + defer os.Remove(f.Name()) + + err = os.WriteFile(f.Name(), []byte("domain:s:testdomain\r\n"), 0644) + if err != nil { + t.Fatal(err) + } + + req, err := http.NewRequest("GET", "/connect", nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + id := identity.NewUser() + + id.SetUserName(testuser) + id.SetAuthenticated(true) + + req = identity.AddToRequestCtx(id, req) + + u, _ := url.Parse(gateway) + c := Config{ + HostSelection: "roundrobin", + Hosts: hosts, + PAATokenGenerator: paaTokenMock, + GatewayAddress: u, + RdpOpts: RdpOpts{SplitUserDomain: true}, + TemplateFile: f.Name(), + } + h := c.NewHandler() + + hh := http.HandlerFunc(h.HandleDownload) + hh.ServeHTTP(rr, req) + + data := rdpToMap(strings.Split(rr.Body.String(), rdp.CRLF)) + if data["domain"] != "testdomain" { + t.Errorf("domain key in rdp does not match: got %v want %v", data["domain"], "testdomain") + } +} + func paaTokenMock(ctx context.Context, username string, host string) (string, error) { return username + "_" + host, nil } diff --git a/dev/docker/rdpgw.yaml b/dev/docker/rdpgw.yaml index ee53f9c..5d2cc59 100644 --- a/dev/docker/rdpgw.yaml +++ b/dev/docker/rdpgw.yaml @@ -14,9 +14,6 @@ OpenId: ClientSecret: 01cd304c-6f43-4480-9479-618eb6fd578f Client: UsernameTemplate: "{{ username }}" - NetworkAutoDetect: 0 - BandwidthAutoDetect: 1 - ConnectionType: 6 Security: PAATokenSigningKey: prettypleasereplacemeinproductio Caps: