diff --git a/admin.go b/admin.go new file mode 100644 index 0000000..1d95130 --- /dev/null +++ b/admin.go @@ -0,0 +1,95 @@ +package main + +import ( + "context" + "net/http" + "os/signal" + "syscall" + "time" + + "github.com/gin-gonic/gin" + "github.com/robfig/cron/v3" + "github.com/sethvargo/go-password/password" + log "github.com/sirupsen/logrus" +) + +type AdminConfig struct { + Users []User `json:"users"` + Secrets SecretsConfig `json:"secrets"` + Addr string `json:"addr"` +} + +type SecretsConfig struct { + PasswordPepper string `json:"password_pepper"` + ContextKey string `json:"context_key"` + ContextExpiration int `json:"context_expiration"` + ScryptN int `json:"scrypt_n"` + ScryptR int `json:"scrypt_r"` + ScryptP int `json:"scrypt_p"` +} + +func NewAdmin() *AdminConfig { + pepper, _ := password.Generate(20, 5, 0, false, false) + ctx, _ := password.Generate(20, 5, 0, false, false) + + a := &AdminConfig{ + Addr: "0.0.0.0:8080", + Secrets: SecretsConfig{ + PasswordPepper: pepper, + ContextKey: ctx, + ContextExpiration: 3600, + ScryptN: 32768, + ScryptR: 8, + ScryptP: 1, + }, + Users: make([]User, 0), + } + + return a +} + +func (a *AdminConfig) Run() { + log.WithFields(log.Fields{}).Debugf("starting") + defer log.WithFields(log.Fields{}).Debugf("done") + + // Create context that listens for the interrupt signal from the OS. + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer stop() + + r := gin.Default() + r.GET("/ping", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "message": "pong", + }) + }) + + srv := &http.Server{ + Addr: ":8080", + Handler: r, + } + + go func() { + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.WithFields(log.Fields{"call": "http.ListenAndServe", "attr": a.Addr, "error": err}).Errorf("") + } + }() + + c := cron.New(cron.WithLocation(time.UTC)) + c.AddFunc("00 * * *", func() { cfg.Run() }) + + // Listen for the interrupt signal. + <-ctx.Done() + + // Restore default behavior on the interrupt signal and notify user of shutdown. + stop() + log.WithFields(log.Fields{"call": "stop"}).Warnf("shutting down") + + // The context is used to inform the server it has 5 seconds to finish + // the request it is currently handling + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := srv.Shutdown(ctx); err != nil { + log.WithFields(log.Fields{"call": "http.Shutdown", "error": err}).Errorf("shutting down") + } + +} diff --git a/backup.go b/backup.go index 335257b..6627d39 100644 --- a/backup.go +++ b/backup.go @@ -51,22 +51,12 @@ func main() { } if *isDaemon { - cfg.Start(nil) - server := NewServer(cfg.Admin.Addr, cfg.Admin.Username, cfg.Admin.Password) - server.Run() - } else { - e := NewEmail(time.Now()) - cfg.Start(e) - defer cfg.Stop(e) - - cfg.Run(e) - - cfg.Cleanup(e) - - if err := e.Send(); err != nil { - log.Printf("Cannot send email (%s)", err) + if cfg.Admin == nil { + cfg.Admin = NewAdmin() } - + cfg.Admin.Run() + } else { + cfg.Run() os.Exit(0) } diff --git a/config.go b/config.go index 12e146b..f398540 100644 --- a/config.go +++ b/config.go @@ -19,19 +19,13 @@ type Config struct { Email EmailConfig `json:"email"` Apps []AppConfig `json:"apps"` Timezone string `json:"timezone"` + Admin *AdminConfig `json:"admin"` Debug bool `json:"debug"` - Admin AdminConfig `json:"admin"` box map[string]*Box `json:"-"` apps map[string]*App `json:"-"` timezone *time.Location `json:"-"` } -type AdminConfig struct { - Addr string `json:"addr"` - Username string `json:"username"` - Password string `json:"password"` -} - type BoxConfig struct { Addr string `json:"addr"` User string `json:"user"` @@ -159,34 +153,31 @@ func (c *Config) LoadFile(path string) error { return nil } -func (c *Config) Start(e *Email) { +// Run config +func (c *Config) Run() { log.WithFields(log.Fields{}).Debugf("starting") defer log.WithFields(log.Fields{}).Debugf("done") + e := NewEmail(time.Now()) + var wg sync.WaitGroup + + // Setup boxes for _, b := range c.box { wg.Add(1) go func(box *Box) { defer wg.Done() if err := box.Open(); err != nil { log.WithFields(log.Fields{"name": box.name, "call": "Open", "error": err}).Errorf("") - if e != nil { - e.AddItem(fmt.Sprintf(" - Box : %s is down", box.name)) - } + e.AddItem(fmt.Sprintf(" - Box : %s is down", box.name)) return } }(b) } wg.Wait() -} -// Run config -func (c *Config) Run(e *Email) { - log.WithFields(log.Fields{}).Debugf("starting") - defer log.WithFields(log.Fields{}).Debugf("done") - - var wg sync.WaitGroup + // Run each app for _, a := range cfg.apps { wg.Add(1) go func(app *App) { @@ -199,13 +190,7 @@ func (c *Config) Run(e *Email) { wg.Wait() - return -} - -func (c *Config) Cleanup(e *Email) { - log.WithFields(log.Fields{}).Debugf("starting") - defer log.WithFields(log.Fields{}).Debugf("done") - + // Cleanup for _, a := range cfg.apps { for _, src := range a.sources { if b, ok := c.box[src.Box()]; ok { @@ -233,23 +218,15 @@ func (c *Config) Cleanup(e *Email) { log.WithFields(log.Fields{"box": b.name, "fs": fs.path}).Warnf("not backed up") e.AddItem(fmt.Sprintf(" - Src : Folder not backed up (%s)", b.name+":"+fs.path)) } - if len(fs.destApps) == 0 && len(fs.srcApps) == 0 && fs.managed { + if len(fs.destApps) == 0 && !fs.backedUp && fs.managed { log.WithFields(log.Fields{"box": b.name, "fs": fs.path}).Warnf("managed") e.AddItem(fmt.Sprintf(" - Dest : Folder managed (%s)", b.name+":"+fs.path)) } - if fs.managed { - log.WithFields(log.Fields{"box": b.name, "fs": fs.path, "src": len(fs.srcApps), "dest": len(fs.destApps)}).Warnf("managed") - } } } } -} - -func (c *Config) Stop(e *Email) { - log.WithFields(log.Fields{}).Debugf("starting") - defer log.WithFields(log.Fields{}).Debugf("done") - + // Stop for _, b := range c.box { if err := b.Close(); err != nil { log.WithFields(log.Fields{"name": b.name, "call": "Close", "error": err}).Errorf("") @@ -261,4 +238,6 @@ func (c *Config) Stop(e *Email) { log.WithFields(log.Fields{"call": "email.Send", "error": err}).Errorf("") } } + + return } diff --git a/cron.go b/cron.go deleted file mode 100644 index 06ab7d0..0000000 --- a/cron.go +++ /dev/null @@ -1 +0,0 @@ -package main diff --git a/go.mod b/go.mod index 62b8a62..9005d2e 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,8 @@ go 1.16 require ( github.com/gin-gonic/gin v1.9.1 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect + github.com/sethvargo/go-password v0.2.0 // indirect github.com/silenceper/pool v1.0.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect diff --git a/go.sum b/go.sum index 5e17c11..29ad36e 100644 --- a/go.sum +++ b/go.sum @@ -44,6 +44,10 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/sethvargo/go-password v0.2.0 h1:BTDl4CC/gjf/axHMaDQtw507ogrXLci6XRiLc7i/UHI= +github.com/sethvargo/go-password v0.2.0/go.mod h1:Ym4Mr9JXLBycr02MFuVQ/0JHidNetSgbzutTr3zsYXE= github.com/silenceper/pool v1.0.0 h1:JTCaA+U6hJAA0P8nCx+JfsRCHMwLTfatsm5QXelffmU= github.com/silenceper/pool v1.0.0/go.mod h1:3DN13bqAbq86Lmzf6iUXWEPIWFPOSYVfaoceFvilKKI= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= diff --git a/server.go b/server.go deleted file mode 100644 index 1534fc1..0000000 --- a/server.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "net/http" - - "github.com/gin-gonic/gin" -) - -type Server struct { - Addr string - Username string - Password string -} - -func NewServer(addr, username, password string) *Server { - s := &Server{ - Addr: serverAddr, - Username: serverUsername, - Password: serverPassword, - } - - if addr != "" { - s.Addr = addr - } - if username != "" { - s.Username = username - } - if password != "" { - s.Password = password - } - - return s - -} - -func (s *Server) Run() { - r := gin.Default() - r.GET("/ping", func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{ - "message": "pong", - }) - }) - r.Run(s.Addr) // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080") -} diff --git a/user.go b/user.go new file mode 100644 index 0000000..f7badf6 --- /dev/null +++ b/user.go @@ -0,0 +1,76 @@ +package main + +import ( + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "errors" + + log "github.com/sirupsen/logrus" + "golang.org/x/crypto/scrypt" +) + +type User struct { + Username string `json:"username"` + Salt string `json:"salt"` + Passwd string `json:"passwd"` +} + +func NewUser(name, passwd string) (*User, error) { + log.WithFields(log.Fields{"name": name}).Debugf("starting") + defer log.WithFields(log.Fields{"name": name}).Debugf("done") + + for _, v := range cfg.Admin.Users { + if v.Username == name { + err := errors.New("user already exists") + log.WithFields(log.Fields{"name": name, "error": err}).Errorf("") + return nil, err + } + } + + u := &User{ + Username: name, + } + + salt := make([]byte, 32) + if _, err := rand.Read(salt); err != nil { + log.WithFields(log.Fields{"name": name, "call": "rand.Read", "error": err}).Errorf("") + return nil, err + } + u.Salt = hex.EncodeToString(salt) + + if pass, err := u.HashPassword(passwd); err != nil { + log.WithFields(log.Fields{"name": name, "call": "HashPassword", "error": err}).Errorf("") + return nil, err + } else { + u.Passwd = hex.EncodeToString(pass) + } + + return u, nil +} + +func (u *User) HashPassword(passwd string) ([]byte, error) { + log.WithFields(log.Fields{}).Debugf("starting") + defer log.WithFields(log.Fields{}).Debugf("done") + + //peppering the pass + hash := hmac.New(sha256.New, []byte(cfg.Admin.Secrets.PasswordPepper)) + hash.Write([]byte(passwd)) + hashPass := hash.Sum(nil) + + //salting the hash + salt := make([]byte, 32) + if _, err := hex.Decode(salt, []byte(u.Salt)); err != nil { + log.WithFields(log.Fields{"call": "hex.Decode", "error": err}).Errorf("") + return nil, err + } + + if h, err := scrypt.Key(hashPass, salt, cfg.Admin.Secrets.ScryptN, cfg.Admin.Secrets.ScryptR, cfg.Admin.Secrets.ScryptP, 32); err != nil { + log.WithFields(log.Fields{"call": "scrypt.Key", "error": err}).Errorf("") + return h, err + } else { + return h, nil + } + +}