You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

238 lines
6.2 KiB

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, "'", "'\\''") + "'"
}