frida-js
Version:
Pure-JS bindings to control Frida from node.js & browsers.
214 lines (181 loc) • 7.96 kB
text/typescript
import * as os from 'os';
import { Readable, PassThrough } from 'stream';
interface FridaDownloadOptions {
/**
* The Frida version to download - either a version like '16.0.19' or 'latest'
*/
version: string;
// The specific values below are the currently available platforms & architectures for Frida
// server as of v16.0.19.
/**
* The target Frida platform. Not all platforms are available, and these do not exactly match
* Node'js os.platform() values. If no platform is specified, this defaults to the current platform.
*
* If the platform + arch combination requested is not available, an error will be raised during
* the download process.
*/
platform?:
| 'android'
| 'freebsd'
| 'linux'
| 'macos'
| 'qnx'
| 'windows';
/**
* The target Frida architecture. Not all architecture are available, and these do not exactly match
* Node'js os.arch() values. If no arch is specified, this defaults to the current architecture.
*
* If the platform + arch combination requested is not available, an error will be raised during
* the download process.
*/
arch?:
| 'arm'
| 'arm64'
| 'arm64e'
| 'arm64-musl'
| 'armeabi'
| 'x86'
| 'x86_64'
| 'x86_64-musl'
| 'armhf'
| 'mips'
| 'mipsel'
| 'mips64'
| 'mips64el';
/**
* Optionally a GitHub token can be provided. If set it will be used to download Frida, thereby
* avoiding some GitHub rate limiting that can otherwise block downloads. If not set, a token
* from process.env.GITHUB_TOKEN will be used if available, or the request will be sent
* anonymously.
*/
ghToken?: string;
/**
* Optionally an SRI hash can be provided, which will be checked against the resulting server
* binary that's streamed. If the download does not match the hash at completion, the
* stream will thrown an error. This should be in the standard Subresource Integrity Hash format.
*
* This can only be used for fully specified download versions, as using the latest version,
* or automatic arch/platform logic, can never guarantee returning the same hash every time.
* To protect against this, if this is specified but any of version, platform & arch are
* not, an error will be thrown.
*
* Note that this is the hash of the binary itself, after extraction (the same output as the
* returned stream) not just the hash of the initial download.
*
* To get the hash for a given set of download options, pass the options to
* `calculateFridaSRI({ version, arch, platform })` to fetch & calculate a current hash directly.
*/
sri?: string;
}
export async function downloadFridaServer(options: FridaDownloadOptions) {
const { version, platform, arch, sri, ghToken } = options;
if (sri && (!version || version === 'latest' || !platform || !arch)) {
throw new Error(
'SRI cannot be used to download Frida unless fixed version, platform & arch values are provided'
);
}
const releaseDetails = await getFridaReleaseDetails(
version,
ghToken || process.env.GITHUB_TOKEN
);
const downloadUrl = findFridaDownloadUrl(releaseDetails.assets, {
version,
platform: platform || os.platform(),
arch: arch || os.arch()
});
return fetchFridaServer(downloadUrl, sri);
}
export async function getFridaReleaseDetails(version?: string, ghToken?: string): Promise<any> {
const headers: { Authorization: string } | {} = !!ghToken
? { Authorization: `token ${ghToken}` }
: {}
const response = await fetch(
`https://api.github.com/repos/frida/frida/releases/${
version && version !== 'latest'
? `tags/${version}`
: 'latest'
}`,
{ headers }
);
if (!response.ok) {
console.warn(`Frida releases ${response.status} response, body: `, await response.text(), '\n');
throw new Error(`Frida releases request rejected with ${response.status}`);
}
return response.json();
}
export type CalculateSRIOptions = Pick<FridaDownloadOptions, 'ghToken'> &
Required<Pick<FridaDownloadOptions, 'version' | 'arch' | 'platform'>>;
/**
* Calculates an SRI value for the given parameters, on a trust-on-first-download basis.
*
* This fetches and extracts the Frida server for the given parameters, and calculates the
* SRI details for the given content. Optionally, specific preferred hash algorithms can
* be provided if required.
*
* This returns a promise, resolving to an array of strings, one for each algorithm used.
* Any of these values can be used as an SRI argument for `downloadFridaServer`.
*/
export async function calculateFridaSRI(
options: CalculateSRIOptions,
hashOptions: { algorithms?: string[] } = {}
): Promise<string[]> {
if (!options.version || options.version === 'latest' || !options.arch || !options.platform) {
throw new Error('Cannot calculate SRI without fixed version, platform & arch values');
}
// We import SSRI on demand, just when it's required, to avoid bundling in browsers etc.
const SSRI = await import('ssri');
const fridaStream = await downloadFridaServer(options);
const results = await SSRI.fromStream(fridaStream, hashOptions);
return Object.values(results).flatMap((hash) => hash.toString());
}
function findFridaDownloadUrl(
assets: Array<{ name: string, browser_download_url: string }>,
releaseOptions: { version: string, platform: string, arch: string }
) {
let { version, arch, platform } = releaseOptions;
// Map some os.platform()/arch() results into Frida format:
if (platform === 'darwin') platform = 'macos';
if (platform === 'win32') platform = 'windows';
if (arch === 'x64') { arch = 'x86_64' };
let extension = platform === 'windows' ? 'exe\\.xz' : 'xz';
const assetRegex = new RegExp(`frida-server-[\\d.\\.]+-${platform}-${arch}\\.${extension}`);
const asset = assets.find((asset: { name: string }) => asset.name.match(assetRegex));
if (!asset) {
console.warn(`No Frida release asset found matching ${assetRegex.toString()}`);
throw new Error(`No ${version} frida-server download available for ${platform} ${arch}`);
}
return asset.browser_download_url;
}
async function fetchFridaServer(
downloadUrl: string,
sri?: string
): Promise<Readable> {
// We delay these imports until here, since they're not needed in most cases (where
// you're not downloading) and they can be heavy, so good to avoid in browser bundles:
const [{ XzReadableStream }, SSRI] = await Promise.all([
import('xz-decompress'),
import('ssri')
]);
const resultStream = sri
? SSRI.integrityStream({ integrity: sri })
: new PassThrough();
const assetDownload = await fetch(downloadUrl);
if (!assetDownload.ok) {
throw new Error(`Frida server download was unsuccessful, returned ${assetDownload.status}`);
}
if (!assetDownload.body) {
throw new Error('No body available for Frida server download');
}
// Actually start streaming the body next tick, to ensure there's time to set up
// any error handlers required on the returned stream before it begins.
setTimeout(() => {
// Decompress the .xz file to a raw file stream (no tar - it's a single file)
const decodeStream = Readable.fromWeb(
// Node web stream & DOM web stream types aren't a perfect match so we have to cast:
new XzReadableStream(assetDownload.body!) as any
);
decodeStream.pipe(resultStream);
decodeStream.on('error', (e) => resultStream.emit('error', e));
}, 0);
return resultStream;
}