Initial commit

This commit is contained in:
2021-12-19 17:30:51 +01:00
commit a589014106
65 changed files with 7437 additions and 0 deletions

189
server/config/config.go Normal file
View File

@ -0,0 +1,189 @@
package config
import (
"fmt"
"github.com/BurntSushi/toml"
"os"
"path/filepath"
"reflect"
"strconv"
"strings"
)
var globConfig *Config
type Config struct {
Profile struct {
Dir string `toml:"Dir"`
Default struct {
Password string `toml:"Password"`
NetworkMode int `toml:"NetworkMode"`
Configurable bool `toml:"Configurable"`
RunLevel int `toml:"RunLevel"`
StartupInformation bool `toml:"StartupInformation"`
ExitAfter string `toml:"ExitAfter"`
KeepOnExit bool `toml:"KeepOnExit"`
} `toml:"default"`
Dynamic struct {
Enable bool `toml:"Enable"`
Password string `toml:"Password"`
NetworkMode int `toml:"NetworkMode"`
Configurable bool `toml:"Configurable"`
RunLevel int `toml:"RunLevel"`
StartupInformation bool `toml:"StartupInformation"`
ExitAfter string `toml:"ExitAfter"`
KeepOnExit bool `toml:"KeepOnExit"`
} `toml:"dynamic"`
} `toml:"profile"`
Api struct {
Port uint16 `toml:"Port"`
Configure struct {
Binary string `toml:"Binary"`
Man string `toml:"Man"`
} `toml:"configure"`
} `toml:"api"`
SSH struct {
Port uint16 `toml:"Port"`
Keyfile string `toml:"Keyfile"`
Passphrase string `toml:"Passphrase"`
} `toml:"ssh"`
Database struct {
Sqlite3File string `toml:"Sqlite3File"`
} `toml:"Database"`
Network struct {
Default struct {
Subnet string `toml:"Subnet"`
} `toml:"default"`
Isolate struct {
Subnet string `toml:"Subnet"`
} `toml:"isolate"`
} `toml:"network"`
Logging struct {
Level string `toml:"Level"`
OutputFile string `toml:"OutputFile"`
ErrorFile string `toml:"ErrorFile"`
ConsoleOutput bool `toml:"ConsoleOutput"`
ConsoleError bool `toml:"ConsoleError"`
} `toml:"logging"`
}
func InitConfig(includeEnv bool) (*Config, error) {
configFiles := []string{
"./docker4ssh.conf",
"~/.docker4ssh",
"~/.config/docker4ssh.conf",
"/etc/docker4ssh/docker4ssh.conf",
}
for _, file := range configFiles {
if _, err := os.Stat(file); !os.IsNotExist(err) {
return LoadConfig(file, includeEnv)
}
}
return nil, fmt.Errorf("no speicfied config file (%s) could be found", strings.Join(configFiles, ", "))
}
func LoadConfig(file string, includeEnv bool) (*Config, error) {
config := &Config{}
if _, err := toml.DecodeFile(file, config); err != nil {
return nil, err
}
// make paths absolute
dir := filepath.Dir(file)
config.Profile.Dir = absoluteFile(dir, config.Profile.Dir)
config.Api.Configure.Binary = absoluteFile(dir, config.Api.Configure.Binary)
config.Api.Configure.Man = absoluteFile(dir, config.Api.Configure.Man)
config.SSH.Keyfile = absoluteFile(dir, config.SSH.Keyfile)
config.Database.Sqlite3File = absoluteFile(dir, config.Database.Sqlite3File)
config.Logging.OutputFile = absoluteFile(dir, config.Logging.OutputFile)
config.Logging.ErrorFile = absoluteFile(dir, config.Logging.ErrorFile)
if includeEnv {
if err := updateFromEnv(config); err != nil {
return nil, err
}
}
return config, nil
}
func absoluteFile(path, file string) string {
if filepath.IsAbs(file) {
return file
}
return filepath.Join(path, file)
}
// updateFromEnv looks up if specific environment variable are given which can
// also be used to configure the program.
// Every key in the config file can also be specified via environment variables.
// The env variable syntax is SECTION_KEY -> e.g. DEFAULT_PASSWORD or API_PORT
func updateFromEnv(config *Config) error {
re := reflect.ValueOf(config).Elem()
rt := re.Type()
for i := 0; i < re.NumField(); i++ {
rf := re.Field(i)
ree := rt.Field(i)
if err := envParseField(strings.ToUpper(ree.Tag.Get("toml")), rf); err != nil {
return err
}
}
return nil
}
func envParseField(prefix string, value reflect.Value) error {
for j := 0; j < value.NumField(); j++ {
rtt := value.Type().Field(j)
rff := value.Field(j)
if rff.Kind() == reflect.Struct {
if err := envParseField(fmt.Sprintf("%s_%s", prefix, strings.ToUpper(rtt.Tag.Get("toml"))), rff); err != nil {
return err
}
continue
}
envName := fmt.Sprintf("%s_%s", prefix, strings.ToUpper(rtt.Tag.Get("toml")))
val, ok := os.LookupEnv(envName)
if !ok {
continue
}
var expected string
switch rff.Kind() {
case reflect.String:
rff.SetString(val)
continue
case reflect.Bool:
b, err := strconv.ParseBool(val)
if err == nil {
rff.SetBool(b)
continue
}
expected = "true / false (boolean)"
case reflect.Uint16:
ui, err := strconv.ParseUint(val, 10, 16)
if err == nil {
rff.SetUint(ui)
continue
}
expected = "number (uint16)"
default:
return fmt.Errorf("parsed not implemented config type '%s'", rff.Kind())
}
return fmt.Errorf("failed to parse environment variable '%s': cannot parse value '%s' as %s", envName, val, expected)
}
return nil
}
func GetConfig() *Config {
return globConfig
}
func SetConfig(config *Config) {
globConfig = config
}

254
server/config/profile.go Normal file
View File

@ -0,0 +1,254 @@
package config
import (
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/json"
"fmt"
"github.com/BurntSushi/toml"
"go.uber.org/zap"
"hash"
"os"
"path/filepath"
"regexp"
"strings"
)
type Profile struct {
name string
Username *regexp.Regexp
Password *regexp.Regexp
passwordHashAlgo hash.Hash
NetworkMode int
Configurable bool
RunLevel int
StartupInformation bool
ExitAfter string
KeepOnExit bool
Image string
ContainerID string
}
func (p *Profile) Name() string {
return p.name
}
func (p *Profile) Match(user string, password []byte) bool {
// username should only be nil if profile was generated from Config.Profile.Dynamic
if p.Username == nil || p.Username.MatchString(user) {
if p.passwordHashAlgo != nil {
password = p.passwordHashAlgo.Sum(password)
}
return p.Password.Match(password)
}
return false
}
type preProfile struct {
Username string
Password string
NetworkMode int
Configurable bool
RunLevel int
StartupInformation bool
ExitAfter string
KeepOnExit bool
Image string
Container string
}
func LoadProfileFile(path string, defaultPreProfile preProfile) (Profiles, error) {
var rawProfile map[string]interface{}
if _, err := toml.DecodeFile(path, &rawProfile); err != nil {
return nil, err
}
profiles, err := parseRawProfile(rawProfile, path, defaultPreProfile)
if err != nil {
return nil, err
}
return profiles, nil
}
func LoadProfileDir(path string, defaultPreProfile preProfile) (Profiles, error) {
dir, err := os.ReadDir(path)
if err != nil {
return nil, err
}
var profiles Profiles
for i, profileConf := range dir {
p, err := LoadProfileFile(filepath.Join(path, profileConf.Name()), defaultPreProfile)
if err != nil {
return nil, err
}
profiles = append(profiles, p...)
zap.S().Debugf("Pre-loaded file %d (%s) with %d profile(s)", i+1, profileConf.Name(), len(p))
}
return profiles, nil
}
func parseRawProfile(rawProfile map[string]interface{}, path string, defaultPreProfile preProfile) (profiles []*Profile, err error) {
var count int
for key, value := range rawProfile {
rawValue, err := json.Marshal(value)
if err != nil {
return nil, err
}
pp := preProfile{
NetworkMode: 3,
RunLevel: 1,
StartupInformation: true,
}
if err = json.Unmarshal(rawValue, &pp); err != nil {
return nil, fmt.Errorf("failed to parse %s profile conf file %s: %v", key, path, err)
}
var rawUsername string
if rawUsername = strings.TrimPrefix(pp.Username, "regex:"); rawUsername == pp.Username {
rawUsername = strings.ReplaceAll(rawUsername, "*", ".*")
}
if !strings.HasSuffix(rawUsername, "$") {
rawUsername += "$"
}
username, err := regexp.Compile("(?m)" + rawUsername)
if err != nil {
return nil, fmt.Errorf("failed to parse %s profile username regex for conf file %s: %v", key, path, err)
}
var rawPassword string
if rawPassword = strings.TrimPrefix(pp.Password, "regex:"); rawUsername == pp.Password {
rawPassword = strings.ReplaceAll(rawPassword, "*", ".*")
}
algo, rawPasswordOrHash := getHash(rawPassword)
if algo == nil && rawPasswordOrHash == "" {
rawPasswordOrHash = ".*"
}
if !strings.HasSuffix(rawPasswordOrHash, "$") {
rawPasswordOrHash += "$"
}
password, err := regexp.Compile("(?m)" + rawPasswordOrHash)
if err != nil {
return nil, fmt.Errorf("failed to parse %s profile password regex for conf file %s: %v", key, path, err)
}
if (pp.Image == "") == (pp.Container == "") {
return nil, fmt.Errorf("failed to interpret %s profile image / container definition for conf file %s: `Image` or `Container` must be specified, not both nor none of them", key, path)
}
profiles = append(profiles, &Profile{
name: key,
Username: username,
Password: password,
passwordHashAlgo: algo,
NetworkMode: pp.NetworkMode,
Configurable: pp.Configurable,
RunLevel: pp.RunLevel,
StartupInformation: pp.StartupInformation,
ExitAfter: pp.ExitAfter,
KeepOnExit: pp.KeepOnExit,
Image: pp.Image,
ContainerID: pp.Container,
})
count++
zap.S().Debugf("Pre-loaded profile %s (%d)", key, count)
}
return
}
type Profiles []*Profile
func (ps Profiles) GetByName(name string) (*Profile, bool) {
for _, profile := range ps {
if profile.name == name {
return profile, true
}
}
return nil, false
}
func (ps Profiles) Match(user string, password []byte) (*Profile, bool) {
for _, profile := range ps {
if profile.Match(user, password) {
return profile, true
}
}
return nil, false
}
func DefaultPreProfileFromConfig(config *Config) preProfile {
defaultProfile := config.Profile.Default
return preProfile{
Password: defaultProfile.Password,
NetworkMode: defaultProfile.NetworkMode,
Configurable: defaultProfile.Configurable,
RunLevel: defaultProfile.RunLevel,
StartupInformation: defaultProfile.StartupInformation,
ExitAfter: defaultProfile.ExitAfter,
KeepOnExit: defaultProfile.KeepOnExit,
}
}
func HardcodedPreProfile() preProfile {
return preProfile{
NetworkMode: 3,
RunLevel: 1,
StartupInformation: true,
}
}
func DynamicProfileFromConfig(config *Config, defaultPreProfile preProfile) (Profile, error) {
raw, err := json.Marshal(config.Profile.Dynamic)
if err != nil {
return Profile{}, err
}
json.Unmarshal(raw, &defaultPreProfile)
algo, rawPasswordOrHash := getHash(defaultPreProfile.Password)
if algo == nil && rawPasswordOrHash == "" {
rawPasswordOrHash = ".*"
}
password, err := regexp.Compile("(?m)" + rawPasswordOrHash)
if err != nil {
return Profile{}, fmt.Errorf("failed to parse password regex: %v ", err)
}
return Profile{
name: "",
Username: nil,
Password: password,
passwordHashAlgo: algo,
NetworkMode: defaultPreProfile.NetworkMode,
Configurable: defaultPreProfile.Configurable,
RunLevel: defaultPreProfile.RunLevel,
StartupInformation: defaultPreProfile.StartupInformation,
ExitAfter: defaultPreProfile.ExitAfter,
KeepOnExit: defaultPreProfile.KeepOnExit,
}, nil
}
func getHash(password string) (algo hash.Hash, raw string) {
split := strings.SplitN(password, ":", 1)
if len(split) == 1 {
return nil, password
} else {
raw = split[1]
}
switch split[0] {
case "sha1":
algo = sha1.New()
case "sha256":
algo = sha256.New()
case "sha512":
algo = sha512.New()
default:
algo = nil
}
return
}