From bfa617ac9ae0f53787c1d4dd312b70bcd073daf9 Mon Sep 17 00:00:00 2001 From: Dadoum Date: Thu, 26 Oct 2023 02:12:33 +0200 Subject: [PATCH 1/6] Fix the early expire bug hopefully. --- source/imobiledevice/package.d | 4 ++++ source/sideload/package.d | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/source/imobiledevice/package.d b/source/imobiledevice/package.d index d532e06..6a555e0 100644 --- a/source/imobiledevice/package.d +++ b/source/imobiledevice/package.d @@ -236,6 +236,10 @@ public class MisagentClient { misagent_client_new(device.handle, service, &handle).assertSuccess(); } + void install(Plist profile) { + misagent_install(handle, profile.handle).assertSuccess(); + } + ~this() { if (handle) { // it may be null if an exception has been thrown TODO: switch from a constructor to a static function to fix that. misagent_client_free(handle).assertSuccess(); diff --git a/source/sideload/package.d b/source/sideload/package.d index dfe8675..00ae200 100644 --- a/source/sideload/package.d +++ b/source/sideload/package.d @@ -3,6 +3,7 @@ module sideload; import std.algorithm.iteration; import std.algorithm.searching; import std.array; +import std.concurrency; import std.datetime; import file = std.file; import std.format; @@ -189,7 +190,11 @@ void sideloadFull( progressCallback(progress, "Installing the application on the device (Transfer)"); } - import std.concurrency; + // This is negligible in terms of time + foreach (profile; provisioningProfiles.values()) { + misagentClient.install(Plist.fromMemory(profile.encodedProfile)); + } + Tid parentTid = thisTid(); installationProxyClient.install(remoteAppFolder, options, (command, statusPlist) { try { From 5af35c38a1a2d439384e9acd04ab6bb399647412 Mon Sep 17 00:00:00 2001 From: Dadoum Date: Thu, 26 Oct 2023 03:20:15 +0200 Subject: [PATCH 2/6] Fix a silly issue with that last fix --- source/sideload/package.d | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/sideload/package.d b/source/sideload/package.d index 00ae200..60fed8e 100644 --- a/source/sideload/package.d +++ b/source/sideload/package.d @@ -192,7 +192,7 @@ void sideloadFull( // This is negligible in terms of time foreach (profile; provisioningProfiles.values()) { - misagentClient.install(Plist.fromMemory(profile.encodedProfile)); + misagentClient.install(new PlistData(profile.encodedProfile)); } Tid parentTid = thisTid(); From 1a22c041175a6c09eae6643ea9b2069cb6bfbf12 Mon Sep 17 00:00:00 2001 From: Dadoum Date: Fri, 27 Oct 2023 19:19:43 +0200 Subject: [PATCH 3/6] Add SideStore pairing file tool --- dub.json | 2 +- dub.selections.json | 2 +- linux/gtk/ui/devicewidget.d | 5 +- linux/gtk/ui/sideloadergtkapplication.d | 2 +- linux/gtk/ui/toolselectionwindow.d | 93 +++++++ source/imobiledevice/house_arrest.d | 176 ++++++++++++ source/imobiledevice/package.d | 65 +++++ source/imobiledevice/userpref.d | 26 ++ source/sideload/package.d | 2 +- source/tools/package.d | 22 ++ source/tools/sidestorepairingfile.d | 130 ++++++++- source/usbmuxd/c.d | 343 ++++++++++++++++++++++++ source/usbmuxd/package.d | 27 ++ 13 files changed, 882 insertions(+), 13 deletions(-) create mode 100644 linux/gtk/ui/toolselectionwindow.d create mode 100644 source/imobiledevice/house_arrest.d create mode 100644 source/imobiledevice/userpref.d create mode 100644 source/tools/package.d create mode 100644 source/usbmuxd/c.d create mode 100644 source/usbmuxd/package.d diff --git a/dub.json b/dub.json index 8780f37..598a98a 100644 --- a/dub.json +++ b/dub.json @@ -22,7 +22,7 @@ "intel-intrinsics": "~>1.11.15", "plist-d": { "repository": "git+https://github.com/Dadoum/libplist-d.git", - "version": "30d152e88767611e10048b25777ecb5f9075f87c" + "version": "5020d8e45ca2c77183a44ce04053ccbf8bc83262" }, "provision": { "repository": "git+https://github.com/Dadoum/Provision.git", diff --git a/dub.selections.json b/dub.selections.json index 77d6178..37e7835 100644 --- a/dub.selections.json +++ b/dub.selections.json @@ -23,7 +23,7 @@ "isfreedesktop": "0.1.1", "memutils": "1.0.9", "plist": "~master", - "plist-d": {"version":"30d152e88767611e10048b25777ecb5f9075f87c","repository":"git+https://github.com/Dadoum/libplist-d.git"}, + "plist-d": {"version":"5020d8e45ca2c77183a44ce04053ccbf8bc83262","repository":"git+https://github.com/Dadoum/libplist-d.git"}, "provision": {"version":"533dca306b86f9c7801354b78f5187addb58b740","repository":"git+https://github.com/Dadoum/Provision.git"}, "requests": "2.1.1", "silly": "1.2.0-dev.2", diff --git a/linux/gtk/ui/devicewidget.d b/linux/gtk/ui/devicewidget.d index 0277424..3f92fd8 100644 --- a/linux/gtk/ui/devicewidget.d +++ b/linux/gtk/ui/devicewidget.d @@ -26,6 +26,7 @@ import sideload; import ui.authentication.authenticationassistant; import ui.sideloadprogresswindow; import ui.sideloadergtkapplication; +import ui.toolselectionwindow; import ui.utils; class DeviceWidget: PreferencesGroup { @@ -77,7 +78,9 @@ class DeviceWidget: PreferencesGroup { } void showTools(iDevice device) { - + auto rootWindow = cast(Window) this.getRoot(); + auto toolSelectionWindow = new ToolSelectionWindow(rootWindow, device); + toolSelectionWindow.show(); } void selectApplication() { diff --git a/linux/gtk/ui/sideloadergtkapplication.d b/linux/gtk/ui/sideloadergtkapplication.d index 3ff7919..220d12e 100644 --- a/linux/gtk/ui/sideloadergtkapplication.d +++ b/linux/gtk/ui/sideloadergtkapplication.d @@ -152,7 +152,7 @@ class SideloaderGtkApplication: Application { mainWindow.removeDeviceWidget(deviceInfo); break; default: - log.warnF!"Device %s (%s) made something unknown, event number: %d"(deviceInfo, deviceInfo.connType, event.event); + log.warnF!"Device %s (%s) triggered an unknown event (event number: %d)."(deviceInfo, deviceInfo.connType, event.event); break; } }); diff --git a/linux/gtk/ui/toolselectionwindow.d b/linux/gtk/ui/toolselectionwindow.d new file mode 100644 index 0000000..0ce8496 --- /dev/null +++ b/linux/gtk/ui/toolselectionwindow.d @@ -0,0 +1,93 @@ +module ui.toolselectionwindow; + +import core.thread; + +import std.concurrency; + +import adw.ActionRow; + +import gdk.Cursor; + +import gtk.Dialog; +import gtk.ListBox; +import gtk.MessageDialog; +import gtk.ScrolledWindow; +import gtk.Window; + +import imobiledevice; + +import tools; +import tools.sidestorepairingfile; + +import ui.utils; + +class ToolSelectionWindow: Dialog { + ListBox toolListBox; + + Cursor defaultCursor; + Cursor waitCursor; + + this(Window parent, iDevice device) { + this.setTitle("Additional tools"); + this.setTransientFor(parent); + this.setDefaultSize(400, 400); + this.setModal(true); + + defaultCursor = this.getCursor(); + waitCursor = new Cursor("wait", defaultCursor); + + auto scroll = new ScrolledWindow(); + + Tool[] tools = cast(Tool[]) [ + new SideStoreTool(device) + ]; + + toolListBox = new ListBox(); { + foreach (tool; tools) { + ActionRow toolRow = new ActionRow(); + toolRow.setTitle(tool.name()); + + string diagnostic = tool.diagnostic(); + toolRow.setActivatable(diagnostic == null); + if (diagnostic != null) { + toolRow.setTooltipText(diagnostic); + } + toolRow.addOnActivated((_) { + setBusy(true); + new Thread({ + uiTry!(() => tool.run((string message, bool canCancel = true) { + Tid parentTid = thisTid(); + runInUIThread({ + auto messageDialog = new MessageDialog(this, DialogFlags.MODAL, MessageType.INFO, canCancel ? ButtonsType.OK_CANCEL : ButtonsType.OK, message); + messageDialog.addOnResponse((response, _) { + if (canCancel) { + parentTid.send(response != ResponseType.OK); + } else { + parentTid.send(true); + } + messageDialog.close(); + }); + messageDialog.show(); + }); + return receiveOnly!bool(); + }) + )(this); + runInUIThread({ + setBusy(false); + }); + }).start(); + }); + + toolListBox.append(toolRow); + } + } + + scroll.setChild(toolListBox); + this.setChild(scroll); + } + + void setBusy(bool val) { + this.setSensitive(!val); + this.setCursor(val ? waitCursor : defaultCursor); + } +} diff --git a/source/imobiledevice/house_arrest.d b/source/imobiledevice/house_arrest.d new file mode 100644 index 0000000..40dd8e0 --- /dev/null +++ b/source/imobiledevice/house_arrest.d @@ -0,0 +1,176 @@ +/** + * @file libimobiledevice/house_arrest.h + * @brief Access app folders and their contents. + * \internal + * + * Copyright (c) 2013-2014 Martin Szulecki All Rights Reserved. + * Copyright (c) 2010 Nikias Bassen, All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +module imobiledevice.house_arrest; + +import plist.c; + +import imobiledevice.afc; +import imobiledevice.libimobiledevice; +import imobiledevice.lockdown; + +import dynamicloader; + +mixin makeBindings; +@libimobiledevice extern(C): + +/** Service identifier passed to lockdownd_start_service() to start the house arrest service */ +enum HOUSE_ARREST_SERVICE_NAME = "com.apple.mobile.house_arrest"; + +/** Error Codes */ +enum house_arrest_error_t +{ + HOUSE_ARREST_E_SUCCESS = 0, + HOUSE_ARREST_E_INVALID_ARG = -1, + HOUSE_ARREST_E_PLIST_ERROR = -2, + HOUSE_ARREST_E_CONN_FAILED = -3, + HOUSE_ARREST_E_INVALID_MODE = -4, + HOUSE_ARREST_E_UNKNOWN_ERROR = -256 +} + +struct house_arrest_client_private; /**< \private */ +alias house_arrest_client_t = house_arrest_client_private*; /**< The client handle. */ + +/* Interface */ + +/** + * Connects to the house_arrest service on the specified device. + * + * @param device The device to connect to. + * @param service The service descriptor returned by lockdownd_start_service. + * @param client Pointer that will point to a newly allocated + * housearrest_client_t upon successful return. + * + * @return HOUSE_ARREST_E_SUCCESS on success, HOUSE_ARREST_E_INVALID_ARG when + * client is NULL, or an HOUSE_ARREST_E_* error code otherwise. + */ +house_arrest_error_t house_arrest_client_new (idevice_t device, lockdownd_service_descriptor_t service, house_arrest_client_t* client); + +/** + * Starts a new house_arrest service on the specified device and connects to it. + * + * @param device The device to connect to. + * @param client Pointer that will point to a newly allocated + * house_arrest_client_t upon successful return. Must be freed using + * house_arrest_client_free() after use. + * @param label The label to use for communication. Usually the program name. + * Pass NULL to disable sending the label in requests to lockdownd. + * + * @return HOUSE_ARREST_E_SUCCESS on success, or an HOUSE_ARREST_E_* error + * code otherwise. + */ +house_arrest_error_t house_arrest_client_start_service (idevice_t device, house_arrest_client_t* client, const(char)* label); + +/** + * Disconnects an house_arrest client from the device and frees up the + * house_arrest client data. + * + * @note After using afc_client_new_from_house_arrest_client(), make sure + * you call afc_client_free() before calling this function to ensure + * a proper cleanup. Do not call this function if you still need to + * perform AFC operations since it will close the connection. + * + * @param client The house_arrest client to disconnect and free. + * + * @return HOUSE_ARREST_E_SUCCESS on success, HOUSE_ARREST_E_INVALID_ARG when + * client is NULL, or an HOUSE_ARREST_E_* error code otherwise. + */ +house_arrest_error_t house_arrest_client_free (house_arrest_client_t client); + +/** + * Sends a generic request to the connected house_arrest service. + * + * @param client The house_arrest client to use. + * @param dict The request to send as a plist of type PLIST_DICT. + * + * @note If this function returns HOUSE_ARREST_E_SUCCESS it does not mean + * that the request was successful. To check for success or failure you + * need to call house_arrest_get_result(). + * @see house_arrest_get_result + * + * @return HOUSE_ARREST_E_SUCCESS if the request was successfully sent, + * HOUSE_ARREST_E_INVALID_ARG if client or dict is invalid, + * HOUSE_ARREST_E_PLIST_ERROR if dict is not a plist of type PLIST_DICT, + * HOUSE_ARREST_E_INVALID_MODE if the client is not in the correct mode, + * or HOUSE_ARREST_E_CONN_FAILED if a connection error occurred. + */ +house_arrest_error_t house_arrest_send_request (house_arrest_client_t client, plist_t dict); + +/** + * Send a command to the connected house_arrest service. + * Calls house_arrest_send_request() internally. + * + * @param client The house_arrest client to use. + * @param command The command to send. Currently, only VendContainer and + * VendDocuments are known. + * @param appid The application identifier to pass along with the . + * + * @note If this function returns HOUSE_ARREST_E_SUCCESS it does not mean + * that the command was successful. To check for success or failure you + * need to call house_arrest_get_result(). + * @see house_arrest_get_result + * + * @return HOUSE_ARREST_E_SUCCESS if the command was successfully sent, + * HOUSE_ARREST_E_INVALID_ARG if client, command, or appid is invalid, + * HOUSE_ARREST_E_INVALID_MODE if the client is not in the correct mode, + * or HOUSE_ARREST_E_CONN_FAILED if a connection error occurred. + */ +house_arrest_error_t house_arrest_send_command (house_arrest_client_t client, const(char)* command, const(char)* appid); + +/** + * Retrieves the result of a previously sent house_arrest_request_* request. + * + * @param client The house_arrest client to use + * @param dict Pointer that will be set to a plist containing the result to + * the last performed operation. It holds a key 'Status' with the value + * 'Complete' on success or a key 'Error' with an error description as + * value. The caller is responsible for freeing the returned plist. + * + * @return HOUSE_ARREST_E_SUCCESS if a result plist was retrieved, + * HOUSE_ARREST_E_INVALID_ARG if client is invalid, + * HOUSE_ARREST_E_INVALID_MODE if the client is not in the correct mode, + * or HOUSE_ARREST_E_CONN_FAILED if a connection error occurred. + */ +house_arrest_error_t house_arrest_get_result (house_arrest_client_t client, plist_t* dict); + +/** + * Creates an AFC client using the given house_arrest client's connection + * allowing file access to a specific application directory requested by + * functions like house_arrest_request_vendor_documents(). + * + * @param client The house_arrest client to use. + * @param afc_client Pointer that will be set to a newly allocated afc_client_t + * upon successful return. + * + * @note After calling this function the house_arrest client will go in an + * AFC mode that will only allow calling house_arrest_client_free(). + * Only call house_arrest_client_free() if all AFC operations have + * completed since it will close the connection. + * + * @return AFC_E_SUCCESS if the afc client was successfully created, + * AFC_E_INVALID_ARG if client is invalid or was already used to create + * an afc client, or an AFC_E_* error code returned by + * afc_client_new_with_service_client(). + */ +afc_error_t afc_client_new_from_house_arrest_client (house_arrest_client_t client, afc_client_t* afc_client); + diff --git a/source/imobiledevice/package.d b/source/imobiledevice/package.d index 6a555e0..bcb6349 100644 --- a/source/imobiledevice/package.d +++ b/source/imobiledevice/package.d @@ -1,6 +1,7 @@ module imobiledevice; public import imobiledevice.afc; +public import imobiledevice.house_arrest; public import imobiledevice.installation_proxy; public import imobiledevice.libimobiledevice; public import imobiledevice.lockdown; @@ -15,6 +16,7 @@ import std.string; import std.traits; import plist; +import plist.c; class iMobileDeviceException(T): Exception { this(T error, string file = __FILE__, int line = __LINE__) { @@ -117,6 +119,31 @@ public class LockdowndClient { return new LockdowndServiceDescriptor(descriptor); } + public string startSession(string hostId) { + char* sessionId; + lockdownd_start_session(handle, hostId.toStringz(), &sessionId, null).assertSuccess(); + return cast(string) sessionId.fromStringz(); + } + + public void stopSession(string sessionId) { + lockdownd_stop_session(handle, sessionId.toStringz()).assertSuccess(); + } + + public Plist opIndexAssign(Plist value, string domain, string key) { + if (!value.owns) { + value = value.copy(); + } + lockdownd_set_value(handle, domain.toStringz(), key.toStringz(), value.handle).assertSuccess(); + value.owns = false; + return value; + } + + public Plist opIndex(string domain, string key) { + plist_t ret; + lockdownd_get_value(handle, domain.toStringz(), key.toStringz(), &ret).assertSuccess(); + return Plist.wrap(ret); + } + public lockdownd_error_t pair() { return lockdownd_pair(handle, null); // note: the error is expected within the normal execution flow, so no throw } @@ -163,6 +190,12 @@ public class InstallationProxyClient { }, cb).assertSuccess(); } + Plist browse(Plist options = null) { + plist_t result; + instproxy_browse(handle, options ? options.handle : null, &result).assertSuccess(); + return Plist.wrap(result); + } + ~this() { if (handle) { // it may be null if an exception has been thrown TODO: switch from a constructor to a static function to fix that. instproxy_client_free(handle).assertSuccess(); @@ -180,6 +213,10 @@ public class AFCClient { afc_client_new(device.handle, service, &handle).assertSuccess(); } + public this(HouseArrestClient houseArrestClient) { + afc_client_new_from_house_arrest_client(houseArrestClient.handle, &handle).assertSuccess(); + } + ~this() { if (handle) { // it may be null if an exception has been thrown TODO: switch from a constructor to a static function to fix that. afc_client_free(handle).assertSuccess(); @@ -246,3 +283,31 @@ public class MisagentClient { } } } + +public class HouseArrestClient { + house_arrest_client_t handle; + + public this(iDevice device, LockdowndServiceDescriptor service) { + house_arrest_client_new(device.handle, service, &handle).assertSuccess(); + } + + public this(iDevice device, string label = null) { + house_arrest_client_start_service(device.handle, &handle, label.toStringz()).assertSuccess(); + } + + void sendCommand(string command, string appId) { + house_arrest_send_command(handle, command.toStringz(), appId.toStringz()).assertSuccess(); + } + + Plist getResult() { + plist_t ret; + house_arrest_get_result(handle, &ret).assertSuccess(); + return Plist.wrap(ret); + } + + ~this() { + if (handle) { // it may be null if an exception has been thrown TODO: switch from a constructor to a static function to fix that. + house_arrest_client_free(handle).assertSuccess(); + } + } +} diff --git a/source/imobiledevice/userpref.d b/source/imobiledevice/userpref.d new file mode 100644 index 0000000..a7b87d9 --- /dev/null +++ b/source/imobiledevice/userpref.d @@ -0,0 +1,26 @@ +module imobiledevice.userpref; + +// Bindings done manually +import plist.c; + +import imobiledevice.libimobiledevice; + +import dynamicloader; + +mixin makeBindings; +@libimobiledevice extern(C): + +enum userpref_error_t { + USERPREF_E_SUCCESS = 0, + USERPREF_E_INVALID_ARG = -1, + USERPREF_E_NOENT = -2, + USERPREF_E_INVALID_CONF = -3, + USERPREF_E_SSL_ERROR = -4, + USERPREF_E_READ_ERROR = -5, + USERPREF_E_WRITE_ERROR = -6, + USERPREF_E_UNKNOWN_ERROR = -256 +} + +userpref_error_t userpref_read_pair_record(const char* udid, plist_t* pair_record); + +userpref_error_t pair_record_get_host_id(plist_t pair_record, char** host_id); diff --git a/source/sideload/package.d b/source/sideload/package.d index 60fed8e..8fa2470 100644 --- a/source/sideload/package.d +++ b/source/sideload/package.d @@ -146,7 +146,7 @@ void sideloadFull( scope misagentService = lockdownClient.startService("com.apple.misagent"); scope misagentClient = new MisagentClient(device, misagentService); - scope afcService = lockdownClient.startService("com.apple.afc"); + scope afcService = lockdownClient.startService(AFC_SERVICE_NAME); scope afcClient = new AFCClient(device, afcService); string stagingDir = "PublicStaging"; diff --git a/source/tools/package.d b/source/tools/package.d new file mode 100644 index 0000000..2aa9d0c --- /dev/null +++ b/source/tools/package.d @@ -0,0 +1,22 @@ +module tools; + +import imobiledevice; + +abstract class Tool { + iDevice device; + LockdowndClient lockdowndClient; + + this(iDevice device, LockdowndClient lockdowndClient) { + this.device = device; + this.lockdowndClient = lockdowndClient; + } + + /// Name of the tool + abstract string name(); + + /// Returns null if the action can be performed, otherwise gives a diagnostic on why it is not available. + abstract string diagnostic(); + + /// Returns success, + abstract void run(bool delegate(string message, bool canCancel = true) notify); +} diff --git a/source/tools/sidestorepairingfile.d b/source/tools/sidestorepairingfile.d index 9aef704..329fc5d 100644 --- a/source/tools/sidestorepairingfile.d +++ b/source/tools/sidestorepairingfile.d @@ -1,14 +1,128 @@ module tools.sidestorepairingfile; +import std.algorithm; +import std.array; +import std.format; + +import slf4d; + +import plist; + import imobiledevice; +import usbmuxd; -void askForTrust(iDevice device) { - scope lockdownClient = new LockdowndClient(device, "sideloader.trust-service"); - scope pairing = lockdownClient.pair(); -} +import tools; + +class SideStoreTool: Tool { + string[] sideStoreBundles; + + this(iDevice device) { + super(device, new LockdowndClient(device, "sideloader.sidestore-trust")); + + scope installationProxyService = lockdowndClient.startService("com.apple.mobile.installation_proxy"); + scope installationProxyClient = new InstallationProxyClient(device, installationProxyService); + + sideStoreBundles = installationProxyClient.browse([ + "ApplicationType": "User".pl, + "ReturnAttributes": [ + "CFBundleIdentifier".pl + ].pl + ].pl).array().native() + .filter!((elem) => elem["CFBundleIdentifier"].str().native().startsWith("com.SideStore.SideStore")) + .map!((elem) => elem["CFBundleIdentifier"].str().native()) + .array(); + } + + override string name() { + return "Set-up SideStore's pairing file"; + } + + override string diagnostic() { + return sideStoreBundles.length > 0 ? null : "SideStore is not installed on the device."; + } + + override void run(bool delegate(string message, bool canCancel = true) notify) { + auto log = getLogger(); + log.info("Placing SideStore pairing file."); + + assert(diagnostic() == null); + { + lockdownd_error_t error; + do { + error = lockdowndClient.pair(); + with(lockdownd_error_t) switch (error) { + case LOCKDOWN_E_SUCCESS: + break; + case LOCKDOWN_E_PASSWORD_PROTECTED: + if (notify("Please unlock your phone. (press OK to try again)")) { + return; + } + break; + case LOCKDOWN_E_PAIRING_DIALOG_RESPONSE_PENDING: + if (notify("Please trust the computer. (press OK to try again)")) { + return; + } + break; + case LOCKDOWN_E_USER_DENIED_PAIRING: + if (notify("You refused to trust the computer.", false)) { + return; + } + break; + default: + if (notify("Unknown error, please check that the device is unlocked and trusts the computer (press OK to try again)")) { + return; + } + break; + } + } while (error != lockdownd_error_t.LOCKDOWN_E_SUCCESS); + } + + string udid = device.udid(); + scope pairRecord = Plist.fromMemory(readPairRecord(udid)).dict(); + pairRecord["UDID"] = udid.pl; + log.debug_("Pairing file obtained"); + + string hostId = pairRecord["HostID"].str().native(); + // string sessionId = lockdowndClient.startSession(hostId); + // scope(exit) lockdowndClient.stopSession(sessionId); + + lockdowndClient["com.apple.mobile.wireless_lockdown", "EnableWifiDebugging"] = true.pl; + log.debug_("Wireless connections enabled"); + + foreach (sideStoreBundleId; sideStoreBundles) { + log.debugF!"Starting House Arrest for %s"(sideStoreBundleId); + scope houseArrest = new HouseArrestClient(device); + // We could only mount documents but that code snippet might be useful to clear caches in the future + // so having all that code ready is cool + houseArrest.sendCommand("VendContainer", sideStoreBundleId); + Plist result = houseArrest.getResult(); + if (Plist error = "Error" in result.dict()) { + log.errorF!"Error occured while House Arrest set-up: %s"(result); + string value = error.str().native(); + if (notify( + format!"Cannot access to the app container for %s! Are you sure it's official SideStore app?"(sideStoreBundleId) + ~ (sideStoreBundleId.length > 1 ? " (press OK to do it for the others app bundles)" : ""), sideStoreBundleId.length > 1 + )) { + return; + } + continue; + } + + log.debugF!"Starting AFC for %s"(sideStoreBundleId); + scope afcClient = new AFCClient(houseArrest); + + log.debug_("Writing file"); + auto remoteFile = afcClient.open("/Documents/ALTPairingFile.mobiledevicepairing", AFCFileMode.AFC_FOPEN_WRONLY); + scope(exit) afcClient.close(remoteFile); + + ubyte[] fileData = cast(ubyte[]) pairRecord.toXml(); -/// returns: success? -/// throws if sidestore isn't installed. -bool sendSideStorePairingFile(iDevice device) { - return false; + uint bytesWrote = 0; + while (bytesWrote < fileData.length) { + bytesWrote += afcClient.write(remoteFile, fileData); + } + log.debug_("Done!"); + } + notify("The pairing file has been successfully set up for " ~ sideStoreBundles.join(", ") ~ ".", false); + } } diff --git a/source/usbmuxd/c.d b/source/usbmuxd/c.d new file mode 100644 index 0000000..69cc8f5 --- /dev/null +++ b/source/usbmuxd/c.d @@ -0,0 +1,343 @@ +/* + * usbmuxd.h - A client library to talk to the usbmuxd daemon. + * + * Copyright (C) 2009-2018 Nikias Bassen + * Copyright (C) 2014 Martin Szulecki + * Copyright (C) 2009 Paul Sladen + * + * This library is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation, either version 2.1 of the + * License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +module usbmuxd.c; + +import dynamicloader; + +version (Windows) { + version (MinGW) { + enum libusbmuxd = LibImport("libusbmuxd-2.0.dll"); + } else { + enum libusbmuxd = LibImport("usbmuxd.dll"); + } +} else version (OSX) { + enum libusbmuxd = LibImport("libusbmuxd-2.0.6.dylib"); +} else { + enum libusbmuxd = LibImport("libusbmuxd-2.0.so.6", "libusbmuxd.so.2"); +} + +mixin makeBindings; +@libusbmuxd extern (C): + +/** Device lookup options for usbmuxd_get_device. */ +enum usbmux_lookup_options +{ + DEVICE_LOOKUP_USBMUX = 1 << 1, /**< include USBMUX devices during lookup */ + DEVICE_LOOKUP_NETWORK = 1 << 2, /**< include network devices during lookup */ + DEVICE_LOOKUP_PREFER_NETWORK = 1 << 3 /**< prefer network connection if device is available via USBMUX *and* network */ +} + +/** Type of connection a device is available on */ +enum usbmux_connection_type +{ + CONNECTION_TYPE_USB = 1, + CONNECTION_TYPE_NETWORK = 2 +} + +/** + * Device information structure holding data to identify the device. + * The relevant 'handle' should be passed to 'usbmuxd_connect()', to + * start a proxy connection. The value 'handle' should be considered + * opaque and no presumption made about the meaning of its value. + */ +struct usbmuxd_device_info_t +{ + uint handle; + uint product_id; + char[44] udid; + usbmux_connection_type conn_type; + char[200] conn_data; +} + +/** + * event types for event callback function + */ +enum usbmuxd_event_type +{ + UE_DEVICE_ADD = 1, + UE_DEVICE_REMOVE = 2, + UE_DEVICE_PAIRED = 3 +} + +/** + * Event structure that will be passed to the callback function. + * 'event' will contains the type of the event, and 'device' will contains + * information about the device. + */ +struct usbmuxd_event_t +{ + int event; + usbmuxd_device_info_t device; +} + +/** + * Callback function prototype. + */ +alias usbmuxd_event_cb_t = void function (const(usbmuxd_event_t)* event, void* user_data); + +/** + * Subscription context type. + */ +struct usbmuxd_subscription_context; +alias usbmuxd_subscription_context_t = usbmuxd_subscription_context*; + +/** + * Subscribe a callback function to be called upon device add/remove events. + * This method can be called multiple times to register multiple callbacks + * since every subscription will have its own context (returned in the + * first parameter). + * + * @param context A pointer to a usbmuxd_subscription_context_t that will be + * set upon creation of the subscription. The returned context must be + * passed to usbmuxd_events_unsubscribe() to unsubscribe the callback. + * @param callback A callback function that is executed when an event occurs. + * @param user_data Custom data passed on to the callback function. The data + * needs to be kept available until the callback function is unsubscribed. + * + * @return 0 on success or a negative errno value. + */ +int usbmuxd_events_subscribe (usbmuxd_subscription_context_t* context, usbmuxd_event_cb_t callback, void* user_data); + +/** + * Unsubscribe callback function + * + * @param context A valid context as returned from usbmuxd_events_subscribe(). + * + * @return 0 on success or a negative errno value. + */ +int usbmuxd_events_unsubscribe (usbmuxd_subscription_context_t context); + +/** + * Subscribe a callback (deprecated) + * + * @param callback A callback function that is executed when an event occurs. + * @param user_data Custom data passed on to the callback function. The data + * needs to be kept available until the callback function is unsubscribed. + * + * @return 0 on success or negative on error. + * + * @note Deprecated. Use usbmuxd_events_subscribe and usbmuxd_events_unsubscribe instead. + * @see usbmuxd_events_subscribe + */ +int usbmuxd_subscribe (usbmuxd_event_cb_t callback, void* user_data); + +/** + * Unsubscribe callback (deprecated) + * + * @return 0 on success or negative on error. + * + * @note Deprecated. Use usbmuxd_events_subscribe and usbmuxd_events_unsubscribe instead. + * @see usbmuxd_events_unsubscribe + */ +int usbmuxd_unsubscribe (); + +/** + * Contacts usbmuxd and retrieves a list of connected devices. + * + * @param device_list A pointer to an array of usbmuxd_device_info_t + * that will hold records of the connected devices. The last record + * is a null-terminated record with all fields set to 0/NULL. + * @note The user has to free the list returned. + * + * @return number of attached devices, zero on no devices, or negative + * if an error occured. + */ +int usbmuxd_get_device_list (usbmuxd_device_info_t** device_list); + +/** + * Frees the device list returned by an usbmuxd_get_device_list call + * + * @param device_list A pointer to an array of usbmuxd_device_info_t to free. + * + * @return 0 on success, -1 on error. + */ +int usbmuxd_device_list_free (usbmuxd_device_info_t** device_list); + +/** + * Looks up the device specified by UDID and returns device information. + * + * @note This function only considers devices connected through USB. To + * query devices available via network, use usbmuxd_get_device(). + * + * @see usbmuxd_get_device + * + * @param udid A device UDID of the device to look for. If udid is NULL, + * This function will return the first device found. + * @param device Pointer to a previously allocated (or static) + * usbmuxd_device_info_t that will be filled with the device info. + * + * @return 0 if no matching device is connected, 1 if the device was found, + * or a negative value on error. + */ +int usbmuxd_get_device_by_udid (const(char)* udid, usbmuxd_device_info_t* device); + +/** + * Looks up the device specified by UDID with given options and returns + * device information. + * + * @param udid A device UDID of the device to look for. If udid is NULL, + * this function will return the first device found. + * @param device Pointer to a previously allocated (or static) + * usbmuxd_device_info_t that will be filled with the device info. + * @param options Specifying what device connection types should be + * considered during lookup. Accepts bitwise or'ed values of + * usbmux_lookup_options. + * If 0 (no option) is specified it will default to DEVICE_LOOKUP_USBMUX. + * To lookup both USB and network-connected devices, pass + * DEVICE_LOOKUP_USBMUX | DEVICE_LOOKUP_NETWORK. If a device is available + * both via USBMUX *and* network, it will select the USB connection. + * This behavior can be changed by adding DEVICE_LOOKUP_PREFER_NETWORK + * to the options in which case it will select the network connection. + * + * @see enum usbmux_lookup_options + * + * @return 0 if no matching device is connected, 1 if the device was found, + * or a negative value on error. + */ +int usbmuxd_get_device (const(char)* udid, usbmuxd_device_info_t* device, usbmux_lookup_options options); + +/** + * Request proxy connection to the specified device and port. + * + * @param handle returned in the usbmux_device_info_t structure via + * usbmuxd_get_device() or usbmuxd_get_device_list(). + * + * @param tcp_port TCP port number on device, in range 0-65535. + * common values are 62078 for lockdown, and 22 for SSH. + * + * @return socket file descriptor of the connection, or a negative errno + * value on error. + */ +int usbmuxd_connect (const uint handle, const ushort tcp_port); + +/** + * Disconnect. For now, this just closes the socket file descriptor. + * + * @param sfd socket file descriptor returned by usbmuxd_connect() + * + * @return 0 on success, -1 on error. + */ +int usbmuxd_disconnect (int sfd); + +/** + * Send data to the specified socket. + * + * @param sfd socket file descriptor returned by usbmuxd_connect() + * @param data buffer to send + * @param len size of buffer to send + * @param sent_bytes how many bytes sent + * + * @return 0 on success, a negative errno value otherwise. + */ +int usbmuxd_send (int sfd, const(char)* data, uint len, uint* sent_bytes); + +/** + * Receive data from the specified socket. + * + * @param sfd socket file descriptor returned by usbmuxd_connect() + * @param data buffer to put the data to + * @param len number of bytes to receive + * @param recv_bytes number of bytes received + * @param timeout how many milliseconds to wait for data + * + * @return 0 on success, a negative errno value otherwise. + */ +int usbmuxd_recv_timeout (int sfd, char* data, uint len, uint* recv_bytes, uint timeout); + +/** + * Receive data from the specified socket with a default timeout. + * + * @param sfd socket file descriptor returned by usbmuxd_connect() + * @param data buffer to put the data to + * @param len number of bytes to receive + * @param recv_bytes number of bytes received + * + * @return 0 on success, a negative errno value otherwise. + */ +int usbmuxd_recv (int sfd, char* data, uint len, uint* recv_bytes); + +/** + * Reads the SystemBUID + * + * @param buid pointer to a variable that will be set to point to a newly + * allocated string with the System BUID returned by usbmuxd + * + * @return 0 on success, a negative errno value otherwise. + */ +int usbmuxd_read_buid (char** buid); + +/** + * Read a pairing record + * + * @param record_id the record identifier of the pairing record to retrieve + * @param record_data pointer to a variable that will be set to point to a + * newly allocated buffer containing the pairing record data + * @param record_size pointer to a variable that will be set to the size of + * the buffer returned in record_data + * + * @return 0 on success, a negative error value otherwise. + */ +int usbmuxd_read_pair_record (const(char)* record_id, char** record_data, uint* record_size); + +/** + * Save a pairing record + * + * @param record_id the record identifier of the pairing record to save + * @param record_data buffer containing the pairing record data + * @param record_size size of the buffer passed in record_data + * + * @return 0 on success, a negative error value otherwise. + */ +int usbmuxd_save_pair_record (const(char)* record_id, const(char)* record_data, uint record_size); + +/** + * Save a pairing record with device identifier + * + * @param record_id the record identifier of the pairing record to save + * @param device_id the device identifier of the connected device, or 0 + * @param record_data buffer containing the pairing record data + * @param record_size size of the buffer passed in record_data + * + * @return 0 on success, a negative error value otherwise. + */ +int usbmuxd_save_pair_record_with_device_id (const(char)* record_id, uint device_id, const(char)* record_data, uint record_size); + +/** + * Delete a pairing record + * + * @param record_id the record identifier of the pairing record to delete. + * + * @return 0 on success, a negative errno value otherwise. + */ +int usbmuxd_delete_pair_record (const(char)* record_id); + +/** + * Enable or disable the use of inotify extension. Enabled by default. + * Use 0 to disable and 1 to enable inotify support. + * This only has an effect on linux systems if inotify support has been built + * in. Otherwise and on all other platforms this function has no effect. + */ +void libusbmuxd_set_use_inotify (int set); + +void libusbmuxd_set_debug_level (int level); + +/* USBMUXD_H */ diff --git a/source/usbmuxd/package.d b/source/usbmuxd/package.d new file mode 100644 index 0000000..54a03ff --- /dev/null +++ b/source/usbmuxd/package.d @@ -0,0 +1,27 @@ +module usbmuxd; + +import core.stdc.stdlib; + +import std.string; + +import usbmuxd.c; + +class UsbmuxdException: Exception { + this(int error, string file = __FILE__, int line = __LINE__) { + super(format!"usbmuxd error: %d"(error), file, line); + } +} + +void assertSuccess(int err) { + if (err < 0) + throw new UsbmuxdException(err); +} + +ubyte[] readPairRecord(string udid) { + char* dataPtr; + uint length; + usbmuxd_read_pair_record(udid.toStringz(), &dataPtr, &length).assertSuccess(); + ubyte[] data = cast(ubyte[]) dataPtr[0..length].dup; + free(dataPtr); + return data; +} From 58015db18bd97200d3bf995e3dc8a9aa7e812977 Mon Sep 17 00:00:00 2001 From: Dadoum Date: Fri, 27 Oct 2023 19:21:27 +0200 Subject: [PATCH 4/6] Remove userpref.d --- source/imobiledevice/userpref.d | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 source/imobiledevice/userpref.d diff --git a/source/imobiledevice/userpref.d b/source/imobiledevice/userpref.d deleted file mode 100644 index a7b87d9..0000000 --- a/source/imobiledevice/userpref.d +++ /dev/null @@ -1,26 +0,0 @@ -module imobiledevice.userpref; - -// Bindings done manually -import plist.c; - -import imobiledevice.libimobiledevice; - -import dynamicloader; - -mixin makeBindings; -@libimobiledevice extern(C): - -enum userpref_error_t { - USERPREF_E_SUCCESS = 0, - USERPREF_E_INVALID_ARG = -1, - USERPREF_E_NOENT = -2, - USERPREF_E_INVALID_CONF = -3, - USERPREF_E_SSL_ERROR = -4, - USERPREF_E_READ_ERROR = -5, - USERPREF_E_WRITE_ERROR = -6, - USERPREF_E_UNKNOWN_ERROR = -256 -} - -userpref_error_t userpref_read_pair_record(const char* udid, plist_t* pair_record); - -userpref_error_t pair_record_get_host_id(plist_t pair_record, char** host_id); From bbd37b637ddcf018df46711f0ceddb935fb99108 Mon Sep 17 00:00:00 2001 From: Dadoum Date: Fri, 27 Oct 2023 19:31:27 +0200 Subject: [PATCH 5/6] Close the tools window when the device is unplugged --- linux/gtk/ui/devicewidget.d | 9 ++++++++- linux/gtk/ui/mainwindow.d | 4 +++- linux/gtk/ui/toolselectionwindow.d | 2 +- source/tools/sidestorepairingfile.d | 2 +- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/linux/gtk/ui/devicewidget.d b/linux/gtk/ui/devicewidget.d index 3f92fd8..4c58257 100644 --- a/linux/gtk/ui/devicewidget.d +++ b/linux/gtk/ui/devicewidget.d @@ -32,6 +32,7 @@ import ui.utils; class DeviceWidget: PreferencesGroup { iDevice device; LockdowndClient lockdowndClient; + Window toolSelectionWindow; this(iDeviceInfo deviceInfo) { string udid = deviceInfo.udid; @@ -79,7 +80,7 @@ class DeviceWidget: PreferencesGroup { void showTools(iDevice device) { auto rootWindow = cast(Window) this.getRoot(); - auto toolSelectionWindow = new ToolSelectionWindow(rootWindow, device); + toolSelectionWindow = new ToolSelectionWindow(rootWindow, device); toolSelectionWindow.show(); } @@ -120,4 +121,10 @@ class DeviceWidget: PreferencesGroup { fileChooser.show(); } + + void closeWindows() { + if (toolSelectionWindow) { + toolSelectionWindow.close(); + } + } } diff --git a/linux/gtk/ui/mainwindow.d b/linux/gtk/ui/mainwindow.d index 6afccc6..6fd011f 100644 --- a/linux/gtk/ui/mainwindow.d +++ b/linux/gtk/ui/mainwindow.d @@ -117,7 +117,9 @@ class MainWindow: Window { void removeDeviceWidget(iDeviceInfo deviceId) { if (deviceId in deviceWidgets) { - deviceWidgets[deviceId].unparent(); + auto deviceWidget = deviceWidgets[deviceId]; + deviceWidget.unparent(); + deviceWidget.closeWindows(); deviceWidgets.remove(deviceId); if (deviceWidgets.length == 0) { connectDeviceLabel.show(); diff --git a/linux/gtk/ui/toolselectionwindow.d b/linux/gtk/ui/toolselectionwindow.d index 0ce8496..bec800f 100644 --- a/linux/gtk/ui/toolselectionwindow.d +++ b/linux/gtk/ui/toolselectionwindow.d @@ -58,7 +58,7 @@ class ToolSelectionWindow: Dialog { uiTry!(() => tool.run((string message, bool canCancel = true) { Tid parentTid = thisTid(); runInUIThread({ - auto messageDialog = new MessageDialog(this, DialogFlags.MODAL, MessageType.INFO, canCancel ? ButtonsType.OK_CANCEL : ButtonsType.OK, message); + auto messageDialog = new MessageDialog(this, DialogFlags.DESTROY_WITH_PARENT | DialogFlags.MODAL, MessageType.INFO, canCancel ? ButtonsType.OK_CANCEL : ButtonsType.OK, message); messageDialog.addOnResponse((response, _) { if (canCancel) { parentTid.send(response != ResponseType.OK); diff --git a/source/tools/sidestorepairingfile.d b/source/tools/sidestorepairingfile.d index 329fc5d..e44dabe 100644 --- a/source/tools/sidestorepairingfile.d +++ b/source/tools/sidestorepairingfile.d @@ -69,7 +69,7 @@ class SideStoreTool: Tool { } break; default: - if (notify("Unknown error, please check that the device is unlocked and trusts the computer (press OK to try again)")) { + if (notify("Unknown error, please check that the device is plugged correctly, unlocked and trusts the computer. (press OK to try again)")) { return; } break; From 88f658dfc71f3b1065904ff28802396df76c11ac Mon Sep 17 00:00:00 2001 From: Dadoum Date: Sat, 28 Oct 2023 01:01:07 +0200 Subject: [PATCH 6/6] Don't show any error dialog when no device are connected --- source/imobiledevice/package.d | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/source/imobiledevice/package.d b/source/imobiledevice/package.d index bcb6349..083bac0 100644 --- a/source/imobiledevice/package.d +++ b/source/imobiledevice/package.d @@ -81,7 +81,11 @@ public class iDevice { public static @property iDeviceInfo[] deviceList() { int len; idevice_info_t* names; - idevice_get_device_list_extended(&names, &len).assertSuccess(); + auto res = idevice_get_device_list_extended(&names, &len); + if (res == idevice_error_t.IDEVICE_E_NO_DEVICE) { + return []; + } + res.assertSuccess(); return names[0..len].map!((s) => iDeviceInfo(cast(string) s.udid.fromStringz, cast(iDeviceConnectionType) s.conn_type)).array; }