'use strict'
const { fetchApi } = require('./util/fetch')
const Instance = require('./instance')
const InstanceUpdater = require('./instance-updater')
const { v4: uuidv4 } = require('uuid')
const util = require('util')
const fs = require('fs')
const { compress, uploadFile } = require('./images')
/**
* @typedef {object} ProjectKey
* @property {string} identifier
* @property {string} label
* @property {string} key
* @property {'ssh'|'adb'} kind - public key
* @property {string} fingerprint
* @property {string} createdAt - ISO datetime string
* @property {string} updatedAt - ISO datetime string
*/
/**
* @typedef {object} ProjectQuotas
* @property {number} cores - Number of available CPU cores
*/
/**
* Instances of this class are returned from {@link Corellium#projects}, {@link
* Corellium#getProject}, and {@link Corellium#projectNamed}. They should not
* be created using the constructor.
* @hideconstructor
*/
class Project {
constructor (client, id) {
this.client = client
this.api = this.client.api
this.id = id
this.token = null
this.updater = new InstanceUpdater(this)
}
/**
* Reload the project info. This currently consists of name and quotas, but
* will likely include more in the future.
* @example
* project.refresh();
*/
async refresh () {
this.info = await fetchApi(this, `/projects/${this.id}`)
}
/**
* Returns refreshed authentication token
* @return {string} token
* @example
* let token = await project.getToken()
*/
async getToken () {
return await this.client.getToken()
}
/**
* Returns an array of the {@link Instance}s in this project.
* @returns {Promise<Instance[]>} The instances in this project
* @example <caption>Finding the first instance with a given name</caption>
* const instances = await project.instances();
* const instance = instances.find(instance => instance.name === 'Test Device');
*/
async instances () {
const instances = await fetchApi(this, `/projects/${this.id}/instances`)
return await Promise.all(instances.map(info => new Instance(this, info)))
}
/**
* Returns the {@link Instance} with the given ID.
* @param {string} id
* @returns {Promise<Instance>}
* @example
* await project.getInstance('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa');
*/
async getInstance (id) {
const info = await fetchApi(this, `/instances/${id}`)
return new Instance(this, info)
}
/**
* @typedef {Object} vmmio - paremeters to export a VM address space range (and IRQ & DMA functionality)
* over TCP to different models running on different machines or inside a different VM
* @property {string} start - start address for beginning of vMMIO range
* @property {string} size - size of the range to use for vMMIO
* @property {string} irq - system IRQs, 1-16 ranges must be specified
* @property {string} port - tcp port for vMMIO usage
*/
/**
* Creates an instance and returns the {@link Instance} object. The options
* are passed directly to the API.
*
* @param {Object} options - The options for instance creation. These are
* the same as the JSON options passed to the instance creation API
* endpoint. For a full list of possible options, see the API documentation.
* @param {string} options.flavor - The device flavor, such as `iphone6`
* @param {string} options.os - The device operating system version
* @param {string} options.ipsw - The ID of a previously uploaded image in the project to use as the firmware
* @param {string} options.osbuild - The device operating system build
* @param {string} [options.snapshot] - The ID of snapshot to clone this device off of
* @param {string} [options.name] - The device name
* @param {string} [options.patches] - Instance patches, such as `jailbroken` (default), `nonjailbroken` or `corelliumd` which is non-jailbroken with API agent.
* @param {Object} [options.bootOptions] - Boot options for the instance
* @param {string} [options.bootOptions.kernelSlide] - Change the Kernel slide value for an iOS device.
* When not set, the slide will default to zero. When set to an empty value, the slide will be randomized.
* @param {string} [options.bootOptions.udid] - Predefined Unique Device ID (UDID) for iOS device
* @param {string} [options.bootOptions.screen] - Change the screen metrics for Ranchu devices `XxY[:DPI]`, e.g. `720x1280:280`
* @param {string[]} [options.bootOptions.additionalTags] - Addition features to utilize for the device, valid options include:<br>
* `kalloc` : Enable kalloc/kfree trace access via GDB (Enterprise only)<br>
* `gpu` : Enable cloud GPU acceleration (Extra costs incurred, cloud only)<br>
* `no-keyboard` : Enable keyboard passthrough from web interface<br>
* `nodevmode` : Disable developer mode on iOS 16+<br>
* `sep-cons-ext` : Patch SEPOS to print debug messages to console<br>
* `iboot-jailbreak` : Patch iBoot to disable signature checks<br>
* `llb-jailbreak` : Patch LLB to disable signature checks<br>
* `rom-jailbreak` : Patch BootROM to disable signature checks<br>
* @param {KernelImage} [options.bootOptions.kernel] - Custom kernel to pass to the device on creation.
* @param {vmmio[]} [vmmio] - VMMIO options for external MMIO support
* @returns {Promise<Instance>}
*
* @example <caption>Creating an instance and waiting for it to start its first boot</caption>
* const instance = await project.createInstance({
* flavor: 'iphone6',
* os: '11.3',
* name: 'Test Device',
* osbuild: '15E216',
* patches: 'corelliumd',
* bootOptions: {
* udid: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
* },
* });
* 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)
}
/**
* Get the VPN configuration to connect to the project network. This is only
* available for cloud. At least one instance must be on in the project.
*
* @param {string} type - Could be either "ovpn" or "tblk" to select between OpenVPN and TunnelBlick configuration formats.
* TunnelBlick files are delivered as a ZIP file and OpenVPN configuration is just a text file.
* @param {string} clientUUID - An arbitrary UUID to uniquely associate this VPN configuration with so it can be later identified
* in a list of connected clients. Optional.
* @returns {Promise<Buffer>}
* @example
* await project.vpnConfig('ovpn', undefined)
*/
async vpnConfig (type = 'ovpn', clientUUID) {
if (!clientUUID) clientUUID = uuidv4()
const response = await fetchApi(
this,
`/projects/${this.id}/vpn-configs/${clientUUID}.${type}`,
{ response: 'raw' }
)
return await response.buffer()
}
/** Destroy this project.
* @example
* project.destroy();
*/
async destroy () {
return await fetchApi(this, `/projects/${this.id}`, {
method: 'DELETE'
})
}
/**
* The project quotas.
* @returns {ProjectQuotas}
* @example
* // Create map of supported devices.
* let supported = {};
* (await corellium.supported()).forEach(modelInfo => {
* supported[modelInfo.name] = modelInfo;
* });
*
* // Get how many CPUs we're currently using.
* let cpusUsed = 0;
* instances.forEach(instance => {
* cpusUsed += supported[instance.flavor].quotas.cpus;
* });
*
* console.log('Used: ' + cpusUsed + '/' + project.quotas.cpus);
*/
get quotas () {
return this.info.quotas
}
set quotas (quotas) {
this.setQuotas(quotas)
}
/**
* Sets the project quotas. Only the cores property is currently respected.
*
* @param {ProjectQuotas} quotas
*/
async setQuotas (quotas) {
this.info.quotas = Object.assign({}, this.info.quotas, quotas)
await fetchApi(this, `/projects/${this.id}`, {
method: 'PATCH',
json: {
quotas: {
cores: quotas.cores || quotas.cpus
}
}
})
}
/**
* How much of the project's quotas are currently used. To ensure this information is up to date, call {@link Project#refresh()} first.
* @property {number} cores - Number of used CPU cores
* @example
* project.quotasUsed();
*/
get quotasUsed () {
return this.info.quotasUsed
}
/** The project's name.
* @example
* project.name();
*/
get name () {
return this.info.name
}
/**
* Returns a list of {@link Role}s associated with this project, showing who has permissions over this project.
*
* This function is only available to domain and project administrators.
* @return {Role[]}
* @example
* await project.roles();
*/
async roles () {
const roles = await this.client.roles()
return roles.get(this.id)
}
/**
* Give permissions to this project for a {@link Team} or a {@link User} (adds a {@link Role}).
*
* This function is only available to domain and project administrators.
* @param {User|Team} grantee - must be an instance of {@link User} or {@link Team}
* @param {string} type - user ID
* @example
* project.createRole(grantee, 'user');
*/
async createRole (grantee, type = 'user') {
await this.client.createRole(this.id, grantee, type)
}
/**
* Returns a list of authorized keys associated with the project. When a new
* instance is created in this project, its authorized_keys (iOS) or adbkeys
* (Android) will be populated with these keys by default. Adding or
* removing keys from the project will have no effect on existing instances.
*
* @returns {Promise<ProjectKey[]>}
* @example
* let keys = project.keys();
* for(let key of keys)
* console.log(key);
*/
async keys () {
return await this.client.projectKeys(this.id)
}
/**
* Add a public key to project.
*
* @param {string} key - public key, as formatted in a .pub file
* @param {'ssh'|'adb'} kind
* @param {string} [label] - defaults to the public key comment, if present
*
* @returns {Promise<ProjectKey>}
* @example
* project.addKey('ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA+eDLGqe+nefGQ2LjvXDlTXDuF33ZHD9wHk/oEICKYd', 'ssh', 'SSH Key');
*/
async addKey (key, kind = 'ssh', label = null) {
return await this.client.addProjectKey(this.id, key, kind, label)
}
/**
* Delete public key from the project
* @param {string} keyId
* @example
* project.deleteKey('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa');
*/
async deleteKey (keyId) {
return await this.client.deleteProjectKey(this.id, keyId)
}
/**
* Delete an IoT firmware
* @param {FirmwareImage} firmwareImage
*/
async deleteIotFirmware (firmwareImage) {
return await this.deleteImage(firmwareImage)
}
/**
* Delete a kernel
* @param {KernelImage} kernelImage
*/
async deleteKernel (kernelImage) {
return await this.deleteImage(kernelImage)
}
/**
* Delete a Image
* @param {Image} image
*/
async deleteImage (image) {
return await fetchApi(this, `/images/${image.id}`, {
method: 'DELETE'
})
}
/**
* Add a custom IoT firmware image to a project for use in creating new instances.
*
* @param {string} filePath - The path on the local file system to get the firmware file.
* @param {string} name - The name of the file to identify the file on the server. Usually the basename of the path.
* @param {Project~progressCallback} [progress] - The callback for file upload progress information.
*
* @returns {Promise<FirmwareImage>}
*/
async uploadIotFirmware (filePath, name, progress) {
return await this.uploadKernel(filePath, name, progress)
}
/**
* Add a kernel image to a project for use in creating new instances.
*
* @param {string} filePath - The path on the local file system to get the kernel file.
* @param {string} name - The name of the file to identify the file on the server. Usually the basename of the path.
* @param {Project~progressCallback} [progress] - The callback for file upload progress information.
*
* @returns {Promise<KernelImage>}
*/
async uploadKernel (filePath, name, progress) {
let tmpfile = null
const data = await util.promisify(fs.readFile)(filePath)
tmpfile = await compress(data, name)
const image = await this.uploadImage('kernel', tmpfile, name, progress)
if (tmpfile) {
fs.unlinkSync(tmpfile)
}
return { id: image.id, name: image.name }
}
/**
* Add a vmfile image to a project for use in creating new instances.
* @param {string} filePath - The path on the local file system to get the vmfile file
* @param {string} name - The name of the file to identify the file on the server, usually the basename of the path.
* @param {Project~progressCallback} [progress] - The callback for the file upload progress information.
*
* @returns {Promise<string>}
*/
async uploadVmfile (filePath, name, progress) {
const imageId = uuidv4()
const token = await this.getToken()
const url =
this.api +
'/projects/' +
encodeURIComponent(this.id) +
'/image-upload/' +
encodeURIComponent('vmfile') +
'/' +
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.
*
* @param {string} type - E.g. fw for the main firmware image.
* @param {string} filePath - The path on the local file system to get the file.
* @param {string} name - The name of the file to identify the file on the server. Usually the basename of the path.
* @param {Project~progressCallback} [progress] - The callback for file upload progress information.
*
* @returns {Promise<Image>}
*/
async uploadImage (type, filePath, name, progress) {
const imageId = uuidv4()
const token = await this.getToken()
const url =
this.api +
'/projects/' +
encodeURIComponent(this.id) +
'/image-upload/' +
encodeURIComponent(type) +
'/' +
encodeURIComponent(imageId) +
'/' +
encodeURIComponent(name)
await uploadFile(token, url, filePath, progress)
return { id: imageId, name }
}
}
module.exports = Project