// Package dlmessage implementiert Apple's DLMessage RPC-Protocol das von // MobileBackup2, NotificationProxy und ähnlichen Apple-Services genutzt wird. // // Wire format: // // [4 bytes: total length big-endian] [XML plist payload] // // Payload ist ein plist-Array: // // [, , , ...] // // Beispiel für DLMessageProcessMessage: // // ["DLMessageProcessMessage", {}] // // Reference: libimobiledevice's MobileBackup2-Protocol-Reverse-Engineering // + verifiziert aus TechLockdown's libimobiledevice.MobileBackup2Client // Symbol-Liste (Receive, SendFiles, Start, BaseVersionExchange). package dlmessage import ( "bytes" "encoding/binary" "errors" "fmt" "io" "howett.net/plist" ) // DLMessage-Type-Konstanten — Apple's enum für ProcessMessage-Arten. const ( TypeVersionExchange = "DLMessageVersionExchange" TypeDeviceReady = "DLMessageDeviceReady" TypeProcessMessage = "DLMessageProcessMessage" TypeStatusResponse = "DLMessageStatusResponse" TypeDisconnect = "DLMessageDisconnect" TypePing = "DLMessagePing" // File-Operations TypeContentsOfDirectory = "DLContentsOfDirectory" TypeDownloadFiles = "DLMessageDownloadFiles" TypeUploadFiles = "DLMessageUploadFiles" TypeCopyItem = "DLMessageCopyItem" TypeRemoveItems = "DLMessageRemoveItems" TypeCreateDirectory = "DLMessageCreateDirectory" TypeMoveItems = "DLMessageMoveItems" TypeGetFreeDiskSpace = "DLMessageGetFreeDiskSpace" ) // Conn ist ein Wrapper über eine generische bidirektionale Connection // die DLMessage-Frames lesen/schreiben kann. Die underlying Connection // kommt von go-ios's DeviceConnectionInterface. type Conn struct { rw io.ReadWriter } // New erzeugt einen Conn der DLMessages über `rw` sendet/empfängt. func New(rw io.ReadWriter) *Conn { return &Conn{rw: rw} } // DebugMode dumpt alle Send/Receive-bytes als hex. Aktiviere via env REBREAK_DLMSG_DEBUG=1. var DebugMode = false // Send schreibt eine DLMessage als [length-prefix][xml-plist]. // // args wird zu einem plist-Array gemacht. Erstes Element ist der // Message-Type-String, danach kommen die ProcessMessage-Daten. // // Beispiel: // // conn.Send(TypeProcessMessage, map[string]interface{}{ // "MessageName": "Hello", // "ProtocolVersion": "2.1", // }) func (c *Conn) Send(messageType string, args ...interface{}) error { arr := make([]interface{}, 0, 1+len(args)) arr = append(arr, messageType) arr = append(arr, args...) var buf bytes.Buffer enc := plist.NewEncoderForFormat(&buf, plist.XMLFormat) if err := enc.Encode(arr); err != nil { return fmt.Errorf("dlmessage: encode plist: %w", err) } // length-prefix als big-endian uint32 payload := buf.Bytes() hdr := make([]byte, 4) binary.BigEndian.PutUint32(hdr, uint32(len(payload))) if DebugMode { fmt.Printf("[dlmsg→] %d bytes payload (header: %x)\n", len(payload), hdr) preview := payload if len(preview) > 500 { preview = preview[:500] } fmt.Printf("[dlmsg→] %s\n", string(preview)) } if _, err := c.rw.Write(hdr); err != nil { return fmt.Errorf("dlmessage: write header: %w", err) } if _, err := c.rw.Write(payload); err != nil { return fmt.Errorf("dlmessage: write payload: %w", err) } return nil } // Receive liest eine DLMessage und returnt den Type-String + die restlichen // Array-Elemente als []interface{}. func (c *Conn) Receive() (messageType string, args []interface{}, err error) { // 4-byte length-prefix hdr := make([]byte, 4) if _, err = io.ReadFull(c.rw, hdr); err != nil { return "", nil, fmt.Errorf("dlmessage: read header: %w", err) } length := binary.BigEndian.Uint32(hdr) if length == 0 { return "", nil, errors.New("dlmessage: zero-length frame") } if length > 64*1024*1024 { return "", nil, fmt.Errorf("dlmessage: frame too large (%d bytes)", length) } // payload payload := make([]byte, length) if _, err = io.ReadFull(c.rw, payload); err != nil { return "", nil, fmt.Errorf("dlmessage: read payload: %w", err) } if DebugMode { fmt.Printf("[dlmsg←] %d bytes payload (header: %x)\n", len(payload), hdr) preview := payload if len(preview) > 500 { preview = preview[:500] } fmt.Printf("[dlmsg←] %s\n", string(preview)) fmt.Printf("[dlmsg←] hex: %x\n", preview[:min(80, len(preview))]) } // decode plist-array var arr []interface{} if _, derr := plist.Unmarshal(payload, &arr); derr != nil { return "", nil, fmt.Errorf("dlmessage: parse plist: %w", derr) } if len(arr) == 0 { return "", nil, errors.New("dlmessage: empty array") } t, ok := arr[0].(string) if !ok { return "", nil, fmt.Errorf("dlmessage: first element not string: %T", arr[0]) } return t, arr[1:], nil } // SendProcessMessage sendet `DLMessageProcessMessage` mit dem command-dict. // Convenience für den häufigsten Send-Pattern. func (c *Conn) SendProcessMessage(cmd map[string]interface{}) error { return c.Send(TypeProcessMessage, cmd) } // SendStatusResponse sendet `DLMessageStatusResponse` mit error-code + msg. func (c *Conn) SendStatusResponse(errorCode int, errorStr string, errorDict map[string]interface{}) error { return c.Send(TypeStatusResponse, errorCode, errorStr, errorDict) } // ReceiveProcessMessage erwartet als Antwort einen DLMessageProcessMessage und // returnt das command-dict. Strict — wirft Error bei anderem Type. func (c *Conn) ReceiveProcessMessage() (map[string]interface{}, error) { t, args, err := c.Receive() if err != nil { return nil, err } if t != TypeProcessMessage { return nil, fmt.Errorf("dlmessage: expected ProcessMessage, got %s (args: %v)", t, args) } if len(args) == 0 { return nil, errors.New("dlmessage: ProcessMessage has no payload") } dict, ok := args[0].(map[string]interface{}) if !ok { return nil, fmt.Errorf("dlmessage: ProcessMessage payload not a dict: %T", args[0]) } return dict, nil }