From 7f781eb61215c84bd0f79838fbcb5efd66121677 Mon Sep 17 00:00:00 2001 From: Nikias Bassen Date: Sat, 30 Dec 2023 01:27:42 +0100 Subject: [PATCH] tools: Add afcclient utility --- configure.ac | 12 + docs/afcclient.1 | 76 +++ tools/Makefile.am | 12 +- tools/afcclient.c | 1294 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 1393 insertions(+), 1 deletion(-) create mode 100644 docs/afcclient.1 create mode 100644 tools/afcclient.c diff --git a/configure.ac b/configure.ac index 8f24522d5..4f3f901d9 100644 --- a/configure.ac +++ b/configure.ac @@ -43,6 +43,18 @@ LT_INIT PKG_CHECK_MODULES(libusbmuxd, libusbmuxd-2.0 >= $LIBUSBMUXD_VERSION) PKG_CHECK_MODULES(libplist, libplist-2.0 >= $LIBPLIST_VERSION) PKG_CHECK_MODULES(limd_glue, libimobiledevice-glue-1.0 >= $LIMD_GLUE_VERSION) +AC_ARG_WITH([readline], + [AS_HELP_STRING([--without-readline], + [build without support for libreadline (default is yes)])], + [check_libreadline=false], + [check_libreadline=true]) +if test "$check_libreadline" = "true"; then + PKG_CHECK_MODULES(readline, readline >= 1.0, have_readline=yes, have_readline=no) + if test "x$have_readline" = "xyes"; then + AC_DEFINE(HAVE_READLINE, 1, [Define if readline library is available]) + fi +fi +AM_CONDITIONAL([HAVE_READLINE],[test "x$have_readline" = "xyes"]) # Checks for header files. AC_CHECK_HEADERS([stdint.h stdlib.h string.h sys/time.h]) diff --git a/docs/afcclient.1 b/docs/afcclient.1 new file mode 100644 index 000000000..ca7cb8607 --- /dev/null +++ b/docs/afcclient.1 @@ -0,0 +1,76 @@ +.TH "afcclient" 1 +.SH NAME +afcclient \- Interact with AFC/HouseArrest service on a connected device. +.SH SYNOPSIS +.B afcclient +[OPTIONS] [COMMAND ...] + +.SH DESCRIPTION + +Utility to interact with AFC/HouseArrest service. This allows access to parts +of the filesystem on an iOS device. + +\f[B]afcclient\f[] can be used interactively with a command prompt, or run a single command and exit. + +.SH COMMANDS +.TP +.B devinfo +print device information +.TP +.B info PATH +print file attributes of file at PATH +.TP +.B ls PATH +print directory contents of PATH +.TP +.B mv OLD NEW +rename file OLD to NEW +.TP +.B mkdir PATH +create directory at PATH +.TP +.B ln [-s] FILE [LINK] +Create a (symbolic) link to file named LINKNAME. \f[B]NOTE: This feature has been disabled in newer versions of iOS\f[]. +.TP +.B rm PATH +remove item at PATH +.TP +.B get PATH [LOCALPATH] +transfer file at PATH from device to LOCALPATH, or current directory if omitted. If LOCALPATH is a directory, the file will be stored inside the directory. +\f[B]WARNING\f[]: Existing files will be overwritten! +.TP +.B put LOCALPATH [PATH] +transfer local file at LOCALPATH to device at PATH, or current directory if omitted. If PATH is a directory, the file will be stored inside the directory. +\f[B]WARNING\f[]: Existing files will be overwritten! +.TP + +.SH OPTIONS +.TP +.B \-u, \-\-udid UDID +target specific device by UDID +.TP +.B \-n, \-\-network +connect to network device (not recommended, since the connection might be terminated at any time) +.TP +.B \--container +Access the app container directory of the app with given \f[B]appid\f[] +.TP +.B \--documents +Access the Documents directory of the app with given \f[B]appid\f[] +.TP +.B \-h, \-\-help +Prints usage information +.TP +.B \-d, \-\-debug +Enable communication debugging +.TP +.B \-v, \-\-version +Prints version information + +.SH AUTHOR +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 bd93631eb..4cac1fc80 100644 --- a/tools/Makefile.am +++ b/tools/Makefile.am @@ -31,7 +31,8 @@ bin_PROGRAMS = \ idevicedevmodectl \ idevicenotificationproxy \ idevicecrashreport \ - idevicesetlocation + idevicesetlocation \ + afcclient idevicebtlogger_SOURCES = idevicebtlogger.c iidevicebtlogger_CFLAGS = $(AM_CFLAGS) @@ -132,3 +133,12 @@ idevicesetlocation_SOURCES = idevicesetlocation.c idevicesetlocation_CFLAGS = $(AM_CFLAGS) idevicesetlocation_LDFLAGS = $(AM_LDFLAGS) idevicesetlocation_LDADD = $(top_builddir)/src/libimobiledevice-1.0.la + +afcclient_SOURCES = afcclient.c +afcclient_CFLAGS = $(AM_CFLAGS) +afcclient_LDFLAGS = $(AM_LDFLAGS) +if HAVE_READLINE + afcclient_CFLAGS += $(readline_CFLAGS) + afcclient_LDFLAGS += $(readline_LIBS) +endif +afcclient_LDADD = $(top_builddir)/src/libimobiledevice-1.0.la $(limd_glue_LIBS) diff --git a/tools/afcclient.c b/tools/afcclient.c new file mode 100644 index 000000000..81dc5c5aa --- /dev/null +++ b/tools/afcclient.c @@ -0,0 +1,1294 @@ +/* + * afcclient.c + * Utility to interact with AFC/HoustArrest service on the device + * + * Inspired by https://github.com/emonti/afcclient + * But entirely rewritten from scratch. + * + * Copyright (c) 2023 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 +#endif + +#define TOOL_NAME "afcclient" + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef WIN32 +#include +#include +#include +#define sleep(x) Sleep(x*1000) +#define S_IFMT 0170000 /* [XSI] type of file mask */ +#define S_IFIFO 0010000 /* [XSI] named pipe (fifo) */ +#define S_IFCHR 0020000 /* [XSI] character special */ +#define S_IFBLK 0060000 /* [XSI] block special */ +#define S_IFLNK 0120000 /* [XSI] symbolic link */ +#define S_IFSOCK 0140000 /* [XSI] socket */ +#define S_ISBLK(m) (((m) & S_IFMT) == S_IFBLK) /* block special */ +#define S_ISCHR(m) (((m) & S_IFMT) == S_IFCHR) /* char special */ +#define S_ISFIFO(m) (((m) & S_IFMT) == S_IFIFO) /* fifo or socket */ +#define S_ISLNK(m) (((m) & S_IFMT) == S_IFLNK) /* symbolic link */ +#define S_ISSOCK(m) (((m) & S_IFMT) == S_IFSOCK) /* socket */ +#else +#include +#include +#endif + +#ifdef HAVE_READLINE +#include +#include +#endif + +#include +#include +#include +#include +#include + +#include + +#undef st_mtime +#undef st_birthtime +struct afc_file_stat { + uint16_t st_mode; + uint16_t st_nlink; + uint64_t st_size; + uint64_t st_mtime; + uint64_t st_birthtime; + uint32_t st_blocks; +}; + +static char* udid = NULL; +static int connected = 0; +static int use_network = 0; +static idevice_subscription_context_t context = NULL; +static char* curdir = NULL; +static size_t curdir_len = 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]\n", (name ? name + 1: argv[0])); + fprintf(is_error ? stderr : stdout, + "\n" + "Interact with AFC/HouseArrest service on a connected device.\n" + "\n" + "OPTIONS:\n" + " -u, --udid UDID target specific device by UDID\n" + " -n, --network connect to network device (not recommended!)\n" + " --container Access container of given app\n" + " --documents Access Documents directory of given app\n" + " -h, --help prints usage information\n" \ + " -d, --debug enable communication debugging\n" \ + " -v, --version prints version information\n" \ + "\n" + ); + fprintf(is_error ? stderr : stdout, + "\n" \ + "Homepage: <" PACKAGE_URL ">\n" + "Bug Reports: <" PACKAGE_BUGREPORT ">\n" + ); +} + +#define OPT_DOCUMENTS 1 +#define OPT_CONTAINER 2 + +int stop_requested = 0; + +static void handle_signal(int sig) +{ + stop_requested++; +#ifdef WIN32 + GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0); +#else + kill(getpid(), SIGINT); +#endif +} + +static void handle_help(afc_client_t afc, int argc, char** argv) +{ + printf("Available commands:\n"); + printf("help - print list of available commands\n"); + printf("devinfo - print device information\n"); + printf("info PATH - print file attributes of file at PATH\n"); + printf("ls [-l] PATH - print directory contents of PATH\n"); + printf("mv OLD NEW - rename file OLD to NEW\n"); + printf("mkdir PATH - create directory at PATH\n"); + printf("ln [-s] FILE [LINK] - create a (symbolic) link to file named LINKNAME\n"); + printf(" NOTE: This feature has been disabled in newer versions of iOS.\n"); + printf("rm PATH - remove item at PATH\n"); + printf("get PATH [LOCALPATH] - transfer file at PATH from device to LOCALPATH\n"); + printf("put LOCALPATH [PATH] - transfer local file at LOCALPATH to device at PATH\n"); + printf("\n"); +} + +static const char* path_get_basename(const char* path) +{ + const char *p = strrchr(path, '/'); + return p ? p + 1 : path; +} + +static int timeval_subtract(struct timeval *result, struct timeval *x, struct timeval *y) +{ + /* Perform the carry for the later subtraction by updating y. */ + if (x->tv_usec < y->tv_usec) { + int nsec = (y->tv_usec - x->tv_usec) / 1000000 + 1; + y->tv_usec -= 1000000 * nsec; + y->tv_sec += nsec; + } + if (x->tv_usec - y->tv_usec > 1000000) { + int nsec = (x->tv_usec - y->tv_usec) / 1000000; + y->tv_usec += 1000000 * nsec; + y->tv_sec -= nsec; + } + /* Compute the time remaining to wait. + tv_usec is certainly positive. */ + result->tv_sec = x->tv_sec - y->tv_sec; + result->tv_usec = x->tv_usec - y->tv_usec; + /* Return 1 if result is negative. */ + return x->tv_sec < y->tv_sec; +} + +struct str_item { + size_t len; + char* str; +}; + +static char* get_absolute_path(const char *path) +{ + if (*path == '/') { + return strdup(path); + } else { + size_t len = curdir_len + 1 + strlen(path) + 1; + char* result = (char*)malloc(len); + if (!strcmp(curdir, "/")) { + snprintf(result, len, "/%s", path); + } else { + snprintf(result, len, "%s/%s", curdir, path); + } + return result; + } +} + +static char* get_realpath(const char* path) +{ + if (!path) return NULL; + + int is_absolute = 0; + if (*path == '/') { + is_absolute = 1; + } + + const char* p = path; + if (is_absolute) { + while (*p == '/') p++; + } + if (*p == '\0') { + return strdup("/"); + } + + int c_count = 1; + const char* start = p; + const char* end = p; + struct str_item* comps = NULL; + + while (*p) { + if (*p == '/') { + p++; + end = p-1; + while (*p == '/') p++; + if (*p == '\0') break; + struct str_item* newcomps = (struct str_item*)realloc(comps, sizeof(struct str_item)*c_count); + if (!newcomps) { + free(comps); + printf("%s: out of memory?!\n", __func__); + return NULL; + } + comps = newcomps; + char *comp = (char*)malloc(end-start+1); + strncpy(comp, start, end-start); + comp[end-start] = '\0'; + comps[c_count-1].len = end-start; + comps[c_count-1].str = comp; + c_count++; + start = p; + end = p; + } + p++; + } + if (p > start) { + if (start == end) { + end = p; + } + struct str_item* newcomps = (struct str_item*)realloc(comps, sizeof(struct str_item)*c_count); + if (!newcomps) { + free(comps); + printf("%s: out of memory?!\n", __func__); + return NULL; + } + comps = newcomps; + char *comp = (char*)malloc(end-start+1); + strncpy(comp, start, end-start); + comp[end-start] = '\0'; + comps[c_count-1].len = end-start; + comps[c_count-1].str = comp; + } + + struct str_item* comps_final = (struct str_item*)malloc(sizeof(struct str_item)*(c_count+1)); + int o = 1; + if (is_absolute) { + comps_final[0].len = 1; + comps_final[0].str = (char*)"/"; + } else { + comps_final[0].len = curdir_len; + comps_final[0].str = curdir; + } + size_t o_len = comps_final[0].len; + + for (int i = 0; i < c_count; i++) { + if (!strcmp(comps[i].str, "..")) { + o--; + continue; + } else if (!strcmp(comps[i].str, ".")) { + continue; + } + o_len += comps[i].len; + comps_final[o].str = comps[i].str; + comps_final[o].len = comps[i].len; + o++; + } + + o_len += o; + char* result = (char*)malloc(o_len); + char* presult = result; + for (int i = 0; i < o; i++) { + if (i > 0 && strcmp(comps_final[i-1].str, "/") != 0) { + *presult = '/'; + presult++; + } + strncpy(presult, comps_final[i].str, comps_final[i].len); + presult+=comps_final[i].len; + *presult = '\0'; + } + if (presult == result) { + *presult = '/'; + presult++; + *presult = 0; + } + + for (int i = 0; i < c_count; i++) { + free(comps[i].str); + } + free(comps); + free(comps_final); + + return result; +} + +static void handle_devinfo(afc_client_t afc, int argc, char** argv) +{ + char **info = NULL; + afc_error_t err = afc_get_device_info(afc, &info); + if (err == AFC_E_SUCCESS && info) { + int i; + for (i = 0; info[i]; i += 2) { + printf("%s: %s\n", info[i], info[i+1]); + } + } else { + printf("Error: Failed to get device info: %d\n", err); + } + afc_dictionary_free(info); +} + +static int get_file_info_stat(afc_client_t afc, const char* path, struct afc_file_stat *stbuf) +{ + char **info = NULL; + afc_error_t ret = afc_get_file_info(afc, path, &info); + memset(stbuf, 0, sizeof(struct afc_file_stat)); + if (ret != AFC_E_SUCCESS) { + return -1; + } else if (!info) { + return -1; + } else { + // get file attributes from info list + int i; + for (i = 0; info[i]; i += 2) { + if (!strcmp(info[i], "st_size")) { + stbuf->st_size = atoll(info[i+1]); + } else if (!strcmp(info[i], "st_blocks")) { + stbuf->st_blocks = atoi(info[i+1]); + } else if (!strcmp(info[i], "st_ifmt")) { + if (!strcmp(info[i+1], "S_IFREG")) { + stbuf->st_mode = S_IFREG; + } else if (!strcmp(info[i+1], "S_IFDIR")) { + stbuf->st_mode = S_IFDIR; + } else if (!strcmp(info[i+1], "S_IFLNK")) { + stbuf->st_mode = S_IFLNK; + } else if (!strcmp(info[i+1], "S_IFBLK")) { + stbuf->st_mode = S_IFBLK; + } else if (!strcmp(info[i+1], "S_IFCHR")) { + stbuf->st_mode = S_IFCHR; + } else if (!strcmp(info[i+1], "S_IFIFO")) { + stbuf->st_mode = S_IFIFO; + } else if (!strcmp(info[i+1], "S_IFSOCK")) { + stbuf->st_mode = S_IFSOCK; + } + } else if (!strcmp(info[i], "st_nlink")) { + stbuf->st_nlink = atoi(info[i+1]); + } else if (!strcmp(info[i], "st_mtime")) { + stbuf->st_mtime = (time_t)(atoll(info[i+1]) / 1000000000); + } else if (!strcmp(info[i], "st_birthtime")) { /* available on iOS 7+ */ + stbuf->st_birthtime = (time_t)(atoll(info[i+1]) / 1000000000); + } + } + afc_dictionary_free(info); + } + return 0; +} + +static void handle_file_info(afc_client_t afc, int argc, char** argv) +{ + if (argc < 1) { + printf("Error: Missing PATH.\n"); + return; + } + + char **info = NULL; + char* abspath = get_absolute_path(argv[0]); + if (!abspath) { + printf("Error: Invalid argument\n"); + return; + } + afc_error_t err = afc_get_file_info(afc, abspath, &info); + if (err == AFC_E_SUCCESS && info) { + int i; + for (i = 0; info[i]; i += 2) { + printf("%s: %s\n", info[i], info[i+1]); + } + } else { + printf("Error: Failed to get file info for %s: %d\n", argv[0], err); + } + afc_dictionary_free(info); + free(abspath); +} + +static void print_file_info(afc_client_t afc, const char* path, int list_verbose) +{ + struct afc_file_stat st; + get_file_info_stat(afc, path, &st); + if (list_verbose) { + char timebuf[64]; + time_t t = st.st_mtime; + if (S_ISDIR(st.st_mode)) { + printf("drwxr-xr-x"); + } else if (S_ISLNK(st.st_mode)) { + printf("lrwxrwxrwx"); + } else { + if (S_ISFIFO(st.st_mode)) { + printf("f"); + } else if (S_ISBLK(st.st_mode)) { + printf("b"); + } else if (S_ISCHR(st.st_mode)) { + printf("c"); + } else if (S_ISSOCK(st.st_mode)) { + printf("s"); + } else { + printf("-"); + } + printf("rw-r--r--"); + } + printf(" "); + printf("%4d", st.st_nlink); + printf(" "); + printf("mobile"); + printf(" "); + printf("mobile"); + printf(" "); + printf("%10lld", (long long)st.st_size); + printf(" "); +#ifdef WIN32 + strftime(timebuf, 64, "%d %b %Y %H:%M:%S", localtime(&t)); +#else + strftime(timebuf, 64, "%d %h %Y %H:%M:%S", localtime(&t)); +#endif + printf("%s", timebuf); + printf(" "); + } + if (S_ISDIR(st.st_mode)) { + cprintf(FG_CYAN); + } else if (S_ISLNK(st.st_mode)) { + cprintf(FG_MAGENTA); + } else if (S_ISREG(st.st_mode)) { + cprintf(FG_DEFAULT); + } else { + cprintf(FG_YELLOW); + } + cprintf("%s" COLOR_RESET "\n", path_get_basename(path)); +} + +static void handle_list(afc_client_t afc, int argc, char** argv) +{ + const char* path = NULL; + int list_verbose = 0; + if (argc < 1) { + path = curdir; + } else { + if (!strcmp(argv[0], "-l")) { + list_verbose = 1; + if (argc == 2) { + path = argv[1]; + } else { + path = curdir; + } + } else { + path = argv[0]; + } + } + char* abspath = get_absolute_path(path); + if (!abspath) { + printf("Error: Invalid argument\n"); + return; + } + int abspath_is_root = strcmp(abspath, "/") == 0; + size_t abspath_len = (abspath_is_root) ? 0 : strlen(abspath); + char** entries = NULL; + afc_error_t err = afc_read_directory(afc, abspath, &entries); + if (err == AFC_E_READ_ERROR) { + print_file_info(afc, abspath, list_verbose); + return; + } else if (err != AFC_E_SUCCESS) { + printf("Error: Failed to list '%s': %d\n", path, err); + free(abspath); + return; + } + + char** p = entries; + while (p && *p) { + if (strcmp(".", *p) == 0 || strcmp("..", *p) == 0) { + p++; + continue; + } + size_t len = abspath_len + 1 + strlen(*p) + 1; + char* testpath = (char*)malloc(len); + if (abspath_is_root) { + snprintf(testpath, len, "/%s", *p); + } else { + snprintf(testpath, len, "%s/%s", abspath, *p); + } + print_file_info(afc, testpath, list_verbose); + free(testpath); + p++; + } + afc_dictionary_free(entries); + free(abspath); +} + +static void handle_rename(afc_client_t afc, int argc, char** argv) +{ + if (argc != 2) { + printf("Error: Invalid number of arguments\n"); + return; + } + char* srcpath = get_absolute_path(argv[0]); + if (!srcpath) { + printf("Error: Invalid argument\n"); + return; + } + char* dstpath = get_absolute_path(argv[1]); + if (!dstpath) { + free(srcpath); + printf("Error: Invalid argument\n"); + return; + } + afc_error_t err = afc_rename_path(afc, srcpath, dstpath); + if (err != AFC_E_SUCCESS) { + printf("Error: Failed to rename '%s' -> '%s': %d\n", argv[0], argv[1], err); + } + free(srcpath); + free(dstpath); +} + +static void handle_mkdir(afc_client_t afc, int argc, char** argv) +{ + for (int i = 0; i < argc; i++) { + char* abspath = get_absolute_path(argv[i]); + if (!abspath) { + printf("Error: Invalid argument '%s'\n", argv[i]); + continue; + } + afc_error_t err = afc_make_directory(afc, abspath); + if (err != AFC_E_SUCCESS) { + printf("Error: Failed to create directory '%s': %d\n", argv[i], err); + } + free(abspath); + } +} + +static void handle_link(afc_client_t afc, int argc, char** argv) +{ + if (argc < 2) { + printf("Error: Invalid number of arguments\n"); + return; + } + afc_link_type_t link_type = AFC_HARDLINK; + if (!strcmp(argv[0], "-s")) { + argc--; + argv++; + link_type = AFC_SYMLINK; + } + if (argc < 1 || argc > 2) { + printf("Error: Invalid number of arguments\n"); + return; + } + const char *link_name = (argc == 1) ? path_get_basename(argv[0]) : argv[1]; + char* abs_link_name = get_absolute_path(link_name); + if (!abs_link_name) { + printf("Error: Invalid argument\n"); + return; + } + afc_error_t err = afc_make_link(afc, link_type, argv[0], link_name); + if (err != AFC_E_SUCCESS) { + printf("Error: Failed to create %s link for '%s' at '%s': %d\n", (link_type == AFC_HARDLINK) ? "hard" : "symbolic", argv[0], link_name, err); + } +} + +static void handle_remove(afc_client_t afc, int argc, char** argv) +{ + for (int i = 0; i < argc; i++) { + char* abspath = get_absolute_path(argv[i]); + if (!abspath) { + printf("Error: Invalid argument '%s'\n", argv[i]); + continue; + } + afc_error_t err = afc_remove_path(afc, abspath); + if (err != AFC_E_SUCCESS) { + printf("Error: Failed to remove '%s': %d\n", argv[i], err); + } + free(abspath); + } +} + +static void handle_get(afc_client_t afc, int argc, char** argv) +{ + if (argc < 1 || argc > 2) { + printf("Error: Invalid number of arguments\n"); + return; + } + char *srcpath = NULL; + char* dstpath = NULL; + if (argc == 1) { + srcpath = get_absolute_path(argv[0]); + dstpath = strdup(path_get_basename(argv[0])); + } else { + srcpath = get_absolute_path(argv[0]); + dstpath = strdup(argv[1]); + } + + char **info = NULL; + uint64_t file_size = 0; + afc_get_file_info(afc, srcpath, &info); + if (info) { + char **p = info; + while (p && *p) { + if (!strcmp(*p, "st_size")) { + p++; + file_size = (uint64_t)strtoull(*p, NULL, 10); + break; + } + p++; + } + } + uint64_t fh = 0; + afc_error_t err = afc_file_open(afc, srcpath, AFC_FOPEN_RDONLY, &fh); + if (err != AFC_E_SUCCESS) { + free(srcpath); + free(dstpath); + printf("Error: Failed to open file '%s': %d\n", argv[0], err); + return; + } + FILE *f = fopen(dstpath, "wb"); + if (!f && errno == EISDIR) { + const char* basen = path_get_basename(argv[0]); + size_t len = strlen(dstpath) + 1 + strlen(basen) + 1; + char* newdst = (char*)malloc(len); + snprintf(newdst, len, "%s/%s", dstpath, basen); + f = fopen(newdst, "wb"); + free(newdst); + } + if (f) { + struct timeval t1; + struct timeval t2; + struct timeval tdiff; + size_t bufsize = 0x100000; + char* buf = malloc(bufsize); + size_t total = 0; + int progress = 0; + int lastprog = 0; + if (file_size > 0x400000) { + progress = 1; + gettimeofday(&t1, NULL); + } + while (err == AFC_E_SUCCESS) { + uint32_t bytes_read = 0; + size_t chunk = 0; + err = afc_file_read(afc, fh, buf, bufsize, &bytes_read); + if (bytes_read == 0) { + break; + } + while (chunk < bytes_read) { + size_t wr = fwrite(buf+chunk, 1, bytes_read-chunk, f); + if (wr == 0) { + if (progress) { + printf("\n"); + } + printf("Error: Failed to write to local file\n"); + break; + } + chunk += wr; + } + total += chunk; + if (progress) { + int prog = (int)((double)total / (double)file_size * 100.0f); + if (prog > lastprog) { + gettimeofday(&t2, NULL); + timeval_subtract(&tdiff, &t2, &t1); + double time_in_sec = (double)tdiff.tv_sec + (double)tdiff.tv_usec/1000000; + printf("\r%d%% (%0.1f MB/s) ", prog, (double)total/1048576.0f / time_in_sec); + fflush(stdout); + lastprog = prog; + } + } + } + if (progress) { + printf("\n"); + } + if (err != AFC_E_SUCCESS) { + printf("Error: Failed to read from file '%s': %d\n", argv[0], err); + } + free(buf); + fclose(f); + } else { + printf("Error: Failed to open local file '%s': %s\n", dstpath, strerror(errno)); + } + afc_file_close(afc, fh); + free(srcpath); +} + +static void handle_put(afc_client_t afc, int argc, char** argv) +{ + if (argc < 1 || argc > 2) { + printf("Error: Invalid number of arguments\n"); + return; + } + + char *dstpath = NULL; + if (argc == 1) { + dstpath = get_absolute_path(path_get_basename(argv[0])); + } else { + dstpath = get_absolute_path(argv[1]); + } + + uint64_t fh = 0; + FILE *f = fopen(argv[0], "rb"); + if (f) { + afc_error_t err = afc_file_open(afc, dstpath, AFC_FOPEN_RW, &fh); + if (err == AFC_E_OBJECT_IS_DIR) { + const char* basen = path_get_basename(argv[0]); + size_t len = strlen(dstpath) + 1 + strlen(basen) + 1; + char* newdst = (char*)malloc(len); + snprintf(newdst, len, "%s/%s", dstpath, basen); + free(dstpath); + dstpath = get_absolute_path(newdst); + free(newdst); + err = afc_file_open(afc, dstpath, AFC_FOPEN_RW, &fh); + } + if (err != AFC_E_SUCCESS) { + printf("Error: Failed to open file '%s' on device: %d\n", argv[1], err); + } else { + struct timeval t1; + struct timeval t2; + struct timeval tdiff; + struct stat fst; + int progress = 0; + size_t bufsize = 0x100000; + char* buf = malloc(bufsize); + + fstat(fileno(f), &fst); + if (fst.st_size >= 0x400000) { + progress = 1; + gettimeofday(&t1, NULL); + } + size_t total = 0; + int lastprog = 0; + while (err == AFC_E_SUCCESS) { + uint32_t bytes_read = fread(buf, 1, bufsize, f); + if (bytes_read == 0) { + if (!feof(f)) { + if (progress) { + printf("\n"); + } + printf("Error: Failed to read from local file\n"); + } + break; + } + uint32_t chunk = 0; + while (chunk < bytes_read) { + uint32_t bytes_written = 0; + err = afc_file_write(afc, fh, buf+chunk, bytes_read-chunk, &bytes_written); + if (err != AFC_E_SUCCESS) { + if (progress) { + printf("\n"); + } + printf("Error: Failed to write to device file\n"); + break; + } + chunk += bytes_written; + } + total += chunk; + if (progress) { + int prog = (int)((double)total / (double)fst.st_size * 100.0f); + if (prog > lastprog) { + gettimeofday(&t2, NULL); + timeval_subtract(&tdiff, &t2, &t1); + double time_in_sec = (double)tdiff.tv_sec + (double)tdiff.tv_usec/1000000; + printf("\r%d%% (%0.1f MB/s) ", prog, (double)total/1048576.0f / time_in_sec); + fflush(stdout); + lastprog = prog; + } + } + } + printf("\n"); + free(buf); + afc_file_close(afc, fh); + } + fclose(f); + } else { + printf("Error: Failed to open local file '%s': %s\n", argv[0], strerror(errno)); + } + free(dstpath); +} + +static void handle_pwd(afc_client_t afc, int argc, char** argv) +{ + printf("%s\n", curdir); +} + +static void handle_cd(afc_client_t afc, int argc, char** argv) +{ + if (argc != 1) { + printf("Error: Invalid number of arguments\n"); + return; + } + + if (!strcmp(argv[0], ".")) { + return; + } + + if (!strcmp(argv[0], "..")) { + if (!strcmp(curdir, "/")) { + return; + } + char *p = strrchr(curdir, '/'); + if (!p) { + strcpy(curdir, "/"); + return; + } + if (p == curdir) { + *(p+1) = '\0'; + } else { + *p = '\0'; + } + return; + } + + char* path = get_realpath(argv[0]); + int is_dir = 0; + char **info = NULL; + afc_error_t err = afc_get_file_info(afc, path, &info); + if (err == AFC_E_SUCCESS && info) { + int i; + for (i = 0; info[i]; i += 2) { + if (!strcmp(info[i], "st_ifmt")) { + if (!strcmp(info[i+1], "S_IFDIR")) { + is_dir = 1; + } + break; + } + } + afc_dictionary_free(info); + } else { + printf("Error: Failed to get file info for %s: %d\n", path, err); + free(path); + return; + } + + if (!is_dir) { + printf("Error: '%s' is not a valid directory\n", path); + free(path); + return; + } + + free(curdir); + curdir = path; + curdir_len = strlen(curdir); +} + +#ifndef HAVE_READLINE +#ifdef WIN32 +#define BS_CC '\b' +#else +#define BS_CC 0x7f +#define getch getchar +#endif +static void get_input(char *buf, int maxlen) +{ + int len = 0; + int c; + + while ((c = getch())) { + if ((c == '\r') || (c == '\n')) { + break; + } + if (isprint(c)) { + if (len < maxlen-1) + buf[len++] = c; + } else if (c == BS_CC) { + if (len > 0) { + fputs("\b \b", stdout); + len--; + } + } + } + buf[len] = 0; +} +#endif + +static void parse_cmdline(int* p_argc, char*** p_argv, const char* cmdline) +{ + char **argv = NULL; + int argc = 0; + size_t maxlen = strlen(cmdline); + const char* pos = cmdline; + const char* qpos = NULL; + char *tmpbuf = NULL; + int tmplen = 0; + int is_error = 0; + + /* skip initial whitespace */ + while (isspace(*pos)) pos++; + maxlen -= (pos - cmdline); + + tmpbuf = (char*)malloc(maxlen+1); + + while (!is_error) { + if (*pos == '\\') { + pos++; + switch (*pos) { + case '"': + case '\'': + case '\\': + case ' ': + tmpbuf[tmplen++] = *pos; + pos++; + break; + default: + printf("Error: Invalid escape sequence\n"); + is_error++; + break; + } + } else if (*pos == '"' || *pos == '\'') { + if (!qpos) { + qpos = pos; + } else { + qpos = NULL; + } + pos++; + } else if (*pos == '\0' || (!qpos && isspace(*pos))) { + tmpbuf[tmplen] = '\0'; + if (*pos == '\0' && qpos) { + printf("Error: Unmatched `%c`\n", *qpos); + is_error++; + break; + } + char** new_argv = (char**)realloc(argv, (argc+1)*sizeof(char*)); + if (new_argv == NULL) { + printf("Error: Out of memory?!\n"); + is_error++; + break; + } + argv = new_argv; + /* shrink buffer to actual argument size */ + argv[argc] = (char*)realloc(tmpbuf, tmplen+1); + if (!argv[argc]) { + printf("Error: Out of memory?!\n"); + is_error++; + break; + } + argc++; + tmpbuf = NULL; + if (*pos == '\0') { + break; + } + maxlen -= tmplen; + tmpbuf = (char*)malloc(maxlen+1); + tmplen = 0; + while (isspace(*pos)) pos++; + } else { + tmpbuf[tmplen++] = *pos; + pos++; + } + } + if (tmpbuf) { + free(tmpbuf); + } + if (is_error) { + int i; + for (i = 0; argv && i < argc; i++) free(argv[i]); + free(argv); + return; + } + + *p_argv = argv; + *p_argc = argc; +} + +static int process_args(afc_client_t afc, int argc, char** argv) +{ + if (!strcmp(argv[0], "q") || !strcmp(argv[0], "quit") || !strcmp(argv[0], "exit")) { + return -1; + } + else if (!strcmp(argv[0], "help")) { + handle_help(afc, argc, argv); + } + else if (!strcmp(argv[0], "devinfo") || !strcmp(argv[0], "deviceinfo")) { + handle_devinfo(afc, argc-1, argv+1); + } + else if (!strcmp(argv[0], "info")) { + handle_file_info(afc, argc-1, argv+1); + } + else if (!strcmp(argv[0], "ls") || !strcmp(argv[0], "list")) { + handle_list(afc, argc-1, argv+1); + } + else if (!strcmp(argv[0], "mv") || !strcmp(argv[0], "rename")) { + handle_rename(afc, argc-1, argv+1); + } + else if (!strcmp(argv[0], "mkdir")) { + handle_mkdir(afc, argc-1, argv+1); + } + else if (!strcmp(argv[0], "ln")) { + handle_link(afc, argc-1, argv+1); + } + else if (!strcmp(argv[0], "rm") || !strcmp(argv[0], "remove")) { + handle_remove(afc, argc-1, argv+1); + } + else if (!strcmp(argv[0], "get")) { + handle_get(afc, argc-1, argv+1); + } + else if (!strcmp(argv[0], "put")) { + handle_put(afc, argc-1, argv+1); + } + else if (!strcmp(argv[0], "pwd")) { + handle_pwd(afc, argc-1, argv+1); + } + else if (!strcmp(argv[0], "cd")) { + handle_cd(afc, argc-1, argv+1); + } + else { + printf("Unknown command '%s'. Type 'help' to get a list of available commands.\n", argv[0]); + } + return 0; +} + +static void start_cmdline(afc_client_t afc) +{ + while (!stop_requested) { + int argc = 0; + char **argv = NULL; + char prompt[128]; + int plen = curdir_len; + char *ppath = curdir; + int plim = (int)(sizeof(prompt)/2)-8; + if (plen > plim) { + ppath = curdir + (plen - plim); + plen = plim; + } + snprintf(prompt, 128, FG_BLACK BG_LIGHT_GRAY "afc:" COLOR_RESET FG_BRIGHT_YELLOW BG_BLUE "%.*s" COLOR_RESET " > ", plen, ppath); +#ifdef HAVE_READLINE + char* cmd = readline(prompt); + if (!cmd || !*cmd) { + free(cmd); + continue; + } + add_history(cmd); + parse_cmdline(&argc, &argv, cmd); +#else + char cmdbuf[4096]; + printf("%s", prompt); + fflush(stdout); + get_input(cmdbuf, sizeof(cmdbuf)); + parse_cmdline(&argc, &argv, cmdbuf); +#endif +#ifdef HAVE_READLINE + free(cmd); +#endif + /* process arguments */ + if (argv && argv[0]) { + if (process_args(afc, argc, argv) < 0) { + break; + } + } + } +} + +static void device_event_cb(const idevice_event_t* event, void* userdata) +{ + if (use_network && event->conn_type != CONNECTION_NETWORK) { + return; + } else 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) { + connected = 1; + } + } else if (event->event == IDEVICE_DEVICE_REMOVE) { + if (strcmp(udid, event->udid) == 0) { + connected = 0; + printf("\n[disconnected]\n"); + handle_signal(SIGINT); + } + } +} + +int main(int argc, char** argv) +{ + const char* appid = NULL; + int ret = 0; + idevice_t device = NULL; + lockdownd_client_t lockdown = NULL; + lockdownd_error_t ldret = LOCKDOWN_E_UNKNOWN_ERROR; + lockdownd_service_descriptor_t service = NULL; + afc_client_t afc = NULL; + house_arrest_client_t house_arrest = NULL; + const char* service_name = AFC_SERVICE_NAME; + int use_container = 0; + + int c = 0; + const struct option longopts[] = { + { "udid", required_argument, NULL, 'u' }, + { "network", no_argument, NULL, 'n' }, + { "help", no_argument, NULL, 'h' }, + { "debug", no_argument, NULL, 'd' }, + { "version", no_argument, NULL, 'v' }, + { "documents", required_argument, NULL, OPT_DOCUMENTS }, + { "container", required_argument, NULL, OPT_CONTAINER }, + { NULL, 0, NULL, 0} + }; + + signal(SIGTERM, handle_signal); +#ifndef WIN32 + signal(SIGQUIT, handle_signal); + signal(SIGPIPE, SIG_IGN); +#endif + + while ((c = getopt_long(argc, argv, "du:nhv", longopts, NULL)) != -1) { + switch (c) { + case 'd': + idevice_set_debug_level(1); + break; + case 'u': + if (!*optarg) { + fprintf(stderr, "ERROR: UDID must not be empty!\n"); + print_usage(argc, argv, 1); + return 2; + } + udid = strdup(optarg); + break; + case 'n': + use_network = 1; + break; + case 'h': + print_usage(argc, argv, 0); + return 0; + case 'v': + printf("%s %s", TOOL_NAME, PACKAGE_VERSION); +#ifdef HAVE_READLINE + printf(" (readline)"); +#endif + printf("\n"); + return 0; + case OPT_DOCUMENTS: + if (!*optarg) { + fprintf(stderr, "ERROR: '--documents' requires a non-empty app ID!\n"); + print_usage(argc, argv, 1); + return 2; + } + appid = optarg; + use_container = 0; + break; + case OPT_CONTAINER: + if (!*optarg) { + fprintf(stderr, "ERROR: '--container' requires a not-empty app ID!\n"); + print_usage(argc, argv, 1); + return 2; + } + appid = optarg; + use_container = 1; + break; + default: + print_usage(argc, argv, 1); + return 2; + } + } + + argc -= optind; + argv += optind; + + int num = 0; + idevice_info_t *devices = NULL; + idevice_get_device_list_extended(&devices, &num); + int count = 0; + for (int i = 0; i < num; i++) { + if (devices[i]->conn_type == CONNECTION_NETWORK && use_network) { + count++; + } else if (devices[i]->conn_type == CONNECTION_USBMUXD) { + count++; + } + } + idevice_device_list_extended_free(devices); + if (count == 0) { + fprintf(stderr, "No device found. Plug in a device or pass UDID with -u to wait for device to be available.\n"); + return 1; + } + + idevice_events_subscribe(&context, device_event_cb, NULL); + + while (!connected && !stop_requested) { +#ifdef WIN32 + Sleep(100); +#else + usleep(100000); +#endif + } + if (stop_requested) { + return 0; + } + + ret = idevice_new_with_options(&device, udid, (use_network) ? IDEVICE_LOOKUP_NETWORK : IDEVICE_LOOKUP_USBMUX); + if (ret != IDEVICE_E_SUCCESS) { + if (udid) { + fprintf(stderr, "ERROR: Device %s not found!\n", udid); + } else { + fprintf(stderr, "ERROR: No device found!\n"); + } + return 1; + } + + do { + if (LOCKDOWN_E_SUCCESS != (ldret = lockdownd_client_new_with_handshake(device, &lockdown, TOOL_NAME))) { + fprintf(stderr, "ERROR: Could not connect to lockdownd: %s (%d)\n", lockdownd_strerror(ldret), ldret); + ret = 1; + break; + } + + if (appid) { + service_name = HOUSE_ARREST_SERVICE_NAME; + } + + ldret = lockdownd_start_service(lockdown, service_name, &service); + if (ldret != LOCKDOWN_E_SUCCESS) { + fprintf(stderr, "ERROR: Failed to start service %s: %s (%d)\n", service_name, lockdownd_strerror(ldret), ldret); + ret = 1; + break; + } + + if (appid) { + house_arrest_client_new(device, service, &house_arrest); + if (!house_arrest) { + fprintf(stderr, "Could not start document sharing service!\n"); + ret = 1; + break; + } + + if (house_arrest_send_command(house_arrest, use_container ? "VendContainer": "VendDocuments", appid) != HOUSE_ARREST_E_SUCCESS) { + fprintf(stderr, "Could not send house_arrest command!\n"); + ret = 1; + break; + } + + plist_t dict = NULL; + if (house_arrest_get_result(house_arrest, &dict) != HOUSE_ARREST_E_SUCCESS) { + fprintf(stderr, "Could not get result from document sharing service!\n"); + break; + } + plist_t node = plist_dict_get_item(dict, "Error"); + if (node) { + char *str = NULL; + plist_get_string_val(node, &str); + fprintf(stderr, "ERROR: %s\n", str); + if (str && !strcmp(str, "InstallationLookupFailed")) { + fprintf(stderr, "The App '%s' is either not present on the device, or the 'UIFileSharingEnabled' key is not set in its Info.plist. Starting with iOS 8.3 this key is mandatory to allow access to an app's Documents folder.\n", appid); + } + free(str); + plist_free(dict); + break; + } + plist_free(dict); + afc_client_new_from_house_arrest_client(house_arrest, &afc); + } else { + afc_client_new(device, service, &afc); + } + lockdownd_service_descriptor_free(service); + lockdownd_client_free(lockdown); + lockdown = NULL; + + curdir = strdup("/"); + curdir_len = 1; + + if (argc > 0) { + // command line mode + process_args(afc, argc, argv); + } else { + // interactive mode + start_cmdline(afc); + } + + } while (0); + + if (afc) { + afc_client_free(afc); + } + if (lockdown) { + lockdownd_client_free(lockdown); + } + idevice_free(device); + + return ret; +}