tools: Add idevicedevmodectl tool
diff --git a/docs/Makefile.am b/docs/Makefile.am
index 9cdf82e..4a4c56f 100644
--- a/docs/Makefile.am
+++ b/docs/Makefile.am
@@ -16,6 +16,7 @@
 	idevicecrashreport.1 \
 	idevicename.1 \
 	idevicedebug.1 \
+	idevicedevmodectl.1 \
 	idevicenotificationproxy.1 \
 	idevicesetlocation.1
 
diff --git a/docs/idevicedevmodectl.1 b/docs/idevicedevmodectl.1
new file mode 100644
index 0000000..5edaa80
--- /dev/null
+++ b/docs/idevicedevmodectl.1
@@ -0,0 +1,58 @@
+.TH "idevicedevmodectl" 1
+.SH NAME
+idevicedevmodectl \- Enable Developer Mode on iOS 16+ devices or print the current status.
+.SH SYNOPSIS
+.B idevicedevmodectl
+COMMAND
+[OPTIONS]
+
+.SH DESCRIPTION
+
+Enable Developer Mode on iOS 16+ devices or print the current status.
+
+.SH NOTE
+Passcode-protected devices will NOT allow enabling of Developer Mode from the command line. It has to be enabled on the device itself under Settings -> Privacy & Security -> Developer Mode.
+The \f[B]enable\f[] command will try to enable it, and tell you if that's the case.
+If the menu is not shown, you may use the \f[B]reveal\f[] command to reveal it.
+
+.SH COMMANDS
+.TP
+.B list
+Prints the Developer Mode status of all connected devices, or for a specific one if \f[B]\-\-udid\f[] is given.
+.TP
+.B enable
+Enable Developer Mode (device will reboot), and confirm it after device booted up again.
+.TP
+.B arm
+Arm the Developer Mode (device will reboot)
+.TP
+.B confirm
+Confirm enabling of Developer Mode
+.TP
+.B reveal
+Reveal the Developer Mode menu on the device under Settings -> Privacy & Security
+
+.SH OPTIONS
+.TP
+.B \-u, \-\-udid UDID
+target specific device by UDID
+.TP
+.B \-n, \-\-network
+connect to network device
+.TP
+.B \-d, \-\-debug
+enable communication debugging
+.TP
+.B \-h, \-\-help
+print usage information
+.TP
+.B \-v, \-\-version
+print version information
+
+.SH AUTHORS
+Nikias Bassen
+
+.SH ON THE WEB
+https://libimobiledevice.org
+
+https://github.com/libimobiledevice/libimobiledevice
diff --git a/tools/Makefile.am b/tools/Makefile.am
index e8ef3ab..47e05b2 100644
--- a/tools/Makefile.am
+++ b/tools/Makefile.am
@@ -27,6 +27,7 @@
 	idevicedebugserverproxy \
 	idevicediagnostics \
 	idevicedebug \
+	idevicedevmodectl \
 	idevicenotificationproxy \
 	idevicecrashreport \
 	idevicesetlocation
@@ -111,6 +112,11 @@
 idevicedebug_LDFLAGS = $(AM_LDFLAGS) $(limd_glue_LIBS)
 idevicedebug_LDADD = $(top_builddir)/src/libimobiledevice-1.0.la $(top_builddir)/common/libinternalcommon.la
 
+idevicedevmodectl_SOURCES = idevicedevmodectl.c
+idevicedevmodectl_CFLAGS = $(AM_CFLAGS) $(limd_glue_CFLAGS)
+idevicedevmodectl_LDFLAGS = $(AM_LDFLAGS) $(limd_glue_LIBS)
+idevicedevmodectl_LDADD = $(top_builddir)/src/libimobiledevice-1.0.la $(top_builddir)/common/libinternalcommon.la
+
 idevicenotificationproxy_SOURCES = idevicenotificationproxy.c
 idevicenotificationproxy_CFLAGS = $(AM_CFLAGS)
 idevicenotificationproxy_LDFLAGS = $(AM_LDFLAGS)
diff --git a/tools/idevicedevmodectl.c b/tools/idevicedevmodectl.c
new file mode 100644
index 0000000..739bc13
--- /dev/null
+++ b/tools/idevicedevmodectl.c
@@ -0,0 +1,451 @@
+/*
+ * idevicedevmodectl.c
+ * List or enable Developer Mode on iOS 16+ devices
+ *
+ * Copyright (c) 2022 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
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#define TOOL_NAME "idevicedevmodectl"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <getopt.h>
+#include <sys/stat.h>
+#include <errno.h>
+#ifndef WIN32
+#include <signal.h>
+#endif
+
+#ifdef WIN32
+#include <windows.h>
+#define __usleep(x) Sleep(x/1000)
+#else
+#include <arpa/inet.h>
+#define __usleep(x) usleep(x)
+#endif
+
+#include <libimobiledevice/libimobiledevice.h>
+#include <libimobiledevice/lockdown.h>
+#include <libimobiledevice/property_list_service.h>
+#include <libimobiledevice-glue/utils.h>
+
+#define AMFI_LOCKDOWN_SERVICE_NAME "com.apple.amfi.lockdown"
+
+static char* udid = NULL;
+static int use_network = 0;
+
+static void print_usage(int argc, char **argv, int is_error)
+{
+	char *name = strrchr(argv[0], '/');
+	fprintf(is_error ? stderr : stdout, "Usage: %s [OPTIONS] COMMAND\n", (name ? name + 1: argv[0]));
+	fprintf(is_error ? stderr : stdout,
+		"\n"
+		"Enable Developer Mode on iOS 16+ devices or print the current status.\n"
+		"\n"
+		"Where COMMAND is one of:\n"
+		"  list          Print the Developer Mode status of all connected devices\n"
+                "                or for a specific one if --udid is given.\n"
+		"  enable        Enable Developer Mode (device will reboot),\n"
+		"                and confirm it after device booted up again.\n"
+		"\n"
+		"  arm           Arm the Developer Mode (device will reboot)\n"
+		"  confirm       Confirm enabling of Developer Mode\n"
+		"  reveal        Reveal the Developer Mode menu on the device\n"
+		"\n"
+		"The following OPTIONS are accepted:\n"
+		"  -u, --udid UDID       target specific device by UDID\n"
+		"  -n, --network         connect to network device\n"
+		"  -d, --debug           enable communication debugging\n"
+		"  -h, --help            print usage information\n"
+		"  -v, --version         print version information\n"
+		"\n"
+		"Homepage:    <" PACKAGE_URL ">\n"
+		"Bug Reports: <" PACKAGE_BUGREPORT ">\n"
+	);
+}
+
+enum {
+	OP_LIST,
+	OP_ENABLE,
+	OP_ARM,
+	OP_CONFIRM,
+	OP_REVEAL,
+	NUM_OPS
+};
+#define DEV_MODE_REVEAL   0
+#define DEV_MODE_ARM      1
+#define DEV_MODE_ENABLE   2
+
+static int get_developer_mode_status(const char* device_udid, int _use_network)
+{
+	idevice_error_t ret;
+	idevice_t device = NULL;
+	lockdownd_client_t lockdown = NULL;
+	lockdownd_error_t lerr = LOCKDOWN_E_UNKNOWN_ERROR;
+	plist_t val = NULL;
+
+	ret = idevice_new_with_options(&device, device_udid, (_use_network) ? IDEVICE_LOOKUP_NETWORK : IDEVICE_LOOKUP_USBMUX);
+	if (ret != IDEVICE_E_SUCCESS) {
+		return -1;
+	}
+
+	if (LOCKDOWN_E_SUCCESS != (lerr = lockdownd_client_new_with_handshake(device, &lockdown, TOOL_NAME))) {
+		idevice_free(device);
+		return -1;
+	}
+
+	lerr = lockdownd_get_value(lockdown, "com.apple.security.mac.amfi", "DeveloperModeStatus", &val);
+	if (lerr != LOCKDOWN_E_SUCCESS) {
+		fprintf(stderr, "ERROR: Could not get DeveloperModeStatus: %s\nPlease note that this feature is only available on iOS 16+.\n", lockdownd_strerror(lerr));
+		lockdownd_client_free(lockdown);
+		idevice_free(device);
+		return -2;
+	}
+
+	uint8_t dev_mode_status = 0;
+	plist_get_bool_val(val, &dev_mode_status);
+	plist_free(val);
+
+	lockdownd_client_free(lockdown);
+	idevice_free(device);
+
+	return dev_mode_status;
+}
+
+static int amfi_service_send_msg(property_list_service_client_t amfi, plist_t msg)
+{
+	int res;
+	property_list_service_error_t perr;
+
+	perr = property_list_service_send_xml_plist(amfi, plist_copy(msg));
+	if (perr != PROPERTY_LIST_SERVICE_E_SUCCESS) {
+		fprintf(stderr, "Could not send request to device: %d\n", perr);
+		res = 2;
+	} else {
+		plist_t reply = NULL;
+		perr = property_list_service_receive_plist(amfi, &reply);
+		if (perr == PROPERTY_LIST_SERVICE_E_SUCCESS) {
+			uint8_t success = 0;
+			plist_t val = plist_dict_get_item(reply, "Error");
+			if (val) {
+				const char* err = plist_get_string_ptr(val, NULL);
+				fprintf(stderr, "Request failed: %s\n", err);
+				if (strstr(err, "passcode")) {
+					res = 2;
+				} else {
+					res = 1;
+				}
+			} else {
+				val = plist_dict_get_item(reply, "success");
+				if (val) {
+					plist_get_bool_val(val, &success);
+				}
+				if (success) {
+					res = 0;
+				} else {
+					res = 1;
+				}
+			}
+		} else {
+			fprintf(stderr, "Could not receive reply from device: %d\n", perr);
+			res = 2;
+		}
+		plist_free(reply);
+	}
+	return res;
+}
+
+static int amfi_send_action(idevice_t device, unsigned int action)
+{
+	lockdownd_client_t lockdown = NULL;
+	lockdownd_service_descriptor_t service = NULL;
+	lockdownd_error_t lerr;
+
+	if (LOCKDOWN_E_SUCCESS != (lerr = lockdownd_client_new_with_handshake(device, &lockdown, TOOL_NAME))) {
+		fprintf(stderr, "ERROR: Could not connect to lockdownd, error code %d\n", lerr);
+		return 1;
+	}
+
+	lerr = lockdownd_start_service(lockdown, AMFI_LOCKDOWN_SERVICE_NAME, &service);
+	if (lerr != LOCKDOWN_E_SUCCESS) {
+		fprintf(stderr, "Could not start service %s: %s\nPlease note that this feature is only available on iOS 16+.\n", AMFI_LOCKDOWN_SERVICE_NAME, lockdownd_strerror(lerr));
+		lockdownd_client_free(lockdown);
+		return 1;
+	}
+	lockdownd_client_free(lockdown);
+	lockdown = NULL;
+
+	property_list_service_client_t amfi = NULL;
+	if (property_list_service_client_new(device, service, &amfi) != PROPERTY_LIST_SERVICE_E_SUCCESS) {
+		fprintf(stderr, "Could not connect to %s on device\n", AMFI_LOCKDOWN_SERVICE_NAME);
+		if (service)
+			lockdownd_service_descriptor_free(service);
+		idevice_free(device);
+		return 1;
+	}
+	lockdownd_service_descriptor_free(service);
+
+	plist_t dict = plist_new_dict();
+	plist_dict_set_item(dict, "action", plist_new_uint(action));
+
+	int result = amfi_service_send_msg(amfi, dict);
+	plist_free(dict);
+
+	property_list_service_client_free(amfi);
+	amfi = NULL;
+
+	return result;
+}
+
+static int device_connected = 0;
+
+static void device_event_cb(const idevice_event_t* event, void* userdata)
+{
+	if (use_network && event->conn_type != CONNECTION_NETWORK) {
+		return;
+	}
+	if (!use_network && event->conn_type != CONNECTION_USBMUXD) {
+		return;
+	}
+	if (event->event == IDEVICE_DEVICE_ADD) {
+		if (!udid) {
+			udid = strdup(event->udid);
+		}
+		if (strcmp(udid, event->udid) == 0) {
+			device_connected = 1;
+		}
+	} else if (event->event == IDEVICE_DEVICE_REMOVE) {
+		if (strcmp(udid, event->udid) == 0) {
+			device_connected = 0;
+		}
+	}
+}
+
+
+#define WAIT_INTERVAL 200000
+#define WAIT_MAX(x) (x * (1000000 / WAIT_INTERVAL))
+#define WAIT_FOR(cond, timeout) { int __repeat = WAIT_MAX(timeout); while (!(cond) && __repeat-- > 0) { __usleep(WAIT_INTERVAL); } }
+
+int main(int argc, char *argv[])
+{
+	idevice_t device = NULL;
+	idevice_error_t ret = IDEVICE_E_UNKNOWN_ERROR;
+	lockdownd_client_t lockdown = NULL;
+	lockdownd_error_t lerr = LOCKDOWN_E_UNKNOWN_ERROR;
+	int res = 0;
+	int i;
+	int op = -1;
+	plist_t val = NULL;
+
+	int c = 0;
+	const struct option longopts[] = {
+		{ "debug", no_argument, NULL, 'd' },
+		{ "help", no_argument, NULL, 'h' },
+		{ "udid", required_argument, NULL, 'u' },
+		{ "network", no_argument, NULL, 'n' },
+		{ "version", no_argument, NULL, 'v' },
+		{ NULL, 0, NULL, 0}
+	};
+
+#ifndef WIN32
+	signal(SIGPIPE, SIG_IGN);
+#endif
+	/* parse cmdline args */
+	while ((c = getopt_long(argc, argv, "dhu:nv", longopts, NULL)) != -1) {
+		switch (c) {
+		case 'd':
+			idevice_set_debug_level(1);
+			break;
+		case 'u':
+			if (!*optarg) {
+				fprintf(stderr, "ERROR: UDID argument must not be empty!\n");
+				print_usage(argc, argv, 1);
+				return 2;
+			}
+			udid = optarg;
+			break;
+		case 'n':
+			use_network = 1;
+			break;
+		case 'h':
+			print_usage(argc, argv, 0);
+			return 0;
+		case 'v':
+			printf("%s %s\n", TOOL_NAME, PACKAGE_VERSION);
+			return 0;
+		default:
+			print_usage(argc, argv, 1);
+			return 2;
+		}
+	}
+	argc -= optind;
+	argv += optind;
+
+	if (!argv[0]) {
+		fprintf(stderr, "ERROR: Missing command.\n");
+		print_usage(argc+optind, argv-optind, 1);
+		return 2;
+	}
+
+	i = 0;
+	if (!strcmp(argv[i], "list")) {
+		op = OP_LIST;
+	}
+	else if (!strcmp(argv[i], "enable")) {
+		op = OP_ENABLE;
+	}
+	else if (!strcmp(argv[i], "arm")) {
+		op = OP_ARM;
+	}
+	else if (!strcmp(argv[i], "confirm")) {
+		op = OP_CONFIRM;
+	}
+	else if (!strcmp(argv[i], "reveal")) {
+		op = OP_REVEAL;
+	}
+
+	if ((op == -1) || (op >= NUM_OPS)) {
+		fprintf(stderr, "ERROR: Unsupported command '%s'\n", argv[i]);
+		print_usage(argc+optind, argv-optind, 1);
+		return 2;
+	}
+
+	if (op == OP_LIST) {
+		idevice_info_t *dev_list = NULL;
+
+		if (idevice_get_device_list_extended(&dev_list, &i) < 0) {
+			fprintf(stderr, "ERROR: Unable to retrieve device list!\n");
+			return -1;
+		}
+		if (i > 0) {
+			printf("%-40s    %s\n", "Device", "DeveloperMode");
+		}
+		for (i = 0; dev_list[i] != NULL; i++) {
+			if (dev_list[i]->conn_type == CONNECTION_USBMUXD && use_network) continue;
+			if (dev_list[i]->conn_type == CONNECTION_NETWORK && !use_network) continue;
+			if (udid && (strcmp(dev_list[i]->udid, udid) != 0)) continue;
+			int mode = get_developer_mode_status(dev_list[i]->udid, use_network);
+			const char *mode_str = "N/A";
+			if (mode == 1) {
+				mode_str = "enabled";
+			} else if (mode == 0) {
+				mode_str = "disabled";
+			}
+			printf("%-40s    %s\n", dev_list[i]->udid, mode_str);
+		}
+		idevice_device_list_extended_free(dev_list);
+
+		return 0;
+	}
+
+	idevice_subscription_context_t context = NULL;
+	idevice_events_subscribe(&context, device_event_cb, NULL);
+
+	WAIT_FOR(device_connected, 10);
+
+	ret = idevice_new_with_options(&device, udid, (use_network) ? IDEVICE_LOOKUP_NETWORK : IDEVICE_LOOKUP_USBMUX);
+	if (ret != IDEVICE_E_SUCCESS) {
+		if (udid) {
+			printf("No device found with udid %s.\n", udid);
+		} else {
+			printf("No device found.\n");
+		}
+		return 1;
+	}
+
+	if (!udid) {
+		idevice_get_udid(device, &udid);
+	}
+
+	if (LOCKDOWN_E_SUCCESS != (lerr = lockdownd_client_new_with_handshake(device, &lockdown, TOOL_NAME))) {
+		fprintf(stderr, "ERROR: Could not connect to lockdownd, error code %d\n", lerr);
+		idevice_free(device);
+		return 1;
+	}
+
+	lerr = lockdownd_get_value(lockdown, "com.apple.security.mac.amfi", "DeveloperModeStatus", &val);
+	lockdownd_client_free(lockdown);
+	lockdown = NULL;
+	if (lerr != LOCKDOWN_E_SUCCESS) {
+		fprintf(stderr, "ERROR: Could not get DeveloperModeStatus: %s\nPlease note that this feature is only available on iOS 16+.\n", lockdownd_strerror(lerr));
+		idevice_free(device);
+		return 1;
+	}
+
+	uint8_t dev_mode_status = 0;
+	plist_get_bool_val(val, &dev_mode_status);
+
+	if ((op == OP_ENABLE || op == OP_ARM) && dev_mode_status) {
+		if (dev_mode_status) {
+			printf("DeveloperMode is already enabled.\n");
+			return 0;
+		}
+		res = 0;
+	} else {	
+		if (op == OP_ENABLE || op == OP_ARM) {
+			res = amfi_send_action(device, DEV_MODE_ARM);
+			if (res == 0) {
+				if (op == OP_ARM) {
+					printf("%s: Developer Mode armed, device will reboot now.\n", udid);
+				} else {
+					printf("%s: Developer Mode armed, waiting for reboot...\n", udid);
+
+					// waiting for device to disconnect...
+					WAIT_FOR(!device_connected, 20);
+
+					// waiting for device to reconnect...
+					WAIT_FOR(device_connected, 60);
+
+					res = amfi_send_action(device, DEV_MODE_ENABLE);
+					if (res == 0) {
+						printf("%s: Developer Mode successfully enabled.\n", udid);
+					} else {
+						printf("%s: Failed to enable developer mode (%d)\n", udid, res);
+					}
+				}
+			} else if (res == 2) {
+				amfi_send_action(device, DEV_MODE_REVEAL);
+				printf("%s: Developer Mode could not be enabled because the device has a passcode set. You have to enable it on the device itself under Settings -> Privacy & Security -> Developer Mode.\n", udid);
+			} else {
+				printf("%s: Failed to arm Developer Mode (%d)\n", udid, res);
+			}
+		} else if (op == OP_CONFIRM) {
+			res = amfi_send_action(device, DEV_MODE_ENABLE);
+			if (res == 0) {
+				printf("%s: Developer Mode successfully enabled.\n", udid);
+			} else {
+				printf("%s: Failed to enable Developer Mode (%d)\n", udid, res);
+			}
+		} else if (op == OP_REVEAL) {
+			res = amfi_send_action(device, DEV_MODE_REVEAL);
+			if (res == 0) {
+				printf("%s: Developer Mode menu revealed successfully.\n", udid);
+			} else {
+				printf("%s: Failed to reveal Developer Mode menu (%d)\n", udid, res);
+			}
+		}
+	}
+
+	idevice_free(device);
+
+	return res;
+}