Skip to content

Commit 6844df0

Browse files
author
sanex3339
committed
Added loader for javascript-obfuscator
1 parent 82744a4 commit 6844df0

File tree

9 files changed

+349
-179
lines changed

9 files changed

+349
-179
lines changed

readme.md renamed to README.md

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# javascript-obfuscator plugin for Webpack
1+
# javascript-obfuscator plugin and loader for Webpack
22

33
[![npm version](https://badge.fury.io/js/webpack-obfuscator.svg)](https://badge.fury.io/js/webpack-obfuscator)
44

@@ -8,7 +8,7 @@ Install the package with NPM and add it to your devDependencies:
88

99
`npm install --save-dev webpack-obfuscator`
1010

11-
### Usage:
11+
### Plugin usage:
1212

1313
```javascript
1414
var JavaScriptObfuscator = require('webpack-obfuscator');
@@ -17,9 +17,32 @@ var JavaScriptObfuscator = require('webpack-obfuscator');
1717

1818
// webpack plugins array
1919
plugins: [
20-
new JavaScriptObfuscator ({
21-
rotateStringArray: true
22-
}, ['excluded_bundle_name.js'])
20+
new JavaScriptObfuscator ({
21+
rotateStringArray: true
22+
}, ['excluded_bundle_name.js'])
23+
]
24+
```
25+
26+
### Loader usage:
27+
28+
Define a rule in your webpack config and use the obfuscator-loader as the last of your loaders for your modules. You can add the **enforce: 'post'** flag to ensure the loader will be called after normal loaders:
29+
30+
```javascript
31+
// webpack loader rules array
32+
rules: [
33+
{
34+
test: /\.js$/,
35+
exclude: [
36+
path.resolve(__dirname, 'excluded_file_name.js')
37+
],
38+
enforce: 'post',
39+
use: {
40+
loader: 'obfuscator-loader',
41+
options: {
42+
rotateStringArray: true
43+
}
44+
}
45+
}
2346
]
2447
```
2548

@@ -30,7 +53,7 @@ Options for [javascript-obfuscator](https://github.com/javascript-obfuscator/jav
3053

3154
**Warning:** right now plugin does not support `sourceMap` and `sourceMapMode` options!
3255

33-
### excludes
56+
### excludes (plugin only)
3457
Type: `Array` or `String` Default: `[]`
3558

3659
Bundle name is output file name after webpack compilation. With multiple webpack entries you can set bundle name in `output` object with aliases `[name]` or `[id]`.

dist/index.d.ts

Lines changed: 0 additions & 13 deletions
This file was deleted.

index.ts

Lines changed: 2 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,116 +1,3 @@
1-
"use strict";
1+
import {WebpackObfuscatorPlugin} from './plugin';
22

3-
import { Compiler, compilation } from 'webpack';
4-
import JavaScriptObfuscator, { ObfuscatorOptions } from 'javascript-obfuscator';
5-
import { RawSource } from 'webpack-sources';
6-
import multimatch from 'multimatch';
7-
import { RawSourceMap } from 'source-map';
8-
const transferSourceMap = require("multi-stage-sourcemap").transfer;
9-
10-
class WebpackObfuscator {
11-
/**
12-
* @type {string}
13-
*/
14-
private static readonly baseIdentifiersPrefix: string = 'a';
15-
16-
public excludes: string[] = [];
17-
18-
constructor(
19-
public options: ObfuscatorOptions = {},
20-
excludes?: string | string[]
21-
) {
22-
this.excludes = this.excludes.concat(excludes || []);
23-
}
24-
25-
public apply(compiler: Compiler): void {
26-
const isDevServer = process.argv.find(v => v.includes('webpack-dev-server'));
27-
28-
if (isDevServer) {
29-
console.info(
30-
'JavascriptObfuscator is disabled on webpack-dev-server as the reloading scripts ',
31-
'and the obfuscator can interfere with each other and break the build');
32-
return;
33-
}
34-
35-
const pluginName = this.constructor.name;
36-
37-
compiler.hooks.emit.tap(pluginName, (compilation: compilation.Compilation) => {
38-
let identifiersPrefixCounter: number = 0;
39-
const sourcemapOutput: {[index:string]: string} = {};
40-
41-
compilation.chunks.forEach(chunk => {
42-
chunk.files.forEach((fileName: string) => {
43-
if (this.options.sourceMap && fileName.toLowerCase().endsWith('.map')) {
44-
let srcName = fileName.toLowerCase().substr(0, fileName.length - 4);
45-
46-
if (!this.shouldExclude(srcName)) {
47-
const transferredSourceMap = transferSourceMap({
48-
fromSourceMap: sourcemapOutput[srcName],
49-
toSourceMap: compilation.assets[fileName].source()
50-
});
51-
const finalSourcemap = JSON.parse(transferredSourceMap);
52-
53-
finalSourcemap['sourcesContent'] = JSON.parse(compilation.assets[fileName].source())['sourcesContent'];
54-
compilation.assets[fileName] = new RawSource(JSON.stringify(finalSourcemap));
55-
}
56-
57-
return;
58-
}
59-
60-
if (!fileName.toLowerCase().endsWith('.js') || this.shouldExclude(fileName)) {
61-
return;
62-
}
63-
64-
const asset = compilation.assets[fileName]
65-
const { inputSource, inputSourceMap } = this.extractSourceAndSourceMap(asset);
66-
const { obfuscatedSource, obfuscationSourceMap } = this.obfuscate(inputSource, fileName, identifiersPrefixCounter);
67-
68-
if (this.options.sourceMap && inputSourceMap) {
69-
sourcemapOutput[fileName] = obfuscationSourceMap;
70-
}
71-
72-
compilation.assets[fileName] = new RawSource(obfuscatedSource);
73-
identifiersPrefixCounter++;
74-
});
75-
});
76-
});
77-
}
78-
79-
private shouldExclude(filePath: string): boolean {
80-
return multimatch(filePath, this.excludes).length > 0
81-
}
82-
83-
private extractSourceAndSourceMap(asset: any): { inputSource: string, inputSourceMap: RawSourceMap } {
84-
if (asset.sourceAndMap) {
85-
const { source, map } = asset.sourceAndMap();
86-
return { inputSource: source, inputSourceMap: map };
87-
} else {
88-
return {
89-
inputSource: asset.source(),
90-
inputSourceMap: asset.map()
91-
}
92-
}
93-
}
94-
95-
private obfuscate(
96-
javascript: string,
97-
fileName: string,
98-
identifiersPrefixCounter: number
99-
): { obfuscatedSource: string, obfuscationSourceMap: string } {
100-
const obfuscationResult = JavaScriptObfuscator.obfuscate(
101-
javascript,
102-
{
103-
identifiersPrefix: `${WebpackObfuscator.baseIdentifiersPrefix}${identifiersPrefixCounter}`,
104-
sourceMapFileName: fileName + '.map',
105-
...this.options
106-
}
107-
);
108-
109-
return {
110-
obfuscatedSource: obfuscationResult.getObfuscatedCode(),
111-
obfuscationSourceMap: obfuscationResult.getSourceMap()
112-
}
113-
}
114-
}
115-
116-
export = WebpackObfuscator;
3+
module.exports = WebpackObfuscatorPlugin;

loader/index.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
"use strict";
2+
3+
import JavaScriptObfuscator from 'javascript-obfuscator';
4+
import estraverse from 'estraverse';
5+
import * as ESTree from 'estree';
6+
import loaderUtils from 'loader-utils';
7+
import * as acorn from 'acorn';
8+
9+
class WebpackObfuscatorLoaderHelper {
10+
/**
11+
* @type {acorn.Options['sourceType'][]}
12+
*/
13+
private static readonly sourceTypes: acorn.Options['sourceType'][] = [
14+
'script',
15+
'module'
16+
];
17+
18+
/**
19+
* @param {string} sourceCode
20+
* @returns {string}
21+
*/
22+
public static getCommentedSource (sourceCode: string): string {
23+
// Parses source code and collects require expression nodes
24+
const entries: {
25+
start: number;
26+
end: number;
27+
}[] = [];
28+
const astTree: ESTree.Program = WebpackObfuscatorLoaderHelper.parseCode(sourceCode);
29+
30+
estraverse.traverse(astTree, {
31+
enter: (node: ESTree.Node): void => {
32+
if (WebpackObfuscatorLoaderHelper.isRequire(node) && node.range) {
33+
entries.push({
34+
start: node.range[0],
35+
end: node.range[1],
36+
});
37+
}
38+
}
39+
});
40+
41+
// Wraps requires in conditional comments
42+
let commentedSource: string = sourceCode.slice();
43+
44+
entries
45+
.sort((a, b) => b.end - a.end)
46+
.forEach((n) => {
47+
const before = commentedSource.slice(0, n.start);
48+
const mid = commentedSource.slice(n.start, n.end);
49+
const after = commentedSource.slice(n.end);
50+
51+
commentedSource = `${before}/* javascript-obfuscator:disable */${mid}/* javascript-obfuscator:enable */${after}`;
52+
});
53+
54+
return commentedSource;
55+
}
56+
57+
/**
58+
* @param {string} sourceCode
59+
* @returns {ESTree.Program}
60+
*/
61+
private static parseCode (sourceCode: string): ESTree.Program {
62+
const sourceTypeLength: number = WebpackObfuscatorLoaderHelper.sourceTypes.length;
63+
64+
for (let i: number = 0; i < sourceTypeLength; i++) {
65+
try {
66+
return WebpackObfuscatorLoaderHelper.parseType(sourceCode, WebpackObfuscatorLoaderHelper.sourceTypes[i]);
67+
} catch (error) {
68+
if (i < sourceTypeLength - 1) {
69+
continue;
70+
}
71+
72+
throw new Error(error);
73+
}
74+
}
75+
76+
throw new Error('Acorn parsing error');
77+
}
78+
79+
/**
80+
* @param {string} sourceCode
81+
* @param {acorn.Options["sourceType"]} sourceType
82+
* @returns {Program}
83+
*/
84+
private static parseType (
85+
sourceCode: string,
86+
sourceType: acorn.Options['sourceType']
87+
): ESTree.Program {
88+
const config: acorn.Options = {sourceType};
89+
90+
return <any>acorn.parse(sourceCode, config);
91+
}
92+
93+
/**
94+
* @param {ESTree.Node} node
95+
* @returns {boolean}
96+
*/
97+
private static isRequire (node: ESTree.Node) {
98+
return node.type === 'CallExpression'
99+
&& node.callee.type === 'Identifier'
100+
&& node.callee.name === 'require';
101+
}
102+
}
103+
104+
/**
105+
* JavaScript Obfuscator loader based on `obfuscator-loader` package
106+
*/
107+
function Loader (sourceCode: string) {
108+
// Obfuscates commented source code
109+
// @ts-ignore
110+
const options = loaderUtils.getOptions(this) || {};
111+
const commentedSourceCode: string = WebpackObfuscatorLoaderHelper.getCommentedSource(sourceCode);
112+
const obfuscationResult = JavaScriptObfuscator.obfuscate(commentedSourceCode, options);
113+
114+
return obfuscationResult.getObfuscatedCode();
115+
}
116+
117+
export = Loader;

0 commit comments

Comments
 (0)