208 lines
5.2 KiB
Go
208 lines
5.2 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"zlh-agent/internal/mods"
|
|
"zlh-agent/internal/state"
|
|
)
|
|
|
|
type jsonError struct {
|
|
Error string `json:"error"`
|
|
}
|
|
|
|
func HandleGameMods(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
writeJSONError(w, http.StatusMethodNotAllowed, "GET only")
|
|
return
|
|
}
|
|
|
|
_, serverRoot, ok := requireMinecraftGame(w)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
resp, err := mods.ScanMods(serverRoot)
|
|
if err != nil {
|
|
writeJSONError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, resp)
|
|
}
|
|
|
|
func HandleGameModsInstall(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
writeJSONError(w, http.StatusMethodNotAllowed, "POST only")
|
|
return
|
|
}
|
|
|
|
_, serverRoot, ok := requireMinecraftGame(w)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
var req mods.InstallRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeJSONError(w, http.StatusBadRequest, "invalid json")
|
|
return
|
|
}
|
|
|
|
filename := strings.TrimSpace(req.Filename)
|
|
if strings.TrimSpace(strings.ToLower(req.Source)) != "modrinth" {
|
|
filename = ""
|
|
if u, err := url.Parse(req.ArtifactURL); err == nil {
|
|
base := filepath.Base(u.Path)
|
|
safeBase := base != "." && base != "/" &&
|
|
!strings.Contains(base, "..") &&
|
|
!strings.Contains(base, "/") &&
|
|
!strings.Contains(base, "\\") &&
|
|
!strings.Contains(base, "~") &&
|
|
!strings.ContainsRune(base, 0)
|
|
if safeBase {
|
|
valid := true
|
|
for _, r := range base {
|
|
if r <= 31 || r == 127 || r == ' ' || r == '\t' || r == '\n' || r == '\r' {
|
|
valid = false
|
|
break
|
|
}
|
|
if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '.' || r == '_' || r == '-' || r == '+') {
|
|
valid = false
|
|
break
|
|
}
|
|
}
|
|
if valid {
|
|
filename = base
|
|
}
|
|
}
|
|
}
|
|
if filename == "" {
|
|
name := strings.TrimSpace(req.ModID)
|
|
if strings.TrimSpace(req.Version) != "" {
|
|
name = name + "-" + strings.TrimSpace(req.Version)
|
|
}
|
|
name = strings.Map(func(r rune) rune {
|
|
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '.' || r == '_' || r == '-' {
|
|
return r
|
|
}
|
|
return '_'
|
|
}, name)
|
|
if len(name) > 64 {
|
|
name = name[:64]
|
|
}
|
|
if name == "" {
|
|
name = "unknown"
|
|
}
|
|
filename = name + ".jar"
|
|
}
|
|
}
|
|
|
|
modsDir := filepath.Join(serverRoot, "mods")
|
|
enabledPath := filepath.Join(modsDir, filename)
|
|
disabledPath := enabledPath + ".disabled"
|
|
if _, err := os.Stat(enabledPath); err == nil {
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"status": "already-installed",
|
|
"fileName": filename,
|
|
"enabled": true,
|
|
})
|
|
return
|
|
}
|
|
if _, err := os.Stat(disabledPath); err == nil {
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"status": "already-installed",
|
|
"fileName": filename,
|
|
"enabled": false,
|
|
})
|
|
return
|
|
}
|
|
|
|
resp, err := mods.InstallCurated(serverRoot, req)
|
|
if err != nil {
|
|
writeJSONError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
_ = resp
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"status": "installed",
|
|
"fileName": filename,
|
|
})
|
|
}
|
|
|
|
func HandleGameModByID(w http.ResponseWriter, r *http.Request) {
|
|
_, serverRoot, ok := requireMinecraftGame(w)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
modID := strings.TrimPrefix(r.URL.Path, "/game/mods/")
|
|
if modID == "" || strings.Contains(modID, "/") || !mods.IsValidModID(modID) {
|
|
writeJSONError(w, http.StatusBadRequest, "invalid mod_id")
|
|
return
|
|
}
|
|
|
|
switch r.Method {
|
|
case http.MethodPatch:
|
|
var req mods.PatchRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeJSONError(w, http.StatusBadRequest, "invalid json")
|
|
return
|
|
}
|
|
resp, err := mods.SetEnabled(serverRoot, modID, req.Enabled)
|
|
if err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
writeJSONError(w, http.StatusNotFound, "mod not found")
|
|
return
|
|
}
|
|
writeJSONError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, resp)
|
|
case http.MethodDelete:
|
|
resp, err := mods.DeleteMod(serverRoot, modID)
|
|
if err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
writeJSONError(w, http.StatusNotFound, "mod not found")
|
|
return
|
|
}
|
|
writeJSONError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, resp)
|
|
default:
|
|
writeJSONError(w, http.StatusMethodNotAllowed, "PATCH or DELETE only")
|
|
}
|
|
}
|
|
|
|
func requireMinecraftGame(w http.ResponseWriter) (*state.Config, string, bool) {
|
|
cfg, err := state.LoadConfig()
|
|
if err != nil {
|
|
writeJSONError(w, http.StatusBadRequest, "no config loaded")
|
|
return nil, "", false
|
|
}
|
|
if strings.ToLower(cfg.ContainerType) != "game" {
|
|
writeJSONError(w, http.StatusBadRequest, "not a game container")
|
|
return nil, "", false
|
|
}
|
|
if strings.ToLower(cfg.Game) != "minecraft" {
|
|
writeJSONError(w, http.StatusNotImplemented, "unsupported game")
|
|
return nil, "", false
|
|
}
|
|
return cfg, mods.ResolveServerRoot(cfg), true
|
|
}
|
|
|
|
func writeJSON(w http.ResponseWriter, status int, payload any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
_ = json.NewEncoder(w).Encode(payload)
|
|
}
|
|
|
|
func writeJSONError(w http.ResponseWriter, status int, msg string) {
|
|
writeJSON(w, status, jsonError{Error: msg})
|
|
}
|