Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
Change Log

v5.11.0
---
* Pro API: added `identifierNamesCache` support. The CLI's `--identifier-names-cache-path` now works with `--pro-api-token`, and `getIdentifierNamesCache()` on an `obfuscatePro()` result returns the cache

v5.10.1
---
* Fixed `controlFlowFlattening` changing `let` and `const` declarations into `var` inside functions passed as call arguments or used as operands, when `var` is the prevailing kind of variables. Fixes https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1471
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "javascript-obfuscator",
"version": "5.10.1",
"version": "5.11.0",
"description": "JavaScript obfuscator",
"keywords": [
"obfuscator",
Expand Down
8 changes: 8 additions & 0 deletions src/cli/JavaScriptObfuscatorCLI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import * as commander from 'commander';
import * as path from 'path';

import { TIdentifierNamesCache } from '../types/TIdentifierNamesCache';
import { TInputCLIOptions } from '../types/options/TInputCLIOptions';
import { TInputOptions } from '../types/options/TInputOptions';

Expand Down Expand Up @@ -750,6 +751,13 @@ export class JavaScriptObfuscatorCLI implements IInitializable {

this.obfuscatedCodeFileUtils.writeFile(outputCodePath, result.getObfuscatedCode());

const identifierNamesCache: TIdentifierNamesCache = result.getIdentifierNamesCache();

// Writing `null` would turn the cache off for the next file
if (identifierNamesCache) {
this.identifierNamesCacheFileUtils.writeFile(identifierNamesCache);
}

// Write source map if enabled and available
if (options.sourceMap && result.getSourceMap()) {
const outputSourceMapPath: string = this.obfuscatedCodeFileUtils.getOutputSourceMapPath(
Expand Down
9 changes: 9 additions & 0 deletions src/interfaces/pro-api/IProApiClient.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { TInputOptions } from '../../types/options/TInputOptions';
import { TIdentifierNamesCache } from '../../types/TIdentifierNamesCache';

export interface IProObfuscationResultInit {
code: string;
sourceMap?: string;
identifierNamesCache?: TIdentifierNamesCache;
}

/**
* Simplified obfuscation result for Pro API responses
* Does not extend IInitializable since results come from the API
Expand Down Expand Up @@ -112,6 +118,9 @@ export interface IProApiStreamMessage {
/** Source map (for 'result' or 'chunk_end' type) */
sourceMap?: string;

/** Identifier names cache, when the request sent one (for 'result' or 'chunk_end' type) */
identifierNamesCache?: TIdentifierNamesCache;

/** Field name for chunk: 'code' or 'sourceMap' (for 'chunk' type) */
field?: 'code' | 'sourceMap';

Expand Down
11 changes: 7 additions & 4 deletions src/pro-api/ProApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
IProApiStreamMessage,
IProCustomPreset,
IProObfuscationResult,
IProObfuscationResultInit,
IProQuota,
TProApiProgressCallback
} from '../interfaces/pro-api/IProApiClient';
Expand Down Expand Up @@ -533,18 +534,18 @@ export class ProApiClient {
throw new ApiError('No result received from API', 500);
}

return new ProApiObfuscationResult(result.code, result.sourceMap || '');
return new ProApiObfuscationResult(result);
}

/**
* Reassemble chunked streaming response
* Handles both chunked format (chunk/chunk_end) and direct result format
*/
// eslint-disable-next-line complexity
private reassembleChunkedResponse(messages: IProApiStreamMessage[]): { code: string; sourceMap: string } {
private reassembleChunkedResponse(messages: IProApiStreamMessage[]): IProObfuscationResultInit {
const codeChunks: string[] = [];
const sourceMapChunks: string[] = [];
let result = { code: '', sourceMap: '' };
let result: IProObfuscationResultInit = { code: '', sourceMap: '', identifierNamesCache: null };

for (const message of messages) {
switch (message.type) {
Expand All @@ -564,13 +565,15 @@ export class ProApiClient {
case 'chunk_end':
result.code = codeChunks.join('');
result.sourceMap = (sourceMapChunks.join('') || message.sourceMap) ?? '';
result.identifierNamesCache = message.identifierNamesCache ?? null;

break;

case 'result':
result = {
code: message.code ?? '',
sourceMap: message.sourceMap ?? ''
sourceMap: message.sourceMap ?? '',
identifierNamesCache: message.identifierNamesCache ?? null
};

break;
Expand Down
8 changes: 5 additions & 3 deletions src/pro-api/ProApiObfuscationResult.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { TIdentifierNamesCache } from '../types/TIdentifierNamesCache';
import { IProObfuscationResult } from '../interfaces/pro-api/IProApiClient';
import { IProObfuscationResult, IProObfuscationResultInit } from '../interfaces/pro-api/IProApiClient';

/**
* Pro API Obfuscation Result
Expand All @@ -8,10 +8,12 @@ import { IProObfuscationResult } from '../interfaces/pro-api/IProApiClient';
export class ProApiObfuscationResult implements IProObfuscationResult {
private readonly obfuscatedCode: string;
private readonly sourceMapValue: string;
private readonly identifierNamesCache: TIdentifierNamesCache;

public constructor(code: string, sourceMap: string = '') {
public constructor({ code, sourceMap = '', identifierNamesCache = null }: IProObfuscationResultInit) {
this.obfuscatedCode = code;
this.sourceMapValue = sourceMap;
this.identifierNamesCache = identifierNamesCache;
}

public getObfuscatedCode(): string {
Expand All @@ -23,7 +25,7 @@ export class ProApiObfuscationResult implements IProObfuscationResult {
}

public getIdentifierNamesCache(): TIdentifierNamesCache {
return null;
return this.identifierNamesCache;
}

public toString(): string {
Expand Down
81 changes: 81 additions & 0 deletions test/functional-tests/cli/JavaScriptObfuscatorCLI.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1590,6 +1590,87 @@ describe('JavaScriptObfuscatorCLI', function (): void {
}
});
});

describe('Variant #5: `--identifier-names-cache-path` option', () => {
const identifierNamesCachePath: string = path.join(
outputDirName,
'pro-api-identifier-names-cache.json'
);
const proApiOutputPath: string = path.join(outputDirName, 'pro-api-output5.js');

const obfuscateWithProApiAndCache = (): Promise<void> =>
JavaScriptObfuscatorCLI.obfuscate([
'node',
'javascript-obfuscator',
proApiFilePath,
'--output',
proApiOutputPath,
'--pro-api-token',
'test-token-123',
'--vm-obfuscation',
'true',
'--rename-globals',
'true',
'--identifier-names-cache-path',
identifierNamesCachePath
]);

afterEach(() => {
[identifierNamesCachePath, proApiOutputPath].forEach((filePath: string) => {
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
});
});

it('should send the cache file contents and write the cache returned by the API', async () => {
const identifierNamesCacheFromFile = {
globalIdentifiers: { fromPreviousFile: '_0x4d5e6f' },
propertyIdentifiers: {}
};
const identifierNamesCacheFromApi = {
globalIdentifiers: { fromPreviousFile: '_0x4d5e6f', test: '_0x1a2b3c' },
propertyIdentifiers: {}
};

fs.writeFileSync(identifierNamesCachePath, JSON.stringify(identifierNamesCacheFromFile));

fetchStub = sinon.stub(global, 'fetch').resolves({
ok: true,
status: 200,
text: async () =>
JSON.stringify({
type: 'result',
code: 'var obfuscated=1;',
identifierNamesCache: identifierNamesCacheFromApi
})
} as Response);

await obfuscateWithProApiAndCache();

const sentOptions = JSON.parse(fetchStub.firstCall.args[1].body).options;

assert.deepEqual(sentOptions.identifierNamesCache, identifierNamesCacheFromFile);
assert.deepEqual(
JSON.parse(fs.readFileSync(identifierNamesCachePath, 'utf8')),
identifierNamesCacheFromApi
);
});

it('should leave the cache file unchanged when the API returns no cache', async () => {
fs.writeFileSync(identifierNamesCachePath, '');

fetchStub = sinon.stub(global, 'fetch').resolves({
ok: true,
status: 200,
text: async () => JSON.stringify({ type: 'result', code: 'var obfuscated=1;' })
} as Response);

await obfuscateWithProApiAndCache();

assert.strictEqual(fs.readFileSync(identifierNamesCachePath, 'utf8'), '');
});
});
});

describe('`--pro-api-token` with a preset the local obfuscator cannot expand', () => {
Expand Down
60 changes: 59 additions & 1 deletion test/unit-tests/pro-api/ProApiClient.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,65 @@ describe('ProApiClient', () => {
});
});

describe('Variant #10: client-side blob upload for large files', () => {
describe('Variant #10: identifier names cache', () => {
const identifierNamesCacheFromApi = {
globalIdentifiers: { sharedHelper: '_0x1a2b3c' },
propertyIdentifiers: {}
};

it('should return the identifier names cache from a `result` message', async () => {
const client = new ProApiClient({ apiToken: 'test-token' });

fetchStub.resolves(
new Response(
JSON.stringify({
type: 'result',
code: 'var a=1;',
identifierNamesCache: identifierNamesCacheFromApi
}),
{ status: 200 }
)
);

const result = await client.obfuscate('const a = 1;', {
vmObfuscation: true,
identifierNamesCache: {}
});

assert.deepEqual(result.getIdentifierNamesCache(), identifierNamesCacheFromApi);
});

it('should return the identifier names cache from a `chunk_end` message', async () => {
const client = new ProApiClient({ apiToken: 'test-token' });

const chunks = [
JSON.stringify({ type: 'chunk', field: 'code', data: 'var a', index: 0, total: 2 }),
JSON.stringify({ type: 'chunk', field: 'code', data: '=1;', index: 1, total: 2 }),
JSON.stringify({ type: 'chunk_end', identifierNamesCache: identifierNamesCacheFromApi })
].join('\n');

fetchStub.resolves(new Response(chunks, { status: 200 }));

const result = await client.obfuscate('const a = 1;', {
vmObfuscation: true,
identifierNamesCache: {}
});

assert.deepEqual(result.getIdentifierNamesCache(), identifierNamesCacheFromApi);
});

it('should return null when the API sends no identifier names cache', async () => {
const client = new ProApiClient({ apiToken: 'test-token' });

fetchStub.resolves(new Response(JSON.stringify({ type: 'result', code: 'var a=1;' }), { status: 200 }));

const result = await client.obfuscate('const a = 1;', { vmObfuscation: true });

assert.isNull(result.getIdentifierNamesCache());
});
});

describe('Variant #11: client-side blob upload for large files', () => {
const UPLOAD_TOKEN_URL = 'https://obfuscator.io/api/v1/upload/token';
// Matches the server's inline cap: 4,400,000 bytes, just under
// Vercel's 4.5 MB (decimal) request body limit
Expand Down
Loading