mirror of
https://github.com/bolkedebruin/rdpgw.git
synced 2026-03-27 14:36:36 +00:00
Finalize rdp templating
This commit is contained in:
2
Makefile
2
Makefile
@@ -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
15
UPGRADING.md
Normal 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`.
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package parsers
|
package rdp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package parsers
|
package rdp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user