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 }