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
This commit is contained in:
2026-05-29 23:53:33 +03:00
parent d418ae2b54
commit e1cc999ea8
5 changed files with 131 additions and 166 deletions

View File

@@ -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})
}

View File

@@ -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})
}

View File

@@ -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,
})

View File

@@ -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)
}

View File

@@ -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