From 25ed1188ed970a19675befef12afe68045565c4a Mon Sep 17 00:00:00 2001 From: Julien Dessaux Date: Sun, 17 Nov 2024 00:05:22 +0100 Subject: chore(tfstated): refactor helpers to their own package --- cmd/tfstated/delete.go | 7 ++++--- cmd/tfstated/get.go | 5 +++-- cmd/tfstated/helpers.go | 36 ------------------------------------ cmd/tfstated/lock.go | 13 +++++++------ cmd/tfstated/post.go | 9 +++++---- cmd/tfstated/unlock.go | 11 ++++++----- pkg/basic_auth/middleware.go | 16 +++++++--------- pkg/database/accounts.go | 5 +++-- pkg/helpers/crypto.go | 21 +++++++++++++++++++++ pkg/helpers/error.go | 17 +++++++++++++++++ pkg/helpers/json.go | 25 +++++++++++++++++++++++++ pkg/model/account.go | 19 ++----------------- 12 files changed, 100 insertions(+), 84 deletions(-) delete mode 100644 cmd/tfstated/helpers.go create mode 100644 pkg/helpers/crypto.go create mode 100644 pkg/helpers/error.go create mode 100644 pkg/helpers/json.go diff --git a/cmd/tfstated/delete.go b/cmd/tfstated/delete.go index 3b708d5..d594073 100644 --- a/cmd/tfstated/delete.go +++ b/cmd/tfstated/delete.go @@ -5,22 +5,23 @@ import ( "net/http" "git.adyxax.org/adyxax/tfstated/pkg/database" + "git.adyxax.org/adyxax/tfstated/pkg/helpers" ) func handleDelete(db *database.DB) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { - _ = errorResponse(w, http.StatusBadRequest, + helpers.ErrorResponse(w, http.StatusBadRequest, fmt.Errorf("no state path provided, cannot DELETE /")) return } if success, err := db.DeleteState(r.URL.Path); err != nil { - _ = errorResponse(w, http.StatusInternalServerError, err) + helpers.ErrorResponse(w, http.StatusInternalServerError, err) } else if success { w.WriteHeader(http.StatusOK) } else { - _ = errorResponse(w, http.StatusNotFound, + helpers.ErrorResponse(w, http.StatusNotFound, fmt.Errorf("state path not found: %s", r.URL.Path)) } }) diff --git a/cmd/tfstated/get.go b/cmd/tfstated/get.go index 6bc4466..3310560 100644 --- a/cmd/tfstated/get.go +++ b/cmd/tfstated/get.go @@ -5,6 +5,7 @@ import ( "net/http" "git.adyxax.org/adyxax/tfstated/pkg/database" + "git.adyxax.org/adyxax/tfstated/pkg/helpers" ) func handleGet(db *database.DB) http.Handler { @@ -12,13 +13,13 @@ func handleGet(db *database.DB) http.Handler { w.Header().Set("Cache-Control", "no-store, no-cache") if r.URL.Path == "/" { - _ = errorResponse(w, http.StatusBadRequest, + helpers.ErrorResponse(w, http.StatusBadRequest, fmt.Errorf("no state path provided, cannot GET /")) return } if data, err := db.GetState(r.URL.Path); err != nil { - _ = errorResponse(w, http.StatusInternalServerError, err) + helpers.ErrorResponse(w, http.StatusInternalServerError, err) } else { w.WriteHeader(http.StatusOK) _, _ = w.Write(data) diff --git a/cmd/tfstated/helpers.go b/cmd/tfstated/helpers.go deleted file mode 100644 index 33e14a4..0000000 --- a/cmd/tfstated/helpers.go +++ /dev/null @@ -1,36 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "log/slog" - "net/http" -) - -func decode(r *http.Request, data any) error { - if err := json.NewDecoder(r.Body).Decode(&data); err != nil { - return fmt.Errorf("failed to decode json: %w", err) - } - return nil -} - -func encode(w http.ResponseWriter, status int, data any) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - if err := json.NewEncoder(w).Encode(data); err != nil { - slog.Error("failed to encode json", "err", err) - return fmt.Errorf("failed to encode json: %w", err) - } - return nil -} - -func errorResponse(w http.ResponseWriter, status int, err error) error { - type errorResponse struct { - Msg string `json:"msg"` - Status int `json:"status"` - } - return encode(w, status, &errorResponse{ - Msg: fmt.Sprintf("%+v", err), - Status: status, - }) -} diff --git a/cmd/tfstated/lock.go b/cmd/tfstated/lock.go index bab9c6b..80e3575 100644 --- a/cmd/tfstated/lock.go +++ b/cmd/tfstated/lock.go @@ -7,6 +7,7 @@ import ( "time" "git.adyxax.org/adyxax/tfstated/pkg/database" + "git.adyxax.org/adyxax/tfstated/pkg/helpers" ) type lockRequest struct { @@ -34,27 +35,27 @@ func (l *lockRequest) valid() []error { func handleLock(db *database.DB) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { - _ = encode(w, http.StatusBadRequest, + _ = helpers.Encode(w, http.StatusBadRequest, fmt.Errorf("no state path provided, cannot LOCK /")) return } var lock lockRequest - if err := decode(r, &lock); err != nil { - _ = encode(w, http.StatusBadRequest, err) + if err := helpers.Decode(r, &lock); err != nil { + _ = helpers.Encode(w, http.StatusBadRequest, err) return } if errs := lock.valid(); len(errs) > 0 { - _ = encode(w, http.StatusBadRequest, + _ = helpers.Encode(w, http.StatusBadRequest, fmt.Errorf("invalid lock: %+v", errs)) return } if success, err := db.SetLockOrGetExistingLock(r.URL.Path, &lock); err != nil { - _ = errorResponse(w, http.StatusInternalServerError, err) + helpers.ErrorResponse(w, http.StatusInternalServerError, err) } else if success { w.WriteHeader(http.StatusOK) } else { - _ = encode(w, http.StatusConflict, lock) + _ = helpers.Encode(w, http.StatusConflict, lock) } }) } diff --git a/cmd/tfstated/post.go b/cmd/tfstated/post.go index 674eaba..86344b1 100644 --- a/cmd/tfstated/post.go +++ b/cmd/tfstated/post.go @@ -6,13 +6,14 @@ import ( "net/http" "git.adyxax.org/adyxax/tfstated/pkg/database" + "git.adyxax.org/adyxax/tfstated/pkg/helpers" "git.adyxax.org/adyxax/tfstated/pkg/model" ) func handlePost(db *database.DB) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { - _ = errorResponse(w, http.StatusBadRequest, + helpers.ErrorResponse(w, http.StatusBadRequest, fmt.Errorf("no state path provided, cannot POST /"), ) return @@ -22,15 +23,15 @@ func handlePost(db *database.DB) http.Handler { data, err := io.ReadAll(r.Body) if err != nil || len(data) == 0 { - _ = errorResponse(w, http.StatusBadRequest, err) + helpers.ErrorResponse(w, http.StatusBadRequest, err) return } account := r.Context().Value(model.AccountContextKey{}).(*model.Account) if idMismatch, err := db.SetState(r.URL.Path, account.Id, data, id); err != nil { if idMismatch { - _ = errorResponse(w, http.StatusConflict, err) + helpers.ErrorResponse(w, http.StatusConflict, err) } else { - _ = errorResponse(w, http.StatusInternalServerError, err) + helpers.ErrorResponse(w, http.StatusInternalServerError, err) } } else { w.WriteHeader(http.StatusOK) diff --git a/cmd/tfstated/unlock.go b/cmd/tfstated/unlock.go index e8bddd9..c003d8d 100644 --- a/cmd/tfstated/unlock.go +++ b/cmd/tfstated/unlock.go @@ -5,27 +5,28 @@ import ( "net/http" "git.adyxax.org/adyxax/tfstated/pkg/database" + "git.adyxax.org/adyxax/tfstated/pkg/helpers" ) func handleUnlock(db *database.DB) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { - _ = encode(w, http.StatusBadRequest, + _ = helpers.Encode(w, http.StatusBadRequest, fmt.Errorf("no state path provided, cannot LOCK /")) return } var lock lockRequest - if err := decode(r, &lock); err != nil { - _ = encode(w, http.StatusBadRequest, err) + if err := helpers.Decode(r, &lock); err != nil { + _ = helpers.Encode(w, http.StatusBadRequest, err) return } if success, err := db.Unlock(r.URL.Path, &lock); err != nil { - _ = errorResponse(w, http.StatusInternalServerError, err) + helpers.ErrorResponse(w, http.StatusInternalServerError, err) } else if success { w.WriteHeader(http.StatusOK) } else { - _ = encode(w, http.StatusConflict, lock) + _ = helpers.Encode(w, http.StatusConflict, lock) } }) } diff --git a/pkg/basic_auth/middleware.go b/pkg/basic_auth/middleware.go index 7f8fb4a..0e22ad3 100644 --- a/pkg/basic_auth/middleware.go +++ b/pkg/basic_auth/middleware.go @@ -2,10 +2,12 @@ package basic_auth import ( "context" + "fmt" "net/http" "time" "git.adyxax.org/adyxax/tfstated/pkg/database" + "git.adyxax.org/adyxax/tfstated/pkg/helpers" "git.adyxax.org/adyxax/tfstated/pkg/model" ) @@ -15,26 +17,22 @@ func Middleware(db *database.DB) func(http.Handler) http.Handler { username, password, ok := r.BasicAuth() if !ok { w.Header().Set("WWW-Authenticate", `Basic realm="tfstated", charset="UTF-8"`) - http.Error(w, "Unauthorized", http.StatusUnauthorized) + helpers.ErrorResponse(w, http.StatusUnauthorized, fmt.Errorf("Unauthorized")) return } account, err := db.LoadAccountByUsername(username) if err != nil { - http.Error(w, "Internal Server Error", http.StatusInternalServerError) + helpers.ErrorResponse(w, http.StatusInternalServerError, err) return } - if account == nil { - http.Error(w, "Forbidden", http.StatusForbidden) - return - } - if !account.CheckPassword(password) { - http.Error(w, "Forbidden", http.StatusForbidden) + if account == nil || !account.CheckPassword(password) { + helpers.ErrorResponse(w, http.StatusForbidden, fmt.Errorf("Forbidden")) return } now := time.Now().UTC() _, err = db.Exec(`UPDATE accounts SET last_login = ? WHERE id = ?`, now.Unix(), account.Id) if err != nil { - http.Error(w, "Internal Server Error", http.StatusInternalServerError) + helpers.ErrorResponse(w, http.StatusInternalServerError, err) return } ctx := context.WithValue(r.Context(), model.AccountContextKey{}, account) diff --git a/pkg/database/accounts.go b/pkg/database/accounts.go index 6400d5a..f506155 100644 --- a/pkg/database/accounts.go +++ b/pkg/database/accounts.go @@ -7,6 +7,7 @@ import ( "log/slog" "time" + "git.adyxax.org/adyxax/tfstated/pkg/helpers" "git.adyxax.org/adyxax/tfstated/pkg/model" "go.n16f.net/uuid" ) @@ -69,8 +70,8 @@ func (db *DB) InitAdminAccount() error { if err = password.Generate(uuid.V4); err != nil { return fmt.Errorf("failed to generate initial admin password: %w", err) } - salt := model.GenerateSalt() - hash := model.HashPassword(password.String(), salt) + salt := helpers.GenerateSalt() + hash := helpers.HashPassword(password.String(), salt) if _, err = tx.ExecContext(db.ctx, `INSERT INTO accounts(username, salt, password_hash, is_admin) VALUES ("admin", :salt, :hash, TRUE) diff --git a/pkg/helpers/crypto.go b/pkg/helpers/crypto.go new file mode 100644 index 0000000..ce73cd3 --- /dev/null +++ b/pkg/helpers/crypto.go @@ -0,0 +1,21 @@ +package helpers + +import ( + "crypto/sha256" + + "git.adyxax.org/adyxax/tfstated/pkg/scrypto" + "golang.org/x/crypto/pbkdf2" +) + +const ( + PBKDF2Iterations = 600000 + SaltSize = 32 +) + +func GenerateSalt() []byte { + return scrypto.RandomBytes(SaltSize) +} + +func HashPassword(password string, salt []byte) []byte { + return pbkdf2.Key([]byte(password), salt, PBKDF2Iterations, 32, sha256.New) +} diff --git a/pkg/helpers/error.go b/pkg/helpers/error.go new file mode 100644 index 0000000..006759d --- /dev/null +++ b/pkg/helpers/error.go @@ -0,0 +1,17 @@ +package helpers + +import ( + "fmt" + "net/http" +) + +func ErrorResponse(w http.ResponseWriter, status int, err error) { + type errorResponse struct { + Msg string `json:"msg"` + Status int `json:"status"` + } + _ = Encode(w, status, &errorResponse{ + Msg: fmt.Sprintf("%+v", err), + Status: status, + }) +} diff --git a/pkg/helpers/json.go b/pkg/helpers/json.go new file mode 100644 index 0000000..664a984 --- /dev/null +++ b/pkg/helpers/json.go @@ -0,0 +1,25 @@ +package helpers + +import ( + "encoding/json" + "fmt" + "log/slog" + "net/http" +) + +func Decode(r *http.Request, data any) error { + if err := json.NewDecoder(r.Body).Decode(&data); err != nil { + return fmt.Errorf("failed to decode json: %w", err) + } + return nil +} + +func Encode(w http.ResponseWriter, status int, data any) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + if err := json.NewEncoder(w).Encode(data); err != nil { + slog.Error("failed to encode json", "err", err) + return fmt.Errorf("failed to encode json: %w", err) + } + return nil +} diff --git a/pkg/model/account.go b/pkg/model/account.go index 4336dfa..7a69685 100644 --- a/pkg/model/account.go +++ b/pkg/model/account.go @@ -1,17 +1,10 @@ package model import ( - "crypto/sha256" "crypto/subtle" "time" - "git.adyxax.org/adyxax/tfstated/pkg/scrypto" - "golang.org/x/crypto/pbkdf2" -) - -const ( - PBKDF2Iterations = 600000 - SaltSize = 32 + "git.adyxax.org/adyxax/tfstated/pkg/helpers" ) type AccountContextKey struct{} @@ -28,14 +21,6 @@ type Account struct { } func (account *Account) CheckPassword(password string) bool { - hash := HashPassword(password, account.Salt) + hash := helpers.HashPassword(password, account.Salt) return subtle.ConstantTimeCompare(hash, account.PasswordHash) == 1 } - -func GenerateSalt() []byte { - return scrypto.RandomBytes(SaltSize) -} - -func HashPassword(password string, salt []byte) []byte { - return pbkdf2.Key([]byte(password), salt, PBKDF2Iterations, 32, sha256.New) -} -- cgit v1.2.3