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:
@@ -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})
|
||||
}
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user