mirror of
https://github.com/bytedream/docker4ssh.git
synced 2025-06-27 09:50:31 +02:00
Initial commit
This commit is contained in:
189
server/config/config.go
Normal file
189
server/config/config.go
Normal 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
254
server/config/profile.go
Normal 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
|
||||
}
|
Reference in New Issue
Block a user