bolkedebruin / rdpgw

Remote Desktop Gateway in Go for deploying on Linux/BSD/Kubernetes
Apache License 2.0
698 stars 115 forks source link

Allow passing of configuration with environment variables. #40

Closed alphabet5 closed 1 year ago

alphabet5 commented 2 years ago

I was unable to get viper to allow setting of config items with environment variables. I was able to get it working by switching to koanf instead. There are a few other benefits to koanf, but the largest drawback is that it is case sensitive. The current implementation I have working allows for all lowercase, but the .yaml file is required to match the camel case defined in the configuration structs.

package config

import (
    "github.com/knadh/koanf"
    "github.com/knadh/koanf/parsers/yaml"
    "github.com/knadh/koanf/providers/confmap"
    "github.com/knadh/koanf/providers/env"
    "github.com/knadh/koanf/providers/file"
    "log"
    "strings"
)

type Configuration struct {
    Server   ServerConfig   `koanf:"server"`
    OpenId   OpenIDConfig   `koanf:"openid"`
    Caps     RDGCapsConfig  `koanf:"caps"`
    Security SecurityConfig `koanf:"security"`
    Client   ClientConfig   `koanf:"client"`
}

type ServerConfig struct {
    GatewayAddress       string   `koanf:"gatewayaddress"`
    Port                 int      `koanf:"port"`
    CertFile             string   `koanf:"certfile"`
    KeyFile              string   `koanf:"keyfile"`
    Hosts                []string `koanf:"hosts"`
    RoundRobin           bool     `koanf:"roundrobin"`
    SessionKey           string   `koanf:"sessionkey"`
    SessionEncryptionKey string   `koanf:"sessionencryptionkey"`
    SendBuf              int      `koanf:"sendbuf"`
    ReceiveBuf           int      `koanf:"recievebuf"`
}

type OpenIDConfig struct {
    ProviderUrl  string `koanf:"providerurl"`
    ClientId     string `koanf:"clientid"`
    ClientSecret string `koanf:"clientsecret"`
}

type RDGCapsConfig struct {
    SmartCardAuth   bool `koanf:"smartcardauth"`
    TokenAuth       bool `koanf:"tokenauth"`
    IdleTimeout     int  `koanf:"idletimeout"`
    RedirectAll     bool `koanf:"redirectall"`
    DisableRedirect bool `koanf:"disableredirect"`
    EnableClipboard bool `koanf:"enableclipboard"`
    EnablePrinter   bool `koanf:"enableprinter"`
    EnablePort      bool `koanf:"enableport"`
    EnablePnp       bool `koanf:"enablepnp"`
    EnableDrive     bool `koanf:"enabledrive"`
}

type SecurityConfig struct {
    PAATokenEncryptionKey  string `koanf:"paatokenencryptionkey"`
    PAATokenSigningKey     string `koanf:"paatokensigningkey"`
    UserTokenEncryptionKey string `koanf:"usertokenencryptionkey"`
    UserTokenSigningKey    string `koanf:"usertokensigningkey"`
    VerifyClientIp         bool   `koanf:"verifyclientip"`
    EnableUserToken        bool   `koanf:"enableusertoken"`
}

type ClientConfig struct {
    NetworkAutoDetect   int    `koanf:"networkautodetect"`
    BandwidthAutoDetect int    `koanf:"bandwidthautodetect"`
    ConnectionType      int    `koanf:"connectiontype"`
    UsernameTemplate    string `koanf:"usernametemplate"`
    SplitUserDomain     bool   `koanf:"splituserdomain"`
    DefaultDomain       string `koanf:"defaultdomain"`
}

func ToCamel(s string) string {
    s = strings.TrimSpace(s)
    n := strings.Builder{}
    n.Grow(len(s))
    var capNext bool = true
    for i, v := range []byte(s) {
        vIsCap := v >= 'A' && v <= 'Z'
        vIsLow := v >= 'a' && v <= 'z'
        if capNext {
            if vIsLow {
                v += 'A'
                v -= 'a'
            }
        } else if i == 0 {
            if vIsCap {
                v += 'a'
                v -= 'A'
            }
        }
        if vIsCap || vIsLow {
            n.WriteByte(v)
            capNext = false
        } else if vIsNum := v >= '0' && v <= '9'; vIsNum {
            n.WriteByte(v)
            capNext = true
        } else {
            capNext = v == '_' || v == ' ' || v == '-' || v == '.'
            if v == '.' {
                n.WriteByte(v)
            }
        }
    }
    return n.String()
}

var Conf Configuration

func Load(configFile string) Configuration {

    var k = koanf.New(".")

    k.Load(confmap.Provider(map[string]interface{}{
        "Server.CertFile":            "server.pem",
        "Server.KeyFile":             "key.pem",
        "Server.Port":                443,
        "Client.NetworkAutoDetect":   1,
        "Client.BandwidthAutoDetect": 1,
        "Security.VerifyClientIp":    true,
    }, "."), nil)

    if err := k.Load(file.Provider(configFile), yaml.Parser()); err != nil {
        log.Fatalf("Error loading config from file: %v", err)
    }

    if err := k.Load(env.ProviderWithValue("RDPGW_", ".", func(s string, v string) (string, interface{}) {
        key := strings.Replace(strings.ToLower(strings.TrimPrefix(s, "RDPGW_")), "__", ".", -1)
        key = ToCamel(key)
        return key, v
    }), nil); err != nil {
        log.Fatalf("Error loading config from file: %v", err)
    }

    koanfTag := koanf.UnmarshalConf{Tag: "koanf"}
    k.UnmarshalWithConf("Server", &Conf.Server, koanfTag)
    k.UnmarshalWithConf("OpenId", &Conf.OpenId, koanfTag)
    k.UnmarshalWithConf("Caps", &Conf.Caps, koanfTag)
    k.UnmarshalWithConf("Security", &Conf.Security, koanfTag)
    k.UnmarshalWithConf("Client", &Conf.Client, koanfTag)

    return Conf

}

With this, the environment variables must use double underscores for nested config items, and single underscores where there is camel case.

i.e.

Server:
  CertFile: /certs/tls.crt
  KeyFile: /certs/tls.key
OpenId:
  ProviderUrl: https://auth.rdpgw.local
  ClientId: rdpgw

==

RDPGW_SERVER__CERT_FILE="/certs/tls.crt"
RDPGW_SERVER_KEY_FILE="/certs/tls.key"
RDPGW_OPEN_ID__PROVIDER_URL="https://auth.rdpgw.local"
RDPGW_OPEN_ID__CLIENT_ID="rdpgw"
bolkedebruin commented 1 year ago

I like koanf and wouldnt mind switching to it. Could you do a PR?

bolkedebruin commented 1 year ago

@alphabet5 I fixed this in master now. Please take a look.