index.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. #!/usr/bin/env node
  2. const program = require('commander')
  3. const path = require('path')
  4. const chalk = require('chalk')
  5. const version = require('./util/version_compare')
  6. const iPhoneBackup = require('./util/iphone_backup.js').iPhoneBackup
  7. const log = require('./util/log')
  8. var base = path.join(process.env.HOME, '/Library/Application Support/MobileSync/Backup/')
  9. var reportTypes = {
  10. 'apps': require('./reports/apps'),
  11. 'calls': require('./reports/calls'),
  12. 'conversations': require('./reports/conversations'),
  13. 'conversations_full': require('./reports/conversations_full'),
  14. 'cookies': require('./reports/cookies'),
  15. 'list': require('./reports/list'),
  16. 'manifest': require('./reports/manifest'),
  17. 'messages': require('./reports/messages'),
  18. 'notes': require('./reports/notes'),
  19. 'oldnotes': require('./reports/oldnotes'),
  20. 'photolocations': require('./reports/photolocations'),
  21. 'voicemail-files': require('./reports/voicemail-files'),
  22. 'voicemail': require('./reports/voicemail'),
  23. 'webhistory': require('./reports/webhistory'),
  24. 'calls_statistics': require('./reports/calls_statistics'),
  25. 'wifi': require('./reports/wifi'),
  26. 'address_book': require('./reports/address_book'),
  27. 'safari_bookmarks': require('./reports/safari_bookmarks'),
  28. 'pushstore': require('./reports/pushstore'),
  29. 'calendar': require('./reports/calendar'),
  30. 'facebook_profile': require('./reports/facebook_profile'),
  31. 'facebook_messenger_friends': require('./reports/facebook_messenger_friends'),
  32. 'spotify': require('./reports/spotify'),
  33. 'instagram_profile': require('./reports/instagram_profile'),
  34. 'instagram_recent_searches': require('./reports/instagram_recent_searches'),
  35. 'instagram_following_users_coded': require('./reports/instagram_following_users_coded'),
  36. 'instagram_fb_friends': require('./reports/instagram_fb_friends'),
  37. 'speed_dial': require('./reports/speed_dial'),
  38. 'bluetooth_devices': require('./reports/bluetooth_devices'),
  39. 'safari_open_tabs': require('./reports/safari_open_tabs'),
  40. 'safari_recent_searches': require('./reports/safari_recent_searches'),
  41. 'gmail_accounts': require('./reports/gmail_accounts'),
  42. 'gmail_shared_contacts': require('./reports/gmail_shared_contacts'),
  43. 'waze_favorites': require('./reports/waze_favorites'),
  44. 'waze_places': require('./reports/waze_places'),
  45. 'waze_recents': require('./reports/waze_recents'),
  46. 'skype_accounts': require('./reports/skype_accounts'),
  47. 'skype_calls': require('./reports/skype_calls'),
  48. 'viber_contacts': require('./reports/viber_contacts'),
  49. 'viber_calls': require('./reports/viber_calls'),
  50. 'viber_messages': require('./reports/viber_messages')
  51. }
  52. var formatters = {
  53. 'json': require('./formatters/json'),
  54. 'table': require('./formatters/table'),
  55. 'raw': require('./formatters/raw-json'),
  56. 'raw-json': require('./formatters/raw-json'),
  57. 'csv': require('./formatters/csv'),
  58. 'raw-csv': require('./formatters/raw-csv')
  59. }
  60. program
  61. .version('3.0.0')
  62. .option('-l, --list', 'List Backups')
  63. .option(`-b, --backup <backup>`, 'Backup ID')
  64. .option(`-d, --dir <directory>`, `Backup Directory (default: ${base})`)
  65. .option('-r, --report <report_type>', 'Select a report type. see below for a full list.')
  66. .option('-i, --id <id>', 'Specify an ID for filtering certain reports')
  67. .option('-f, --formatter <type>', 'Specify output format. default: table')
  68. .option(`-e, --extract <dir>`, 'Extract data for commands. supported by: voicemail-files, manifest')
  69. .option('-o, --output <path>', 'Specify an output directory for files to be written to.')
  70. .option(`-v, --verbose`, 'Verbose debugging output')
  71. .option(` --filter <filter>`, 'Filter output fo r individual reports. See the README for usage.')
  72. .option(' --join-reports', 'Join JSON reports together. (available for -f json or -f raw only!)')
  73. .option(` --no-color`, 'Disable colorized output')
  74. .option(` --dump`, 'alias for "--formatter raw"')
  75. .option(` --quiet`, 'quiet all messages, except for errors and raw output')
  76. program.on('--help', function () {
  77. console.log('')
  78. console.log('Supported Report Types:')
  79. // Generate a list of report types.
  80. for (var i in reportTypes) {
  81. var r = reportTypes[i]
  82. console.log(' ', chalk.green(r.name), (r.supportedVersions ? chalk.gray('(iOS ' + r.supportedVersions + ') ') : '') + '-', r.description)
  83. }
  84. console.log('')
  85. console.log("If you're interested to know how this works, check out my post:")
  86. console.log('https://www.richinfante.com/2017/3/16/reverse-engineering-the-ios-backup')
  87. console.log('')
  88. console.log('Issue tracker:')
  89. console.log('https://github.com/richinfante/iphonebackuptools/issues')
  90. console.log('')
  91. })
  92. process.on('unhandledRejection', (e) => {
  93. console.log('unhandled', e)
  94. process.exit(1)
  95. })
  96. // Parse argv.
  97. program.parse(process.argv)
  98. log.setVerbose(program.quiet ? 0 : (program.verbose ? 2 : 1))
  99. // Save the formatter
  100. program.formatter = formatters[program.formatter] || formatters.table
  101. // Legacy support for `--dump` flag.
  102. if (program.dump) {
  103. program.formatter = formatters.raw
  104. }
  105. // Disable color for non-ttys.
  106. if (!process.stdout.isTTY) { program.color = false }
  107. // Find the base
  108. base = program.dir || base
  109. log.verbose('Using source:', base)
  110. // Run the main function
  111. main()
  112. async function main () {
  113. if (program.list) {
  114. // Run the list report standalone
  115. await new Promise((resolve, reject) => {
  116. reportTypes.list.func(program, base, resolve, reject)
  117. })
  118. } else if (program.report) {
  119. var reportContents = []
  120. // Turn the report argument into an array of report type names
  121. var selectedTypes = program.report
  122. .split(',')
  123. .map(el => el.trim())
  124. .filter(el => el !== '')
  125. // Add all types if type is 'all'
  126. if (program.report === 'all') {
  127. selectedTypes = []
  128. for (var key in reportTypes) {
  129. if (reportTypes[key].requiresInteractivity === true) {
  130. continue
  131. }
  132. selectedTypes.push(key)
  133. }
  134. }
  135. for (var reportName of selectedTypes) {
  136. // If the report is valid
  137. if (reportTypes[reportName]) {
  138. var report = reportTypes[reportName]
  139. if (selectedTypes.length > 1 && !report.usesPromises) {
  140. log.warning('the report', report.name, 'does not utilize promises.')
  141. log.warning('this may not work')
  142. }
  143. log.begin('run', report.name)
  144. // Check if there's a backup specified and one is required.
  145. if (report.requiresBackup) {
  146. if (!program.backup) {
  147. log.error('use -b or --backup <id> to specify backup.')
  148. process.exit(1)
  149. }
  150. }
  151. try {
  152. if (report.func) {
  153. let contents = await runSingleReport(report, program)
  154. if (contents == null) { log.end(); continue }
  155. reportContents.push({
  156. name: reportName,
  157. contents: contents
  158. })
  159. } else if (report.functions) {
  160. let contents = await runSwitchedReport(report, program)
  161. if (contents == null) { log.end(); continue }
  162. reportContents.push({
  163. name: reportName,
  164. contents: contents
  165. })
  166. }
  167. } catch (e) {
  168. log.error(`Couldn't run '${report.name}'.`)
  169. log.error(e)
  170. }
  171. log.end()
  172. } else {
  173. log.error('Unknown report type:', reportName)
  174. log.error(`It's possible this tool is out-of date.`)
  175. log.error(`https://github.com/richinfante/iphonebackuptools/issues`)
  176. program.outputHelp()
  177. }
  178. }
  179. program.formatter.finalReport(reportContents, program)
  180. } else {
  181. program.outputHelp()
  182. }
  183. }
  184. async function runSwitchedReport (report, program) {
  185. async function createPromise (key, program, backup) {
  186. log.verbose('resolving using promises.')
  187. return new Promise((resolve, reject) => {
  188. report.functions[key](program, backup, resolve, reject)
  189. })
  190. }
  191. // New type of reports
  192. var backup = iPhoneBackup.fromID(program.backup, base)
  193. var flag = false
  194. var value
  195. // Check for a compatible reporting tool.
  196. for (var key in report.functions) {
  197. if (version.versionCheck(backup.iOSVersion, key)) {
  198. if (!report.usesPromises) {
  199. log.verbose('using synchronous call.')
  200. value = report.functions[key](program, backup)
  201. } else {
  202. // Use promises to resolve synchronously
  203. value = await createPromise(key, program, backup)
  204. }
  205. flag = true
  206. break
  207. }
  208. }
  209. if (!flag) {
  210. log.error(`Couldn't run '${report.name}'.`)
  211. log.error(`The report generator '${report.name}' does not support iOS`, backup.iOSVersion)
  212. log.error(`If you think it should, file an issue here:`)
  213. log.error(`https://github.com/richinfante/iphonebackuptools/issues`)
  214. return null
  215. }
  216. return value
  217. }
  218. async function runSingleReport (report, program) {
  219. async function createPromise (program, backup, base) {
  220. log.verbose('resolving using promises.')
  221. return new Promise((resolve, reject) => {
  222. if (report.requiresBackup) {
  223. report.func(program, backup, resolve, reject)
  224. } else {
  225. report.func(program, base, resolve, reject)
  226. }
  227. })
  228. }
  229. async function runReport (backup, base) {
  230. if (!report.usesPromises) {
  231. log.verbose('using synchronous call.')
  232. // Old-style non-promise based report.
  233. if (report.requiresBackup) {
  234. return report.func(program, backup)
  235. } else {
  236. return report.func(program, base)
  237. }
  238. } else {
  239. // Create a promise to resolve this function
  240. // Use promises to resolve synchronously
  241. return createPromise(program, backup, base)
  242. }
  243. }
  244. // New type of reports
  245. var backup = iPhoneBackup.fromID(program.backup, base)
  246. if (report.supportedVersions !== undefined) {
  247. if (version.versionCheck(backup.iOSVersion, report.supportedVersions)) {
  248. return runReport(backup, base)
  249. } else {
  250. log.error(`Couldn't run '${report.name}'.`)
  251. log.error(`The report generator '${report.name}' does not support iOS`, backup.iOSVersion)
  252. log.error(`If you think it should, file an issue here:`)
  253. log.error(`https://github.com/richinfante/iphonebackuptools/issues`)
  254. return null
  255. }
  256. } else {
  257. return runReport(backup, base)
  258. }
  259. }