package main import ( "context" "encoding/hex" "encoding/json" "fmt" "io" "log" "net" "os" "os/exec" "os/signal" "strconv" "strings" "syscall" "time" "tera-clone/services/tera-api/internal/update" "tera-clone/services/tera-api/internal/update/recoveryrequest" "tera-clone/services/tera-api/internal/update/updatesocket" ) func main() { recoveryApplyEnabled := false if getenvBool("TERA_UPDATED_RECOVERY_APPLY_ENABLED", false) { gatePath := getenv("TERA_UPDATED_RECOVERY_GATE", "/etc/tera/recovery-apply-enabled.json") if err := validateRecoveryGate(gatePath, getenv("TERA_UPDATED_PRODUCT", "tera-core-x48-pro"), getenv("TERA_UPDATED_PROFILE", "fet3568j-c")); err != nil { log.Fatalf("tera-updated recovery apply gate failed closed: %v", err) } recoveryApplyEnabled = true } miscDevice := getenv("TERA_UPDATED_MISC_DEVICE", "/dev/mmcblk0p2") manager, err := update.NewManager(update.ManagerOptions{ UpdateRoot: getenv("TERA_UPDATED_ROOT", "/userdata/update"), PolicyPath: getenv("TERA_UPDATED_STORAGE_POLICY", "/userdata/storage-layout.json"), TrustDirectory: getenv("TERA_UPDATED_TRUST_DIR", "/etc/tera/update-trust"), Product: getenv("TERA_UPDATED_PRODUCT", "tera-core-x48-pro"), Profile: getenv("TERA_UPDATED_PROFILE", "fet3568j-c"), CurrentVersion: getenv("TERA_UPDATED_CURRENT_VERSION", "0.1.0-dev"), RecoveryApplyEnabled: recoveryApplyEnabled, RequestRecovery: func(operationID string) error { return recoveryrequest.WriteRequestFile(miscDevice, operationID, false) }, Reboot: func() error { return exec.Command("/sbin/reboot").Run() }, }) if err != nil { log.Fatalf("tera-updated setup failed: %v", err) } socketPath := getenv("TERA_UPDATED_SOCKET", updatesocket.DefaultSocketPath) if err := removeStaleSocket(socketPath); err != nil { log.Fatalf("tera-updated socket setup failed: %v", err) } listener, err := net.Listen("unix", socketPath) if err != nil { log.Fatalf("tera-updated listen failed: %v", err) } defer listener.Close() defer os.Remove(socketPath) if err := os.Chmod(socketPath, 0o660); err != nil { log.Fatalf("tera-updated socket permissions failed: %v", err) } server := updatesocket.Server{Controller: manager, MaxBytes: managerMaxBytes(manager)} serverErrors := make(chan error, 1) go func() { serverErrors <- server.Serve(listener) }() log.Printf("tera-updated serving protected update control on %s", socketPath) ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() select { case <-ctx.Done(): log.Printf("tera-updated stopping") case err := <-serverErrors: if err != nil { log.Printf("tera-updated failed: %v", err) } } _ = listener.Close() select { case <-serverErrors: case <-time.After(3 * time.Second): } } type recoveryGate struct { FormatVersion int `json:"format_version"` Product string `json:"product"` HardwareProfile string `json:"hardware_profile"` Authorized bool `json:"authorized"` TargetEvidenceID string `json:"target_evidence_id"` RecoveryFITSHA256 string `json:"recovery_fit_sha256"` UBootSHA256 string `json:"uboot_sha256"` } func validateRecoveryGate(path, product, profile string) error { info, err := os.Lstat(path) if err != nil { return err } if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 || info.Size() <= 0 || info.Size() > 16*1024 { return fmt.Errorf("gate must be a bounded regular non-symlink file") } file, err := os.Open(path) if err != nil { return err } defer file.Close() decoder := json.NewDecoder(io.LimitReader(file, 16*1024)) decoder.DisallowUnknownFields() var gate recoveryGate if err := decoder.Decode(&gate); err != nil { return err } var trailing any if err := decoder.Decode(&trailing); err != io.EOF { return fmt.Errorf("gate must contain exactly one JSON value") } if gate.FormatVersion != 1 || !gate.Authorized || gate.Product != product || gate.HardwareProfile != profile || !safeGateIdentity(gate.TargetEvidenceID) || !validHexSHA256(gate.RecoveryFITSHA256) || !validHexSHA256(gate.UBootSHA256) { return fmt.Errorf("gate identity, authorization, or artifact hashes are invalid") } return nil } func safeGateIdentity(value string) bool { if len(value) < 1 || len(value) > 128 { return false } for index, character := range value { if character >= 'a' && character <= 'z' || character >= 'A' && character <= 'Z' || character >= '0' && character <= '9' || index > 0 && (character == '.' || character == '_' || character == '-') { continue } return false } return true } func validHexSHA256(value string) bool { if len(value) != 64 { return false } _, err := hex.DecodeString(value) return err == nil } func getenvBool(key string, fallback bool) bool { value := strings.TrimSpace(strings.ToLower(os.Getenv(key))) if value == "" { return fallback } return value == "1" || value == "true" || value == "yes" || value == "on" } func managerMaxBytes(manager *update.Manager) int64 { status, err := manager.Status(context.Background()) if err != nil || status.Capacity.UpdatePackageMax <= 0 { return 1280 << 20 } return status.Capacity.UpdatePackageMax } func removeStaleSocket(path string) error { info, err := os.Lstat(path) if os.IsNotExist(err) { return nil } if err != nil { return err } if info.Mode()&os.ModeSocket == 0 { return fmt.Errorf("refusing to replace non-socket path %s", path) } return os.Remove(path) } func getenv(key, fallback string) string { if value := os.Getenv(key); value != "" { return value } return fallback } func getenvInt64(key string, fallback int64) int64 { value, err := strconv.ParseInt(os.Getenv(key), 10, 64) if err != nil || value <= 0 { return fallback } return value }