sermoni

"Service monitor" / cronjob status service
Log | Files | Refs

commit 32be3293511a53661a8259506a2a8382088d419c
parent 17e376bca14fc1b052285bf996572cf5734b5e76
Author: Vetle Haflan <vetle@haflan.dev>
Date:   Sun, 12 Apr 2020 04:00:55 +0200

Add config package and refactor ++

Diffstat:
Mcmd/sermoni/main.go | 7++++++-
Mgo.mod | 2++
Ainternal/config/config.go | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Minternal/database/database.go | 94+++++--------------------------------------------------------------------------
Ainternal/http/html_dev.go | 18++++++++++++++++++
Minternal/http/http.go | 41+++++++++++++++++++++++++++++++++++++----
6 files changed, 171 insertions(+), 93 deletions(-)

diff --git a/cmd/sermoni/main.go b/cmd/sermoni/main.go @@ -3,6 +3,7 @@ package main import ( "flag" "log" + "sermoni/internal/config" "sermoni/internal/database" smhttp "sermoni/internal/http" ) @@ -17,7 +18,11 @@ func main() { // TODO: Use getopt package instead of flags? //password := flag.String("w", "", "Password for the web interface") flag.Parse() - database.Init(*dbFile) + configured := database.Open(*dbFile) + if !configured { + log.Printf("Setting up new database '%v'\n", *dbFile) + config.InitConfig() + } defer database.Close() log.Printf("Server started listening on port %v\n", *port) smhttp.StartServer(*port) diff --git a/go.mod b/go.mod @@ -4,6 +4,8 @@ go 1.13 require ( github.com/fzipp/gocyclo v0.0.0-20150627053110-6acd4345c835 // indirect + github.com/gorilla/securecookie v1.1.1 + github.com/gorilla/sessions v1.2.0 go.etcd.io/bbolt v1.3.4 golang.org/x/crypto v0.0.0-20200406173513-056763e48d71 ) diff --git a/internal/config/config.go b/internal/config/config.go @@ -0,0 +1,102 @@ +package config + +import ( + "log" + "sermoni/internal/database" + + "github.com/gorilla/securecookie" + "go.etcd.io/bbolt" + "golang.org/x/crypto/bcrypt" +) + +var ( + defaultPassPhrase = []byte("admin") + defaultPageTitle = []byte("sermoni") + + keyPassHash = []byte("passhash") + keyPageTitle = []byte("pagetitle") + keySCHashKey = []byte("schashkey") // Secure cookie hash key + keySCBlockKey = []byte("blockkey") // Secure cookie block key + keySessionKey = []byte("sessionkey") // Session key + keyCSRFKey = []byte("csrfkey") // CSRF protection auth key +) + +// Config is a struct that contains all configuration parameters as []byte data +type Config struct { + PassHash []byte + PageTitle []byte + SCHashKey []byte + SCBlockKey []byte + SessionKey []byte + CSRFKey []byte +} + +// GetConfig Creates a Config struct from the values in database +// Should only be necessary to call once +func GetConfig() (config *Config) { + db := database.GetDB() + db.View(func(tx *bbolt.Tx) error { + b := tx.Bucket(database.BucketKeyConfig) + config = &Config{ + PassHash: b.Get(keyPassHash), + PageTitle: b.Get(keyPageTitle), + SCHashKey: b.Get(keySCHashKey), + SCBlockKey: b.Get(keySCBlockKey), + SessionKey: b.Get(keySessionKey), + CSRFKey: b.Get(keyCSRFKey), + } + return nil + }) + return + +} + +// InitConfig populates the config root bucket with default configurations +// (Web client) passphrase and page title can be reset later +func InitConfig() { + db := database.GetDB() + // TODO: Maybe this belongs elsewhere? + /* TODO: Generate a random _readable_ password if none is given + var passphraseBytes []byte + if passphrase == "" { + passphraseBytes = make([]byte, 24) + rand.Read(passphraseBytes) + passphrase = string(passphraseBytes) + fmt.Printf("Generated passphrase: %v\n", []rune(passphrase)) + } else { + passphraseBytes = []byte(passphrase) + }*/ + //sha256.Sum256([]byte(passphraseBytes)) + + // TODO: Maybe bcrypt is overkill for such a small project? Consider later + passhash, err := bcrypt.GenerateFromPassword(defaultPassPhrase, bcrypt.DefaultCost) + hashKey := securecookie.GenerateRandomKey(32) + blockKey := securecookie.GenerateRandomKey(32) + sessionKey := securecookie.GenerateRandomKey(32) + CSRFKey := securecookie.GenerateRandomKey(32) + check(err) + db.Update(func(tx *bbolt.Tx) error { + var err error + b := tx.Bucket(database.BucketKeyConfig) + err = b.Put(keyPassHash, passhash) + check(err) + err = b.Put(keyPageTitle, defaultPageTitle) + check(err) + err = b.Put(keySCHashKey, hashKey) + check(err) + err = b.Put(keySCBlockKey, blockKey) + check(err) + err = b.Put(keySessionKey, sessionKey) + check(err) + err = b.Put(keyCSRFKey, CSRFKey) + check(err) + return nil + }) +} + +// check for fatal errors +func check(err error) { + if err != nil { + log.Fatal(err) + } +} diff --git a/internal/database/database.go b/internal/database/database.go @@ -7,29 +7,15 @@ import ( "strconv" "time" - "golang.org/x/crypto/bcrypt" - - "github.com/gorilla/securecookie" "go.etcd.io/bbolt" ) -const ( - defaultPassPhrase = "admin" - defaultPageTitle = "sermoni" -) - -// bbolt keys +// bbolt bucket keys var ( BucketKeyConfig = []byte("config") // bucket key for config bucket key BucketKeyEvents = []byte("events") // bucket key for events bucket BucketKeyServices = []byte("services") // bucket key for services bucket BucketKeyServiceTokens = []byte("service-tokens") // bucket key for service-tokens bucket - - keyPassHash = []byte("passhash") - keyPageTitle = []byte("pagetitle") - keySCHashKey = []byte("schashkey") // Secure cookie hash key - keySCBlockKey = []byte("blockkey") // Secure cookie block key - keyCSRFKey = []byte("csrfkey") // CSRF protection auth key ) // ErrConfigBucket is returned when bbolt is unable to open the config bucket @@ -45,15 +31,15 @@ func check(err error) { } } -// Init opens the database for the given file name or creates it if it doesn't exist. -// It also populates it with essential configuration data if required. -func Init(dbFileName string) error { - log.Printf("Init db '%v'\n", dbFileName) +// Open opens the database for the given file name or creates it if it doesn't exist. +// Returns true if the database is already configured, false if it was just created. +func Open(dbFileName string) (configured bool) { var err error db, err = bbolt.Open(dbFileName, 0600, &bbolt.Options{Timeout: 1 * time.Second}) check(err) // Create the necessary bbolt buckets if they don't exist db.Update(func(tx *bbolt.Tx) error { + configured = tx.Bucket(BucketKeyConfig) != nil _, err = tx.CreateBucketIfNotExists(BucketKeyConfig) check(err) _, err = tx.CreateBucketIfNotExists(BucketKeyServices) @@ -64,75 +50,7 @@ func Init(dbFileName string) error { check(err) return nil }) - // Check if the config is initialized - configure if not - var configured bool - db.View(func(tx *bbolt.Tx) error { - b := tx.Bucket(BucketKeyConfig) - if b == nil { - panic(ErrConfigBucket) - } - passhash := b.Get(keyPassHash) - configured = passhash != nil - return nil - }) - if !configured { - initAuthKeys() - Reconfigure(defaultPassPhrase, defaultPageTitle) - } - return nil -} - -// Reconfigure takes a passphrase and a page title for the web page, -// generates hash for the password and updates the database with this -// new configuration. -func Reconfigure(passphrase string, pageTitle string) { - // TODO: Maybe this belongs elsewhere? - /* TODO: Generate a random _readable_ password if none is given - var passphraseBytes []byte - if passphrase == "" { - passphraseBytes = make([]byte, 24) - rand.Read(passphraseBytes) - passphrase = string(passphraseBytes) - fmt.Printf("Generated passphrase: %v\n", []rune(passphrase)) - } else { - passphraseBytes = []byte(passphrase) - }*/ - //sha256.Sum256([]byte(passphraseBytes)) - - // TODO: Maybe bcrypt is overkill for such a small project? Consider later - passhash, err := bcrypt.GenerateFromPassword([]byte(passphrase), bcrypt.DefaultCost) - check(err) - db.Update(func(tx *bbolt.Tx) error { - b := tx.Bucket(BucketKeyConfig) - if b == nil { - return ErrConfigBucket - } - var err error - err = b.Put(keyPassHash, passhash) - check(err) - err = b.Put(keyPageTitle, []byte(pageTitle)) - check(err) - return nil - }) -} - -// initAuthKeys generates session and CSRF protection authentitication keys -// and persists them to DB -func initAuthKeys() { - hashKey := securecookie.GenerateRandomKey(32) - blockKey := securecookie.GenerateRandomKey(32) - db.Update(func(tx *bbolt.Tx) error { - var err error - b := tx.Bucket(BucketKeyConfig) - if b == nil { - panic("the config bucket does not exist") - } - err = b.Put(keySCHashKey, hashKey) - check(err) - err = b.Put(keySCBlockKey, blockKey) - check(err) - return nil - }) + return configured } // Close is just a wrapper around db.Close() in order to keep all database diff --git a/internal/http/html_dev.go b/internal/http/html_dev.go @@ -0,0 +1,18 @@ +// +build !PRODUCTION + +package http + +import "io/ioutil" + +func check(err error) { + if err != nil { + panic(err) + } +} + +// In dev mode, the file is read from the generated html file +func getWebsite() []byte { + htmlData, err := ioutil.ReadFile("ui/dist/index.html") + check(err) + return htmlData +} diff --git a/internal/http/http.go b/internal/http/http.go @@ -4,16 +4,49 @@ import ( "fmt" "log" "net/http" + + "sermoni/internal/config" + + "github.com/gorilla/sessions" ) -// StartServer starts the server at the given port +var store *sessions.CookieStore +var conf *config.Config + +// StartServer initializes the session store given the session key and starts +// the server at the given port func StartServer(port int) { - http.HandleFunc("/", staticHandler) + conf = config.GetConfig() + store = sessions.NewCookieStore(conf.SessionKey) + + http.HandleFunc("/", homeHandler) + http.HandleFunc("/login", loginHandler) + log.Fatal(http.ListenAndServe(fmt.Sprintf(":%v", port), nil)) } -func staticHandler(res http.ResponseWriter, r *http.Request) { +func homeHandler(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() - res.Write(getWebsite()) + w.Write(getWebsite()) + return +} + +func authorized(session *sessions.Session) bool { + val := session.Values["authenticated"] + auth, ok := val.(bool) + return ok && auth +} + +func loginHandler(w http.ResponseWriter, r *http.Request) { + session, _ := store.Get(r, "session") + if authorized(session) { + log.Println("Authenticated session requested website") + w.Write([]byte("logged in")) + } else { + log.Println("New session requested website") + session.Values["authenticated"] = true + log.Println(session.Save(r, w)) + w.Write([]byte("Not logged in")) + } return }