apps/rebreak-binder-mac/ — neue macOS-App die User durch den kompletten Self-Bind-Prozess führt: Welcome → Preflight → Supervise → Enroll → Configure (MDM-Push + Pre/Post-Check) → Sideload Lock-Profile (AirDrop). 3-Layer Smart-Resume: supervised? + Enrollment-Profil installed (cfgutil Ground-Truth)? + MDM-Ack fresh (NanoMDM-DB via ssh+psql)? Services: DeviceDetector (ideviceinfo + cfgutil), SuperviseRunner (spawnt supervise-magic CLI), MDMClient (PUT /v1/enqueue?push=1, Apple XML-Plist, identisch zum server-watcher-Format), MDMStatus (DB-Real- Check + ManagedApplicationList-Result-Read). Plus: - fix(supervise-magic): EOF nach ProcessMessage Response (ErrorCode=0) ist Success, nicht Error — vermeidet false-fail bei iPhone-Restore- Reboot - feat(mdm-profiles): rebreak-content-filter-mdm.mobileconfig als MDM-Push-Variante (ohne ConsentText, ohne globales allowAppRemoval= false — per-app via managed-state) End-to-End validiert: App-Push via Ad-Hoc-Manifest (silent), Managed- State via ManagedApplicationList-Query, NEFilter-Mode nach App-Force- Quit, Lock-Profile non-removable nach Sideload. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
362 lines
11 KiB
Go
362 lines
11 KiB
Go
// File-server-Loop für MobileBackup2 Restore-Mode.
|
|
//
|
|
// Während Restore drives das iPhone das gespräch:
|
|
// - Es schickt DLMessageDownloadFiles mit Filenamen
|
|
// - Es erwartet Datei-Content via custom binary chunks (NICHT DLMessage!)
|
|
//
|
|
// Wire-Format für File-Transfer (auf demselben Socket wie DLMessage):
|
|
//
|
|
// → [4-byte filename-length big-endian][filename UTF-8]
|
|
// → [4-byte code=12 DATA][4-byte 0 length] (start)
|
|
// → [4-byte code=12 DATA][4-byte chunk-length][chunk-data] (repeat)
|
|
// → [4-byte code=0 DONE][4-byte 0]
|
|
//
|
|
// Nach allen Files:
|
|
// → [4-byte 0 — no more files]
|
|
// → DLMessageStatusResponse [0, "___EmptyParameterString___", {}]
|
|
//
|
|
// Codes:
|
|
//
|
|
// CODE_FILE_DONE = 0 (end-of-file marker)
|
|
// CODE_FILE_DATA = 12 (data chunk)
|
|
// CODE_FILE_ERROR = 6 (error)
|
|
package mobilebackup2
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
|
|
"github.com/raynis/rebreak-supervise-magic/internal/dlmessage"
|
|
)
|
|
|
|
// 1-byte chunk-codes (matched libimobiledevice's CODE_FILE_DATA etc).
|
|
const (
|
|
fileCodeDone byte = 0x00 // CODE_SUCCESS — end of file
|
|
fileCodeData byte = 0x0c // CODE_FILE_DATA — chunk
|
|
fileCodeError byte = 0x06 // CODE_ERROR_LOCAL — file not found / error
|
|
|
|
// Apple sentinel: status-response "no error".
|
|
emptyParameterString = "___EmptyParameterString___"
|
|
)
|
|
|
|
// FileProvider liefert Datei-Content für einen relativen Pfad.
|
|
// Returnt (content, true) wenn vorhanden, ([], false) sonst.
|
|
type FileProvider func(relpath string) ([]byte, bool)
|
|
|
|
// ServeFiles fängt das iPhone-driven file-serve-loop ab.
|
|
// Loopt bis DLMessageDisconnect oder Error.
|
|
//
|
|
// Files werden via `provider` geliefert. Wenn provider eine angefragte
|
|
// Datei nicht hat: Server returnt CODE_FILE_ERROR.
|
|
//
|
|
// onProgress wird bei jedem Mess-event aufgerufen (für UI/Log).
|
|
func (c *Client) ServeFiles(provider FileProvider, onProgress func(event string, info string)) error {
|
|
if onProgress == nil {
|
|
onProgress = func(string, string) {}
|
|
}
|
|
|
|
// Wenn iPhone uns ein ProcessMessage:Response mit ErrorCode=0 schickt,
|
|
// signalisiert das: Restore-Operation completed successfully. Danach
|
|
// trennt das iPhone die USB-Verbindung um in den Restore-Mode zu booten →
|
|
// wir bekommen ein EOF. Das ist KEIN Fehler — es ist erwartetes Verhalten.
|
|
successConfirmed := false
|
|
|
|
for {
|
|
t, args, err := c.dl.Receive()
|
|
if err != nil {
|
|
if successConfirmed {
|
|
// iPhone hat success-Response geschickt + disconnected um zu rebooten.
|
|
onProgress("ServeFiles", "device disconnected after successful Response — reboot expected")
|
|
return nil
|
|
}
|
|
return fmt.Errorf("serve: receive: %w", err)
|
|
}
|
|
|
|
switch t {
|
|
case dlmessage.TypeProcessMessage:
|
|
if len(args) == 0 {
|
|
continue
|
|
}
|
|
dict, ok := args[0].(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
msgName, _ := dict["MessageName"].(string)
|
|
// 2026-05-28 DEBUG: log full dict to understand what iPhone sends
|
|
onProgress("ProcessMessage", fmt.Sprintf("%s | full=%v", msgName, dict))
|
|
if msgName == "DLMessageDisconnect" || msgName == "Disconnect" {
|
|
return nil
|
|
}
|
|
// "Response" = iPhone signals operation completed — NICHT antworten,
|
|
// nur weiter loopen + auf nächste Message warten.
|
|
// Wenn ErrorCode=0 → success → nachfolgender EOF ist erwartet.
|
|
if msgName == "Response" {
|
|
if ec, ok := dict["ErrorCode"]; ok {
|
|
// ErrorCode kann uint64, int64, int etc. sein — alle als „0" prüfen
|
|
switch v := ec.(type) {
|
|
case uint64:
|
|
if v == 0 {
|
|
successConfirmed = true
|
|
}
|
|
case int64:
|
|
if v == 0 {
|
|
successConfirmed = true
|
|
}
|
|
case int:
|
|
if v == 0 {
|
|
successConfirmed = true
|
|
}
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
// Andere Sub-Messages — Status OK respond.
|
|
if err := c.sendStatusOK(); err != nil {
|
|
return err
|
|
}
|
|
|
|
case dlmessage.TypeStatusResponse:
|
|
// Device-Status — wir loggen + machen weiter
|
|
onProgress("StatusResponse", fmt.Sprintf("%v", args))
|
|
// Falls error: abbrechen
|
|
if len(args) > 0 {
|
|
if code, ok := args[0].(uint64); ok && code != 0 {
|
|
return fmt.Errorf("serve: device error code=%d: %v", code, args)
|
|
}
|
|
}
|
|
|
|
case dlmessage.TypeGetFreeDiskSpace:
|
|
// Device fragt nach freiem Speicher — wir behaupten Schubladen voll Platz
|
|
onProgress("GetFreeDiskSpace", "")
|
|
// Antwort: [0, errStr, freeSpaceUint64]
|
|
if err := c.dl.Send(dlmessage.TypeStatusResponse, uint64(0), emptyParameterString, uint64(1<<40)); err != nil {
|
|
return err
|
|
}
|
|
|
|
case dlmessage.TypeContentsOfDirectory:
|
|
// Device möchte Verzeichnis listen — wir antworten mit empty dict
|
|
dirname := ""
|
|
if len(args) > 0 {
|
|
dirname, _ = args[0].(string)
|
|
}
|
|
onProgress("ContentsOfDirectory", dirname)
|
|
// Antwort: status [0, "", {}]
|
|
if err := c.dl.Send(dlmessage.TypeStatusResponse, uint64(0), emptyParameterString, map[string]interface{}{}); err != nil {
|
|
return err
|
|
}
|
|
|
|
case dlmessage.TypeCreateDirectory, dlmessage.TypeMoveItems, dlmessage.TypeRemoveItems, dlmessage.TypeCopyItem:
|
|
onProgress(t, "")
|
|
if err := c.sendStatusOK(); err != nil {
|
|
return err
|
|
}
|
|
|
|
case dlmessage.TypeDownloadFiles:
|
|
// Device fragt files an — args[0] ist []string
|
|
var fileList []string
|
|
if len(args) > 0 {
|
|
if arr, ok := args[0].([]interface{}); ok {
|
|
for _, item := range arr {
|
|
if s, ok := item.(string); ok {
|
|
fileList = append(fileList, s)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
onProgress("DownloadFiles", fmt.Sprintf("count=%d", len(fileList)))
|
|
if err := c.sendFiles(fileList, provider, onProgress); err != nil {
|
|
return fmt.Errorf("serve: send files: %w", err)
|
|
}
|
|
|
|
case dlmessage.TypeUploadFiles:
|
|
// iPhone will UNS files uploaden (sein current state).
|
|
// Wir ACK + receive raw upload-stream.
|
|
onProgress("UploadFiles", "device uploading")
|
|
if err := c.sendStatusOK(); err != nil {
|
|
return err
|
|
}
|
|
if err := c.receiveFiles(onProgress); err != nil {
|
|
return fmt.Errorf("serve: receive files: %w", err)
|
|
}
|
|
|
|
case dlmessage.TypeDisconnect:
|
|
onProgress("Disconnect", "explicit")
|
|
return nil
|
|
|
|
default:
|
|
// unbekannter Typ — log + status OK
|
|
onProgress("Unknown", t)
|
|
if err := c.sendStatusOK(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Client) sendStatusOK() error {
|
|
return c.dl.Send(dlmessage.TypeStatusResponse, uint64(0), emptyParameterString, map[string]interface{}{})
|
|
}
|
|
|
|
// sendFiles antwortet auf DLMessageDownloadFiles. Wire-format aus libimobiledevice
|
|
// mb2_handle_send_file verifiziert:
|
|
//
|
|
// per file:
|
|
// [4-byte BE filename-length][filename UTF-8]
|
|
// if file exists:
|
|
// per chunk: [4-byte BE (1+chunk-size)][1-byte 0x0c CODE_DATA][chunk-data]
|
|
// end-of-file: [4-byte BE 1][1-byte 0x00 CODE_SUCCESS]
|
|
// else:
|
|
// [4-byte BE 1][1-byte 0x06 CODE_ERROR_LOCAL]
|
|
//
|
|
// after all files:
|
|
// [4-byte BE 0] (terminator = "no more files")
|
|
// DLMessageStatusResponse [0, "___EmptyParameterString___", {<errors>}]
|
|
func (c *Client) sendFiles(fileList []string, provider FileProvider, onProgress func(string, string)) error {
|
|
for _, fname := range fileList {
|
|
content, ok := provider(fname)
|
|
onProgress("send-file", fmt.Sprintf("%s (%d bytes, found=%v)", fname, len(content), ok))
|
|
|
|
// filename header
|
|
if err := c.writeRawU32(uint32(len(fname))); err != nil {
|
|
return err
|
|
}
|
|
if err := c.deviceConn.Send([]byte(fname)); err != nil {
|
|
return fmt.Errorf("write filename: %w", err)
|
|
}
|
|
|
|
if !ok {
|
|
// ERROR: [length=1][CODE_ERROR_LOCAL]
|
|
if err := c.writeRawU32(1); err != nil {
|
|
return err
|
|
}
|
|
if err := c.deviceConn.Send([]byte{fileCodeError}); err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
|
|
// DATA chunks
|
|
const maxChunk = 32 * 1024
|
|
for offset := 0; offset < len(content); offset += maxChunk {
|
|
end := offset + maxChunk
|
|
if end > len(content) {
|
|
end = len(content)
|
|
}
|
|
chunk := content[offset:end]
|
|
// [length=1+chunk][CODE_DATA][chunk]
|
|
if err := c.writeRawU32(uint32(1 + len(chunk))); err != nil {
|
|
return err
|
|
}
|
|
if err := c.deviceConn.Send([]byte{fileCodeData}); err != nil {
|
|
return err
|
|
}
|
|
if err := c.deviceConn.Send(chunk); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// END-OF-FILE: [length=1][CODE_SUCCESS]
|
|
if err := c.writeRawU32(1); err != nil {
|
|
return err
|
|
}
|
|
if err := c.deviceConn.Send([]byte{fileCodeDone}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Terminator: [length=0]
|
|
if err := c.writeRawU32(0); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Final status
|
|
return c.sendStatusOK()
|
|
}
|
|
|
|
func (c *Client) writeRawU32(v uint32) error {
|
|
buf := make([]byte, 4)
|
|
binary.BigEndian.PutUint32(buf, v)
|
|
return c.deviceConn.Send(buf)
|
|
}
|
|
|
|
// receiveFiles empfängt iPhone's file-upload-stream nach DLMessageUploadFiles.
|
|
// Wire-format mirror von sendFiles (gleiche frames, andere Richtung):
|
|
//
|
|
// per file:
|
|
// [4-byte BE path-len][path]
|
|
// per chunk: [4-byte BE (1+chunk-size)][1-byte code][data]
|
|
// end-of-file: [4-byte BE 1][1-byte 0x00 SUCCESS]
|
|
// terminator: [4-byte BE 0]
|
|
//
|
|
// Wir discardieren den Inhalt — iPhone's "backup-of-backup" interessiert uns nicht,
|
|
// nur dass wir den Stream sauber durchlesen damit das Protokoll weitergeht.
|
|
func (c *Client) receiveFiles(onProgress func(string, string)) error {
|
|
reader := c.deviceConn.Reader()
|
|
totalFiles := 0
|
|
totalBytes := uint64(0)
|
|
|
|
for {
|
|
// path-length-prefix
|
|
var pathLenBuf [4]byte
|
|
if _, err := io.ReadFull(reader, pathLenBuf[:]); err != nil {
|
|
return fmt.Errorf("read path-len: %w", err)
|
|
}
|
|
pathLen := binary.BigEndian.Uint32(pathLenBuf[:])
|
|
if pathLen == 0 {
|
|
// terminator — no more files
|
|
onProgress("upload-done", fmt.Sprintf("%d files, %d bytes total", totalFiles, totalBytes))
|
|
return nil
|
|
}
|
|
if pathLen > 65536 {
|
|
return fmt.Errorf("upload-path too large: %d", pathLen)
|
|
}
|
|
|
|
// path-bytes
|
|
pathBuf := make([]byte, pathLen)
|
|
if _, err := io.ReadFull(reader, pathBuf); err != nil {
|
|
return fmt.Errorf("read path: %w", err)
|
|
}
|
|
path := string(pathBuf)
|
|
totalFiles++
|
|
|
|
// chunks
|
|
fileBytes := uint64(0)
|
|
for {
|
|
var chunkLenBuf [4]byte
|
|
if _, err := io.ReadFull(reader, chunkLenBuf[:]); err != nil {
|
|
return fmt.Errorf("read chunk-len for %s: %w", path, err)
|
|
}
|
|
chunkLen := binary.BigEndian.Uint32(chunkLenBuf[:])
|
|
if chunkLen == 0 {
|
|
// some implementations send zero-length as terminator
|
|
break
|
|
}
|
|
// 1-byte code
|
|
var code [1]byte
|
|
if _, err := io.ReadFull(reader, code[:]); err != nil {
|
|
return fmt.Errorf("read chunk-code: %w", err)
|
|
}
|
|
dataLen := chunkLen - 1
|
|
if dataLen > 0 {
|
|
discard := make([]byte, dataLen)
|
|
if _, err := io.ReadFull(reader, discard); err != nil {
|
|
return fmt.Errorf("read chunk-data: %w", err)
|
|
}
|
|
fileBytes += uint64(dataLen)
|
|
}
|
|
// code 0x00 (CODE_SUCCESS) = end-of-file
|
|
if code[0] == fileCodeDone {
|
|
break
|
|
}
|
|
}
|
|
totalBytes += fileBytes
|
|
onProgress("upload-file", fmt.Sprintf("%s (%d bytes)", path, fileBytes))
|
|
}
|
|
}
|
|
|
|
// ErrServeAborted ist ein Sentinel den FileProvider zurückgeben kann um Loop
|
|
// zu beenden (z.B. bei Disconnect-Trigger).
|
|
var ErrServeAborted = errors.New("serve aborted")
|