sermoni

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

commit 3d6bc0e631ba154d31263043b77d6d7b65d6b2d1
parent 30ff6b1729422747c3ffff607519f18f8c463c8b
Author: Vetle Haflan <vetle@haflan.dev>
Date:   Mon, 13 Apr 2020 13:06:15 +0200

Work on login and general request handling

- Deal with logins
- Go back to sha256 instead of bcrypt
- Finish CSRF logic. What remains is :
  * A middleware, just need to decide where to put it
  * A cryptosecure CSRF token generator

Diffstat:
Minternal/config/config.go | 8+++-----
Minternal/http/auth.go | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Minternal/http/events.go | 13+++++++++++++
Minternal/http/http.go | 22++++++++++++++--------
Minternal/services/services.go | 2++
Mui/src/Login.vue | 10+++++++---
Mui/src/requests.js | 15++++++++++-----
7 files changed, 109 insertions(+), 47 deletions(-)

diff --git a/internal/config/config.go b/internal/config/config.go @@ -1,12 +1,12 @@ package config import ( + "crypto/sha256" "log" "sermoni/internal/database" "github.com/gorilla/securecookie" "go.etcd.io/bbolt" - "golang.org/x/crypto/bcrypt" ) var ( @@ -62,15 +62,13 @@ func InitConfig() { }*/ //sha256.Sum256([]byte(passphraseBytes)) - // TODO: Maybe bcrypt is overkill for such a small project? Consider later - passhash, err := bcrypt.GenerateFromPassword(defaultPassPhrase, bcrypt.DefaultCost) + passhash := sha256.Sum256([]byte(defaultPassPhrase)) 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) + err = b.Put(keyPassHash, passhash[:]) check(err) err = b.Put(keyPageTitle, defaultPageTitle) check(err) diff --git a/internal/http/auth.go b/internal/http/auth.go @@ -1,10 +1,11 @@ package http import ( + "crypto/sha256" "encoding/json" - "log" - "net/http" + "io/ioutil" "math/rand" + "net/http" "strings" "time" @@ -13,23 +14,31 @@ import ( // Deal with login, logout, and general security stuff -// initHandler checks two things: +const ( + keyAuthenticated = "authenticated" + keyCSRFToken = "csrfToken" + keyPassphrase = "passphrase" + keySessionName = "session" + headerCSRFToken = "X-Csrf-Token" +) + +// initHandler checks two things: // 1. If a CSRF token exists for the given session. Otherwise it creates it // 2. Whether the session is authenticated // It then returns an object on the form {"auth": true, "csrftoken": "<long string>"} // This is requested immediately when the website is loaded. func initHandler(w http.ResponseWriter, r *http.Request) { session, _ := store.Get(r, "session") - val := session.Values["csrftoken"] + val := session.Values[keyCSRFToken] token, ok := val.(string) if !ok { token = temporary32CharRandomString() - session.Values["csrftoken"] = token + session.Values[keyCSRFToken] = token session.Save(r, w) // TODO: Error handling, as always } b, _ := json.Marshal(struct { - CSRFToken string `json:"csrftoken"` - Authenticated bool `json:"authenticated"` + CSRFToken string `json:"csrftoken"` + Authenticated bool `json:"authenticated"` }{ token, authorized(session), @@ -37,30 +46,38 @@ func initHandler(w http.ResponseWriter, r *http.Request) { w.Write(b) } - func loginHandler(w http.ResponseWriter, r *http.Request) { - session, _ := store.Get(r, "session") + session, _ := store.Get(r, keySessionName) if authorized(session) { - log.Println("Authenticated session requested website") - w.Write([]byte("logged in")) + return + } + defer r.Body.Close() // needed? + content, err := ioutil.ReadAll(r.Body) + check(err) + var data map[string]string + json.Unmarshal(content, &data) + passphrase := data[keyPassphrase] + passhash := sha256.Sum256([]byte(passphrase)) + if string(passhash[:]) == string(conf.PassHash) { + session.Values[keyAuthenticated] = true + err = session.Save(r, w) + check(err) + w.WriteHeader(http.StatusOK) // Not needed, just for readability? } else { - log.Println("New session requested website") - session.Values["authenticated"] = true - log.Println(session.Save(r, w)) - w.Write([]byte("Not logged in")) + w.WriteHeader(http.StatusUnauthorized) } - return } func logoutHandler(w http.ResponseWriter, r *http.Request) { - session, _ := store.Get(r, "session") - session.Values["authenticated"] = false - log.Println(session.Save(r, w)) + session, _ := store.Get(r, keySessionName) + session.Values[keyAuthenticated] = false + err := session.Save(r, w) + check(err) w.Write([]byte("Logged out")) } func authorized(session *sessions.Session) bool { - val := session.Values["authenticated"] + val := session.Values[keyAuthenticated] auth, ok := val.(bool) return ok && auth } @@ -68,8 +85,8 @@ func authorized(session *sessions.Session) bool { // Middleware for the simple sermoni authentication scheme func auth(handler http.HandlerFunc) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Store is the global CookieStore - session, _ := store.Get(r, "session") + // store is the global CookieStore + session, _ := store.Get(r, keySessionName) if !authorized(session) { status := http.StatusUnauthorized http.Error(w, http.StatusText(status), status) @@ -79,17 +96,34 @@ func auth(handler http.HandlerFunc) http.Handler { }) } +func csrfCheckPassed(r *http.Request, session *sessions.Session) bool { + // CSRF protect anything but GET requests + if r.Method == http.MethodGet { + return true + } + val := session.Values[keyCSRFToken] + rightToken, ok := val.(string) + if !ok { + panic("no CSRF token found") + } + if tokenHeader := r.Header[headerCSRFToken]; tokenHeader == nil { + return false + } else { + return tokenHeader[0] == rightToken + } +} + // not cryptosecure, only for testing! // thanks: https://yourbasic.org/golang/generate-random-string/ func temporary32CharRandomString() string { rand.Seed(time.Now().UnixNano()) - chars := []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZÅÄÖ" + - "abcdefghijklmnopqrstuvwxyzåäö" + - "0123456789") + chars := []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "abcdefghijklmnopqrstuvwxyz" + + "0123456789") length := 32 var b strings.Builder for i := 0; i < length; i++ { - b.WriteRune(chars[rand.Intn(len(chars))]) + b.WriteRune(chars[rand.Intn(len(chars))]) } return b.String() } diff --git a/internal/http/events.go b/internal/http/events.go @@ -1,10 +1,23 @@ package http import ( + "encoding/json" "net/http" + "sermoni/internal/events" ) func getEvents(w http.ResponseWriter, r *http.Request) { + // Create a mapping from service id to name + /* Eventually? + serviceIdName := make(map[int]string) + services := services.GetAll() + for _, service := range { + serviceIdName[service.ID] = service.Name + } + */ + + events := events.GetAll() + json.Marshal(events) return } diff --git a/internal/http/http.go b/internal/http/http.go @@ -22,19 +22,19 @@ func StartServer(port int) { router := mux.NewRouter() router.HandleFunc("/", homeHandler) - router.HandleFunc("/init", initHandler) - router.HandleFunc("/login", loginHandler) + router.HandleFunc("/init", initHandler).Methods(http.MethodGet) + router.HandleFunc("/login", loginHandler).Methods(http.MethodPost) router.Handle("/logout", auth(logoutHandler)) - router.Handle("/services", auth(getServices)).Methods("GET") - router.Handle("/services", auth(postService)).Methods("POST") - router.Handle("/services/{id:[0-9]+}", auth(deleteService)).Methods("DELETE") + router.Handle("/services", auth(getServices)).Methods(http.MethodGet) + router.Handle("/services", auth(postService)).Methods(http.MethodPost) + router.Handle("/services/{id:[0-9]+}", auth(deleteService)).Methods(http.MethodDelete) //router.Handle("/services/{id:[0-9]+}", putService).Methods("PUT") (TODO) - router.Handle("/events", auth(getEvents)).Methods("GET") - router.Handle("/events/{id:[0-9]+}", auth(deleteEvent)).Methods("DELETE") + router.Handle("/events", auth(getEvents)).Methods(http.MethodGet) + router.Handle("/events/{id:[0-9]+}", auth(deleteEvent)).Methods(http.MethodDelete) - router.HandleFunc("/report", reportEvent).Methods("POST") + router.HandleFunc("/report", reportEvent).Methods(http.MethodPost) http.Handle("/", router) log.Fatal(http.ListenAndServe(fmt.Sprintf(":%v", port), nil)) } @@ -44,3 +44,9 @@ func homeHandler(w http.ResponseWriter, r *http.Request) { w.Write(getWebsite()) return } + +func check(err error) { + if err != nil { + panic(err) + } +} diff --git a/internal/services/services.go b/internal/services/services.go @@ -14,6 +14,7 @@ var ( keyServiceName = []byte("name") keyServiceDescription = []byte("description") keyServicePeriod = []byte("period") + keyServiceMaxEvents = []byte("maxevents") ) // Service describes a service that is expected to report @@ -22,6 +23,7 @@ type Service struct { Name string `json:"name"` // service name, usually on the format 'service @ server' Description string `json:"description"` // more detailed description of the service ExpectationPeriod uint64 `json:"period"` // set if the service is expected to report periodically, format is UnixTime (milli?) + MaxNumberEvents uint64 `json:"maxevents"` // set if the service is expected to report periodically, format is UnixTime (milli?) } // GetByToken returns the service structure associated with the token string, if there diff --git a/ui/src/Login.vue b/ui/src/Login.vue @@ -11,6 +11,7 @@ </template> <script> + import api from "./requests.js"; export default { name: "Login", data() { @@ -20,9 +21,12 @@ }, methods: { enter() { - if (this.passphrase == "correct") { - this.$emit("login"); - } + api.login( + this.passphrase, + success => { + this.$emit("login"); + } + ); } }, mounted() { diff --git a/ui/src/requests.js b/ui/src/requests.js @@ -24,6 +24,7 @@ var csrfToken; // { // url: "/services" , // method: "POST", +// data: { "newId": 8234 } // expectedStatus: 201, // error: errData => { console.log(errData); }, // success: successData => { console.log(successData); } @@ -39,14 +40,15 @@ function request(jsonRequest) { jsonRequest.expectedStatus = 200 } if (this.readyState === XMLHttpRequest.DONE) { + const data = this.responseText ? JSON.parse(this.responseText) : {}; if (this.status !== jsonRequest.expectedStatus) { if (jsonRequest.error) { - jsonRequest.error(this.status, JSON.parse(this.responseText)); + jsonRequest.error(this.status, data); } else { console.error(this.status + ": " + this.responseText); } } else if (jsonRequest.success) { - jsonRequest.success(JSON.parse(this.responseText)); + jsonRequest.success(data); } } }; @@ -55,8 +57,9 @@ function request(jsonRequest) { } else { xhttp.open("GET", jsonRequest.url, true); } - xhttp.setRequestHeader("X-CSRFToken", csrfToken); + xhttp.setRequestHeader("X-Csrf-Token", csrfToken); if (jsonRequest.data) { + xhttp.setRequestHeader("Content-Type", "application/json"); xhttp.send(JSON.stringify(jsonRequest.data)); } else { xhttp.send(); @@ -72,13 +75,15 @@ function init(successHandler, errorHandler) { successHandler(data); } }, - error: errorHandler ? errorHandler : (data) => console.log(data) + error: errorHandler }); } -function login(successHandler, errorHandler) { +function login(passphrase, successHandler, errorHandler) { request({ url: "/login", + method: "POST", + data: { passphrase: passphrase }, success: successHandler, error: errorHandler });