package minecraft import ( "crypto/sha256" "encoding/json" "fmt" "io" "log" "net/http" "os" "path/filepath" "strconv" "strings" "sync" "syscall" "zlh-agent/internal/provcommon" "zlh-agent/internal/state" ) var ( fabricProxyArtifactRoot = "minecraft/fabric/fabric-proxy-lite" fabricAPIArtifactRoot = "minecraft/fabric/fabric-api" fabricProxyConfigPath = "minecraft/fabric/FabricProxy-Lite.toml" ) var ( fabricProxyMapOnce sync.Once fabricProxyMap map[string]string fabricProxyMapErr error ) func resolveProxyVersion(mcVersion string) (string, error) { return resolveMappedArtifactPath(mcVersion, loadFabricProxyMap, "fabricproxy-lite", func(value string) string { return value }) } func InjectFabricProxyLite(cfg state.Config) error { if !shouldInjectFabricProxyLite(cfg) { return nil } modsDir, targetPath, err := ensureSystemModsDir(cfg) if err != nil { log.Printf("[mods] vmid=%d type=system name=fabricproxy-lite error=%v", cfg.VMID, err) return err } version, err := resolveProxyVersion(cfg.Version) if err != nil { log.Printf("[mods] vmid=%d type=system name=fabricproxy-lite error=%v", cfg.VMID, err) return err } source := proxyArtifactURL(version) log.Printf("[mods] vmid=%d type=system name=fabricproxy-lite source=artifact version=%s", cfg.VMID, version) if err := installSystemMod(source, targetPath, modsDir); err != nil { err = fmt.Errorf("install fabricproxy-lite: %w", err) log.Printf("[mods] vmid=%d type=system name=fabricproxy-lite error=%v", cfg.VMID, err) return err } log.Printf("[mods] vmid=%d type=system name=fabricproxy-lite installed=true", cfg.VMID) fabricAPIPath := filepath.Join(modsDir, "fabric-api.jar") fabricAPISource, err := resolveFabricAPIArtifactURL(cfg.Version) if err != nil { log.Printf("[mods] vmid=%d type=system name=fabric-api error=%v", cfg.VMID, err) return err } log.Printf("[mods] vmid=%d type=system name=fabric-api source=artifact", cfg.VMID) if err := installSystemMod(fabricAPISource, fabricAPIPath, modsDir); err != nil { err = fmt.Errorf("install fabric-api: %w", err) log.Printf("[mods] vmid=%d type=system name=fabric-api error=%v", cfg.VMID, err) return err } log.Printf("[mods] vmid=%d type=system name=fabric-api installed=true", cfg.VMID) if err := installFabricProxyConfig(cfg); err != nil { log.Printf("[mods] vmid=%d type=system name=fabricproxy-lite-config error=%v", cfg.VMID, err) return err } return nil } func shouldInjectFabricProxyLite(cfg state.Config) bool { if !strings.EqualFold(cfg.ContainerType, "game") { return false } if !strings.EqualFold(cfg.Game, "minecraft") { return false } if !strings.EqualFold(cfg.Variant, "vanilla") { return false } return strings.EqualFold(cfg.InternalProfile, "vanilla-fabric") } func loadFabricProxyMap() (map[string]string, error) { fabricProxyMapOnce.Do(func() { raw, err := fetchArtifact(proxyMapURL()) if err != nil { fabricProxyMapErr = fmt.Errorf("read fabricproxy-lite map: %w", err) return } fabricProxyMap, fabricProxyMapErr = parseArtifactMap(raw, "fabricproxy-lite") }) if fabricProxyMapErr != nil { return nil, fabricProxyMapErr } return fabricProxyMap, nil } func proxyMapURL() string { return provcommon.BuildArtifactURL(filepath.ToSlash(filepath.Join(fabricProxyArtifactRoot, "map.json"))) } func proxyArtifactURL(version string) string { rel := filepath.ToSlash(filepath.Join( fabricProxyArtifactRoot, version, fmt.Sprintf("FabricProxy-Lite-%s.jar", version), )) return provcommon.BuildArtifactURL(rel) } func resolveFabricAPIArtifactURL(mcVersion string) (string, error) { mcVersion = strings.TrimSpace(mcVersion) if mcVersion == "" { return "", fmt.Errorf("empty minecraft version") } if _, err := parseVersion(mcVersion); err != nil { return "", fmt.Errorf("invalid minecraft version %q: %w", mcVersion, err) } rel := filepath.ToSlash(filepath.Join(fabricAPIArtifactRoot, mcVersion, "fabric-api.jar")) return provcommon.BuildArtifactURL(rel), nil } func resolveMappedArtifactPath(mcVersion string, loadMap func() (map[string]string, error), name string, valueToPath func(string) string) (string, error) { mappings, err := loadMap() if err != nil { return "", err } mcVersion = strings.TrimSpace(mcVersion) if mcVersion == "" { return "", fmt.Errorf("empty minecraft version") } compareVersion, err := parseVersion(mcVersion) if err != nil { return "", fmt.Errorf("invalid minecraft version %q: %w", mcVersion, err) } if value, ok := mappings[mcVersion]; ok { return valueToPath(value), nil } for spec, value := range mappings { start, end, ok := parseVersionRange(spec) if !ok { continue } if compareVersionSlices(compareVersion, start) >= 0 && compareVersionSlices(compareVersion, end) <= 0 { return valueToPath(value), nil } } return "", fmt.Errorf("no %s mapping for minecraft version %q", name, mcVersion) } func parseArtifactMap(raw []byte, name string) (map[string]string, error) { var mappings map[string]string if err := json.Unmarshal(raw, &mappings); err != nil { return nil, fmt.Errorf("parse %s map: %w", name, err) } if len(mappings) == 0 { return nil, fmt.Errorf("%s map is empty", name) } for spec, version := range mappings { spec = strings.TrimSpace(spec) version = strings.TrimSpace(version) if spec == "" || version == "" { return nil, fmt.Errorf("%s map contains empty key or value", name) } if start, end, ok := parseVersionRange(spec); ok { if compareVersionSlices(start, end) > 0 { return nil, fmt.Errorf("invalid %s range %q", name, spec) } continue } if _, err := parseVersion(spec); err != nil { return nil, fmt.Errorf("invalid %s version key %q: %w", name, spec, err) } } return mappings, nil } func ensureSystemModsDir(cfg state.Config) (string, string, error) { serverDir := provcommon.ServerDir(cfg) info, err := os.Stat(serverDir) if err != nil { return "", "", fmt.Errorf("stat server dir: %w", err) } modsDir := filepath.Join(serverDir, "mods") created := false if _, err := os.Stat(modsDir); err != nil { if !os.IsNotExist(err) { return "", "", fmt.Errorf("stat mods dir: %w", err) } created = true } if err := os.MkdirAll(modsDir, info.Mode().Perm()); err != nil { return "", "", fmt.Errorf("create mods dir: %w", err) } if err := syncOwnershipAndPerms(modsDir, info); err != nil { return "", "", fmt.Errorf("sync mods dir ownership: %w", err) } log.Printf("[provision] vmid=%d profile=vanilla-fabric step=mods_dir created=%t", cfg.VMID, created) return modsDir, filepath.Join(modsDir, "fabricproxy-lite.jar"), nil } func installFabricProxyConfig(cfg state.Config) error { serverDir := provcommon.ServerDir(cfg) info, err := os.Stat(serverDir) if err != nil { return fmt.Errorf("stat server dir: %w", err) } configDir := filepath.Join(serverDir, "config") created := false if _, err := os.Stat(configDir); err != nil { if !os.IsNotExist(err) { return fmt.Errorf("stat config dir: %w", err) } created = true } if err := os.MkdirAll(configDir, info.Mode().Perm()); err != nil { return fmt.Errorf("create config dir: %w", err) } if err := syncOwnershipAndPerms(configDir, info); err != nil { return fmt.Errorf("sync config dir ownership: %w", err) } targetPath := filepath.Join(configDir, "FabricProxy-Lite.toml") if err := installSystemFile(provcommon.BuildArtifactURL(fabricProxyConfigPath), targetPath, configDir, 0o644); err != nil { return fmt.Errorf("install FabricProxy-Lite.toml: %w", err) } log.Printf("[provision] vmid=%d profile=vanilla-fabric step=config_dir created=%t", cfg.VMID, created) log.Printf("[mods] vmid=%d type=system name=FabricProxy-Lite.toml installed=true", cfg.VMID) return nil } func parseVersionRange(spec string) ([]int, []int, bool) { parts := strings.Split(spec, "-") if len(parts) != 2 { return nil, nil, false } start, err := parseVersion(parts[0]) if err != nil { return nil, nil, false } end, err := parseVersion(parts[1]) if err != nil { return nil, nil, false } return start, end, true } func parseVersion(raw string) ([]int, error) { raw = strings.TrimSpace(raw) if raw == "" { return nil, fmt.Errorf("empty version") } parts := strings.Split(raw, ".") out := make([]int, 0, len(parts)) for _, part := range parts { part = strings.TrimSpace(part) if part == "" { return nil, fmt.Errorf("invalid version %q", raw) } n, err := strconv.Atoi(part) if err != nil { return nil, fmt.Errorf("invalid version segment %q", part) } out = append(out, n) } return out, nil } func compareVersionSlices(a, b []int) int { maxLen := max(len(b), len(a)) for i := 0; i < maxLen; i++ { ai := 0 if i < len(a) { ai = a[i] } bi := 0 if i < len(b) { bi = b[i] } switch { case ai < bi: return -1 case ai > bi: return 1 } } return 0 } func fetchArtifact(url string) ([]byte, error) { client := &http.Client{} resp, err := client.Get(url) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("status %d", resp.StatusCode) } return io.ReadAll(resp.Body) } func installSystemMod(url, dst, modsDir string) error { payload, err := fetchArtifact(url) if err != nil { return err } if same, err := sameFileChecksum(dst, payload); err == nil && same { return syncFileOwnership(dst, modsDir) } tmp, err := os.CreateTemp(filepath.Dir(dst), "fabricproxy-lite-*.jar") if err != nil { return err } tmpPath := tmp.Name() defer func() { _ = os.Remove(tmpPath) }() if _, err := tmp.Write(payload); err != nil { _ = tmp.Close() return err } if err := tmp.Close(); err != nil { return err } if err := os.Rename(tmpPath, dst); err != nil { return err } return syncFileOwnership(dst, modsDir) } func installSystemFile(url, dst, referenceDir string, mode os.FileMode) error { payload, err := fetchArtifact(url) if err != nil { return err } if same, err := sameFileChecksum(dst, payload); err == nil && same { return syncFileOwnershipWithMode(dst, referenceDir, mode) } tmp, err := os.CreateTemp(filepath.Dir(dst), "zlh-system-file-*") if err != nil { return err } tmpPath := tmp.Name() defer func() { _ = os.Remove(tmpPath) }() if _, err := tmp.Write(payload); err != nil { _ = tmp.Close() return err } if err := tmp.Close(); err != nil { return err } if err := os.Rename(tmpPath, dst); err != nil { return err } return syncFileOwnershipWithMode(dst, referenceDir, mode) } func sameFileChecksum(path string, payload []byte) (bool, error) { current, err := os.ReadFile(path) if err != nil { return false, err } currentSum := sha256.Sum256(current) newSum := sha256.Sum256(payload) return currentSum == newSum, nil } func syncOwnershipAndPerms(path string, info os.FileInfo) error { if err := os.Chmod(path, info.Mode().Perm()); err != nil { return err } stat, ok := info.Sys().(*syscall.Stat_t) if !ok { return nil } return os.Chown(path, int(stat.Uid), int(stat.Gid)) } func syncFileOwnership(path, referenceDir string) error { return syncFileOwnershipWithMode(path, referenceDir, 0o644) } func syncFileOwnershipWithMode(path, referenceDir string, mode os.FileMode) error { info, err := os.Stat(referenceDir) if err != nil { return err } if err := os.Chmod(path, mode); err != nil { return err } stat, ok := info.Sys().(*syscall.Stat_t) if !ok { return nil } return os.Chown(path, int(stat.Uid), int(stat.Gid)) }