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.
189 lines
5.7 KiB
189 lines
5.7 KiB
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
|
|
}
|
|
|