v2 #2

Merged
shoopea merged 29 commits from v2 into master 2023-08-21 20:03:11 +02:00
8 changed files with 196 additions and 95 deletions
Showing only changes of commit ca3d8177bd - Show all commits

95
admin.go Normal file
View File

@ -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")
}
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -1 +0,0 @@
package main

2
go.mod
View File

@ -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

4
go.sum
View File

@ -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=

View File

@ -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")
}

76
user.go Normal file
View File

@ -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
}
}