From 01605fcde474a21bf89d99b6512e3301144f0f5e Mon Sep 17 00:00:00 2001 From: Matthew Hershberger Date: Mon, 19 Aug 2024 13:51:48 -0400 Subject: [PATCH] feat(fal): CORE-8718 - Add support for downloading firmware assets automatically --- README.md | 20 +++++- examples/create-device-with-firmware-asset.js | 0 package-lock.json | 2 +- package.json | 2 +- src/project.js | 69 +++++++++++++++++-- src/util/fetch.js | 9 +-- 6 files changed, 90 insertions(+), 12 deletions(-) create mode 100644 examples/create-device-with-firmware-asset.js diff --git a/README.md b/README.md index adb7157..3f8223e 100644 --- a/README.md +++ b/README.md @@ -266,7 +266,7 @@ Creates a new instance with the given options. The following options are support - `irq`: system IRQs, 1-16 ranges must be specified - `port`: tcp port for vMMIO usage -Example: +#### Example: ```javascript= // create instance @@ -282,6 +282,24 @@ let instance = await project.createInstance({ await instance.finishRestore(); ``` +#### Example: Handling Firmware Assets + +``` +❯ node myscript.js +Error: This instance requires additional firmware assets. To automatically download firmware assets and associate them +with your domain, set the environment variable FETCH_FIRMWARE_ASSETS=1 +``` + +Some recent firmwares require additional files that must be downloaded by the api client and associated with your domain. +The Corellium API can download these resources and associated them with your domain. To enable automatic download, set +the environment variable `FETCH_FIRMWARE_ASSETS=1`. + +``` +❯ env FETCH_FIRMWARE_ASSETS=1 node myscript.js +Creating ios device... +Created 741d5b9c-01dd-4878-b16f-8d6aa513c9c4 +``` + ## class Instance **Note:** instances of class `Instance` are only supposed to be retrieved by `Project#instances()`, `Project#getInstance()`, or `Project#createInstance`. diff --git a/examples/create-device-with-firmware-asset.js b/examples/create-device-with-firmware-asset.js new file mode 100644 index 0000000..e69de29 diff --git a/package-lock.json b/package-lock.json index aafeeec..82ddfc6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@corellium/corellium-api", - "version": "1.6.4", + "version": "1.7.6", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index d6bb9e4..139fce2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@corellium/corellium-api", - "version": "1.7.6", + "version": "1.8.0", "description": "Supported nodejs library for interacting with the Corellium service and VMs", "main": "src/corellium.js", "husky": { diff --git a/src/project.js b/src/project.js index 47bd821..a61951b 100644 --- a/src/project.js +++ b/src/project.js @@ -7,6 +7,9 @@ const { v4: uuidv4 } = require('uuid') const util = require('util') const fs = require('fs') const { compress, uploadFile } = require('./images') +const path = require('path') +const os = require('os') +const { fetch } = require('./util/fetch') /** * @typedef {object} ProjectKey @@ -138,11 +141,42 @@ class Project { * await instance.finishRestore(); */ async createInstance (options) { - const { id } = await fetchApi(this, '/instances', { - method: 'POST', - json: Object.assign({}, options, { project: this.id }) - }) - return await this.getInstance(id) + try { + const { id } = await fetchApi(this, '/instances', { + method: 'POST', + json: Object.assign({}, options, { project: this.id }) + }) + return await this.getInstance(id) + } catch (err) { + if (err.field === 'firmware_asset') { + if (process.env.FETCH_FIRMWARE_ASSETS !== '1') { + throw new Error('This instance requires additional firmware assets. To automatically download firmware assets and associate them with your domain, set the environment variable FETCH_FIRMWARE_ASSETS=1') + } + if (err.originalError.missingFwAssets && err.originalError.missingFwAssets.length > 0) { + for (const firmwareAssetUrl of err.originalError.missingFwAssets) { + const response = await fetch(firmwareAssetUrl, { response: 'raw' }) + const fwAssetPath = path.join(os.tmpdir(), `${uuidv4()}.fwasset`) + if (response.ok) { + const stream = fs.createWriteStream(fwAssetPath) + response.body.pipe(stream) + + await new Promise(resolve => response.body.on('finish', () => { + stream.end() + resolve() + })) + + await new Promise(resolve => stream.on('finish', () => { resolve() })) + + await this.uploadFirmwareAsset(fwAssetPath, encodeURIComponent(firmwareAssetUrl), () => {}) + } + } + } + + return this.createInstance(options) + } else { + throw err + } + } } /** @@ -395,6 +429,31 @@ class Project { return { id: imageId, name } } + /** + * Add a firmware asset image to a proejct for use in creating new instances. + * @param filePath - The path on the local file system to get the firmware asset file + * @param name - The name of the file to identify the file on the server, usually the full url + * @param progress + * @returns {Promise<{name, id: *}>} + */ + async uploadFirmwareAsset (filePath, name, progress) { + const imageId = uuidv4() + const token = await this.getToken() + const url = + this.api + + '/projects/' + + encodeURIComponent(this.id) + + '/image-upload/' + + encodeURIComponent('fwasset') + + '/' + + encodeURIComponent(imageId) + + '/' + + encodeURIComponent(name) + + await uploadFile(token, url, filePath, progress) + return { id: imageId, name } + } + /** * Add an image to the project. These images may be removed at any time and are meant to facilitate creating a new Instance with images. * diff --git a/src/util/fetch.js b/src/util/fetch.js index c9d9f4a..2828209 100644 --- a/src/util/fetch.js +++ b/src/util/fetch.js @@ -14,18 +14,19 @@ class CorelliumError extends Error { this.name = this.constructor.name this.field = error.field this.code = code + this.originalError = error } } async function fetch (url, options) { - if (options.headers === undefined) options.headers = {} + if (options && options.headers === undefined) options.headers = {} - if (options.json !== undefined) { + if (options && options.json !== undefined) { options.body = JSON.stringify(options.json) options.headers['Content-Type'] = 'application/json' delete options.json } - if (options.token !== undefined) { + if (options && options.token !== undefined) { options.headers.Authorization = options.token } @@ -46,7 +47,7 @@ async function fetch (url, options) { throw new Error(`${options.method || 'GET'} ${url} -- ${res.status} ${res.statusText}`) } - if (options.response === 'raw') return res + if (options && options.response === 'raw') return res return await res.json() }