agent.js

  1. 'use strict'
  2. const WebSocket = require('ws')
  3. const stream = require('stream')
  4. const { sleep } = require('./util/sleep')
  5. /**
  6. * @typedef {object} CommandResult
  7. * @property {integer} id - ID
  8. * @property {boolean} success - command result
  9. */
  10. /**
  11. * @typedef {object} ShellExecResult
  12. * @property {integer} id - ID
  13. * @property {integer} exit-status
  14. * @property {string} output - command output
  15. * @property {boolean} success - command result
  16. */
  17. /**
  18. * @typedef {object} FridaPsResult
  19. * @property {integer} id - ID
  20. * @property {integer} exit-status -
  21. * @property {string} output - frida-ps output
  22. * @property {boolean} success - command result
  23. */
  24. /**
  25. * @typedef {object} AppListEntry
  26. * @property {string} applicationType
  27. * @property {string} bundleID
  28. * @property {integer} date
  29. * @property {integer} diskUsage
  30. * @property {boolean} isLaunchable
  31. * @property {string} name
  32. * @property {boolean} running
  33. */
  34. /**
  35. * @typedef {object} StatEntry
  36. * @property {integer} atime
  37. * @property {integer} ctime
  38. * @property {object[]} entries
  39. * @property {integer} entries[].atime
  40. * @property {integer} entries[].stime
  41. * @property {integer} entries[].gid
  42. * @property {integer} entries[].mode
  43. * @property {integer} entries[].mtime
  44. * @property {string} entries[].name
  45. * @property {integer} entries[].size
  46. * @property {integer} entries[].uid
  47. * @property {integer} gid
  48. * @property {integer} mode
  49. * @property {integer} mtime
  50. * @property {string} name
  51. * @property {integer} size
  52. * @property {integer} uid
  53. */
  54. /**
  55. * @typedef {object} ProvisioningProfileInfo
  56. * @property {string} name
  57. * @property {string} uuid
  58. * @property {string} teamId
  59. * @property {string[]} certs
  60. */
  61. /**
  62. * A connection to the agent running on an instance.
  63. *
  64. * Instances of this class
  65. * are returned from {@link Instance#agent} and {@link Instance#newAgent}. They
  66. * should not be created using the constructor.
  67. * @hideconstructor
  68. */
  69. class Agent {
  70. constructor (instance) {
  71. this.instance = instance
  72. this.connected = false
  73. this.uploading = false
  74. this.connectPromise = null
  75. this.id = 0
  76. this._keepAliveTimeout = null
  77. this._startKeepAliveTimeout = null
  78. this._lastPong = null
  79. this._lastPing = null
  80. }
  81. /**
  82. * Ensure the agent is connected.
  83. * @private
  84. */
  85. async connect () {
  86. this.pendingConnect = true
  87. if (!this.connected) {
  88. return await this.reconnect()
  89. }
  90. }
  91. /**
  92. * Ensure the agent is disconnected, then connect the agent.
  93. * @private
  94. */
  95. async reconnect () {
  96. if (this.connected) this.disconnect()
  97. if (this.connectPromise) return this.connectPromise
  98. this.connectPromise = await (async () => {
  99. while (this.pendingConnect) {
  100. try {
  101. await this._connect()
  102. break
  103. } catch (err) {
  104. if (err.stack.includes('Instance likely does not exist')) {
  105. throw err
  106. }
  107. if (err.stack.includes('unexpected server response (502)')) {
  108. // 'Error: unexpected server response (502)' means the device is not likely up yet
  109. await sleep(10 * 1000)
  110. }
  111. if (err.stack.includes('closed before the connection')) {
  112. // Do nothing this is normal when trying to settle a connection for a vm coming up
  113. } else {
  114. await sleep(7.5 * 1000)
  115. }
  116. }
  117. }
  118. this.connectPromise = null
  119. })()
  120. return this.connectPromise
  121. }
  122. async _connect () {
  123. this.pending = new Map()
  124. const endpoint = await this.instance.agentEndpoint()
  125. if (!endpoint) {
  126. this.pendingConnect = false
  127. throw new Error('Instance likely does not exist')
  128. }
  129. // Detect if a disconnection happened before we were able to get the agent endpoint.
  130. if (!this.pendingConnect) throw new Error('connection cancelled')
  131. const ws = new WebSocket(
  132. /^https/.test(endpoint)
  133. ? endpoint.replace(/^https/, 'wss')
  134. : /^http/.test(endpoint)
  135. ? endpoint.replace(/^http/, 'ws')
  136. : endpoint
  137. )
  138. this.ws = ws
  139. ws.on('message', data => {
  140. try {
  141. let message
  142. let id
  143. if (typeof data === 'string') {
  144. message = JSON.parse(data)
  145. id = message.id
  146. } else if (data.length >= 8) {
  147. id = data.readUInt32LE(0)
  148. message = data.slice(8)
  149. }
  150. const handler = this.pending.get(id)
  151. if (handler) {
  152. // will work regardless of whether handler returns a promise
  153. Promise.resolve(handler(null, message)).then(shouldDelete => {
  154. if (shouldDelete) this.pending.delete(id)
  155. })
  156. }
  157. } catch (err) {
  158. console.error('error in agent message handler', err)
  159. }
  160. })
  161. ws.on('close', (code, _reason) => {
  162. this.pending.forEach(handler => {
  163. handler(new Error(`disconnected with code ${code}`))
  164. })
  165. this.pending = new Map()
  166. this._disconnect()
  167. })
  168. return await new Promise((resolve, reject) => {
  169. ws.once('open', () => {
  170. if (this.ws !== ws) {
  171. try {
  172. ws.close()
  173. } catch (e) {
  174. // Swallow ws.close() errors.
  175. }
  176. reject(new Error('connection cancelled'))
  177. return
  178. }
  179. ws.on('error', err => {
  180. this.pending.forEach(handler => {
  181. handler(err)
  182. })
  183. this.pending = new Map()
  184. if (this.ws === ws) {
  185. this._disconnect()
  186. } else {
  187. try {
  188. ws.close()
  189. } catch (e) {
  190. // Swallow ws.close() errors.
  191. }
  192. }
  193. console.error('error in agent socket', err)
  194. })
  195. resolve()
  196. })
  197. ws.once('error', err => {
  198. if (this.ws === ws) {
  199. this._disconnect()
  200. } else {
  201. try {
  202. ws.close()
  203. } catch (e) {
  204. // Swallow ws.close() errors.
  205. }
  206. }
  207. reject(err)
  208. })
  209. })
  210. .then(() => {
  211. this.pendingConnect = false
  212. this.connected = true
  213. clearTimeout(this._startKeepAliveTimeout)
  214. this._startKeepAlive()
  215. })
  216. .catch(async err => {
  217. await this.instance.update()
  218. throw err
  219. })
  220. }
  221. _startKeepAlive () {
  222. if (!this.connected) return
  223. const ws = this.ws
  224. // clean up any existing keepalive timers before registering new ones. This prevents a bug that occurs if _startKeepAlive() is invoked multiple times.
  225. // _startKeepAlive() invoked once -> this._keepAliveTimeout is registered in state with the pong listener relying on that state to clear it.
  226. // _startKeepAlive() invoked second -> overwrites this._keepAliveTimeout.
  227. // Now, when original pong listener goes to clear timer based on this._keepAliveTimeout state, it has lost the reference to the first timer which results in it blowing up at 10 seconds
  228. this._stopKeepAlive();
  229. ws.ping()
  230. this._keepAliveTimeout = setTimeout(() => {
  231. if (this.ws !== ws) {
  232. try {
  233. ws.close()
  234. } catch (e) {
  235. // Swallow ws.close() errors.
  236. }
  237. return
  238. }
  239. const err = new Error('Agent did not get a response to ping in 10 seconds, disconnecting.')
  240. console.error('Agent did not get a response to ping in 10 seconds, disconnecting.')
  241. this.pending.forEach(handler => {
  242. handler(err)
  243. })
  244. this.pending = new Map()
  245. this._disconnect()
  246. }, 10 * 1000)
  247. ws.once('pong', async () => {
  248. if (ws !== this.ws) {
  249. return
  250. }
  251. clearTimeout(this._keepAliveTimeout)
  252. this._keepAliveTimeout = null
  253. if (!this.uploading) {
  254. // use arrow function to ensure the "this" binding references the Agent context, NOT a Timer.
  255. this._startKeepAliveTimeout = setTimeout(() => this._startKeepAlive(), 10 * 1000)
  256. }
  257. })
  258. }
  259. _stopKeepAlive () {
  260. if (this._startKeepAliveTimeout) {
  261. clearTimeout(this._startKeepAliveTimeout)
  262. this._startKeepAliveTimeout = null
  263. }
  264. if (this._keepAliveTimeout) {
  265. clearTimeout(this._keepAliveTimeout)
  266. this._keepAliveTimeout = null
  267. }
  268. }
  269. /**
  270. * Disconnect an agent connection. This is usually only required if a new
  271. * agent connection has been created and is no longer needed, for example
  272. * if the `crashListener` in the example at {@link Agent#crashes} is not
  273. * needed anymore.
  274. * @example
  275. * agent.disconnect();
  276. */
  277. disconnect () {
  278. this.pendingConnect = false
  279. this._disconnect()
  280. }
  281. _disconnect () {
  282. this.connected = false
  283. this._stopKeepAlive()
  284. if (this.ws) {
  285. try {
  286. this.ws.close()
  287. } catch (e) {
  288. // Swallow ws.close() errors.
  289. }
  290. this.ws = null
  291. }
  292. }
  293. /**
  294. * Send a command to the agent.
  295. *
  296. * When the command is responded to with an error, the error is thrown.
  297. * When the command is responded to with success, the handler callback is
  298. * called with the response as an argument.
  299. *
  300. * If the callback returns a value, that value will be returned from
  301. * `command`; otherwise nothing will happen until the next response to the
  302. * command. If the callback throws an exception, that exception will be
  303. * thrown from `command`.
  304. *
  305. * If no callback is specified, it is equivalent to specifying the callback
  306. * `(response) => response`.
  307. *
  308. * @param {string} type - passed in the `type` field of the agent command
  309. * @param {string} op - passed in the `op` field of the agent command
  310. * @param {Object} params - any other parameters to include in the command
  311. * @param {function} [handler=(response) => response] - the handler callback
  312. * @param {function} [uploadHandler] - a kludge for file uploads to work
  313. * @private
  314. */
  315. async command (type, op, params, handler, uploadHandler) {
  316. if (handler === undefined) handler = response => response
  317. const id = this.id
  318. this.id++
  319. const message = Object.assign({ type, op, id }, params)
  320. while (!this.ws) {
  321. await this.connect()
  322. }
  323. this.ws.send(JSON.stringify(message))
  324. if (uploadHandler) uploadHandler(id)
  325. return await new Promise((resolve, reject) => {
  326. this.pending.set(id, async (err, response) => {
  327. if (err) {
  328. reject(err)
  329. return
  330. }
  331. if (response.error) {
  332. reject(Object.assign(new Error(), response.error))
  333. return
  334. }
  335. try {
  336. const result = await handler(response)
  337. if (result !== undefined) {
  338. resolve(result)
  339. return true // stop calling us
  340. }
  341. return false
  342. } catch (e) {
  343. reject(e)
  344. return true
  345. }
  346. })
  347. })
  348. }
  349. sendBinaryData (id, data) {
  350. const idBuffer = Buffer.alloc(8, 0)
  351. idBuffer.writeUInt32LE(id, 0)
  352. if (data) this.ws.send(Buffer.concat([idBuffer, data]))
  353. else this.ws.send(idBuffer)
  354. }
  355. /**
  356. * Wait for the instance to be ready to use. On iOS, this will wait until Springboard has launched.
  357. * @example
  358. * let agent = await instance.agent();
  359. * await agent.ready();
  360. */
  361. async ready () {
  362. await this.command('app', 'ready')
  363. }
  364. /**
  365. * Uninstalls the app with the given bundle ID.
  366. * @param {string} bundleID - The bundle ID of the app to uninstall.
  367. * @param {Agent~progressCallback} progress - The progress callback.
  368. * @example
  369. * await agent.uninstall('com.corellium.demoapp', (progress, status) => {
  370. * console.log(progress, status);
  371. * });
  372. */
  373. async uninstall (bundleID, progress) {
  374. await this.command('app', 'uninstall', { bundleID }, message => {
  375. if (message.success) return message
  376. if (progress && message.progress) progress(message.progress, message.status)
  377. })
  378. }
  379. /**
  380. * Launches the app with the given bundle ID.
  381. * @param {string} bundleID - The bundle ID of the app to launch.
  382. * @example
  383. * await agent.run("com.corellium.demoapp");
  384. */
  385. async run (bundleID) {
  386. await this.command('app', 'run', { bundleID })
  387. }
  388. /**
  389. * Executes a given command
  390. * @param {string} cmd - The cmd to execute
  391. * @return {Promise<ShellExecResult>}
  392. * @example
  393. * await agent.shellExec("uname");
  394. */
  395. async shellExec (cmd) {
  396. return await this.command('app', 'shellExec', { cmd })
  397. }
  398. /**
  399. * Launches the app with the given bundle ID.
  400. * @param {string} bundleID - The bundle ID of the app to launch, for android this is the package name.
  401. * @param {string} activity fully qualified activity to launch from bundleID
  402. * @example
  403. * await agent.runActivity('com.corellium.test.app', 'com.corellium.test.app/com.corellium.test.app.CrashActivity');
  404. */
  405. async runActivity (bundleID, activity) {
  406. await this.command('app', 'run', { bundleID, activity })
  407. }
  408. /**
  409. * Kill the app with the given bundle ID, if it is running.
  410. * @param {string} bundleID - The bundle ID of the app to kill.
  411. * @example
  412. * await agent.kill("com.corellium.demoapp");
  413. */
  414. async kill (bundleID) {
  415. await this.command('app', 'kill', { bundleID })
  416. }
  417. /**
  418. * Returns an array of installed apps.
  419. * @return {Promise<AppListEntry[]>}
  420. * @example
  421. * let appList = await agent.appList();
  422. * for (app of appList) {
  423. * console.log('Found installed app ' + app['bundleID']);
  424. * }
  425. */
  426. async appList () {
  427. const { apps } = await this.command('app', 'list')
  428. return apps
  429. }
  430. /**
  431. * Gets information about the file at the specified path. Fields are atime, mtime, ctime (in seconds after the epoch), size, mode (see mode_t in man 2 stat), uid, gid. If the path specified is a directory, an entries field will be present with
  432. * the same structure (and an additional name field) for each immediate child of the directory.
  433. * @return {Promise<StatEntry>}
  434. * @example
  435. * let scripts = await agent.stat('/data/corellium/frida/scripts/');
  436. */
  437. async stat (path) {
  438. const response = await this.command('file', 'stat', { path })
  439. return response.stat
  440. }
  441. /**
  442. * A callback for file upload progress messages. Can be passed to {@link Agent#upload} and {@link Agent#installFile}
  443. * @callback Agent~uploadProgressCallback
  444. * @param {number} bytes - The number of bytes that has been uploaded.
  445. */
  446. /**
  447. * A callback for progress messages. Can be passed to {@link Agent#install}, {@link Agent#installFile}, {@link Agent#uninstall}.
  448. * @callback Agent~progressCallback
  449. * @param {number} progress - The progress, as a number between 0 and 1.
  450. * @param {string} status - The current status.
  451. */
  452. /**
  453. * Installs an app. The app's IPA must be available on the VM's filesystem. A progress callback may be provided.
  454. *
  455. * @see {@link Agent#upload} to upload a file to the VM's filesystem
  456. * @see {@link Agent#installFile} to handle both the upload and install
  457. *
  458. * @param {string} path - The path of the IPA on the VM's filesystem.
  459. * @param {Agent~progressCallback} [progress] - An optional callback that
  460. * will be called with information on the progress of the installation.
  461. * @async
  462. *
  463. * @example
  464. * await agent.install('/var/tmp/temp.ipa', (progress, status) => {
  465. * console.log(progress, status);
  466. * });
  467. */
  468. async install (path, progress) {
  469. await this.command('app', 'install', { path }, message => {
  470. if (message.success) return message
  471. if (progress && message.progress) progress(message.progress, message.status)
  472. })
  473. }
  474. /**
  475. * Returns an array of Mobile Configuration profile IDs
  476. * @return {Promise<string[]>}
  477. * @example
  478. * let profiles = await agent.profileList();
  479. * for (p of profiles) {
  480. * console.log('Found configuration profile: ' + p);
  481. * }
  482. */
  483. async profileList () {
  484. const { profiles } = await this.command('profile', 'list')
  485. return profiles
  486. }
  487. /**
  488. * Installs Mobile Configuration profile
  489. * @param {Buffer} profile - profile binary
  490. * @example
  491. * var profile = fs.readFileSync(path.join(__dirname, "myprofile.mobileconfig"));
  492. * await agent.installProfile(profile);
  493. */
  494. async installProfile (profile) {
  495. await this.command('profile', 'install', {
  496. profile: Buffer.from(profile).toString('base64')
  497. })
  498. }
  499. /**
  500. * Deletes Mobile Configuration profile
  501. * @param {string} profileID - profile ID
  502. * @example
  503. * await agent.removeProfile('com.test.myprofile');
  504. */
  505. async removeProfile (profileID) {
  506. await this.command('profile', 'remove', { profileID })
  507. }
  508. /**
  509. * Gets Mobile Configuration profile binary
  510. * @param {string} profileID - profile ID
  511. * @return {Promise<Buffer>}
  512. * @example
  513. * var profile = await agent.getProfile('com.test.myprofile');
  514. */
  515. async getProfile (profileID) {
  516. const { profile } = await this.command('profile', 'get', { profileID })
  517. if (!profile) return null
  518. // eslint-disable-next-line new-cap
  519. return new Buffer.from(profile, 'base64')
  520. }
  521. /**
  522. * Returns an array of Provisioning profile descriptions
  523. * @return {Promise<ProvisioningProfileInfo[]>}
  524. * @example
  525. * let profiles = await agent.listProvisioningProfiles();
  526. * for (p of profiles) {
  527. * console.log(p['uuid']);
  528. * }
  529. */
  530. async listProvisioningProfiles () {
  531. const { profiles } = await this.command('provisioning', 'list')
  532. return profiles
  533. }
  534. /**
  535. * Installs Provisioning profile
  536. * @param {Buffer} profile - profile binary
  537. * @param {Boolean} trust - immediately trust installed profile
  538. * @example
  539. * var profile = fs.readFileSync(path.join(__dirname, "embedded.mobileprovision"));
  540. * await agent.installProvisioningProfile(profile, true);
  541. */
  542. async installProvisioningProfile (profile, trust = false) {
  543. await this.command('provisioning', 'install', {
  544. profile: Buffer.from(profile).toString('base64'),
  545. trust: trust
  546. })
  547. }
  548. /**
  549. * Deletes Provisioning profile
  550. * @param {string} profileID - profile ID
  551. * @example
  552. * await agent.removeProvisioningProfile('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa');
  553. */
  554. async removeProvisioningProfile (profileID) {
  555. await this.command('provisioning', 'remove', {
  556. uuid: profileID
  557. })
  558. }
  559. /**
  560. * Approves (makes trusted) profile which will be installed later in a future for example during app installation via Xcode.
  561. * @param {string} certID - profile ID
  562. * @param {string} profileID - profile ID
  563. * @example
  564. * await agent.preApproveProvisioningProfile('Apple Development: my@email.com (NKJDZ3DZJB)', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa');
  565. */
  566. async preApproveProvisioningProfile (certID, profileID) {
  567. await this.command('provisioning', 'preapprove', {
  568. cert: certID,
  569. uuid: profileID
  570. })
  571. }
  572. /**
  573. * Returns a temporary random filename on the VMs filesystem that by the
  574. * time of invocation of this method is guaranteed to be unique.
  575. * @return {Promise<string>}
  576. * @see example at {@link Agent#upload}
  577. */
  578. async tempFile () {
  579. const { path } = await this.command('file', 'temp')
  580. return path
  581. }
  582. /**
  583. * Reads from the specified stream and uploads the data to a file on the VM.
  584. * @param {string} path - The file path to upload the data to.
  585. * @param {ReadableStream} stream - The stream to read the file data from.
  586. * @param {Agent~uploadProgressCallback} progress - The callback for install progress information.
  587. * @example
  588. * const tmpName = await agent.tempFile();
  589. * await agent.upload(tmpName, fs.createReadStream('test.ipa'));
  590. */
  591. async upload (path, stream, progress) {
  592. // Temporarily stop the keepalive as the upload appears to backlog
  593. // the control packets (ping/pong) at the proxy which can cause issues
  594. // and a disconnect
  595. this._stopKeepAlive()
  596. this.uploading = true
  597. await this.command(
  598. 'file',
  599. 'upload',
  600. { path },
  601. message => {
  602. // This is hit after the upload is completed and the agent
  603. // on the other end sends the reply packet of success/fail
  604. // Restart the keepalive as the upload buffer should be cleared
  605. clearTimeout(this._startKeepAliveTimeout)
  606. this._startKeepAlive()
  607. this.uploading = false
  608. // Pass back the message to the command() function to prevent
  609. // blocking or returning an invalid value
  610. return message
  611. },
  612. id => {
  613. let total = 0
  614. stream.on('data', data => {
  615. this.sendBinaryData(id, data)
  616. total += data.length
  617. if (progress) progress(total)
  618. })
  619. stream.on('end', () => {
  620. this.sendBinaryData(id)
  621. })
  622. }
  623. )
  624. }
  625. /**
  626. * Downloads the file at the given path from the VM's filesystem. Returns a node ReadableStream.
  627. * @param {string} path - The path of the file to download.
  628. * @return {Readable}
  629. * @example
  630. * const dl = agent.download('/var/tmp/test.log');
  631. * dl.pipe(fs.createWriteStream('test.log'));
  632. */
  633. download (path) {
  634. let command
  635. const agent = this
  636. return new stream.Readable({
  637. read () {
  638. if (command) return
  639. command = agent.command('file', 'download', { path }, message => {
  640. if (!Buffer.isBuffer(message)) return
  641. if (message.length === 0) return true
  642. this.push(message)
  643. })
  644. command.then(() => this.push(null)).catch(err => this.emit('error', err))
  645. }
  646. })
  647. }
  648. /**
  649. * Reads a packaged app from the provided stream, uploads the app to the VM
  650. * using {@link Agent#upload}, and installs it using {@link Agent#install}.
  651. * @param {ReadableStream} stream - The app to install, the stream will be closed after it is uploaded.
  652. * @param {Agent~progressCallback} installProgress - The callback for install progress information.
  653. * @param {Agent~uploadProgressCallback} uploadProgress - The callback for file upload progress information.
  654. * @example
  655. * await agent.installFile(fs.createReadStream('test.ipa'), (installProgress, installStatus) => {
  656. * console.log(installProgress, installStatus);
  657. * });
  658. */
  659. async installFile (stream, installProgress, uploadProgress) {
  660. const path = await this.tempFile()
  661. await this.upload(path, stream, uploadProgress)
  662. stream.on('close', () => {
  663. stream.destroy()
  664. })
  665. await this.install(path, installProgress)
  666. try {
  667. await this.stat(path)
  668. await this.deleteFile(path)
  669. } catch (err) {
  670. if (!err.message.includes('Stat of file')) {
  671. throw err
  672. }
  673. }
  674. }
  675. /**
  676. * Delete the file at the specified path on the VM's filesystem.
  677. * @param {string} path - The path of the file on the VM's filesystem to delete.
  678. * @example
  679. * await agent.deleteFile('/var/tmp/test.log');
  680. */
  681. async deleteFile (path) {
  682. const response = await this.command('file', 'delete', { path })
  683. return response.path
  684. }
  685. /**
  686. * Change file attributes of the file at the specified path on the VM's filesystem.
  687. * @param {string} path - The path of the file on the VM's filesystem to delete.
  688. * @param {Object} attributes - An object whose members and values are the file attributes to change and what to change them to respectively. File attributes path, mode, uid and gid are supported.
  689. * @return {Promise<CommandResult>}
  690. * @example
  691. * await agent.changeFileAttributes(filePath, {mode: 511});
  692. */
  693. async changeFileAttributes (path, attributes) {
  694. const response = await this.command('file', 'modify', { path, attributes })
  695. return response
  696. }
  697. /**
  698. * Subscribe to crash events for the app with the given bundle ID. The callback will be called as soon as the agent finds a new crash log.
  699. *
  700. * The callback takes two parameters:
  701. * - `err`, which is undefined unless an error occurred setting up or waiting for crash logs
  702. * - `crash`, which contains the full crash report data
  703. *
  704. * **Note:** Since this method blocks the communication channel of the
  705. * agent to wait for crash reports, a new {@link Agent} connection should
  706. * be created with {@link Instance#newAgent}.
  707. *
  708. * @see Agent#disconnect
  709. *
  710. * @example
  711. * const crashListener = await instance.newAgent();
  712. * crashListener.crashes("com.corellium.demoapp", (err, crashReport) => {
  713. * if (err) {
  714. * console.error(err);
  715. * return;
  716. * }
  717. * console.log(crashReport);
  718. * });
  719. */
  720. async crashes (bundleID, callback) {
  721. await this.command('crash', 'subscribe', { bundleID }, async message => {
  722. const path = message.file
  723. const crashReport = await new Promise(resolve => {
  724. const stream = this.download(path)
  725. const buffers = []
  726. stream.on('data', data => {
  727. buffers.push(data)
  728. })
  729. stream.on('end', () => {
  730. resolve(Buffer.concat(buffers))
  731. })
  732. })
  733. await this.deleteFile(path)
  734. callback(null, crashReport.toString('utf8'))
  735. })
  736. }
  737. /** Locks the device software-wise.
  738. * @example
  739. * await agent.lockDevice();
  740. */
  741. async lockDevice () {
  742. await this.command('system', 'lock')
  743. }
  744. /** Unlocks the device software-wise.
  745. * @example
  746. * awaitagent.unlockDevice();
  747. */
  748. async unlockDevice () {
  749. await this.command('system', 'unlock')
  750. }
  751. /** Enables UI Automation.
  752. * @example
  753. * await agent.enableUIAutomation();
  754. */
  755. async enableUIAutomation () {
  756. await this.command('system', 'enableUIAutomation')
  757. }
  758. /** Disables UI Automation.
  759. * @example
  760. * await agent.disableUIAutomation();
  761. */
  762. async disableUIAutomation () {
  763. await this.command('system', 'disableUIAutomation')
  764. }
  765. /** Checks if SSL pinning is enabled. By default SSL pinning is disabled.
  766. * @returns {boolean}
  767. * @example
  768. * let enabled = await agent.isSSLPinningEnabled();
  769. * if (enabled) {
  770. * console.log("enabled");
  771. * } else {
  772. * console.log("disabled");
  773. * }
  774. */
  775. async isSSLPinningEnabled () {
  776. return (await this.command('system', 'isSSLPinningEnabled')).enabled
  777. }
  778. /** Enables SSL pinning.
  779. * @example
  780. * await agent.enableSSLPinning();
  781. */
  782. async enableSSLPinning () {
  783. await this.command('system', 'enableSSLPinning')
  784. }
  785. /** Disables SSL pinning.
  786. * @example
  787. * await agent.disableSSLPinning();
  788. */
  789. async disableSSLPinning () {
  790. await this.command('system', 'disableSSLPinning')
  791. }
  792. /** Shuts down the device.
  793. * @example
  794. * await agent.shutdown();
  795. */
  796. async shutdown () {
  797. await this.command('system', 'shutdown')
  798. }
  799. async acquireDisableAutolockAssertion () {
  800. await this.command('system', 'acquireDisableAutolockAssertion')
  801. }
  802. async releaseDisableAutolockAssertion () {
  803. await this.command('system', 'releaseDisableAutolockAssertion')
  804. }
  805. /** Connect device to WiFi.
  806. * @example
  807. * await agent.connectToWifi();
  808. */
  809. async connectToWifi () {
  810. await this.command('wifi', 'connect')
  811. }
  812. /** Disconnect device from WiFi.
  813. * @example
  814. * await agent.disconnectFromWifi();
  815. */
  816. async disconnectFromWifi () {
  817. await this.command('wifi', 'disconnect')
  818. }
  819. /** Get device property. */
  820. async getProp (property) {
  821. return await this.command('system', 'getprop', { property })
  822. }
  823. /**
  824. * Run frida on the device.
  825. * Please note that both arguments (pid and name) need to be provided as they are required by the Web UI.
  826. * @param {integer} pid
  827. * @param {string} name
  828. * @return {Promise<CommandResult>}
  829. * @example
  830. * await agent.runFrida(449, 'keystore');
  831. */
  832. async runFrida (pid, name) {
  833. return await this.command('frida', 'run-frida', {
  834. target_pid: pid.toString(),
  835. target_name: name.toString()
  836. })
  837. }
  838. /**
  839. * Run frida-ps on the device and return the command's output.
  840. * @return {Promise<FridaPsResult>}
  841. * @example
  842. * let procList = await agent.runFridaPs();
  843. * let lines = procList.output.trim().split('\n');
  844. * lines.shift();
  845. * lines.shift();
  846. * for (const line of lines) {
  847. * const [pid, name] = line.trim().split(/\s+/);
  848. * console.log(pid, name);
  849. * }
  850. */
  851. async runFridaPs () {
  852. return await this.command('frida', 'run-frida-ps')
  853. }
  854. /**
  855. * Run frida-kill on the device.
  856. * @return {Promise<CommandResult>}
  857. * @example
  858. * await agent.runFridaKill();
  859. */
  860. async runFridaKill () {
  861. return await this.command('frida', 'run-frida-kill')
  862. }
  863. }
  864. module.exports = Agent