From 43a62d0837007de4e369b7a32446b4ea312b5ead Mon Sep 17 00:00:00 2001 From: John David White <122345776@umail.ucc.ie> Date: Fri, 3 Apr 2026 11:46:12 +0100 Subject: [PATCH 1/2] Add Argon2id --- src/config/db.go | 713 +++++++++++++++++++++++++---------------------- 1 file changed, 387 insertions(+), 326 deletions(-) diff --git a/src/config/db.go b/src/config/db.go index 53eeeb6..850c9d0 100644 --- a/src/config/db.go +++ b/src/config/db.go @@ -1,326 +1,387 @@ -package config - -import ( - "context" - "database/sql" - "encoding/json" - "errors" - "fyp-api-gateway/src/utils" - "log/slog" - "net/http" - "os" - "time" - - uuid "github.com/google/uuid" - _ "github.com/jackc/pgx/v5/stdlib" - "golang.org/x/crypto/bcrypt" -) - -type Database struct { - Conn *sql.DB -} - -type Server struct { - DB *Database -} - -type LoginInfo struct { - Name string `json:"name"` - Password string `json:"password"` -} - -func NewDatabase(dsn string) (*Database, error) { - db, err := sql.Open("pgx", dsn) - if err != nil { - slog.Error("error opening database connection:", "error", err) - return nil, err - } - - db.SetMaxOpenConns(25) - db.SetMaxIdleConns(25) - db.SetConnMaxLifetime(5 * time.Minute) - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - if err = db.PingContext(ctx); err != nil { - return nil, err - } - - return &Database{Conn: db}, nil -} - -func (d *Database) StartDB(path string) error { - content, err := os.ReadFile(path) - if err != nil { - slog.Error("error reading database file", "error", err) - return err - } - - if _, err = d.Conn.Exec(string(content)); err != nil { - slog.Error("error executing database statement", "error", err) - return err - } - - return nil -} - -func (s *Server) Signup(w http.ResponseWriter, r *http.Request) { - slog.Info("attempting to sign up new user...") - loginInfo := &LoginInfo{} - - if r.Method != http.MethodPost { - slog.Error("invalid method", "method", r.Method) - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) - return - } - - if err := json.NewDecoder(r.Body).Decode(&loginInfo); err != nil { - slog.Error("error decoding loginInfo", "error", err) - http.Error(w, "invalid request body", http.StatusBadRequest) - return - } - defer r.Body.Close() - - var id string - err := s.DB.Conn.QueryRow( - "SELECT id FROM users WHERE username = $1", - loginInfo.Name, - ).Scan(&id) - - if err == nil || id != "" { - slog.Error("error querying user", "error", err) - http.Error(w, "internal server error", http.StatusInternalServerError) - return - } - - hashed_password, err := bcrypt.GenerateFromPassword([]byte(loginInfo.Password), 14) - _, err = s.DB.Conn.Exec(` - INSERT INTO users (username, password, config_yaml) - VALUES ($1, $2, $3);`, - loginInfo.Name, string(hashed_password), utils.DefaultConfigContent, - ) - if err != nil { - slog.Error("error inserting user", "error", err) - http.Error(w, "internal server error", http.StatusInternalServerError) - return - } - - err = InitUserNGINX(loginInfo.Name) - if err != nil { - slog.Error("error initializing user", "error", err) - http.Error(w, "internal server error", http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusOK) -} - -/* -Receive the login info from the management plane and decode it -Check the password used is the same as the one in the database, also check the username is there -If the user has no session, create one and send it back, otherwise return the existing session -*/ -func (s *Server) VerifyLoginInfo(w http.ResponseWriter, r *http.Request) { - slog.Info("validating login information") - loginInfo := &LoginInfo{} - - if r.Method != http.MethodPost { - slog.Error("invalid method", "method", r.Method) - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) - return - } - - if err := json.NewDecoder(r.Body).Decode(&loginInfo); err != nil { - slog.Error("error decoding loginInfo", "error", err) - http.Error(w, "invalid request body", http.StatusBadRequest) - return - } - defer r.Body.Close() - - var storedHash string - err := s.DB.Conn.QueryRow( - "SELECT password FROM users WHERE username = $1", - loginInfo.Name, - ).Scan(&storedHash) - - if errors.Is(err, sql.ErrNoRows) { - slog.Error("user not found", "username", loginInfo.Name) - http.Error(w, "invalid credentials", http.StatusUnauthorized) - return - } - - err = bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(loginInfo.Password)) - if err != nil { - slog.Error("error verifying loginInfo", "error", err) - http.Error(w, "invalid credentials", http.StatusUnauthorized) - return - } - - // check is the user has a session already - var sessionId string - sessionId, isSession := s.sessionExists(loginInfo.Name) - - if !isSession { - sessionId, err = s.createSession(loginInfo.Name) - if err != nil { - slog.Error("error creating session", "error", err) - http.Error(w, "internal server error", http.StatusInternalServerError) - return - } - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ - "sessionId": sessionId, - }) -} - -func (s *Server) sessionExists(username string) (string, bool) { - var sessionId string - err := s.DB.Conn.QueryRow( - "SELECT id FROM sessions WHERE username=$1", - username, - ).Scan(&sessionId) - - if err != nil { - return "", false - } - return sessionId, true -} - -func (s *Server) createSession(name string) (string, error) { - sessionId := uuid.New().String() - expires := time.Now().Add(24 * time.Hour) - - _, err := s.DB.Conn.Exec( - "INSERT INTO sessions(id, username, expires) VALUES ($1, $2, $3)", - sessionId, name, expires, - ) - - if err != nil { - slog.Error("error creating session", "error", err) - return "", err - } - - return sessionId, nil -} - -func (s *Server) ValidateSession(w http.ResponseWriter, r *http.Request) { - slog.Info("validating user session") - sessionId := r.Header.Get("X-Session-ID") - - var username string - err := s.DB.Conn.QueryRow( - "SELECT username FROM sessions WHERE id=$1 AND expires > NOW()", - sessionId, - ).Scan(&username) - - if err != nil { - slog.Error("error querying session", "error", err) - http.Error(w, "unauthorized", http.StatusUnauthorized) - return - } - - w.WriteHeader(http.StatusOK) -} - -func (s *Server) UserConfig(w http.ResponseWriter, r *http.Request) { - slog.Info("received request for user config") - - cookie, err := r.Cookie("session") - if err != nil { - slog.Error("failed getting session id", "error", err) - w.WriteHeader(http.StatusBadRequest) - return - } - - sessionId := cookie.Value - - var gatewayCfg string - err = s.DB.Conn.QueryRow(` - SELECT u.config_yaml - FROM users AS u - JOIN sessions AS s ON u.username = s.username - WHERE s.id = $1 AND s.expires > NOW()`, - sessionId).Scan(&gatewayCfg) - - if err != nil { - slog.Error("error querying session", "error", err) - w.WriteHeader(http.StatusBadRequest) - return - } - - w.Header().Set("Content-Type", "text/yaml") - w.WriteHeader(http.StatusOK) - _, err = w.Write([]byte(gatewayCfg)) - if err != nil { - slog.Error("error writing response", "error", err) - return - } -} - -func RetrieveUserBySessionId(sessionId string) string { - dsn := os.Getenv("DATABASE_URL") - - db, err := sql.Open("pgx", dsn) - if err != nil { - slog.Error("error opening database connection:", "error", err) - return "" - } - - var username string - err = db.QueryRow(` - SELECT username - FROM sessions - WHERE id = $1`, - sessionId).Scan(&username) - - return username -} - -func RetrieveUserConfig(username string) string { - dsn := os.Getenv("DATABASE_URL") - - db, err := sql.Open("pgx", dsn) - if err != nil { - slog.Error("error opening database connection:", "error", err) - return "" - } - - var gatewayCfg string - err = db.QueryRow(` - SELECT config_yaml - FROM users - WHERE username = $1`, - username, - ).Scan(&gatewayCfg) - - return gatewayCfg -} - -func InsertNewConfig(sessionId, gatewayCfg string) error { - dsn := os.Getenv("DATABASE_URL") - - db, err := sql.Open("pgx", dsn) - if err != nil { - return err - } - - _, err = db.Exec(` - UPDATE users AS u - SET config_yaml = $1 - FROM sessions AS s - WHERE s.id = $2 - AND s.expires > NOW() - AND u.username = s.username`, - gatewayCfg, sessionId, - ) - slog.Info("inserted new config to database") - - if err != nil { - return err - } - - return nil -} +package config + +import ( + "context" + "crypto/rand" + "crypto/subtle" + "database/sql" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "fyp-api-gateway/src/utils" + "log/slog" + "net/http" + "os" + "strings" + "time" + + uuid "github.com/google/uuid" + _ "github.com/jackc/pgx/v5/stdlib" + "golang.org/x/crypto/argon2" +) + +type Database struct { + Conn *sql.DB +} + +type Server struct { + DB *Database +} + +type LoginInfo struct { + Name string `json:"name"` + Password string `json:"password"` +} + +func NewDatabase(dsn string) (*Database, error) { + db, err := sql.Open("pgx", dsn) + if err != nil { + slog.Error("error opening database connection:", "error", err) + return nil, err + } + + db.SetMaxOpenConns(25) + db.SetMaxIdleConns(25) + db.SetConnMaxLifetime(5 * time.Minute) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err = db.PingContext(ctx); err != nil { + return nil, err + } + + return &Database{Conn: db}, nil +} + +func (d *Database) StartDB(path string) error { + content, err := os.ReadFile(path) + if err != nil { + slog.Error("error reading database file", "error", err) + return err + } + + if _, err = d.Conn.Exec(string(content)); err != nil { + slog.Error("error executing database statement", "error", err) + return err + } + + return nil +} + +func hashPassword(password string) (string, error) { + salt := make([]byte, 16) + if _, err := rand.Read(salt); err != nil { + return "", err + } + + hash := argon2.IDKey([]byte(password), salt, 1, 64*1024, 4, 32) + + encoded := fmt.Sprintf("$argon2id$v=19$m=65536,t=1,p=4$%s$%s", + base64.RawStdEncoding.EncodeToString(salt), + base64.RawStdEncoding.EncodeToString(hash), + ) + return encoded, nil +} + +func (s *Server) Signup(w http.ResponseWriter, r *http.Request) { + slog.Info("attempting to sign up new user...") + loginInfo := &LoginInfo{} + + if r.Method != http.MethodPost { + slog.Error("invalid method", "method", r.Method) + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + if err := json.NewDecoder(r.Body).Decode(&loginInfo); err != nil { + slog.Error("error decoding loginInfo", "error", err) + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + defer r.Body.Close() + + var id string + err := s.DB.Conn.QueryRow( + "SELECT id FROM users WHERE username = $1", + loginInfo.Name, + ).Scan(&id) + + if err == nil || id != "" { + slog.Error("error querying user", "error", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + + hashed_password, err := hashPassword(loginInfo.Password) + if err != nil { + slog.Error("error hashing password", "error", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + + _, err = s.DB.Conn.Exec(` + INSERT INTO users (username, password, config_yaml) + VALUES ($1, $2, $3);`, + loginInfo.Name, string(hashed_password), utils.DefaultConfigContent, + ) + if err != nil { + slog.Error("error inserting user", "error", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + + err = InitUserNGINX(loginInfo.Name) + if err != nil { + slog.Error("error initializing user", "error", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) +} + +/* Code for this method supplied by https://www.alexedwards.net/blog/how-to-hash-and-verify-passwords-with-argon2-in-go */ +func verifyPassword(password, hash string) (bool, error) { + var m, t, p uint32 + var saltB64, hashB64 string + _, err := fmt.Sscanf(hash, "$argon2id$v=19$m=%d,t=%d,p=%d$%s", + &m, &t, &p, &saltB64) + if err != nil { + return false, err + } + + parts := strings.Split(hash, "$") + if len(parts) != 6 { + return false, fmt.Errorf("invalid hash format") + } + saltB64 = parts[4] + hashB64 = parts[5] + + salt, err := base64.RawStdEncoding.DecodeString(saltB64) + if err != nil { + return false, err + } + storedHash, err := base64.RawStdEncoding.DecodeString(hashB64) + if err != nil { + return false, err + } + + keyLen := uint32(len(storedHash)) + computed := argon2.IDKey([]byte(password), salt, t, m, uint8(p), keyLen) + + if subtle.ConstantTimeCompare(computed, storedHash) != 1 { + return false, nil + } + return true, nil +} + +/* +Receive the login info from the management plane and decode it +Check the password used is the same as the one in the database, also check the username is there +If the user has no session, create one and send it back, otherwise return the existing session +*/ +func (s *Server) VerifyLoginInfo(w http.ResponseWriter, r *http.Request) { + slog.Info("validating login information") + loginInfo := &LoginInfo{} + + if r.Method != http.MethodPost { + slog.Error("invalid method", "method", r.Method) + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + if err := json.NewDecoder(r.Body).Decode(&loginInfo); err != nil { + slog.Error("error decoding loginInfo", "error", err) + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + defer r.Body.Close() + + var storedHash string + err := s.DB.Conn.QueryRow( + "SELECT password FROM users WHERE username = $1", + loginInfo.Name, + ).Scan(&storedHash) + + if errors.Is(err, sql.ErrNoRows) { + slog.Error("user not found", "username", loginInfo.Name) + http.Error(w, "invalid credentials", http.StatusUnauthorized) + return + } + + isCorrect, err := verifyPassword(loginInfo.Password, storedHash) + if err != nil || !isCorrect { + slog.Error("error verifying loginInfo", "error", err) + http.Error(w, "invalid credentials", http.StatusUnauthorized) + return + } + + // check is the user has a session already + var sessionId string + sessionId, isSession := s.sessionExists(loginInfo.Name) + + if !isSession { + sessionId, err = s.createSession(loginInfo.Name) + if err != nil { + slog.Error("error creating session", "error", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "sessionId": sessionId, + }) +} + +func (s *Server) sessionExists(username string) (string, bool) { + var sessionId string + err := s.DB.Conn.QueryRow( + "SELECT id FROM sessions WHERE username=$1", + username, + ).Scan(&sessionId) + + if err != nil { + return "", false + } + return sessionId, true +} + +func (s *Server) createSession(name string) (string, error) { + sessionId := uuid.New().String() + expires := time.Now().Add(24 * time.Hour) + + _, err := s.DB.Conn.Exec( + "INSERT INTO sessions(id, username, expires) VALUES ($1, $2, $3)", + sessionId, name, expires, + ) + + if err != nil { + slog.Error("error creating session", "error", err) + return "", err + } + + return sessionId, nil +} + +func (s *Server) ValidateSession(w http.ResponseWriter, r *http.Request) { + slog.Info("validating user session") + sessionId := r.Header.Get("X-Session-ID") + + var username string + err := s.DB.Conn.QueryRow( + "SELECT username FROM sessions WHERE id=$1 AND expires > NOW()", + sessionId, + ).Scan(&username) + + if err != nil { + slog.Error("error querying session", "error", err) + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + w.WriteHeader(http.StatusOK) +} + +func (s *Server) UserConfig(w http.ResponseWriter, r *http.Request) { + slog.Info("received request for user config") + + cookie, err := r.Cookie("session") + if err != nil { + slog.Error("failed getting session id", "error", err) + w.WriteHeader(http.StatusBadRequest) + return + } + + sessionId := cookie.Value + + var gatewayCfg string + err = s.DB.Conn.QueryRow(` + SELECT u.config_yaml + FROM users AS u + JOIN sessions AS s ON u.username = s.username + WHERE s.id = $1 AND s.expires > NOW()`, + sessionId).Scan(&gatewayCfg) + + if err != nil { + slog.Error("error querying session", "error", err) + w.WriteHeader(http.StatusBadRequest) + return + } + + w.Header().Set("Content-Type", "text/yaml") + w.WriteHeader(http.StatusOK) + _, err = w.Write([]byte(gatewayCfg)) + if err != nil { + slog.Error("error writing response", "error", err) + return + } +} + +func RetrieveUserBySessionId(sessionId string) string { + dsn := os.Getenv("DATABASE_URL") + + db, err := sql.Open("pgx", dsn) + if err != nil { + slog.Error("error opening database connection:", "error", err) + return "" + } + + var username string + err = db.QueryRow(` + SELECT username + FROM sessions + WHERE id = $1`, + sessionId).Scan(&username) + + return username +} + +func RetrieveUserConfig(username string) string { + dsn := os.Getenv("DATABASE_URL") + + db, err := sql.Open("pgx", dsn) + if err != nil { + slog.Error("error opening database connection:", "error", err) + return "" + } + + var gatewayCfg string + err = db.QueryRow(` + SELECT config_yaml + FROM users + WHERE username = $1`, + username, + ).Scan(&gatewayCfg) + + return gatewayCfg +} + +func InsertNewConfig(sessionId, gatewayCfg string) error { + dsn := os.Getenv("DATABASE_URL") + + db, err := sql.Open("pgx", dsn) + if err != nil { + return err + } + + _, err = db.Exec(` + UPDATE users AS u + SET config_yaml = $1 + FROM sessions AS s + WHERE s.id = $2 + AND s.expires > NOW() + AND u.username = s.username`, + gatewayCfg, sessionId, + ) + slog.Info("inserted new config to database") + + if err != nil { + return err + } + + return nil +} From 131fae1c21ec57e6263e5e378612f27099f69e89 Mon Sep 17 00:00:00 2001 From: John David White <122345776@umail.ucc.ie> Date: Sun, 12 Apr 2026 14:36:51 +0100 Subject: [PATCH 2/2] cleanup --- src/collector/configs/alloy-local-config.yaml | 92 +++++++++---------- src/templates/nginx.conf.tmpl | 2 +- test/load/test_local.js | 6 +- 3 files changed, 51 insertions(+), 49 deletions(-) diff --git a/src/collector/configs/alloy-local-config.yaml b/src/collector/configs/alloy-local-config.yaml index 7b6208c..a75bc66 100644 --- a/src/collector/configs/alloy-local-config.yaml +++ b/src/collector/configs/alloy-local-config.yaml @@ -1,50 +1,50 @@ - local.file_match "nginx_logs" { - path_targets = [{ - __path__ = "/var/log/nginx/users/**/*.log", - }] +local.file_match "nginx_logs" { + path_targets = [{ + __path__ = "/var/log/nginx/users/**/*.log", + }] +} + + loki.source.file "nginx_logs" { + targets = local.file_match.nginx_logs.targets + forward_to = [loki.process.nginx_logs.receiver] + tail_from_end = true +} + + loki.process "nginx_logs" { + forward_to = [loki.write.default.receiver] + + stage.regex { + expression = "/var/log/nginx/users/(?P[^/]+)/" + source = "__path__" + } + + stage.labels { + values = { + username = "username", } - - loki.source.file "nginx_logs" { - targets = local.file_match.nginx_logs.targets - forward_to = [loki.process.nginx_logs.receiver] - tail_from_end = true } - - loki.process "nginx_logs" { - forward_to = [loki.write.default.receiver] - - stage.regex { - expression = "/var/log/nginx/users/(?P[^/]+)/" - source = "__path__" - } - - stage.labels { - values = { - username = "username", - } - } - - stage.regex { - expression = "/var/log/nginx/users/[^/]+/(?P[^.]+)\\.log" - source = "__path__" - } - - stage.labels { - values = { - stream = "stream", - job = "\"nginx\"", - } - } + + stage.regex { + expression = "/var/log/nginx/users/[^/]+/(?P[^.]+)\\.log" + source = "__path__" } - - livedebugging { - enabled = true + + stage.labels { + values = { + stream = "stream", + job = "\"nginx\"", + } + } +} + + livedebugging { + enabled = true +} + + loki.write "default" { + endpoint { + url = "http://gateway:3100/loki/api/v1/push" + tenant_id = "tenant1" } - - loki.write "default" { - endpoint { - url = "http://gateway:3100/loki/api/v1/push" - tenant_id = "tenant1" - } - external_labels = {} - } \ No newline at end of file + external_labels = {} +} \ No newline at end of file diff --git a/src/templates/nginx.conf.tmpl b/src/templates/nginx.conf.tmpl index 7cab95c..931f0fd 100644 --- a/src/templates/nginx.conf.tmpl +++ b/src/templates/nginx.conf.tmpl @@ -6,7 +6,7 @@ location /{{ $.Username }}_{{ .Path }} { {{- if .Auth }} auth_basic "Restricted"; - auth_basic_user_file /etc/nginx/.htpasswd; + auth_basic_user_file /etc/apache2/.htpasswd; {{- end }} {{- if .RateLimit }} diff --git a/test/load/test_local.js b/test/load/test_local.js index 5a47798..2fcb448 100644 --- a/test/load/test_local.js +++ b/test/load/test_local.js @@ -2,15 +2,17 @@ import http from 'k6/http'; import { check, sleep } from 'k6'; export const options = { - vus: 50, + vus: 10, duration: '30s' }; export default function () { - const res = http.get('http://localhost:8080/zutto_products'); + const res = http.get('http://localhost:8080/test1_testhttp'); check(res, { 'status is 200': (r) => r.status === 200, 'response time < 100ms': (r) => r.timings.duration < 100, }); + + sleep(1) } \ No newline at end of file