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
mod:
go mod tidy -compat=1.19
go mod tidy -compat=1.20
# ------------------------------------------------------------------------------
# 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
UsernameTemplate string `koanf:"usernametemplate"`
SplitUserDomain bool `koanf:"splituserdomain"`
DefaultDomain string `koanf:"defaultdomain"`
}
func ToCamel(s string) string {

View File

@ -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,

View File

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

View File

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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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
}

View File

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