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:
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
});