Finalize rdp templating

This commit is contained in:
Bolke de Bruin
2023-05-15 10:41:35 +02:00
parent cdc497f365
commit 6b32631434
13 changed files with 128 additions and 44 deletions

View File

@@ -50,7 +50,7 @@ install: build
.PHONY: mod .PHONY: mod
mod: mod:
go mod tidy -compat=1.19 go mod tidy -compat=1.20
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# test # test

15
UPGRADING.md Normal file
View File

@@ -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:
```
# <setting>:<type i or s>:<value>
domain:s:testdomain
connection type:i:2
```
The filename is set under `client > defaults`.

View File

@@ -93,7 +93,6 @@ type ClientConfig struct {
// kept for backwards compatibility // kept for backwards compatibility
UsernameTemplate string `koanf:"usernametemplate"` UsernameTemplate string `koanf:"usernametemplate"`
SplitUserDomain bool `koanf:"splituserdomain"` SplitUserDomain bool `koanf:"splituserdomain"`
DefaultDomain string `koanf:"defaultdomain"`
} }
func ToCamel(s string) string { func ToCamel(s string) string {

View File

@@ -110,7 +110,6 @@ func main() {
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,
}, },
GatewayAddress: url, GatewayAddress: url,
TemplateFile: conf.Client.Defaults, TemplateFile: conf.Client.Defaults,

View File

@@ -1,4 +1,4 @@
package parsers package rdp
import ( import (
"bufio" "bufio"

View File

@@ -1,4 +1,4 @@
package parsers package rdp
import ( import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"

View File

@@ -3,7 +3,10 @@ package rdp
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/rdp/koanf/parsers/rdp"
"github.com/fatih/structs" "github.com/fatih/structs"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/v2"
"log" "log"
"reflect" "reflect"
"strconv" "strconv"
@@ -81,21 +84,38 @@ type RdpSettings struct {
RemoteApplicationProgram string `rdp:"remoteapplicationprogram"` RemoteApplicationProgram string `rdp:"remoteapplicationprogram"`
} }
type RdpBuilder struct { type Builder struct {
Settings RdpSettings Settings RdpSettings
} }
func NewRdp() *RdpBuilder { func NewBuilder() *Builder {
c := RdpSettings{} c := RdpSettings{}
initStruct(&c) initStruct(&c)
return &RdpBuilder{ return &Builder{
Settings: c, 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 var sb strings.Builder
addStructToString(rb.Settings, &sb) addStructToString(rb.Settings, &sb)

View File

@@ -11,7 +11,7 @@ const (
) )
func TestRdpBuilder(t *testing.T) { func TestRdpBuilder(t *testing.T) {
builder := NewRdp() builder := NewBuilder()
builder.Settings.GatewayHostname = "my.yahoo.com" builder.Settings.GatewayHostname = "my.yahoo.com"
builder.Settings.AutoReconnectionEnabled = true builder.Settings.AutoReconnectionEnabled = true
builder.Settings.SmartSizing = true builder.Settings.SmartSizing = true

View File

@@ -2,9 +2,9 @@ package transport
import ( import (
"bufio" "bufio"
"crypto/rand"
"errors" "errors"
"io" "io"
"math/rand"
"net" "net"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
@@ -12,14 +12,14 @@ import (
) )
const ( const (
crlf = "\r\n" crlf = "\r\n"
HttpOK = "HTTP/1.1 200 OK\r\n" HttpOK = "HTTP/1.1 200 OK\r\n"
) )
type LegacyPKT struct { type LegacyPKT struct {
Conn net.Conn Conn net.Conn
ChunkedReader io.Reader ChunkedReader io.Reader
Writer *bufio.Writer Writer *bufio.Writer
} }
func NewLegacy(w http.ResponseWriter) (*LegacyPKT, error) { func NewLegacy(w http.ResponseWriter) (*LegacyPKT, error) {
@@ -27,9 +27,9 @@ func NewLegacy(w http.ResponseWriter) (*LegacyPKT, error) {
if ok { if ok {
conn, rw, err := hj.Hijack() conn, rw, err := hj.Hijack()
l := &LegacyPKT{ l := &LegacyPKT{
Conn: conn, Conn: conn,
ChunkedReader: httputil.NewChunkedReader(rw.Reader), ChunkedReader: httputil.NewChunkedReader(rw.Reader),
Writer: rw.Writer, Writer: rw.Writer,
} }
return l, err return l, err
} }
@@ -37,7 +37,7 @@ func NewLegacy(w http.ResponseWriter) (*LegacyPKT, error) {
return nil, errors.New("cannot hijack connection") 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 buf := make([]byte, 4096) // bufio.defaultBufSize
n, err = t.ChunkedReader.Read(buf) n, err = t.ChunkedReader.Read(buf)
p = make([]byte, n) p = make([]byte, n)

View File

@@ -1,13 +1,13 @@
package web package web
import ( import (
"crypto/rand"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity" "github.com/bolkedebruin/rdpgw/cmd/rdpgw/identity"
"github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-oidc/v3/oidc"
"github.com/patrickmn/go-cache" "github.com/patrickmn/go-cache"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"math/rand"
"net/http" "net/http"
"time" "time"
) )
@@ -116,7 +116,11 @@ func (h *OIDC) Authenticated(next http.Handler) http.Handler {
if !id.Authenticated() { if !id.Authenticated() {
seed := make([]byte, 16) 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) state := hex.EncodeToString(seed)
h.stateStore.Set(state, r.RequestURI, cache.DefaultExpiration) h.stateStore.Set(state, r.RequestURI, cache.DefaultExpiration)
http.Redirect(w, r, h.oAuth2Config.AuthCodeURL(state), http.StatusFound) http.Redirect(w, r, h.oAuth2Config.AuthCodeURL(state), http.StatusFound)

View File

@@ -2,17 +2,15 @@ package web
import ( import (
"context" "context"
"crypto/rand"
"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/bolkedebruin/rdpgw/cmd/rdpgw/rdp"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/v2"
"hash/maphash" "hash/maphash"
"log" "log"
"math/rand" rnd "math/rand"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
@@ -37,12 +35,8 @@ type Config struct {
} }
type RdpOpts struct { type RdpOpts struct {
UsernameTemplate string UsernameTemplate string
SplitUserDomain bool SplitUserDomain bool
DefaultDomain string
NetworkAutoDetect int
BandwidthAutoDetect int
ConnectionType int
} }
type Handler struct { type Handler struct {
@@ -78,7 +72,7 @@ func (c *Config) NewHandler() *Handler {
} }
func (h *Handler) selectRandomHost() string { 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))] host := h.hosts[r.Intn(len(h.hosts))]
return host return host
} }
@@ -154,7 +148,7 @@ func (h *Handler) HandleDownload(w http.ResponseWriter, r *http.Request) {
// split the username into user and domain // split the username into user and domain
var user = id.UserName() var user = id.UserName()
var domain = opts.DefaultDomain var domain = ""
if opts.SplitUserDomain { if opts.SplitUserDomain {
creds := strings.SplitN(id.UserName(), "@", 2) creds := strings.SplitN(id.UserName(), "@", 2)
user = creds[0] user = creds[0]
@@ -178,6 +172,7 @@ func (h *Handler) HandleDownload(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
log.Printf("Cannot generate PAA token for user %s due to %s", user, err) 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) http.Error(w, errors.New("unable to generate gateway credentials").Error(), http.StatusInternalServerError)
return
} }
if h.enableUserToken { if h.enableUserToken {
@@ -185,31 +180,40 @@ func (h *Handler) HandleDownload(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
log.Printf("Cannot generate token for user %s due to %s", user, err) 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) http.Error(w, errors.New("unable to generate gateway credentials").Error(), http.StatusInternalServerError)
return
} }
render = strings.Replace(render, "{{ token }}", userToken, 1) render = strings.Replace(render, "{{ token }}", userToken, 1)
} }
// authenticated // authenticated
seed := make([]byte, 16) 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" fn := hex.EncodeToString(seed) + ".rdp"
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")
d := rdp.NewRdp() var d *rdp.Builder
if h.rdpDefaults == "" {
if h.rdpDefaults != "" { d = rdp.NewBuilder()
var k = koanf.New(".") } else {
if err := k.Load(file.Provider(h.rdpDefaults), parsers.Parser()); err != nil { d, err = rdp.NewBuilderFromFile(h.rdpDefaults)
log.Fatalf("cannot load rdp template file from %s", 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.Username = render
d.Settings.Domain = domain if domain != "" {
d.Settings.Domain = domain
}
d.Settings.FullAddress = host d.Settings.FullAddress = host
d.Settings.GatewayHostname = h.gatewayAddress.Host d.Settings.GatewayHostname = h.gatewayAddress.Host
d.Settings.GatewayCredentialsSource = rdp.SourceCookie d.Settings.GatewayCredentialsSource = rdp.SourceCookie

View File

@@ -8,6 +8,7 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
"os"
"strings" "strings"
"testing" "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) { func paaTokenMock(ctx context.Context, username string, host string) (string, error) {
return username + "_" + host, nil return username + "_" + host, nil
} }

View File

@@ -14,9 +14,6 @@ OpenId:
ClientSecret: 01cd304c-6f43-4480-9479-618eb6fd578f ClientSecret: 01cd304c-6f43-4480-9479-618eb6fd578f
Client: Client:
UsernameTemplate: "{{ username }}" UsernameTemplate: "{{ username }}"
NetworkAutoDetect: 0
BandwidthAutoDetect: 1
ConnectionType: 6
Security: Security:
PAATokenSigningKey: prettypleasereplacemeinproductio PAATokenSigningKey: prettypleasereplacemeinproductio
Caps: Caps: