'use strict'
const { fetch, fetchApi, CorelliumError } = require('./util/fetch')
const Project = require('./project')
const Team = require('./team')
const User = require('./user')
const Role = require('./role')
const WebPlayer = require('./webplayer')
const { I } = require('./input')
const { listImagesMetaData } = require('./images')
/**
* @typedef {object} SupportedDevice
* @property {string} type
* @property {string} name
* @property {string} flavor
* @property {string} description
* @property {string} model
* @property {Object} firmwares
* @property {string} firmwares.version
* @property {string} firmwares.buildid
* @property {string} firmwares.sha256sum
* @property {string} firmwares.sha1sum
* @property {string} firmwares.md5sum
* @property {integer} firmwares.size
* @property {string} firmwares.uniqueid
* @property {string} firmwares.metadata
* @property {string} firmwares.releasedate - ISO datetime string
* @property {string} firmwares.uploaddate - ISO datetime string
* @property {string} firmwares.url
* @property {string} firmwares.orig_url
* @property {string} firmwares.filename
* @property {Object} quotas
* @property {integer} quotas.cores
* @property {integer} quotas.cpus
*/
/**
* @typedef {object} Token
* @property {string} token
* @property {string} expiration
*/
/**
* The Corellium API client.
*/
class Corellium {
/**
* Create a new Corellium client.
* @constructor
* @param {Object} options
* @param {string} options.endpoint - Endpoint URL
* @param {string?} options.apiToken - Login apiToken
* @param {string?} options.username - Login username
* @param {string?} options.password - Login password
* @param {Token?} options.token - Login token
* @param {string?} options.totpToken - Login TOTP (Timebased One Time Password)
* @example
* const corellium = new Corellium({
* endpoint: 'https://app.corellium.com',
* username: 'username',
* password: 'password',
* totpToken: '123456',
* });
*/
constructor (options) {
this.options = options
this.api = options.endpoint + '/api/v1'
this.token = null
this.supportedDevices = null
this._teams = null
}
/**
* Returns refreshed authentication token
* @return {string} token
* @example
* let token = await corellium.getToken()
*/
async getToken () {
const token = this.options.token || this.token
// If the token is more than 15 minutes from expiring, we don't need to refresh it.
if (token) {
const maxExpiration = new Date(new Date().getTime() + 15 * 60 * 1000)
if (Promise.resolve(token) === token) {
const tokenObj = await token
if (tokenObj.expiration > maxExpiration) {
return tokenObj.token
}
}
const expiration = typeof token.expiration === 'string' ? Date.parse(token.expiration) : token.expiration
if (expiration > maxExpiration) {
return token.token
}
}
const postData = {}
const fetchOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
json: postData
}
if (this.options.apiToken) {
postData.apiToken = this.options.apiToken
} else if (this.options.username && this.options.password) {
postData.username = this.options.username
postData.password = this.options.password
if (this.options.totpToken) {
postData.totpToken = this.options.totpToken
}
} else if (token) {
// renew using current token
fetchOptions.headers.Authorization = token.token
}
this.token = (async () => {
const res = await fetch(`${this.api}/tokens`, fetchOptions)
return {
token: res.token,
expiration: new Date(res.expiration)
}
})()
return (await this.token).token
}
/**
* Generate an API token to be used with the API. This is
* non-recoverable, so if it is lost, you must generate a new one.
*
* This can be used to for the login method by passing it as the apiToken
*
* @returns {string} apiToken
*/
async generateApiToken () {
const response = await fetchApi(this, '/apitoken', {
method: 'POST'
})
return response
}
/**
* Remove the currently active api token from this user account.
*/
async removeApiToken () {
await fetchApi(this, '/apitoken', {
method: 'DELETE'
})
}
/**
* Logs into the Corellium API and obtains an authentication token. Does
* nothing if the current authentication token is up to date.
*
* Calling this method is not required, as calling any other method that
* needs an authentication token will do the same thing.
* @example
* await corellium.login();
*/
async login () {
await this.getToken()
}
/**
* Returns an array of {@link Project}s that this client is allowed to
* access.
* @returns {Promise<Project[]>}
* @example
* let projects = await corellium.projects();
* let project = projects.find(project => project.name === "Demo Project");
*/
async projects () {
const projects = await fetchApi(this, '/projects?ids_only=1')
return await Promise.all(projects.map(project => this.getProject(project.id)))
}
/**
* Returns an array of {@link Image}s that this client is allowed to
* access.
* @returns {Promise<Image[]>}
* @example
* let images = await corellium.files();
*/
async files () {
return listImagesMetaData(this)
}
/**
* Returns teams and users belonging to the domain.
*
* This function is only available to administrators.
*
* @returns {Promise<{ teams: Map<string, Team>, users: Map<string, User>}>}
* @example
* let teamsAndUsers = await corellium.getTeamsAndUsers();
*/
async getTeamsAndUsers () {
const teams = (this._teams = new Map())
for (const team of await fetchApi(this, '/teams')) {
teams.set(team.id, new Team(this, team))
}
const users = (this._users = new Map())
for (const user of teams.get('all-users').info.users) {
users.set(user.id, new User(this, user))
}
return { teams, users }
}
/**
* Returns {@link Role}s belonging to the domain.
*
* This function is only available to domain and project administrators.
* @return {Promise<Map<string, Role[]>>}
* @example
* let roles = await corellium.roles();
*/
async roles () {
const roles = (this._roles = new Map())
for (const role of await fetchApi(this, '/roles')) {
let rolesForProject = roles.get(role.project)
if (!rolesForProject) {
rolesForProject = []
roles.set(role.project, rolesForProject)
}
rolesForProject.push(new Role(this, role))
}
return roles
}
/**
* Returns {@link Team}s belonging to the domain.
*
* This function is only available to domain and project administrators.
* @return {Promise<Map<string, Team>>}
* @example
* let teams = await corellium.teams();
*/
async teams () {
return (await this.getTeamsAndUsers()).teams
}
/**
* Returns {@link User}s belonging to the domain.
*
* This function is only available to domain and project administrators.
* @return {Promise<Map<string, User>>}
* @example
* let users = await corellium.users();
*/
async users () {
return (await this.getTeamsAndUsers()).users
}
/**
* Given a user id, returns the {@link User}.
*
* This function is only available to domain and project administrators.
* @returns {Promise<User>}
* @example
* let user = await instance.getUser('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa');
*/
getUser (id) {
return this._users.get(id)
}
/**
* Given a team id, returns the {@link Team}.
*
* This function is only available to domain and project administrators.
* @returns {Promise<Team>}
* @example
* let team = await instance.getTeam('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa');
*/
getTeam (id) {
return this._teams.get(id)
}
/**
* Creates a new user in the domain.
*
* This function is only available to domain administrators.
* @returns {Promise<User>}
* @example
* let user = await instance.createUser("login", "User Name", "user@email.com", "password");
*/
async createUser (login, name, email, password) {
const response = await fetchApi(this, '/users', {
method: 'POST',
json: {
label: name,
name: login,
email,
password
}
})
await this.getTeamsAndUsers()
return this.getUser(response.id)
}
/**
* Destroys a user in the domain.
*
* This function is only available to domain administrators.
* @param {string} id - user ID
* @example
* instance.destroyUser('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa');
*/
async destroyUser (id) {
await fetchApi(this, `/users/${id}`, {
method: 'DELETE'
})
}
/**
* Creates a {@link Role} for a {@link Project} and a {@link Team} or {@link User}.
* @param {string} project - project ID
* @param {User|Team} grantee - must be an instance of {@link User} or {@link Team}
* @param {string} type - user ID
* @example
* instance.createRole(project.id, grantee, 'user');
*/
async createRole (project, grantee, type = 'user') {
let usersOrTeams = grantee instanceof User && 'users'
if (!usersOrTeams) {
usersOrTeams = grantee instanceof Team && 'teams'
}
if (!usersOrTeams) {
// eslint-disable-next-line no-throw-literal
throw 'Grantee not User or Team'
}
await fetchApi(this, `/roles/projects/${project}/${usersOrTeams}/${grantee.id}/roles/${type}`, {
method: 'PUT'
})
}
/**
* Destroys a {@link Role}
* @param {Role} role - role object
* @example
* instance.destroyRole(role);
*/
async destroyRole (role) {
const usersOrTeams = role.isUser ? 'users' : 'teams'
await fetchApi(
this,
`/roles/projects/${role.project}/${usersOrTeams}/${role.grantee.id}/roles/{$role.type}`,
{
method: 'DELETE'
}
)
}
/**
* Returns the {@link Project} with the given ID.
* @param {string} projectId - project ID
* @returns {Promise<Project>}
* @example
* let project = await corellium.getProject('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa');
*/
async getProject (projectId) {
const project = new Project(this, projectId)
await project.refresh()
return project
}
/**
* Creates a {@link Project} with the given name {@link Color} and {@link ProjectSettings}.
* @param {string} name - project name
* @param {integer} color - color
* @param {Object} [settings] - project settings
* @param {boolean} settings.internet-access
* @returns {Promise<Project>}
* @example
* corellium.createProject("TestProject");
*/
async createProject (name, color = 1, settings = { 'internet-access': true }) {
const response = await fetchApi(this, '/projects', {
method: 'POST',
json: {
name,
color,
settings
}
})
return await this.getProject(response.id)
}
/**
* Returns the {@link Project} with the given name. If the project doesn't
* exist, returns undefined.
* @param {string} name - project name to match
* @returns {Promise<Project>}
* @example
* let project = await corellium.projectNamed('Default Project');
*/
async projectNamed (name) {
const projects = await this.projects()
return projects.find(project => project.name === name)
}
/** Returns supported device list
* @return {SupportedDevice[]}
* @example
* let supported = await corellium.supported();
*/
async supported () {
if (!this.supportedDevices) {
this.supportedDevices = await fetchApi(this, '/supported')
}
return this.supportedDevices
}
/** Returns all keys for the project
* @param {string} project - project ID
* @return {ProjectKey[]}
* @example
* let keys = instance.projectKeys(project.id);
* for(let key of keys)
* console.log(key);
*/
async projectKeys (project) {
return await fetchApi(this, `/projects/${project}/keys`)
}
/** Adds key to the project
* @param {string} project - project ID
* @param {string} key - public key, as formatted in a .pub file
* @param {string} kind - key type ('ssh'/'abd')
* @param {string} [label] - key label
* @return {string} key ID
* @example
* let project = instance.getProjectNamed('TestProject');
* instance.addProjectKey(project.id, 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA+eDLGqe+nefGQ2LjvXDlTXDuF33ZHD9wHk/oEICKYd', 'ssh', 'SSH Key');
*/
async addProjectKey (project, key, kind = 'ssh', label = null) {
return await fetchApi(this, `/projects/${project}/keys`, {
method: 'POST',
json: {
key,
label,
kind
}
})
}
/** Adds key to the project
* @param {string} project - project ID
* @param {string} keyId - key ID
* @example
* let project = instance.getProjectNamed('TestProject');
* instance.deleteProjectKey(project.id, 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa');
*/
async deleteProjectKey (project, keyId) {
return await fetchApi(this, `/projects/${project}/keys/${keyId}`, {
method: 'DELETE'
})
}
/**
* Attempts to retrieve Instance by iterating through all projects until the instance is found.
*
* @param {string} instanceId
* @returns {Promise<Instance>}
* @example
* await corellium.instance('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa')
*/
async getInstance (instanceId) {
let lastError
const projects = await this.projects()
for (const project of projects) {
try {
const instance = await project.getInstance(instanceId)
if (instance.info.state !== 'on') {
throw new Error('The instance is not turned on')
}
return instance
} catch (err) {
if (!(err instanceof CorelliumError)) {
throw err
} else {
lastError = err
}
}
}
throw lastError || new Error(`Could not retrieve instance! instanceId=${instanceId}`)
}
}
module.exports = {
Corellium,
CorelliumError,
I,
WebPlayer
}