From e1cc999ea825ca01af6bdc7438a7393966f97b26 Mon Sep 17 00:00:00 2001 From: Vladimir Zagainov Date: Fri, 29 May 2026 23:53:33 +0300 Subject: [PATCH] refactor: deduplicate sha1Hex/writeJSON/writeError into pkg/utils - admin.go: replace local sha1Hex, sha256Hex, writeJSON, writeError with pkg/utils equivalents - auth.go: replace local writeJSON with utils.WriteJSON; rewrite writeError as thin wrapper - cas.go: remove local sha1Hex and unused writeJSON; use utils.SHA1Bytes - pkg/utils.go: add WriteJSON, WriteError; reorder imports --- internal/admin/admin.go | 106 +++++++++++++------------------- internal/api/api.go | 132 ++++++++++++++++++---------------------- internal/auth/auth.go | 15 ++--- internal/cas/cas.go | 18 ++---- pkg/utils/utils.go | 26 +++++++- 5 files changed, 131 insertions(+), 166 deletions(-) diff --git a/internal/admin/admin.go b/internal/admin/admin.go index 80fe8ef..44d349b 100644 --- a/internal/admin/admin.go +++ b/internal/admin/admin.go @@ -5,10 +5,7 @@ import ( "archive/zip" "bytes" "context" - "crypto/sha1" - "crypto/sha256" "crypto/subtle" - "encoding/hex" "encoding/json" "fmt" "io" @@ -18,6 +15,7 @@ import ( "strconv" "strings" + "gitea.mrixs.me/Mrixs/MrixsCraft-server/pkg/utils" "gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/auth" "gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/config" "gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/database" @@ -55,7 +53,7 @@ func (h *Handler) auth(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { token := auth.ExtractBearer(r.Header.Get("Authorization")) if token == "" { - writeError(w, http.StatusUnauthorized, "Missing authorization token") + utils.WriteError(w, http.StatusUnauthorized, "Missing authorization token") return } @@ -70,11 +68,11 @@ func (h *Handler) auth(next http.HandlerFunc) http.HandlerFunc { ).Scan(&userID, &role) if err != nil { - writeError(w, http.StatusUnauthorized, "Invalid token") + utils.WriteError(w, http.StatusUnauthorized, "Invalid token") return } if role != "admin" { - writeError(w, http.StatusForbidden, "Admin access required") + utils.WriteError(w, http.StatusForbidden, "Admin access required") return } @@ -87,7 +85,7 @@ func (h *Handler) ciToken(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { token := r.Header.Get("X-CI-Token") if token == "" || subtle.ConstantTimeCompare([]byte(token), []byte(h.cfg.CIsecret)) != 1 { - writeError(w, http.StatusForbidden, "Invalid CI token") + utils.WriteError(w, http.StatusForbidden, "Invalid CI token") return } next(w, r) @@ -119,7 +117,7 @@ func (h *Handler) listModpacks(w http.ResponseWriter, r *http.Request) { `SELECT id, slug, name, minecraft_version, java_version, server_ip, is_active FROM modpacks ORDER BY created_at DESC`) if err != nil { - writeError(w, http.StatusInternalServerError, "Database error") + utils.WriteError(w, http.StatusInternalServerError, "Database error") return } defer rows.Close() @@ -132,24 +130,24 @@ func (h *Handler) listModpacks(w http.ResponseWriter, r *http.Request) { } modpacks = append(modpacks, m) } - writeJSON(w, http.StatusOK, modpacks) + utils.WriteJSON(w, http.StatusOK, modpacks) } func (h *Handler) createModpack(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil { - writeError(w, http.StatusBadRequest, "Cannot read body") + utils.WriteError(w, http.StatusBadRequest, "Cannot read body") return } var req modpackRequest if err := json.Unmarshal(body, &req); err != nil { - writeError(w, http.StatusBadRequest, "Invalid JSON") + utils.WriteError(w, http.StatusBadRequest, "Invalid JSON") return } if req.Slug == "" || req.Name == "" || req.MinecraftVersion == "" { - writeError(w, http.StatusBadRequest, "slug, name, and minecraft_version are required") + utils.WriteError(w, http.StatusBadRequest, "slug, name, and minecraft_version are required") return } @@ -159,29 +157,29 @@ func (h *Handler) createModpack(w http.ResponseWriter, r *http.Request) { req.Slug, req.Name, req.MinecraftVersion, req.JavaVersion, req.ServerIP, ) if err != nil { - writeError(w, http.StatusConflict, "Modpack with this slug already exists") + utils.WriteError(w, http.StatusConflict, "Modpack with this slug already exists") return } - writeJSON(w, http.StatusCreated, map[string]string{"status": "created"}) + utils.WriteJSON(w, http.StatusCreated, map[string]string{"status": "created"}) } func (h *Handler) updateModpack(w http.ResponseWriter, r *http.Request) { id, err := strconv.Atoi(r.PathValue("id")) if err != nil { - writeError(w, http.StatusBadRequest, "Invalid modpack ID") + utils.WriteError(w, http.StatusBadRequest, "Invalid modpack ID") return } body, err := io.ReadAll(r.Body) if err != nil { - writeError(w, http.StatusBadRequest, "Cannot read body") + utils.WriteError(w, http.StatusBadRequest, "Cannot read body") return } var req modpackRequest if err := json.Unmarshal(body, &req); err != nil { - writeError(w, http.StatusBadRequest, "Invalid JSON") + utils.WriteError(w, http.StatusBadRequest, "Invalid JSON") return } @@ -191,28 +189,28 @@ func (h *Handler) updateModpack(w http.ResponseWriter, r *http.Request) { req.Name, req.Slug, req.MinecraftVersion, req.JavaVersion, req.ServerIP, id, ) if err != nil { - writeError(w, http.StatusInternalServerError, "Update failed") + utils.WriteError(w, http.StatusInternalServerError, "Update failed") return } - writeJSON(w, http.StatusOK, map[string]string{"status": "updated"}) + utils.WriteJSON(w, http.StatusOK, map[string]string{"status": "updated"}) } func (h *Handler) deleteModpack(w http.ResponseWriter, r *http.Request) { id, err := strconv.Atoi(r.PathValue("id")) if err != nil { - writeError(w, http.StatusBadRequest, "Invalid modpack ID") + utils.WriteError(w, http.StatusBadRequest, "Invalid modpack ID") return } _, err = h.db.Pool().Exec(r.Context(), `UPDATE modpacks SET is_active = false WHERE id = $1`, id) if err != nil { - writeError(w, http.StatusInternalServerError, "Delete failed") + utils.WriteError(w, http.StatusInternalServerError, "Delete failed") return } - writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"}) + utils.WriteJSON(w, http.StatusOK, map[string]string{"status": "deleted"}) } // ── File Upload ─────────────────────────────────────────────── @@ -220,7 +218,7 @@ func (h *Handler) deleteModpack(w http.ResponseWriter, r *http.Request) { func (h *Handler) uploadFiles(w http.ResponseWriter, r *http.Request) { slug := r.PathValue("slug") if slug == "" { - writeError(w, http.StatusBadRequest, "Modpack slug is required") + utils.WriteError(w, http.StatusBadRequest, "Modpack slug is required") return } @@ -229,18 +227,18 @@ func (h *Handler) uploadFiles(w http.ResponseWriter, r *http.Request) { `SELECT EXISTS(SELECT 1 FROM modpacks WHERE slug = $1 AND is_active = true)`, slug, ).Scan(&exists) if err != nil || !exists { - writeError(w, http.StatusNotFound, "Modpack not found") + utils.WriteError(w, http.StatusNotFound, "Modpack not found") return } if err := r.ParseMultipartForm(500 << 20); err != nil { - writeError(w, http.StatusBadRequest, "Cannot parse form (max 500 MB)") + utils.WriteError(w, http.StatusBadRequest, "Cannot parse form (max 500 MB)") return } files := r.MultipartForm.File["files"] if len(files) == 0 { - writeError(w, http.StatusBadRequest, "No files uploaded (field name: 'files')") + utils.WriteError(w, http.StatusBadRequest, "No files uploaded (field name: 'files')") return } @@ -267,7 +265,7 @@ func (h *Handler) uploadFiles(w http.ResponseWriter, r *http.Request) { } } - hash := sha1Hex(data) + hash := utils.SHA1Bytes(data) destDir := filepath.Join(h.cfg.CASDir, hash[:2]) os.MkdirAll(destDir, 0o755) os.WriteFile(filepath.Join(destDir, hash), data, 0o644) @@ -285,7 +283,7 @@ func (h *Handler) uploadFiles(w http.ResponseWriter, r *http.Request) { uploaded = append(uploaded, fh.Filename) } - writeJSON(w, http.StatusOK, map[string]interface{}{ + utils.WriteJSON(w, http.StatusOK, map[string]interface{}{ "status": "uploaded", "files": uploaded, "count": len(uploaded), @@ -370,19 +368,19 @@ type manifestLaunch struct { func (h *Handler) generateManifest(w http.ResponseWriter, r *http.Request) { slug := r.PathValue("slug") if slug == "" { - writeError(w, http.StatusBadRequest, "Modpack slug is required") + utils.WriteError(w, http.StatusBadRequest, "Modpack slug is required") return } body, err := io.ReadAll(r.Body) if err != nil { - writeError(w, http.StatusBadRequest, "Cannot read body") + utils.WriteError(w, http.StatusBadRequest, "Cannot read body") return } var req manifestRequest if err := json.Unmarshal(body, &req); err != nil { - writeError(w, http.StatusBadRequest, "Invalid JSON") + utils.WriteError(w, http.StatusBadRequest, "Invalid JSON") return } @@ -392,7 +390,7 @@ func (h *Handler) generateManifest(w http.ResponseWriter, r *http.Request) { WHERE slug = $1 AND is_active = true`, slug, ).Scan(&mp.ID, &mp.Name, &mp.MinecraftVersion, &mp.JavaVersion, &mp.ServerIP) if err != nil { - writeError(w, http.StatusNotFound, "Modpack not found") + utils.WriteError(w, http.StatusNotFound, "Modpack not found") return } @@ -410,7 +408,7 @@ func (h *Handler) generateManifest(w http.ResponseWriter, r *http.Request) { if err != nil { return nil } - hash := sha1Hex(data) + hash := utils.SHA1Bytes(data) casDir := filepath.Join(h.cfg.CASDir, hash[:2]) os.MkdirAll(casDir, 0o755) @@ -452,14 +450,14 @@ func (h *Handler) generateManifest(w http.ResponseWriter, r *http.Request) { data, _ := json.MarshalIndent(manifest, "", " ") os.WriteFile(filepath.Join(manifestDir, "manifest.json"), data, 0o644) - writeJSON(w, http.StatusOK, manifest) + utils.WriteJSON(w, http.StatusOK, manifest) } // ── Launcher Release (CI/CD) ────────────────────────────────── func (h *Handler) launcherRelease(w http.ResponseWriter, r *http.Request) { if err := r.ParseMultipartForm(100 << 20); err != nil { - writeError(w, http.StatusBadRequest, "Cannot parse form (max 100 MB)") + utils.WriteError(w, http.StatusBadRequest, "Cannot parse form (max 100 MB)") return } @@ -469,13 +467,13 @@ func (h *Handler) launcherRelease(w http.ResponseWriter, r *http.Request) { sha256 := r.FormValue("sha256") if version == "" || osParam == "" || arch == "" || sha256 == "" { - writeError(w, http.StatusBadRequest, "version, os, arch, and sha256 are required") + utils.WriteError(w, http.StatusBadRequest, "version, os, arch, and sha256 are required") return } file, header, err := r.FormFile("file") if err != nil { - writeError(w, http.StatusBadRequest, "No file uploaded (field name: 'file')") + utils.WriteError(w, http.StatusBadRequest, "No file uploaded (field name: 'file')") return } defer file.Close() @@ -485,18 +483,18 @@ func (h *Handler) launcherRelease(w http.ResponseWriter, r *http.Request) { data, err := io.ReadAll(file) if err != nil { - writeError(w, http.StatusInternalServerError, "Cannot read uploaded file") + utils.WriteError(w, http.StatusInternalServerError, "Cannot read uploaded file") return } - if got := sha256Hex(data); got != sha256 { - writeError(w, http.StatusBadRequest, fmt.Sprintf("SHA-256 mismatch: expected %s, got %s", sha256, got)) + if got := utils.SHA256Bytes(data); got != sha256 { + utils.WriteError(w, http.StatusBadRequest, fmt.Sprintf("SHA-256 mismatch: expected %s, got %s", sha256, got)) return } dest := filepath.Join(destDir, header.Filename) if err := os.WriteFile(dest, data, 0o755); err != nil { - writeError(w, http.StatusInternalServerError, "Failed to store binary") + utils.WriteError(w, http.StatusInternalServerError, "Failed to store binary") return } @@ -506,34 +504,12 @@ func (h *Handler) launcherRelease(w http.ResponseWriter, r *http.Request) { version, osParam, arch, sha256, dest, ) if err != nil { - writeError(w, http.StatusInternalServerError, "Failed to register release") + utils.WriteError(w, http.StatusInternalServerError, "Failed to register release") return } - writeJSON(w, http.StatusCreated, map[string]string{ + utils.WriteJSON(w, http.StatusCreated, map[string]string{ "status": "released", "version": version, }) } - -// ── Helpers ─────────────────────────────────────────────────── - -func sha1Hex(data []byte) string { - h := sha1.Sum(data) - return hex.EncodeToString(h[:]) -} - -func sha256Hex(data []byte) string { - h := sha256.Sum256(data) - return hex.EncodeToString(h[:]) -} - -func writeJSON(w http.ResponseWriter, status int, v any) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - _ = json.NewEncoder(w).Encode(v) -} - -func writeError(w http.ResponseWriter, status int, msg string) { - writeJSON(w, status, map[string]string{"error": msg}) -} diff --git a/internal/api/api.go b/internal/api/api.go index 3dd31e6..41c5363 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -2,8 +2,6 @@ package api import ( - "crypto/sha1" - "encoding/hex" "encoding/json" "fmt" "image/png" @@ -16,6 +14,7 @@ import ( "gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/auth" "gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/config" "gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/database" + "gitea.mrixs.me/Mrixs/MrixsCraft-server/pkg/utils" ) // Max skin file size: 1 MB (Minecraft skins are ~2-10 KB). @@ -102,16 +101,16 @@ type serverInfo struct { } type launcherLatestResponse struct { - Version string `json:"version"` - Downloads map[string]string `json:"downloads"` // os+arch -> url - SHA256 string `json:"sha256"` - IsMandatory bool `json:"is_mandatory"` + Version string `json:"version"` + Downloads map[string]string `json:"downloads"` // os+arch -> url + SHA256 string `json:"sha256"` + IsMandatory bool `json:"is_mandatory"` } type profileResponse struct { - UUID string `json:"uuid"` - Username string `json:"username"` - Textures *textureInfo `json:"textures,omitempty"` + UUID string `json:"uuid"` + Username string `json:"username"` + Textures *textureInfo `json:"textures,omitempty"` } type textureInfo struct { @@ -142,24 +141,24 @@ type errorResponse struct { func (h *Handler) register(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil { - writeError(w, http.StatusBadRequest, "Cannot read body") + utils.WriteError(w, http.StatusBadRequest, "Cannot read body") return } var req registerRequest if err := json.Unmarshal(body, &req); err != nil { - writeError(w, http.StatusBadRequest, "Invalid JSON") + utils.WriteError(w, http.StatusBadRequest, "Invalid JSON") return } if req.Username == "" || req.Email == "" || req.Password == "" { - writeError(w, http.StatusBadRequest, "Username, email and password are required") + utils.WriteError(w, http.StatusBadRequest, "Username, email and password are required") return } // Basic email validation. if !strings.Contains(req.Email, "@") || !strings.Contains(req.Email, ".") { - writeError(w, http.StatusBadRequest, "Invalid email address") + utils.WriteError(w, http.StatusBadRequest, "Invalid email address") return } @@ -170,18 +169,18 @@ func (h *Handler) register(w http.ResponseWriter, r *http.Request) { req.Username, req.Email, ).Scan(&exists) if err != nil { - writeError(w, http.StatusInternalServerError, "Database error") + utils.WriteError(w, http.StatusInternalServerError, "Database error") return } if exists > 0 { - writeError(w, http.StatusConflict, "Username or email already taken") + utils.WriteError(w, http.StatusConflict, "Username or email already taken") return } uuid := auth.GenerateUUID() passwordHash, err := auth.HashPassword(req.Password) if err != nil { - writeError(w, http.StatusInternalServerError, "Failed to hash password") + utils.WriteError(w, http.StatusInternalServerError, "Failed to hash password") return } @@ -191,28 +190,28 @@ func (h *Handler) register(w http.ResponseWriter, r *http.Request) { req.Username, req.Email, passwordHash, uuid, ) if err != nil { - writeError(w, http.StatusInternalServerError, "Failed to create user") + utils.WriteError(w, http.StatusInternalServerError, "Failed to create user") return } - writeJSON(w, http.StatusCreated, registerResponse{UUID: uuid}) + utils.WriteJSON(w, http.StatusCreated, registerResponse{UUID: uuid}) } func (h *Handler) webLogin(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil { - writeError(w, http.StatusBadRequest, "Cannot read body") + utils.WriteError(w, http.StatusBadRequest, "Cannot read body") return } var req webLoginRequest if err := json.Unmarshal(body, &req); err != nil { - writeError(w, http.StatusBadRequest, "Invalid JSON") + utils.WriteError(w, http.StatusBadRequest, "Invalid JSON") return } if req.Username == "" || req.Password == "" { - writeError(w, http.StatusBadRequest, "Username and password are required") + utils.WriteError(w, http.StatusBadRequest, "Username and password are required") return } @@ -224,7 +223,7 @@ func (h *Handler) webLogin(w http.ResponseWriter, r *http.Request) { ).Scan(&user.ID, &user.Username, &user.PasswordHash, &user.UUID) if err != nil || !auth.VerifyPassword(req.Password, user.PasswordHash) { - writeError(w, http.StatusUnauthorized, "Invalid credentials") + utils.WriteError(w, http.StatusUnauthorized, "Invalid credentials") return } @@ -237,11 +236,11 @@ func (h *Handler) webLogin(w http.ResponseWriter, r *http.Request) { token, token, user.ID, ) if err != nil { - writeError(w, http.StatusInternalServerError, "Failed to create session") + utils.WriteError(w, http.StatusInternalServerError, "Failed to create session") return } - writeJSON(w, http.StatusOK, webLoginResponse{ + utils.WriteJSON(w, http.StatusOK, webLoginResponse{ Token: token, UUID: user.UUID, Username: user.Username, @@ -256,39 +255,39 @@ func (h *Handler) uploadSkin(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, maxSkinSize) if err := r.ParseMultipartForm(maxSkinSize); err != nil { - writeError(w, http.StatusBadRequest, "Cannot parse form (max 1 MB)") + utils.WriteError(w, http.StatusBadRequest, "Cannot parse form (max 1 MB)") return } file, _, err := r.FormFile("skin") if err != nil { - writeError(w, http.StatusBadRequest, "No skin file provided") + utils.WriteError(w, http.StatusBadRequest, "No skin file provided") return } defer file.Close() data, err := io.ReadAll(file) if err != nil { - writeError(w, http.StatusBadRequest, "Cannot read skin file") + utils.WriteError(w, http.StatusBadRequest, "Cannot read skin file") return } if !isValidSkinPNG(data) { - writeError(w, http.StatusBadRequest, "Invalid skin: must be a valid Minecraft skin PNG (64x32, 64x64, 128x128, or 128x64)") + utils.WriteError(w, http.StatusBadRequest, "Invalid skin: must be a valid Minecraft skin PNG (64x32, 64x64, 128x128, or 128x64)") return } - hash := sha1Hex(data) + hash := utils.SHA1Bytes(data) // Store in CAS. destDir := filepath.Join(h.cfg.SkinsDir, hash[:2]) if err := os.MkdirAll(destDir, 0o755); err != nil { - writeError(w, http.StatusInternalServerError, "Failed to store skin") + utils.WriteError(w, http.StatusInternalServerError, "Failed to store skin") return } dest := filepath.Join(destDir, hash+".png") if err := os.WriteFile(dest, data, 0o644); err != nil { - writeError(w, http.StatusInternalServerError, "Failed to write skin") + utils.WriteError(w, http.StatusInternalServerError, "Failed to write skin") return } @@ -300,11 +299,11 @@ func (h *Handler) uploadSkin(w http.ResponseWriter, r *http.Request) { userID, hash, ) if err != nil { - writeError(w, http.StatusInternalServerError, "Failed to update profile") + utils.WriteError(w, http.StatusInternalServerError, "Failed to update profile") return } - writeJSON(w, http.StatusOK, map[string]string{"hash": hash}) + utils.WriteJSON(w, http.StatusOK, map[string]string{"hash": hash}) } func (h *Handler) uploadCape(w http.ResponseWriter, r *http.Request) { @@ -315,40 +314,40 @@ func (h *Handler) uploadCape(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, maxCapeSize) if err := r.ParseMultipartForm(maxCapeSize); err != nil { - writeError(w, http.StatusBadRequest, "Cannot parse form (max 2 MB)") + utils.WriteError(w, http.StatusBadRequest, "Cannot parse form (max 2 MB)") return } file, _, err := r.FormFile("cape") if err != nil { - writeError(w, http.StatusBadRequest, "No cape file provided") + utils.WriteError(w, http.StatusBadRequest, "No cape file provided") return } defer file.Close() data, err := io.ReadAll(file) if err != nil { - writeError(w, http.StatusBadRequest, "Cannot read cape file") + utils.WriteError(w, http.StatusBadRequest, "Cannot read cape file") return } // Validate PNG header. if len(data) < 8 || string(data[:8]) != "\x89PNG\r\n\x1a\n" { - writeError(w, http.StatusBadRequest, "Invalid PNG file") + utils.WriteError(w, http.StatusBadRequest, "Invalid PNG file") return } - hash := sha1Hex(data) + hash := utils.SHA1Bytes(data) // Store in skins dir alongside skins (same CAS layout). destDir := filepath.Join(h.cfg.SkinsDir, hash[:2]) if err := os.MkdirAll(destDir, 0o755); err != nil { - writeError(w, http.StatusInternalServerError, "Failed to store cape") + utils.WriteError(w, http.StatusInternalServerError, "Failed to store cape") return } dest := filepath.Join(destDir, hash+".png") if err := os.WriteFile(dest, data, 0o644); err != nil { - writeError(w, http.StatusInternalServerError, "Failed to write cape") + utils.WriteError(w, http.StatusInternalServerError, "Failed to write cape") return } @@ -360,11 +359,11 @@ func (h *Handler) uploadCape(w http.ResponseWriter, r *http.Request) { userID, hash, ) if err != nil { - writeError(w, http.StatusInternalServerError, "Failed to update profile") + utils.WriteError(w, http.StatusInternalServerError, "Failed to update profile") return } - writeJSON(w, http.StatusOK, map[string]string{"hash": hash}) + utils.WriteJSON(w, http.StatusOK, map[string]string{"hash": hash}) } func (h *Handler) deleteSkin(w http.ResponseWriter, r *http.Request) { @@ -378,11 +377,11 @@ func (h *Handler) deleteSkin(w http.ResponseWriter, r *http.Request) { userID, ) if err != nil { - writeError(w, http.StatusInternalServerError, "Failed to remove skin") + utils.WriteError(w, http.StatusInternalServerError, "Failed to remove skin") return } - writeJSON(w, http.StatusOK, map[string]string{"status": "skin removed"}) + utils.WriteJSON(w, http.StatusOK, map[string]string{"status": "skin removed"}) } func (h *Handler) deleteCape(w http.ResponseWriter, r *http.Request) { @@ -396,11 +395,11 @@ func (h *Handler) deleteCape(w http.ResponseWriter, r *http.Request) { userID, ) if err != nil { - writeError(w, http.StatusInternalServerError, "Failed to remove cape") + utils.WriteError(w, http.StatusInternalServerError, "Failed to remove cape") return } - writeJSON(w, http.StatusOK, map[string]string{"status": "cape removed"}) + utils.WriteJSON(w, http.StatusOK, map[string]string{"status": "cape removed"}) } // authenticateRequest extracts and validates the Bearer token from the request, @@ -408,7 +407,7 @@ func (h *Handler) deleteCape(w http.ResponseWriter, r *http.Request) { func (h *Handler) authenticateRequest(w http.ResponseWriter, r *http.Request) int { token := auth.ExtractBearer(r.Header.Get("Authorization")) if token == "" { - writeError(w, http.StatusUnauthorized, "Missing authorization token") + utils.WriteError(w, http.StatusUnauthorized, "Missing authorization token") return 0 } @@ -419,7 +418,7 @@ func (h *Handler) authenticateRequest(w http.ResponseWriter, r *http.Request) in token, ).Scan(&userID) if err != nil { - writeError(w, http.StatusUnauthorized, "Invalid or expired token") + utils.WriteError(w, http.StatusUnauthorized, "Invalid or expired token") return 0 } return userID @@ -430,7 +429,7 @@ func (h *Handler) launcherLatest(w http.ResponseWriter, r *http.Request) { archParam := r.URL.Query().Get("arch") if osParam == "" || archParam == "" { - writeError(w, http.StatusBadRequest, "os and arch query parameters are required") + utils.WriteError(w, http.StatusBadRequest, "os and arch query parameters are required") return } @@ -443,14 +442,14 @@ func (h *Handler) launcherLatest(w http.ResponseWriter, r *http.Request) { ).Scan(&release.Version, &release.FilePath, &release.SHA256, &release.IsMandatory) if err != nil { - writeError(w, http.StatusNotFound, "No release found for this platform") + utils.WriteError(w, http.StatusNotFound, "No release found for this platform") return } downloadURL := fmt.Sprintf("%s/files/launcher/%s/%s/%s/%s", h.cfg.BaseURL, release.Version, osParam, archParam, filepath.Base(release.FilePath)) - writeJSON(w, http.StatusOK, launcherLatestResponse{ + utils.WriteJSON(w, http.StatusOK, launcherLatestResponse{ Version: release.Version, Downloads: map[string]string{osParam + "_" + archParam: downloadURL}, SHA256: release.SHA256, @@ -463,7 +462,7 @@ func (h *Handler) serversList(w http.ResponseWriter, r *http.Request) { `SELECT slug, name, minecraft_version, server_ip FROM modpacks WHERE is_active = true ORDER BY created_at DESC`) if err != nil { - writeError(w, http.StatusInternalServerError, "Database error") + utils.WriteError(w, http.StatusInternalServerError, "Database error") return } defer rows.Close() @@ -477,13 +476,13 @@ func (h *Handler) serversList(w http.ResponseWriter, r *http.Request) { servers = append(servers, s) } - writeJSON(w, http.StatusOK, serversResponse{Servers: servers}) + utils.WriteJSON(w, http.StatusOK, serversResponse{Servers: servers}) } func (h *Handler) instanceManifest(w http.ResponseWriter, r *http.Request) { slug := r.PathValue("slug") if slug == "" { - writeError(w, http.StatusBadRequest, "Instance slug is required") + utils.WriteError(w, http.StatusBadRequest, "Instance slug is required") return } @@ -494,7 +493,7 @@ func (h *Handler) instanceManifest(w http.ResponseWriter, r *http.Request) { slug, ).Scan(&exists) if err != nil || !exists { - writeError(w, http.StatusNotFound, "Instance not found") + utils.WriteError(w, http.StatusNotFound, "Instance not found") return } @@ -502,7 +501,7 @@ func (h *Handler) instanceManifest(w http.ResponseWriter, r *http.Request) { manifestPath := filepath.Join(h.cfg.CASDir, "..", "manifests", slug, "manifest.json") data, err := os.ReadFile(manifestPath) if err != nil { - writeError(w, http.StatusNotFound, "Manifest not found") + utils.WriteError(w, http.StatusNotFound, "Manifest not found") return } @@ -513,7 +512,7 @@ func (h *Handler) instanceManifest(w http.ResponseWriter, r *http.Request) { func (h *Handler) getProfile(w http.ResponseWriter, r *http.Request) { uuid := r.PathValue("uuid") if uuid == "" { - writeError(w, http.StatusBadRequest, "UUID is required") + utils.WriteError(w, http.StatusBadRequest, "UUID is required") return } @@ -528,7 +527,7 @@ func (h *Handler) getProfile(w http.ResponseWriter, r *http.Request) { uuid, ).Scan(&p.UUID, &p.Username, &skinHash, &capeHash, &isSlim) if err != nil { - writeError(w, http.StatusNotFound, "Player not found") + utils.WriteError(w, http.StatusNotFound, "Player not found") return } @@ -540,7 +539,7 @@ func (h *Handler) getProfile(w http.ResponseWriter, r *http.Request) { } } - writeJSON(w, http.StatusOK, p) + utils.WriteJSON(w, http.StatusOK, p) } func (h *Handler) serveSkin(w http.ResponseWriter, r *http.Request) { @@ -572,11 +571,6 @@ func (h *Handler) serveSkin(w http.ResponseWriter, r *http.Request) { // ── Helpers ─────────────────────────────────────────────────── -func sha1Hex(data []byte) string { - h := sha1.Sum(data) - return hex.EncodeToString(h[:]) -} - // isValidSkinPNG checks that data is a valid PNG with accepted Minecraft skin dimensions. func isValidSkinPNG(data []byte) bool { if len(data) < 8 || string(data[:8]) != "\x89PNG\r\n\x1a\n" { @@ -602,13 +596,3 @@ func deref(s *string) string { } return *s } - -func writeJSON(w http.ResponseWriter, status int, v any) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - _ = json.NewEncoder(w).Encode(v) -} - -func writeError(w http.ResponseWriter, status int, msg string) { - writeJSON(w, status, errorResponse{Error: msg}) -} diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 645092b..a8bb4bf 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -17,6 +17,7 @@ import ( "gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/config" "gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/database" + "gitea.mrixs.me/Mrixs/MrixsCraft-server/pkg/utils" ) // Handler serves Yggdrasil endpoints. @@ -159,7 +160,7 @@ func (h *Handler) authenticate(w http.ResponseWriter, r *http.Request) { }}, } - writeJSON(w, http.StatusOK, resp) + utils.WriteJSON(w, http.StatusOK, resp) } func (h *Handler) refresh(w http.ResponseWriter, r *http.Request) { @@ -216,7 +217,7 @@ func (h *Handler) refresh(w http.ResponseWriter, r *http.Request) { return } - writeJSON(w, http.StatusOK, refreshResponse{ + utils.WriteJSON(w, http.StatusOK, refreshResponse{ AccessToken: newAccessToken, ClientToken: req.ClientToken, SelectedProfile: &profile{ @@ -356,7 +357,7 @@ func (h *Handler) sessionProfile(w http.ResponseWriter, r *http.Request) { Props: props, } - writeJSON(w, http.StatusOK, prof) + utils.WriteJSON(w, http.StatusOK, prof) } // ── Helpers ─────────────────────────────────────────────────── @@ -394,14 +395,8 @@ func GenerateToken() string { return hex.EncodeToString(b) } -func writeJSON(w http.ResponseWriter, status int, v any) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - _ = json.NewEncoder(w).Encode(v) -} - func writeError(w http.ResponseWriter, status int, err, msg string) { - writeJSON(w, status, errorResponse{ + utils.WriteJSON(w, status, errorResponse{ Error: err, ErrorMessage: msg, }) diff --git a/internal/cas/cas.go b/internal/cas/cas.go index db7be5f..289a293 100644 --- a/internal/cas/cas.go +++ b/internal/cas/cas.go @@ -6,14 +6,14 @@ package cas import ( - "crypto/sha1" "crypto/subtle" - "encoding/hex" "net/http" "os" "path/filepath" "strings" + "gitea.mrixs.me/Mrixs/MrixsCraft-server/pkg/utils" + "gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/config" "gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/database" ) @@ -148,7 +148,7 @@ func isValidHash(hash string) bool { // StoreFile writes data to the CAS directory structure. // Returns the SHA-1 hash of the stored data. func StoreFile(casDir string, data []byte) (string, error) { - hash := sha1Hex(data) + hash := utils.SHA1Bytes(data) destDir := filepath.Join(casDir, hash[:2]) if err := os.MkdirAll(destDir, 0o755); err != nil { return "", err @@ -170,7 +170,7 @@ func FileExists(casDir, hash string) bool { // VerifyAndStore writes data to CAS only if the SHA-1 matches the expected hash. // This prevents corrupt or tampered uploads from being stored. func VerifyAndStore(casDir string, data []byte, expectedHash string) (string, error) { - got := sha1Hex(data) + got := utils.SHA1Bytes(data) if subtle.ConstantTimeCompare([]byte(got), []byte(expectedHash)) != 1 { return "", ErrHashMismatch } @@ -188,13 +188,3 @@ var ErrHashMismatch = errHashMismatch type hashMismatchError struct{} func (e *hashMismatchError) Error() string { return "SHA-1 hash mismatch" } - -func sha1Hex(data []byte) string { - h := sha1.Sum(data) - return hex.EncodeToString(h[:]) -} - -func writeJSON(w http.ResponseWriter, status int, v any) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) -} diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 7df8734..4616586 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -1,19 +1,23 @@ -// package utils provides shared utility functions (SHA-1, SHA-256, ZIP, etc.). +// package utils provides shared utility functions (hashing, HTTP helpers, ZIP). + package utils import ( + "archive/zip" "bytes" "crypto/sha1" "crypto/sha256" "encoding/hex" + "encoding/json" "io" + "net/http" "os" "path/filepath" "strings" - - "archive/zip" ) +// ── Hashing ──────────────────────────────────────────────────── + // SHA1Bytes returns the SHA-1 hex string of the given data. func SHA1Bytes(data []byte) string { h := sha1.Sum(data) @@ -41,6 +45,22 @@ func SHA1File(path string) (string, error) { return hex.EncodeToString(h.Sum(nil)), nil } +// ── HTTP helpers ─────────────────────────────────────────────── + +// WriteJSON writes a JSON response with the given status code. +func WriteJSON(w http.ResponseWriter, status int, v any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + _ = json.NewEncoder(w).Encode(v) +} + +// WriteError writes a JSON error response. +func WriteError(w http.ResponseWriter, status int, msg string) { + WriteJSON(w, status, map[string]string{"error": msg}) +} + +// ── ZIP ──────────────────────────────────────────────────────── + // Unzip extracts a ZIP archive to the destination directory. // Returns the list of extracted file paths. // Protects against zip-slip by validating that each entry's target path