package config import ( "crypto/tls" "crypto/x509" "errors" "fmt" "io" "io/fs" "io/ioutil" "os" "path/filepath" "strings" "git.t-juice.club/torjus/ezshare/store" "github.com/pelletier/go-toml" "go.uber.org/zap" "go.uber.org/zap/zapcore" "google.golang.org/grpc/credentials" ) var ErrNotFound = fmt.Errorf("config not found") type Config struct { Server *ServerConfig `toml:"Server"` Client *ClientConfig `toml:"Client"` location string } type CertificatePaths struct { CertificatePath string `toml:"CertificatePath"` CertificateKeyPath string `toml:"CertificateKeyPath"` } type ServerConfig struct { LogLevel string `toml:"LogLevel"` Hostname string `toml:"Hostname"` GRPCEndpoint string `toml:"GRPCEndpoint"` DataStoreConfig *ServerDataStoreConfig `toml:"DataStore"` FileStoreConfig *ServerFileStoreConfig `toml:"FileStore"` GRPC *ServerGRPCConfig `toml:"GRPC"` HTTP *ServerHTTPConfig `toml:"HTTP"` } type ServerFileStoreConfig struct { Type string `toml:"Type"` FSStoreConfig *FSStoreConfig `toml:"Filesystem"` BoltStoreConfig *BoltStoreConfig `toml:"Bolt"` } type ServerDataStoreConfig struct { Type string `toml:"Type"` BoltStoreConfig *BoltStoreConfig `toml:"Bolt"` } type BoltStoreConfig struct { Path string `toml:"Path"` } type FSStoreConfig struct { Dir string `toml:"Dir"` } type ServerGRPCConfig struct { ListenAddr string `toml:"ListenAddr"` CACerts *CertificatePaths `toml:"CACerts"` Certs *CertificatePaths `toml:"Certs"` } type ServerHTTPConfig struct { ListenAddr string `toml:"ListenAddr"` } type ClientConfig struct { DefaultServer string `toml:"DefaultServer"` ServerCertPath string `toml:"ServerCertPath"` Certs *CertificatePaths `toml:"Certs"` } func FromDefault() *Config { cfg := &Config{ Server: &ServerConfig{ LogLevel: "INFO", GRPC: &ServerGRPCConfig{ ListenAddr: ":50051", CACerts: &CertificatePaths{}, Certs: &CertificatePaths{}, }, HTTP: &ServerHTTPConfig{ ListenAddr: ":8089", }, DataStoreConfig: &ServerDataStoreConfig{ BoltStoreConfig: &BoltStoreConfig{}, }, FileStoreConfig: &ServerFileStoreConfig{ BoltStoreConfig: &BoltStoreConfig{}, FSStoreConfig: &FSStoreConfig{}, }, }, Client: &ClientConfig{ Certs: &CertificatePaths{}, }, } return cfg } func FromReader(r io.Reader) (*Config, error) { decoder := toml.NewDecoder(r) c := FromDefault() if err := decoder.Decode(c); err != nil { return nil, fmt.Errorf("unable to read config: %w", err) } return c, nil } func FromFile(path string) (*Config, error) { f, err := os.Open(path) if err != nil { if errors.Is(err, os.ErrNotExist) { return nil, ErrNotFound } return nil, fmt.Errorf("unable to open config-file: %w", err) } defer f.Close() cfg, err := FromReader(f) if err == nil { cfg.location = path } return cfg, err } func FromDefaultLocations() (*Config, error) { defaultLocations := []string{ "ezshare.toml", } userConfigDir, err := os.UserConfigDir() if err == nil { defaultLocations = append(defaultLocations, filepath.Join(userConfigDir, "ezshare", "ezshare.toml")) } for _, location := range defaultLocations { if _, err := os.Stat(location); err == nil { return FromFile(location) } } return nil, fmt.Errorf("config not found") } func (c *Config) UpdateFromEnv() { // Server stuff if val, found := os.LookupEnv("EZSHARE_SERVER_LOGLEVEL"); found { c.Server.LogLevel = val } if val, found := os.LookupEnv("EZSHARE_SERVER_HOSTNAME"); found { c.Server.Hostname = val } if val, found := os.LookupEnv("EZSHARE_SERVER_GRPCENDPOINT"); found { c.Server.GRPCEndpoint = val } if val, found := os.LookupEnv("EZSHARE_SERVER_DATASTORE_TYPE"); found { c.Server.DataStoreConfig.Type = val } if val, found := os.LookupEnv("EZSHARE_SERVER_DATASTORE_BOLT_PATH"); found { c.Server.DataStoreConfig.BoltStoreConfig.Path = val } if val, found := os.LookupEnv("EZSHARE_SERVER_FILESTORE_TYPE"); found { c.Server.FileStoreConfig.Type = val } if val, found := os.LookupEnv("EZSHARE_SERVER_FILESTORE_BOLT_PATH"); found { c.Server.FileStoreConfig.BoltStoreConfig.Path = val } if val, found := os.LookupEnv("EZSHARE_SERVER_FILESTORE_FILESYSTEM_DIR"); found { c.Server.FileStoreConfig.FSStoreConfig.Dir = val } if val, found := os.LookupEnv("EZSHARE_SERVER_GRPC_CACERTS_CERTIFICATEKEYPATH"); found { c.Server.GRPC.CACerts.CertificateKeyPath = val } if val, found := os.LookupEnv("EZSHARE_SERVER_GRPC_CACERTS_CERTIFICATEPATH"); found { c.Server.GRPC.CACerts.CertificatePath = val } if val, found := os.LookupEnv("EZSHARE_SERVER_GRPC_CERTS_CERTIFICATEKEYPATH"); found { c.Server.GRPC.Certs.CertificateKeyPath = val } if val, found := os.LookupEnv("EZSHARE_SERVER_GRPC_CERTS_CERTIFICATEPATH"); found { c.Server.GRPC.Certs.CertificatePath = val } if val, found := os.LookupEnv("EZSHARE_SERVER_GRPC_LISTENADDR"); found { c.Server.GRPC.ListenAddr = val } if val, found := os.LookupEnv("EZSHARE_SERVER_HTTP_LISTENADDR"); found { c.Server.HTTP.ListenAddr = val } // Client stuff if val, found := os.LookupEnv("EZSHARE_CLIENT_DEFAULTSERVER"); found { c.Client.DefaultServer = val } if val, found := os.LookupEnv("EZSHARE_CLIENT_CERTS_CERTIFICATEKEYPATH"); found { c.Client.Certs.CertificateKeyPath = val } if val, found := os.LookupEnv("EZSHARE_CLIENT_CERTS_CERTIFICATEPATH"); found { c.Client.Certs.CertificatePath = val } if val, found := os.LookupEnv("EZSHARE_CLIENT_SERVERCERTPATH"); found { c.Client.ServerCertPath = val } } func (sc *ServerConfig) Valid() error { // Verify that grpc-endpoint is set if sc.GRPCEndpoint == "" { return fmt.Errorf("missing require config-value Server.GRPCEndpoint") } // Verify loglevel switch strings.ToUpper(sc.LogLevel) { case "DEBUG", "INFO", "WARN", "ERROR", "FATAL": break default: return fmt.Errorf("config-value Server.LogLevel is invalid") } // Verify datastore config switch strings.ToLower(sc.DataStoreConfig.Type) { case "memory": break case "bolt": if sc.DataStoreConfig.BoltStoreConfig == nil || sc.DataStoreConfig.BoltStoreConfig.Path == "" { return fmt.Errorf("server datastore is bolt, missing required config value Server.DataStore.Bolt.Path") } default: return fmt.Errorf("config-value Server.DataStore.Type is invalid") } // Verify filestore config switch strings.ToLower(sc.FileStoreConfig.Type) { case "memory": break case "filesystem": if sc.FileStoreConfig.FSStoreConfig == nil || sc.FileStoreConfig.FSStoreConfig.Dir == "" { return fmt.Errorf("server datastore is bolt, missing required config value Server.FileStore.FSStore.Path") } case "bolt": if sc.FileStoreConfig.BoltStoreConfig == nil || sc.FileStoreConfig.BoltStoreConfig.Path == "" { return fmt.Errorf("server datastore is bolt, missing required config value Server.DataStore.Bolt.Path") } } // Verify grpc-config if sc.GRPC.ListenAddr == "" { return fmt.Errorf("missing required config-value Server.GRPC.ListenAddr") } if sc.GRPC.CACerts.CertificateKeyPath == "" { // TODO: Maybe return custom error, so we can create certs if missing return fmt.Errorf("missing require value Server.GRPC.CACerts.CertificateKeyPath") } if sc.GRPC.CACerts.CertificatePath == "" { // TODO: Maybe return custom error, so we can create certs if missing return fmt.Errorf("missing require value Server.GRPC.CACerts.CertificatePath") } if sc.GRPC.Certs.CertificatePath == "" { // TODO: Maybe return custom error, so we can create certs if missing return fmt.Errorf("missing require value Server.GRPC.Certs.CertificatePath") } if sc.GRPC.Certs.CertificateKeyPath == "" { // TODO: Maybe return custom error, so we can create certs if missing return fmt.Errorf("missing require value Server.GRPC.Certs.CertificateKeyPath") } return nil } func (cc *ClientConfig) Valid() error { if cc.Certs.CertificateKeyPath == "" { return fmt.Errorf("missing required value Client.Certs.CertificateKeyPath") } if cc.Certs.CertificatePath == "" { return fmt.Errorf("missing required value Client.Certs.CertificatePath") } if cc.DefaultServer == "" { // TODO: Should probably have its own custom error return fmt.Errorf("missing required value Client.DefaultServer") } if cc.ServerCertPath == "" { // TODO: Should probably have its own custom error return fmt.Errorf("missing required value Client.ServerCertPath") } return nil } func (c *Config) Location() string { return c.location } func (cp *CertificatePaths) GetCertBytes() ([]byte, error) { f, err := os.Open(cp.CertificatePath) if err != nil { return nil, err } return ioutil.ReadAll(f) } func (cp *CertificatePaths) GetKeyBytes() ([]byte, error) { f, err := os.Open(cp.CertificateKeyPath) if err != nil { return nil, err } return ioutil.ReadAll(f) } func (cc *ClientConfig) ServerCertBytes() ([]byte, error) { f, err := os.Open(cc.ServerCertPath) if err != nil { return nil, fmt.Errorf("unable to open server certificate: %w", err) } defer f.Close() data, err := ioutil.ReadAll(f) if err != nil { return nil, fmt.Errorf("unable to read client server certificate: %w", err) } return data, nil } func (cc *ClientConfig) Creds() (credentials.TransportCredentials, error) { srvCertBytes, err := cc.ServerCertBytes() if err != nil { return nil, err } clientCertBytes, err := cc.Certs.GetCertBytes() if err != nil { return nil, fmt.Errorf("unable to read client cert: %w", err) } clientKeyBytes, err := cc.Certs.GetKeyBytes() if err != nil { return nil, fmt.Errorf("unable to read client cert: %w", err) } certPool := x509.NewCertPool() if !certPool.AppendCertsFromPEM(srvCertBytes) { return nil, fmt.Errorf("unable to load ca cert") } clientCert, err := tls.X509KeyPair(clientCertBytes, clientKeyBytes) if err != nil { return nil, fmt.Errorf("unable to load client cert: %s", err) } config := &tls.Config{ Certificates: []tls.Certificate{clientCert}, RootCAs: certPool, } return credentials.NewTLS(config), nil } func CreateDefaultConfigDir() error { userConfigDir, err := os.UserConfigDir() if err != nil { return err } configDirPath := filepath.Join(userConfigDir, "ezshare") info, err := os.Stat(configDirPath) if err != nil { if !errors.Is(err, fs.ErrNotExist) { return err } if err := os.Mkdir(configDirPath, 0755); err != nil { return fmt.Errorf("unable to create config-dir: %w", err) } } else { if !info.IsDir() { return fmt.Errorf("config-directory is not a directory") } } return nil } func DefaultConfigFilePath() (string, error) { userConfigDir, err := os.UserConfigDir() if err != nil { return "", err } return filepath.Join(userConfigDir, "ezshare", "ezshare.toml"), nil } func (c *Config) ToWriter(w io.Writer) error { encoder := toml.NewEncoder(w) return encoder.Encode(c) } func (c *Config) ToDefaultFile() error { if err := CreateDefaultConfigDir(); err != nil { return err } configFilePath, err := DefaultConfigFilePath() if err != nil { return err } _, err = os.Stat(configFilePath) if err != nil { if !errors.Is(err, fs.ErrNotExist) { return fmt.Errorf("error stating config-file: %w", err) } f, err := os.Create(configFilePath) if err != nil { return fmt.Errorf("unable to create config-file: %w", err) } encoder := toml.NewEncoder(f) fmt.Printf("Writing config to '%s'", configFilePath) return encoder.Encode(c) } return fmt.Errorf("config-file already exists") } func (sc *ServerFileStoreConfig) GetStore() (store.FileStore, func() error, error) { nopCloseFunc := func() error { return nil } if strings.EqualFold(sc.Type, "bolt") { s, err := store.NewBoltStore(sc.BoltStoreConfig.Path) if err != nil { return nil, nil, err } return s, s.Close, err } if strings.EqualFold(sc.Type, "filesystem") { s := store.NewFileSystemStore(sc.FSStoreConfig.Dir) return s, nopCloseFunc, nil } if strings.EqualFold(sc.Type, "memory") { return store.NewMemoryStore(), nopCloseFunc, nil } if strings.EqualFold(sc.Type, "bolt") { s, err := store.NewBoltStore(sc.BoltStoreConfig.Path) if err != nil { return nil, nil, err } closeFunc := func() error { return s.Close() } return s, closeFunc, nil } return nil, nil, fmt.Errorf("invalid store config") } func (sc *ServerDataStoreConfig) GetStore() (store.DataStore, func() error, error) { nopCloseFunc := func() error { return nil } if strings.EqualFold(sc.Type, "bolt") { s, err := store.NewBoltStore(sc.BoltStoreConfig.Path) if err != nil { return nil, nil, err } return s, s.Close, err } if strings.EqualFold(sc.Type, "memory") { return store.NewMemoryStore(), nopCloseFunc, nil } if strings.EqualFold(sc.Type, "bolt") { s, err := store.NewBoltStore(sc.BoltStoreConfig.Path) if err != nil { return nil, nil, err } closeFunc := func() error { return s.Close() } return s, closeFunc, nil } return nil, nil, fmt.Errorf("invalid store config") } func (c *ServerConfig) GetLogger() *zap.SugaredLogger { logEncoderConfig := zap.NewProductionEncoderConfig() logEncoderConfig.EncodeCaller = zapcore.ShortCallerEncoder logEncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder logEncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder logEncoderConfig.EncodeDuration = zapcore.StringDurationEncoder rootLoggerConfig := &zap.Config{ Level: zap.NewAtomicLevelAt(zap.DebugLevel), OutputPaths: []string{"stdout"}, ErrorOutputPaths: []string{"stdout"}, Encoding: "console", EncoderConfig: logEncoderConfig, DisableCaller: true, } switch strings.ToUpper(c.LogLevel) { case "DEBUG": rootLoggerConfig.DisableCaller = false rootLoggerConfig.Level = zap.NewAtomicLevelAt(zap.DebugLevel) case "INFO": rootLoggerConfig.Level = zap.NewAtomicLevelAt(zap.InfoLevel) case "WARN", "WARNING": rootLoggerConfig.Level = zap.NewAtomicLevelAt(zap.WarnLevel) case "ERR", "ERROR": rootLoggerConfig.Level = zap.NewAtomicLevelAt(zap.ErrorLevel) } rootLogger, err := rootLoggerConfig.Build() if err != nil { panic(err) } return rootLogger.Sugar() }