package main import ( "archive/tar" "compress/gzip" "fmt" "io" "os" "os/exec" "path/filepath" "sort" "strings" "time" "github.com/towgo/towgo/lib/system" g "tgk-touch/internal/global" ) const firefoxInstallMarkerName = ".firefox-install-ok" var forbiddenFirefoxDebPackages = map[string]struct{}{ "base-files": {}, "base-passwd": {}, "bash": {}, "coreutils": {}, "dash": {}, "debconf": {}, "dpkg": {}, "gcc-8-base": {}, "libblkid1": {}, "libc6": {}, "libgcc1": {}, "libmount1": {}, "libselinux1": {}, "libsystemd0": {}, "libudev1": {}, "login": {}, "mount": {}, "passwd": {}, "perl-base": {}, "sudo": {}, "systemd": {}, "systemd-sysv": {}, "tar": {}, "udev": {}, "util-linux": {}, } func isFirefoxUsable() bool { cmd := exec.Command("firefox", "--version") out, err := cmd.CombinedOutput() ok := err == nil kioskInfof("firefox usability check ok=%v output=%q err=%v", ok, strings.TrimSpace(string(out)), err) return ok } func isFirefoxInstalled() bool { cmd := exec.Command("which", "firefox") out, err := cmd.CombinedOutput() ok := err == nil kioskDebugf("firefox install path check ok=%v output=%q err=%v", ok, strings.TrimSpace(string(out)), err) return ok } func installFirefox() error { bundlePath := firefoxBundlePath() if err := validateFirefoxBundle(bundlePath); err != nil { kioskErrorf("firefox install refused bundle=%s err=%v", bundlePath, err) return err } var p struct { DebPkgPath string `json:"DebPkgPath"` DebPkgName string `json:"DebPkgName"` InstallShellPath string `json:"InstallShellPath"` } p.DebPkgPath = g.Config().Firefox.DebPkgPath p.DebPkgName = g.Config().Firefox.DebPkgName p.InstallShellPath = g.Config().Firefox.InstallShellPath kioskInfof("firefox install begin programPath=%s bundle=%s debPath=%s debName=%s installShell=%s", system.GetPathOfProgram(), bundlePath, p.DebPkgPath, p.DebPkgName, p.InstallShellPath) command := exec.Command("sudo", "sh", "-c", fmt.Sprintf(` echo "=== current work directory ===" pwd echo "" echo "=== keep existing Firefox package ===" echo "skip automatic dpkg purge to avoid touching base system packages" echo "" echo "=== clean previous offline bundle files ===" rm -rf /opt/firefox-deb /opt/firefox-deb.tar.gz 2>/dev/null echo "" echo "=== start Firefox install ===" echo "1. copy offline bundle to /opt..." cp -v %s /opt/firefox-deb.tar.gz || { echo "error: copy offline bundle failed"; exit 1; } echo "" echo "2. extract offline bundle..." cd /opt && tar -xzvf firefox-deb.tar.gz || { echo "error: extract offline bundle failed"; exit 1; } echo "" echo "3. install Firefox deb packages..." cd /opt/firefox-deb && sudo dpkg -i *.deb || { echo "error: install Firefox deb packages failed"; exit 1; } echo "" echo "4. verify Firefox..." firefox --version || { echo "error: Firefox verification failed"; exit 1; } echo "" echo "=== Firefox install complete ===" `, shellQuote(bundlePath))) command.Stdout = os.Stdout command.Stderr = os.Stderr err := command.Run() if err != nil { kioskErrorf("firefox install command failed err=%v", err) return err } kioskInfof("firefox install command finished successfully") return nil } func firefoxInstallMarkerPath() string { return filepath.Join(system.GetPathOfProgram(), "data", firefoxInstallMarkerName) } func hasFirefoxInstallMarker() bool { path := firefoxInstallMarkerPath() info, err := os.Stat(path) ok := err == nil && !info.IsDir() kioskInfof("firefox install marker check path=%s ok=%v err=%v", path, ok, err) return ok } func writeFirefoxInstallMarker(reason string) error { path := firefoxInstallMarkerPath() if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { kioskErrorf("create firefox install marker dir failed path=%s err=%v", filepath.Dir(path), err) return err } content := fmt.Sprintf("reason=%s\ntime=%s\n", reason, time.Now().Format(time.RFC3339)) if err := os.WriteFile(path, []byte(content), 0644); err != nil { kioskErrorf("write firefox install marker failed path=%s err=%v", path, err) return err } kioskInfof("firefox install marker written path=%s reason=%s", path, reason) return nil } func firefoxBundlePath() string { return filepath.Join(system.GetPathOfProgram(), g.Config().Firefox.DebPkgPath, g.Config().Firefox.DebPkgName) } func validateFirefoxBundle(bundlePath string) error { packages, err := firefoxBundlePackageNames(bundlePath) if err != nil { return fmt.Errorf("read firefox bundle packages: %w", err) } if len(packages) == 0 { return fmt.Errorf("firefox bundle has no deb packages") } blocked := unsafeFirefoxDebPackages(packages) if len(blocked) > 0 { return fmt.Errorf("firefox bundle contains blocked base system packages: %s", strings.Join(blocked, ", ")) } kioskInfof("firefox bundle validation passed bundle=%s packages=%d", bundlePath, len(packages)) return nil } func firefoxBundlePackageNames(bundlePath string) ([]string, error) { file, err := os.Open(bundlePath) if err != nil { return nil, err } defer file.Close() gzipReader, err := gzip.NewReader(file) if err != nil { return nil, err } defer gzipReader.Close() reader := tar.NewReader(gzipReader) var packages []string for { header, err := reader.Next() if err == io.EOF { break } if err != nil { return nil, err } if header == nil || header.FileInfo().IsDir() || !strings.HasSuffix(header.Name, ".deb") { continue } name := debPackageName(filepath.Base(header.Name)) if name != "" { packages = append(packages, name) } } sort.Strings(packages) return packages, nil } func debPackageName(fileName string) string { name := strings.TrimSuffix(fileName, ".deb") if packageName, _, ok := strings.Cut(name, "_"); ok { return packageName } return name } func unsafeFirefoxDebPackages(packages []string) []string { seen := make(map[string]struct{}) for _, name := range packages { if _, blocked := forbiddenFirefoxDebPackages[name]; blocked { seen[name] = struct{}{} } } blocked := make([]string, 0, len(seen)) for name := range seen { blocked = append(blocked, name) } sort.Strings(blocked) return blocked } func shellQuote(value string) string { return "'" + strings.ReplaceAll(value, "'", "'\\''") + "'" }