From e32df47ca28ba1d79f7cc4124c4ff48ddac93b92 Mon Sep 17 00:00:00 2001 From: phga Date: Wed, 13 Oct 2021 21:32:56 +0200 Subject: [PATCH] Working DB connections and login --- .gitignore | 3 +- build/docker-compose.yml | 28 +++++- cmd/bs1in/auth.go | 199 ++++++++++++++++++++++++++++++++++++++ cmd/bs1in/bs1in.go | 92 ++++++++++++++++-- cmd/bs1in/config.go | 15 ++- cmd/bs1in/database.go | 43 +++++++- cmd/bs1in/mail_test.go | 8 +- cmd/bs1in/user.go | 116 ++++++++++++++++++++++ go.mod | 1 + go.sum | 2 + web/templates/header.html | 5 + web/templates/login.html | 44 +++++++++ 12 files changed, 528 insertions(+), 28 deletions(-) create mode 100644 cmd/bs1in/auth.go create mode 100644 cmd/bs1in/user.go create mode 100644 web/templates/login.html diff --git a/.gitignore b/.gitignore index 53ef257..f02faed 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ web/static/fonts/* web/data/ configs/* -cmd/bs1in/bs1in \ No newline at end of file +cmd/bs1in/bs1in +/build/.env diff --git a/build/docker-compose.yml b/build/docker-compose.yml index edd046a..ee70061 100644 --- a/build/docker-compose.yml +++ b/build/docker-compose.yml @@ -9,15 +9,15 @@ services: ports: - 8099:6969 volumes: - - /srv/bs1in/configs:CONFIGFOLDER - - /srv/bs1in/web/data:DATAFOLDER - - /srv/bs1in/web/static/fonts:FONTSFOLDER + - /srv/bs1in/configs:/go/src/g.phga.de/phga/bs1in/configs + - /srv/bs1in/web/data:/go/src/g.phga.de/phga/bs1in/web/data + - /srv/bs1in/web/static/fonts:/go/src/g.phga.de/phga/bs1in/web/static/fonts mongo: image: mongo restart: 'no' env_file: - - /srv/gott/.env + - /srv/bs1in/.env ports: - 27017:27017 volumes: @@ -29,4 +29,22 @@ services: ports: - 9099:8081 env_file: - - /srv/bs1in/.env \ No newline at end of file + - /srv/bs1in/.env + + mariadb: + image: mariadb + container_name: mariadb + restart: 'no' + env_file: + - /srv/bs1in/.env + ports: + - '3306:3306' + volumes: + - /srv/mariadb/init:/docker-entrypoint-initdb.d + + adminer: + image: adminer:4.8.0 + container_name: adminer + restart: 'no' + ports: + - '8013:8080' \ No newline at end of file diff --git a/cmd/bs1in/auth.go b/cmd/bs1in/auth.go new file mode 100644 index 0000000..08debda --- /dev/null +++ b/cmd/bs1in/auth.go @@ -0,0 +1,199 @@ +package main + +import ( + "context" + cryptorand "crypto/rand" + "encoding/json" + "io" + "log" + "net/http" + "time" + + "go.mongodb.org/mongo-driver/bson" + "golang.org/x/crypto/bcrypt" +) + +type Session struct { + ID [32]byte `json:"id" bson:"id"` + AuthToken [32]byte `json:"auth_token" bson:"auth_token"` + Timestamp int64 `json:"timestamp" bson:"timestamp"` +} + +// ------------------------------- MIDDLEWARE ---------------------------------- +type ctxKey int + +const currUserKey ctxKey = 0 + +// validateSession verifies that the client has a session that is still valid +func validateSession(h http.HandlerFunc, role string) http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + // Allow to access required files pre login + if r.URL.Path == "/static/css/tt.css" || + r.URL.Path == "/static/js/helper.js" || + r.URL.Path == "/static/fonts/OpenSans-Regular.ttf" { + h(w, r) + return + } + + cookie, err := r.Cookie("SESSION") + + if err == http.ErrNoCookie { + // Redirect to /login + w.Header().Set("Location", "/login?r="+r.URL.Path) + w.WriteHeader(http.StatusFound) + log.Println(err) + return + } else if err != nil { + log.Fatal(err) + return + } + + // TESTING: Show cookie content + // log.Printf("%+v", *cookie) + requestedUser := checkSession([]byte(cookie.Value)) + + if requestedUser == nil { + unsetCookie(w) + w.Header().Set("Location", "/login") + w.WriteHeader(http.StatusFound) + return + } + // Set new timestamp + requestedUser.CurrSession.Timestamp = time.Now().UTC().UnixNano() + // Save to database + updateSession(requestedUser) + + setEncryptedCookie(w, requestedUser) + ctx := context.WithValue(r.Context(), currUserKey, *requestedUser) + requestWithUser := r.WithContext(ctx) + + // ORIGINAL HANDLER + switch role { + case "": + h(w, requestWithUser) + case "admin": + if isAdmin(requestedUser) { + h(w, requestWithUser) + } else { + w.Header().Set("Location", "/") + w.WriteHeader(http.StatusFound) + } + } + }) +} + +func isAdmin(u *User) bool { + if u.Role == "admin" { + log.Println("IS ADMIN") + return true + } else { + log.Println("IS NOT ADMIN") + return false + } +} + +func encryptSession(s Session) (es []byte) { + js, err := json.Marshal(s) + if err != nil { + log.Fatal(err) + } + es, err = encrypt(js, encKey) + if err != nil { + log.Fatal(err) + } + return +} + +func setEncryptedCookie(w http.ResponseWriter, u *User) { + cValue := encryptSession(u.CurrSession) + cookie := &http.Cookie{ + Name: "SESSION", + Value: string(cValue), + MaxAge: int(6 * time.Hour / time.Second), + Path: "/", + HttpOnly: false, + } + http.SetCookie(w, cookie) +} + +func unsetCookie(w http.ResponseWriter) { + cookie := &http.Cookie{ + Name: "SESSION", + MaxAge: -69, + Path: "/", + } + http.SetCookie(w, cookie) +} + +// checkCredentials compares the pw provided by the user to the hashed version +// in the database and returns true for success and false otherwise +func checkCredentials(u *User, pw string) bool { + hashedPw := u.Pass + err := bcrypt.CompareHashAndPassword([]byte(hashedPw), []byte(pw)) + if err != nil { + log.Print(err) + return false + } + return true +} + +// checkSession compares the encrypted session string with the current Session +// set for the user and returns true if they match +func checkSession(receivedSession []byte) *User { + // Test + dec, err := decrypt(receivedSession, encKey) + if err != nil { + log.Println("Encryption key changed, deleting remaining cookies!") + return nil + } + // log.Println(string(dec)) + decryptedSession := Session{} + err = json.Unmarshal(dec, &decryptedSession) + if err != nil { + log.Fatal(err) + } + + // There is no current user but a (mayb) valid session -> get user with session from db + requestedUser, err := getUserBySession(&decryptedSession) + if err != nil { + log.Println("No user with that session found in db:", err) + return nil + } + + // 6 hours ago + maxSessionAge := time.Now().UTC().UnixNano() - int64(6*time.Hour) + + // Check if session is still valid (Other values must match anyways) + if requestedUser.CurrSession.Timestamp >= maxSessionAge { + return requestedUser + } else { + log.Println("User logged out because Session is no longer valid") + return nil + } +} + +func updateSession(u *User) { + filter := bson.D{{"uid", u.UID}} + update := bson.D{{"$set", bson.D{{"curr_session", u.CurrSession}}}} + res, err := colUsers.UpdateOne(mongoCtx, filter, update) + if err != nil { + log.Fatal(err, res) + } +} + +func createSession() (s Session) { + var sID, aToken [32]byte + _, err := io.ReadFull(cryptorand.Reader, sID[:]) + if err != nil { + log.Fatal(err) + } + _, err = io.ReadFull(cryptorand.Reader, aToken[:]) + if err != nil { + log.Fatal(err) + } + // MAYB: Also hash the random number? Any advantages? + // sID := sha256.Sum256(nonce) + // aToken := sha256.Sum256(nonce) + return Session{ID: sID, AuthToken: aToken, Timestamp: time.Now().UTC().UnixNano()} +} diff --git a/cmd/bs1in/bs1in.go b/cmd/bs1in/bs1in.go index efa0758..c5e636e 100644 --- a/cmd/bs1in/bs1in.go +++ b/cmd/bs1in/bs1in.go @@ -12,12 +12,14 @@ import ( ) func main() { - // cheap routing - http.HandleFunc("/", handleIndex) - http.HandleFunc("/db_user", handleDbUser) + http.HandleFunc("/login", handleLogin) + http.HandleFunc("/register", handleRegister) + http.HandleFunc("/logout", validateSession(handleLogout, "")) + http.HandleFunc("/", validateSession(handleIndex, "")) + http.HandleFunc("/admin_panel", validateSession(handleAdminPanel, "admin")) // provide the inc directory to the useragent - http.HandleFunc("/static/", handleStatic) + http.HandleFunc("/static/", validateSession(handleStatic, "")) // listen on port 8080 (I use nginx to proxy this local server) log.Fatalln(http.ListenAndServe(":"+config.WebPort, nil)) } @@ -38,13 +40,85 @@ func handleStatic(w http.ResponseWriter, r *http.Request) { func handleIndex(w http.ResponseWriter, r *http.Request) { // Parses all required html files to provide the actual html that is shipped. t, _ := getTemplate("layouts/base.html", "main.html", "header.html") - t.Execute(w, t) + currUser := r.Context().Value(currUserKey).(User) + td := TmplData{currUser, nil} + t.Execute(w, td) } -func handleDbUser(w http.ResponseWriter, r *http.Request) { - // Parses all required html files to provide the actual html that is shipped. - t, _ := getTemplate("layouts/base.html", "db_user.html", "header.html") - t.Execute(w, t) +func handleAdminPanel(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + t, _ := getTemplate("layouts/base.html", "admin_panel.html", "header.html") + currUser := r.Context().Value(currUserKey).(User) + td := TmplData{currUser, nil} + t.Execute(w, td) + } +} + +func handleRegister(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + t, _ := getTemplate("layouts/base.html", "register.html") + + err := t.Execute(w, nil) + if err != nil { + log.Println(err) + } + } +} + +func handleLogin(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + t, _ := getTemplate("layouts/base.html", "login.html") + + lastURL := r.URL.Query().Get("r") + if len(lastURL) == 0 { + lastURL = "/" + } + td := TmplData{nil, struct{ RedirectTo string }{lastURL}} + + err := t.Execute(w, td) + if err != nil { + log.Println(err) + } + + } else if r.Method == http.MethodPost { + if err := r.ParseForm(); err != nil { + log.Fatal(err) + } + uid := r.PostForm.Get("username") + pw := r.PostForm.Get("password") + + // TESTING: Logging of entered credentials + log.Println("UID: ", uid, " PW: ", pw) + + var requestedUser *User + var err error + requestedUser, err = getUserByUID(uid) + if err != nil { + log.Println("No user with this id exists: ", uid) + w.WriteHeader(http.StatusForbidden) + return + } + + success := checkCredentials(requestedUser, pw) + if success { + // IT IS IMPORTANT TO SET THE COOKIE BEFORE ANY OTHER OUTPUT TO W OCCURS + requestedUser.CurrSession = createSession() + updateSession(requestedUser) + setEncryptedCookie(w, requestedUser) + w.WriteHeader(http.StatusOK) + return + } else { + log.Println("Failed login attempt for user: ", uid) + w.WriteHeader(http.StatusForbidden) + return + } + } +} + +func handleLogout(w http.ResponseWriter, r *http.Request) { + unsetCookie(w) + w.Header().Set("Location", "/") + w.WriteHeader(http.StatusFound) } // ------------------------- TEMPLATE HELPERS ---------------------------------- diff --git a/cmd/bs1in/config.go b/cmd/bs1in/config.go index c3f43eb..d79e470 100644 --- a/cmd/bs1in/config.go +++ b/cmd/bs1in/config.go @@ -8,10 +8,17 @@ import ( type Config struct { WebPort string `json:"web_port"` - DbUser string `json:"db_user"` - DbPass string `json:"db_pass"` - DbHost string `json:"db_host"` - DbPort string `json:"db_port"` + WebDomain string `json:"web_domain"` + WebAdmin string `json:"web_admin"` + WebAdminPass string `json:"web_admin_pass"` + MongoDbUser string `json:"mongodb_user"` + MongoDbPass string `json:"mongodb_pass"` + MongoDbHost string `json:"mongodb_host"` + MongoDbPort string `json:"mongodb_port"` + MariaDbUser string `json:"mariadb_user"` + MariaDbPass string `json:"mariadb_pass"` + MariaDbHost string `json:"mariadb_host"` + MariaDbPort string `json:"mariadb_port"` MailUser string `json:"mail_user"` MailPass string `json:"mail_pass"` MailSmtpServer string `json:"mail_smtp_server"` diff --git a/cmd/bs1in/database.go b/cmd/bs1in/database.go index 760b23a..af6cb81 100644 --- a/cmd/bs1in/database.go +++ b/cmd/bs1in/database.go @@ -7,18 +7,28 @@ import ( "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" + + "database/sql" + + "github.com/go-sql-driver/mysql" + _ "github.com/go-sql-driver/mysql" ) var client mongo.Client -var colDbUsers *mongo.Collection +var colUsers *mongo.Collection var mongoCtx context.Context func init() { + openMongoDBConnection() + openMariaDBConnection() +} + +func openMongoDBConnection() { // Mongo DB setup - clientOptions := options.Client().ApplyURI("mongodb://" + config.DbUser + ":" + config.DbPass + "@" + config.DbHost + ":" + config.DbPort) + clientOptions := options.Client().ApplyURI("mongodb://" + config.MongoDbUser + ":" + config.MongoDbPass + "@" + config.MongoDbHost + ":" + config.MongoDbPort) client, err := mongo.NewClient(clientOptions) if err != nil { - log.Fatal(err) + log.Fatal("DB Error: ", err) } // Cancel if Timeout ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) @@ -27,8 +37,31 @@ func init() { // Connect to DB err = client.Connect(ctx) if err != nil { - log.Fatal(err) + log.Fatal("DB Error: ", err) + } + + colUsers = client.Database("bs1in").Collection("users") +} + +func openMariaDBConnection() { + dbConf := mysql.Config{ + User: config.MariaDbUser, + Passwd: config.MariaDbPass, + Net: "tcp", + Addr: config.MariaDbHost + ":" + config.MariaDbPort, + AllowNativePasswords: true, // Without this it did not work... + } + mariadb, err := sql.Open("mysql", dbConf.FormatDSN()) + if err != nil { + log.Fatal("MariaDB Connection Error: ", err) + } + + err = mariadb.Ping() + if err != nil { + log.Fatal("MariaDB not reachable (Ping Error): ", err) } - colDbUsers = client.Database("bs1in").Collection("db_users") + var version string + mariadb.QueryRow("SELECT VERSION()").Scan(&version) + log.Println("Connected to:", version) } diff --git a/cmd/bs1in/mail_test.go b/cmd/bs1in/mail_test.go index d3e42a9..0c50a25 100644 --- a/cmd/bs1in/mail_test.go +++ b/cmd/bs1in/mail_test.go @@ -5,8 +5,8 @@ import ( ) func TestSendMail(t *testing.T) { - err := sendMail("mrc1rz+8ca4wocgn0mz8@sharklasers.com", "Test Test 123", "Hello, this is just a test") - if err != nil { - t.Error(err) - } + // err := sendMail("pmvfck+22j3zqaeild4k@sharklasers.com", "Test Test 123", "Hello, this is just a test") + // if err != nil { + // t.Error("Mail Error: ", err) + // } } diff --git a/cmd/bs1in/user.go b/cmd/bs1in/user.go new file mode 100644 index 0000000..13059ed --- /dev/null +++ b/cmd/bs1in/user.go @@ -0,0 +1,116 @@ +package main + +import ( + "log" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "golang.org/x/crypto/bcrypt" +) + +// User of the experiment +type User struct { + ID primitive.ObjectID `bson:"_id"` + UID string `bson:"uid"` + Mail string `bson: mail` + Pass string `bson:"pass"` + CurrSession Session `bson:"curr_session"` + Role string `bson:"role"` +} + +func init() { + createUser(config.WebAdmin, "", config.WebAdminPass, "admin") +} + +// createUser creates a new participant and writes it to the database. +func createUser(uid string, email string, pass string, role string) *User { + _, err := getUserByUID(uid) + if err != mongo.ErrNoDocuments { + log.Println("User with that UID already exists") + return nil + } + if len(email) > 0 { + _, err = getUserByEmail(email) + if err != mongo.ErrNoDocuments { + log.Println("User with that email already exists") + return nil + } + } + // salt+hash password to store in DB + hashedPw, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost) + if err != nil { + log.Fatal("Error while creating a new User: ", err) + } + u := &User{ + ID: primitive.NewObjectID(), + UID: uid, + Mail: email, + Pass: string(hashedPw), + Role: role, + } + _, err = colUsers.InsertOne(mongoCtx, u) + if err != nil { + log.Fatal("Error while saving new user to DB: ", err) + } + return u +} + +// generateUserID generates a unique ID for each participant +func generateUserID() (uid string) { + for { + uid = generateRandomString(5, "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890") + // Check if ID is already in use + err := colUsers.FindOne(mongoCtx, bson.M{"uid": uid}).Decode(bson.M{}) + // If not in use stop loop + if err == mongo.ErrNoDocuments { + break + } else { + // Log other errors + log.Fatal(err) + } + } + return +} + +// getUserByUID returns a user with the same UID as uid from the DB +func getUserByUID(uid string) (p *User, err error) { + p = &User{} + err = colUsers.FindOne(mongoCtx, bson.M{"uid": uid}).Decode(p) + if err == mongo.ErrNoDocuments { + return nil, err + } else if err != nil { + log.Fatal(err) + } + + return p, nil +} + +// getUserByEmail returns a user with the same email as email from the DB +func getUserByEmail(email string) (p *User, err error) { + p = &User{} + err = colUsers.FindOne(mongoCtx, bson.M{"mail": email}).Decode(p) + if err == mongo.ErrNoDocuments { + return nil, err + } else if err != nil { + log.Fatal(err) + } + return p, nil +} + +// getUserBySession returns a user with the same session as session from the DB +func getUserBySession(s *Session) (p *User, err error) { + p = &User{} + + // Get only id and authtoken otherwise timestamp might be already overwritten + err = colUsers.FindOne(mongoCtx, bson.M{ + "curr_session.id": s.ID, + "curr_session.auth_token": s.AuthToken, + }).Decode(p) + if err == mongo.ErrNoDocuments { + return nil, err + } else if err != nil { + log.Fatal(err) + } + return p, nil +} diff --git a/go.mod b/go.mod index fb2a774..72bdab8 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.17 require go.mongodb.org/mongo-driver v1.7.3 require ( + github.com/go-sql-driver/mysql v1.6.0 // indirect github.com/go-stack/stack v1.8.0 // indirect github.com/golang/snappy v0.0.1 // indirect github.com/klauspost/compress v1.13.6 // indirect diff --git a/go.sum b/go.sum index ce9b3e7..9000429 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= diff --git a/web/templates/header.html b/web/templates/header.html index 080419c..45b7818 100644 --- a/web/templates/header.html +++ b/web/templates/header.html @@ -1,4 +1,9 @@ {{define "header"}}
+
{{end}} diff --git a/web/templates/login.html b/web/templates/login.html new file mode 100644 index 0000000..b33f9cd --- /dev/null +++ b/web/templates/login.html @@ -0,0 +1,44 @@ +{{define "body"}} +
+ + + + + + +
+ +{{end}} +{{define "scripts"}} + + +{{end}} \ No newline at end of file