diff --git a/.gitignore b/.gitignore index b9dfcf3..4fb4f65 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ +jscrambler_keys.json node_modules npm-debug.log +results/ tmp -.* \ No newline at end of file +.* diff --git a/.jshintrc b/.jshintrc index d93d275..61baa00 100644 --- a/.jshintrc +++ b/.jshintrc @@ -15,12 +15,19 @@ "undef": true, "unused": "vars", "strict": false, + "globalstrict": true, "trailing": false, "validthis": true, + "expr": true, "globals": { + "Buffer": true, "define": true, "module": true, "require": true, - "exports": true + "exports": true, + "setTimeout": true, + "console": true, + "process": true, + "escape": true } } diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..006a6e9 --- /dev/null +++ b/.npmignore @@ -0,0 +1,3 @@ +config.json +jscrambler_keys.json +results/ diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..7891a25 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,22 @@ +module.exports = function (grunt) { + grunt.initConfig({ + clean: { + test: ['results/'] + }, + jasmine_node: { + options: { + forceExit: true, + match: '.', + matchall: true, + extensions: 'js' + }, + test: ['test/specs/'] + } + }); + + grunt.loadNpmTasks('grunt-contrib-clean'); + grunt.loadNpmTasks('grunt-jasmine-node'); + + grunt.registerTask('default', ['test']); + grunt.registerTask('test', ['clean', 'jasmine_node', 'clean']); +}; diff --git a/README.md b/README.md index d2d7a20..ed5c6eb 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,133 @@ -# JSCrambler Client for Node.js +# JScrambler Client for Node.js + +- [RC configuration](#rc-configuration) +- [CLI](#cli) + - [Required Fields](#required-fields) + - [Output to a single file](#output-to-a-single-file) + - [Output multiple files to a directory](#output-multiple-files-to-a-directory) + - [Using minimatch](#using-minimatch) + - [Using configuration file](#using-configuration-file) +- [API](#api) + - [Upload/download example](#uploaddownload-example) +- [JScrambler Options](#jscrambler-options) + - [asserts_elimination](#asserts_elimination) + - [browser_os_lock](#browser_os_lock) + - [constant_folding](#constant_folding) + - [dead_code](#dead_code) + - [dead_code_elimination](#dead_code_elimination) + - [debugging_code_elimination](#debugging_code_elimination) + - [dictionary_compression](#dictionary_compression) + - [domain_lock](#domain_lock) + - [domain_lock_warning_function](#domain_lock_warning_function) + - [dot_notation_elimination](#dot_notation_elimination) + - [exceptions_list](#exceptions_list) + - [expiration_date](#expiration_date) + - [expiration_date_warning_function](#expiration_date_warning_function) + - [function_outlining](#function_outlining) + - [function_reorder](#function_reorder) + - [ignore_files](#ignore_files) + - [literal_hooking](#literal_hooking) + - [duplicate_literals](#duplicate_literals) + - [member_enumeration](#member_enumeration) + - [mode](#mode) + - [name_prefix](#name_prefix) + - [rename_all](#rename_all) + - [rename_local](#rename_local) + - [self_defending](#self_defending) + - [string_splitting](#string_splitting) + - [whitespace](#whitespace) + +## RC configuration +You may put your access and secret keys into a config file if found in [these directories](https://github.com/dominictarr/rc#standards). Besides simplifying the command entry, this has the added benefit of not logging your JScrambler credentials. + +Here's an example of what your `.jscramblerrc` file should look like: + +```javascript +{ + "keys": { + "accessKey": "XXXXXX", + "secretKey": "XXXXXX" + }, + "params": { + "self_defending": "%DEFAULT%" + } +} +``` +Replace the `XXXXXX` fields with your values of course. :) ## CLI -```shell -npm install -g jscrambler +```bash +npm install -g jscrambler@^0 ``` Usage: jscrambler [source files] [options] Options: - -h, --help output usage information - -V, --version output the version number - -c, --config [config] JScrambler configuration options - -o, --output Output directory - -a, --access-key Access key - -s, --secret-key Secret key + --help + -V, --version + -c, --config [config] + -o, --output [output] + -a, --access-key + -s, --secret-key + -h, --host [host] + -p, --port [port] + -v, --api-version [apiVersion] + --asserts-elimination [assertsElimination] + --browser-os-lock [browserOsLock] + --constant-folding + --dead-code + --dead-code-elimination + --debugging-code-elimination [debuggingCodeElimination] + --dictionary-compression + --domain-lock [domainLock] + --domain-lock-warning-function [domainLockWarningFunction] + --dot-notation-elimination + --exceptions-list [exceptionsList] + --expiration-date [expirationDate] + --expiration-date-warning-function [expirationDateWarningFunction] + --function-outlining + --function-reorder + --ignore-files [ignoreFiles] + --literal-hooking [literalHooking] + --literal-duplicates + --member-enumeration + --mode [mode] + --name-prefix [namePrefix] + --rename-all + --rename-local + --self-defending + --string-splitting [stringSplitting] + --whitespace + + +### Required Fields +When making API requests you must pass valid secret and access keys, through the command line or by having a `.jscramblerrc` file. These keys are each 40 characters long, alpha numeric strings, and uppercase. You can find them in your jscramber web dashboard under `My Account > Api access`. In the examples these are shortened to `XXXX` for the sake of readability. + +### Output to a single file +```bash +jscrambler input.js -a XXXX -s XXXX > output.js +``` + +### Output multiple files to a directory +```bash +jscrambler input1.js input2.js -o dest/ -a XXXX -s XXXX +``` + +### Using minimatch +```bash +jscrambler "lib/**/*.js" -o dest/ -a XXXX -s XXXX +``` + +### Using configuration file +```bash +jscrambler input.js -s XXXX -a XXXX -c ./config.json > output.js +``` +where `config.json` is an object optionally containing any of the JScrambler options listed [here](#jscrambler-options), using the structure described [in the RC configuration](#rc-config). + ## API -```shell -npm install jscrambler +```bash +npm install jscrambler@^0 ``` ### Upload/download example ```js @@ -25,8 +135,10 @@ var fs = require('fs-extra'); var jScrambler = require('jscrambler'); var client = new jScrambler.Client({ - accessKey: '', - secretKey: '' + keys: { + accessKey: '', // not needed if you have it on your `.jscramblerrc` + secretKey: '' // not needed if you have it on your `.jscramblerrc` + } }); jScrambler @@ -48,7 +160,10 @@ jScrambler return jScrambler.downloadCode(client, res.id); }) .then(function (res) { - fs.outputFileSync('dist.zip', res); + return fs.writeFileSync('dist.zip', res); + }) + .catch(function (error) { + console.log(error); }); ``` @@ -63,6 +178,24 @@ Type: `String` Remove function definitions and function calls with a given name. +### browser_os_lock +Type: `String` + +Locks a JavaScript application to run only on a specific Browser or Operating System. + +available values: + +* `firefox` +* `chrome` +* `iexplorer` +* `linux` +* `windows` +* `mac_os` +* `tizen` +* `android` +* `ios` + + ### constant_folding Type: `String` @@ -70,6 +203,11 @@ Type: `String` Simplifies constant expressions at compile-time to make your code faster at run-time. +### cwd +Type: `String` + +Sets the current working directory of JScrambler. Use this to compile a project which is located under another folder. + ### dead_code Type: `String` @@ -105,6 +243,13 @@ Type: `String` Locks your project to a list of domains you specify. +### domain_lock_warning_function +Type: `String` + +`name` - your domain lock warning function. + +Executes a function whenever there's a domain lock violation. + ### dot_notation_elimination Type: `String` @@ -119,13 +264,20 @@ Type: `String` There are some names that should never be replaced or reused to create new declarations e.g. document, toUpperCase. Public declarations existing in more than one source file should not be replaced if you submit only a part of the project where they appear. Therefore a list of irreplaceable names and the logic to make distinction between public and local names already exists on JScrambler to avoid touching those names. Use this parameter to add your own exceptions. -### expiration_date: +### expiration_date Type: `String` `date` - date format YYYY/MM/DD Sets your JavaScript to expire after a date of your choosing. +### expiration_date_warning_function +Type: `String` + +`name` - your expiration date warning function. + +Executes a function whenever there's an expiration date violation. + ### function_outlining Type: `String` @@ -154,10 +306,10 @@ Type: `String` Replaces literals by a randomly sized chain of ternary operators. You may configure the minimum and maximum number of predicates per literal, as the occurrence probability of the transformation. This allows you to control how big the obfuscated JavaScript grows and the potency of the transformation. -### literal_duplicates +### duplicate_literals Type: `String` -`%DEFAULT%` - enable literal duplicates +`%DEFAULT%` - enable duplicate literals Replaces literal duplicates by a symbol. @@ -171,15 +323,23 @@ Replaces Browser and HTML DOM objects by a member enumeration. ### mode Type: `String` -`starter` - Standard protection and optimization behavior. Enough for most JavaScript applications -`mobile` - Transformations are applied having into account the limitations and needs of mobile devices -`html5` - Protects your HTML5 and Web Gaming applications by targeting the new HTML5 features +* `starter` - Standard protection and optimization behavior. Enough for most * JavaScript applications +* `mobile` - Transformations are applied having into account the limitations and needs of mobile devices +* `html5` - Protects your HTML5 and Web Gaming applications by targeting the new HTML5 features +* `nodejs` - Protects your Node.js application ### name_prefix Type: `String` Set a prefix to be appended to the new names generated by JScrambler. +### rename_all +Type: `String` + +`%DEFAULT%` - enable rename all + +Renames all identifiers found at your source code. By default, there is a list of JavaScript and HTML DOM names that will not be replaced. If you need to add additional exceptions use the exceptions_list parameter. + ### rename_local Type: `String` @@ -187,7 +347,16 @@ Type: `String` Renames local names only. The best way to replace names without worrying about name dependencies. -### string_splitting: + +### self_defending +Type: `String` + +`%DEFAULT%` - enable self defending + +Obfuscates functions and objects concealing their logic and thwarting code tampering attempts by using anti-tampering and anti-debugging techniques. Attempts to tamper the code will break its functionality and using JavaScript debuggers will trigger defenses to thwart analysis. + + +### string_splitting Type: `String` `occurrences[;concatenation]` diff --git a/bin/jscrambler b/bin/jscrambler index 0e0d72d..174aeea 100755 --- a/bin/jscrambler +++ b/bin/jscrambler @@ -2,41 +2,138 @@ 'use strict'; var commander = require('commander'); -var fs = require('fs-extra'); +var glob = require('glob'); var jScrambler = require('../jscrambler'); +var cli = require('../lib/cli'); var path = require('path'); commander .version(require('../package.json').version) .usage('[source files] [options]') .option('-c, --config [config]', 'JScrambler configuration options') - .option('-o, --output ', 'Output directory') + .option('-o, --output [output]', 'Output directory. If not specified the output is printed.') .option('-a, --access-key ', 'Access key') .option('-s, --secret-key ', 'Secret key') + .option('-h, --host [host]', 'Hostname') + .option('-p, --port [port]', 'Port') + .option('-v, --api-version [apiVersion]', 'Version') + .option('--asserts-elimination [assertsElimination]', 'Remove function definitions and function calls with a given name.') + .option('--browser-os-lock [browserOsLock]', 'Locks a JavaScript application to run only on a specific Browser or Operating System.') + .option('--constant-folding', 'Simplifies constant expressions at compile-time to make your code faster at run-time.') + .option('--dead-code', 'Randomly injects dead code into the source code.') + .option('--dead-code-elimination', 'Removes dead code and void code from your JavaScript.') + .option('--debugging-code-elimination [debuggingCodeElimination', 'Removes statements and public variable declarations used to control the output of debugging messages that help you debug your code.') + .option('--dictionary-compression', 'further shrink your source code') + .option('--domain-lock [domainLock]', 'Locks your project to a list of domains you specify.') + .option('--domain-lock-warning-function [domainLockWarningFunction]', 'Domain lock warning function.') + .option('--dot-notation-elimination', 'Transforms dot notation to subscript notation.') + .option('--exceptions-list [exceptionsList]', 'list of exceptions that will never be replaced or used to create new declarations') + .option('--expiration-date [expirationDate]', 'Sets your JavaScript to expire after a date (YYYY/MM/DD) of your choosing.') + .option('--expiration-date-warning-function [expirationDateWarningFunction]', 'Expiration date warning function.') + .option('--function-outlining', 'Turns statements into new function declarations.') + .option('--function-reorder', 'Randomly reorders your source code\'s function declarations.') + .option('--ignore-files [ignoreFiles]', 'Define a list of files (relative paths) that JScrambler must ignore.') + .option('--literal-hooking [literalHooking]', 'Replaces literals by a randomly sized chain of ternary operators. ') + .option('--literal-duplicates', 'Replaces literal duplicates by a symbol.') + .option('--member-enumeration', 'Replaces Browser and HTML DOM objects by a member enumeration.') + .option('--mode [mode]', 'protection mode starter|mobile|html5|nodejs') + .option('--name-prefix [namePrefix]', 'Set a prefix to be appended to the new names generated by JScrambler.') + .option('--preserve-annotations', 'Preserve JavaScript comments containing annotations.') + .option('--rename-all [mode]', 'Renames all identifiers found at your source code.') + .option('--rename-include [includeList]', 'list of names that you want to force renaming.') + .option('--rename-local', 'Renames local names only.') + .option('--self-defending [threshold]', 'thwarting code tampering attempts by using anti-tampering and anti-debugging techniques.') + .option('--string-splitting [stringSplitting]', 'split strings based on percentage of occurence in the source code input') + .option('--whitespace', 'enable whitespace') .parse(process.argv); -if (!commander.output || !commander.accessKey || !commander.secretKey) { - console.log(commander.help()); + +var globSrc; + +// If -c, --config file was provided +if (commander.config) { + // We're using `commander` (CLI) as the source of all truths, falling back to + // the `config` provided by the file passed as argument + var config = require(path.resolve(commander.config, '.')); + commander.output = commander.output || config.filesDest; + // No need to validate if `keys` are set, later `client` will do so, but first + // will try to load to the configuration through an `rc` file + if (config.keys) { + commander.accessKey = commander.accessKey || config.keys.accessKey; + commander.secretKey = commander.secretKey || config.keys.secretKey; + } + commander.host = commander.host || config.host; + commander.port = commander.port || config.port; + commander.apiVersion = commander.apiVersion || config.apiVersion; + globSrc = config.filesSrc; +} + +// If src paths have been provided +if (commander.args.length > 0) { + globSrc = commander.args; +} + +if (!globSrc) { + commander.outputHelp(); + process.exit(1); } -var filesSrc = commander.args; +var filesSrc = []; +// Iterate `globSrc` to build a list of source files into `filesSrc` +for (var i = 0, l = globSrc.length; i < l; ++i) { + // Calling sync `glob` because async is pointless for the CLI use case + // (as of now at least) + filesSrc = filesSrc.concat(glob.sync(globSrc[i], {dot: true})); +} + +// If there's no output directory and we're handling more than one file we can't +// output to CLI so it's pointless to execute the command. +if (!commander.output && filesSrc.length > 1) { + console.error('Destination must be specified unless only one file is used as input.'); + process.exit(1); +} + +// Setup everything var dest = commander.output; var accessKey = commander.accessKey; var secretKey = commander.secretKey; +var host = commander.host; +var port = commander.port && parseInt(commander.port); +var apiVersion = commander.apiVersion; +var params = cli.mergeAndParseParams(commander, config && config.params); +params.files = filesSrc; -var config = commander.config && require(path.resolve(commander.config, '.')) || {}; -config.files = filesSrc; - -var client = new jScrambler.Client({ - accessKey: accessKey, - secretKey: secretKey -}); - -jScrambler - .uploadCode(client, config) - .then(function (res) { - return jScrambler.downloadCode(client, res.id); - }) - .then(function (res) { - fs.outputFileSync(dest, res); +// Go, go, go +try { + var client = new jScrambler.Client({ + keys: { + accessKey: accessKey, + secretKey: secretKey + }, + host: host, + port: port, + apiVersion: apiVersion }); + + jScrambler + .uploadCode(client, params) + .then(function (res) { + return jScrambler.downloadCode(client, res.id); + }) + .then(function (res) { + if (filesSrc.length === 1 && !dest) { + dest = function (buffer, file) { + process.stdout.write(buffer.toString()); + }; + } + return jScrambler.unzipProject(res, dest); + }) + .fail(function (err) { + // Maybe we could throw here? + console.error(err); + process.exit(1); + }); +} catch (ex) { + console.error(ex.toString()); + process.exit(1); +} diff --git a/jscrambler-client.js b/jscrambler-client.js index 613cf6f..434302a 100644 --- a/jscrambler-client.js +++ b/jscrambler-client.js @@ -1,10 +1,17 @@ 'use strict'; -var _ = require('lodash'); + +var cfg = require('./lib/config'); +var clone = require('lodash.clone'); var crypto = require('crypto'); +var defaults = require('lodash.defaults'); var fs = require('fs'); +var keys = require('lodash.keys'); var needle = require('needle'); var querystring = require('querystring'); var url = require('url'); + +var debug = !!process.env.DEBUG; + /** * @class JScramblerClient * @param {Object} options @@ -17,17 +24,19 @@ var url = require('url'); * @license MIT */ function JScramblerClient (options) { + // Sluggish hack for backwards compatibility + if (options && !options.keys && (options.accessKey || options.secretKey)) { + options.keys = {}; + options.keys.accessKey = options.accessKey; + options.keys.secretKey = options.secretKey; + } + + options.keys = defaults(options.keys || {}, cfg.keys); /** * @member */ - this.options = _.defaults(options || {}, { - accessKey: null, - secretKey: null, - host: 'api.jscrambler.com', - port: 443, - apiVersion: 3 - }); - if (!this.options.accessKey || !this.options.secretKey) + this.options = defaults(options || {}, cfg); + if (!this.options.keys.accessKey || !this.options.keys.secretKey) throw new Error('Missing access or secret keys'); } /** @@ -57,10 +66,15 @@ function buildPath (method, path, params) { */ function buildSortedQuery (params) { // Sorted keys - var keys = _.keys(params).sort(); + var _keys = keys(params).sort(); var query = ''; - for (var i = 0, l = keys.length; i < l; i++) - query += encodeURIComponent(keys[i]) + '=' + encodeURIComponent(params[keys[i]]) + '&'; + for (var i = 0, l = _keys.length; i < l; i++) + query += encodeURIComponent(_keys[i]) + '=' + encodeURIComponent(params[_keys[i]]) + '&'; + query = query + .replace(/\*/g, '%2A') + .replace(/[!'()]/g, escape) + .replace(/%7E/g, '~') + .replace(/\+/g, '%20'); // Strip the last separator and return return query.substring(0, query.length - 1); } @@ -73,7 +87,7 @@ function buildSortedQuery (params) { * @returns {String} The digested signature. */ function generateHmacSignature (method, path, params) { - var paramsCopy = _.clone(params); + var paramsCopy = clone(params); for (var key in params) { if (key.indexOf('file_') !== -1) { paramsCopy[key] = crypto.createHash('md5').update( @@ -82,7 +96,8 @@ function generateHmacSignature (method, path, params) { } var signatureData = method.toUpperCase() + ';' + this.options.host.toLowerCase() + ';' + path + ';' + buildSortedQuery(paramsCopy); - var hmac = crypto.createHmac('sha256', this.options.secretKey.toUpperCase()); + debug && console.log('Signature data: ' + signatureData); + var hmac = crypto.createHmac('sha256', this.options.keys.secretKey.toUpperCase()); hmac.update(signatureData); return hmac.digest('base64'); } @@ -118,9 +133,10 @@ function handleFileParams (params) { properties. */ function signedParams (method, path, params) { - _.defaults(params, { - access_key: this.options.accessKey, - timestamp: new Date().toISOString() + defaults(params, { + access_key: this.options.keys.accessKey, + timestamp: new Date().toISOString(), + user_agent: 'Node' }); if (method === 'POST' && params.files) handleFileParams(params); params.signature = generateHmacSignature.apply(this, arguments); @@ -153,8 +169,19 @@ JScramblerClient.prototype.get = function (path, params, callback) { */ JScramblerClient.prototype.request = function (method, path, params, callback) { var signedData; - var options = {}; + var options = { + open_timeout: 0, + read_timeout: 0 + }; if (!params) params = {}; + else { + var _keys = keys(params); + for (var i = 0, l = _keys.length; i < l; i++) { + if(params[_keys[i]] instanceof Array && _keys[i] !== 'files') { + params[_keys[i]] = params[_keys[i]].join(','); + } + } + } // If post sign data and set the request as multipart if (method === 'POST') { signedData = signedParams.apply(this, arguments); diff --git a/jscrambler.js b/jscrambler.js index 4fa2478..50703fb 100644 --- a/jscrambler.js +++ b/jscrambler.js @@ -1,16 +1,33 @@ -'use strict'; -var JScramblerClient = require('./jscrambler-client'); -var Q = require('q'); /** * A facade to access JScrambler API using JScramblerClient. * @namespace jScramblerFacade * @author José Magalhães (magalhas@gmail.com) * @license MIT */ +'use strict'; + +var assign = require('lodash.assign'); +var config = require('./lib/config'); +var defaults = require('lodash.defaults'); +var fs = require('fs-extra'); +var glob = require('glob'); +var JScramblerClient = require('./jscrambler-client'); +var JSZip = require('jszip'); +var omit = require('lodash.omit'); +var path = require('flavored-path'); +var Q = require('q'); +var size = require('lodash.size'); +var temp = require('temp').track(); +var util = require('util'); + + +var debug = !!process.env.DEBUG; + exports = module.exports = /** @lends jScramblerFacade */ { Client: JScramblerClient, + config: config, /** * Downloads code through the API. * @param {JScramblerClient} client @@ -20,6 +37,7 @@ exports = module.exports = */ downloadCode: function (client, projectId, sourceId) { var deferred = Q.defer(); + debug && console.log('Downloading code', projectId, sourceId); this .pollProject(client, projectId) .then(function () { @@ -31,10 +49,24 @@ exports = module.exports = } else if (!/^.*\.zip$/.test(projectId)) path += '.zip'; client.get(path, null, function (err, res, body) { - if (err) deferred.reject(err); - else if (res.statusCode >= 400) deferred.reject(res); - else deferred.resolve(body); + try { + if (err) deferred.reject(err); + else if (res.statusCode >= 400) { + if (Buffer.isBuffer(body)) { + deferred.reject(JSON.parse(body)); + } else { + deferred.reject(body); + } + } else { + deferred.resolve(body); + } + } catch (ex) { + deferred.reject(body); + } }); + }) + .fail(function () { + deferred.reject.apply(null, arguments); }); return deferred.promise; }, @@ -43,12 +75,29 @@ exports = module.exports = * @param {JScramblerClient} client * @returns {Q.promise} */ - getInfo: function (client) { + getInfo: function (client, projectId) { var deferred = Q.defer(); - client.get('/code.json', null, function (err, res, body) { - if (err) deferred.reject(err); - else if (res.statusCode >= 400) deferred.reject(res); - else deferred.resolve(JSON.parse(body)); + var path = projectId ? '/code/' + projectId + '.json' : '/code.json'; + debug && console.log('Getting info', projectId); + client.get(path, null, function (err, res, body) { + try { + if (err) deferred.reject(err); + else if (res.statusCode >= 400) { + if (Buffer.isBuffer(body)) { + deferred.reject(JSON.parse(body)); + } else { + deferred.reject(body); + } + } else { + if (Buffer.isBuffer(body)) { + deferred.resolve(JSON.parse(body)); + } else { + deferred.resolve(body); + } + } + } catch (ex) { + deferred.reject(body); + } }); return deferred.promise; }, @@ -58,21 +107,24 @@ exports = module.exports = pollProject: function (client, projectId) { var deferred = Q.defer(); var isFinished = function () { + debug && console.log('Polling project', projectId); this - .getInfo(client) + .getInfo(client, projectId) .then(function (res) { - for (var i = 0, l = res.length; i < l; ++i) { - // Find projectId inside the response - if (res[i].id === projectId) { - // Did it finish? - if (res[i].finished_at) { - deferred.resolve(); - return; - } + // Did it finish? + if (res.finished_at) { + if (res.error_id && res.error_id !== '0') { + deferred.reject(res); + } else { + deferred.resolve(res); } + return; } // Try again later... setTimeout(isFinished, 1000); + }) + .fail(function () { + deferred.reject.apply(null, arguments); }); }.bind(this); isFinished(); @@ -86,11 +138,225 @@ exports = module.exports = */ uploadCode: function (client, params) { var deferred = Q.defer(); + + params = assign({}, params); + // If there are no params fallback to `cfg` + var rawParams = omit(params, ['files', 'cwd', 'apiVersion', 'port', 'deleteProject']); + if (Object.keys(rawParams).length === 0) { + params = defaults(params, this.config.params); + } + + params.files = params.files.slice(); + this.zipProject(params.files, params.cwd); + delete params.cwd; + + debug && console.log('Uploading code', util.inspect(params)); client.post('/code.json', params, function (err, res, body) { - if (err) deferred.reject(err); - else if (res.statusCode >= 400) deferred.reject(res); - else deferred.resolve(JSON.parse(body)); + try { + if (err) deferred.reject(err); + else if (res.statusCode >= 400) { + if (Buffer.isBuffer(body)) { + deferred.reject(JSON.parse(body)); + } else { + deferred.reject(body); + } + } else { + if (Buffer.isBuffer(body)) { + deferred.resolve(JSON.parse(body)); + } else { + deferred.resolve(body); + } + } + } catch (ex) { + deferred.reject(body); + } + }.bind(this)); + return deferred.promise; + }, + /** + * Deletes code through the API. + * @param {JScramblerClient} client + * @param {String} projectId + * @returns {Q.promise} + */ + deleteCode: function (client, projectId) { + var deferred = Q.defer(); + debug && console.log('Deleting project', projectId); + client.delete('/code/' + projectId + '.zip', null, function (err, res, body) { + try { + if (err) deferred.reject(err); + else if (res.statusCode >= 400) { + if (Buffer.isBuffer(body)) { + deferred.reject(JSON.parse(body)); + } else { + deferred.reject(body); + } + } + else { + if (Buffer.isBuffer(body)) { + deferred.resolve(JSON.parse(body)); + } else { + deferred.resolve(body); + } + } + } catch (ex) { + deferred.reject(body); + } }); return deferred.promise; + }, + /** + * Common operation sequence intended when using the client. First it + * uploads a project, then it polls the server to download and unzip the + * project into a folder. + * @param {String|Object} configPathOrObject + * @returns {Q.promise} + */ + process: function (configPathOrObject, destCallback) { + var config = typeof configPathOrObject === 'string' ? + require(configPathOrObject) : configPathOrObject; + + var accessKey = config.keys.accessKey; + var secretKey = config.keys.secretKey; + var host = config.host; + var port = config.port; + var apiVersion = config.apiVersion; + // Instance a JScrambler client + var client = new this.Client({ + accessKey: accessKey, + secretKey: secretKey, + host: host, + port: port, + apiVersion: apiVersion + }); + // Check for source files and add them to the parameters + if (!config.filesSrc) { + throw new Error('Source files must be provided.'); + } + // Check if output directory was provided + if (!config.filesDest && !destCallback) { + throw new Error('Output directory must be provided.'); + } + var filesSrc = []; + for (var i = 0, l = config.filesSrc.length; i < l; ++i) { + if (typeof config.filesSrc[i] === 'string') { + filesSrc = filesSrc.concat(glob.sync(config.filesSrc[i], {dot: true})); + } else { + filesSrc.push(config.filesSrc[i]); + } + } + // Prepare object to post + var params = config.params || {}; + params.files = filesSrc; + var self = this; + var projectId; + return this + .uploadCode(client, params) + .then(function (res) { + projectId = res.id; + return self.downloadCode(client, res.id); + }) + .then(function (res) { + return self.unzipProject(res, config.filesDest || destCallback); + }) + .then(function () { + if (config.deleteProject) { + return self.deleteCode(client, projectId); + } + }); + }, + /** + * It zips all files inside the passed parameter into a single zip file. It + * accepts an optional `cwd` parameter. + */ + zipProject: function (files, cwd) { + debug && console.log('Zipping files', util.inspect(files)); + // Flag to detect if any file was added to the zip archive + var hasFiles = false; + // Sanitize `cwd` + if (cwd) { + cwd = path.normalize(cwd); + } + // If it's already a zip file + if (files.length === 1 && /^.*\.zip$/.test(files[0])) { + hasFiles = true; + fs.outputFileSync(temp.openSync({suffix: '.zip'}).path, fs.readFileSync(files[0])); + } else { + var zip = new JSZip(); + for (var i = 0, l = files.length; i < l; ++i) { + // Sanitise path + if (typeof files[i] === 'string') { + files[i] = path.normalize(files[i]); + if (files[i].indexOf('../') === 0) { + files[i] = path.resolve(files[i]); + } + } + // Bypass unwanted patterns from `files` + if (/.*\.(git|hg)(\/.*|$)/.test(files[i].path || files[i])) { + continue; + } + var buffer, name; + var sPath; + if (cwd && files[i].indexOf && files[i].indexOf(cwd) !== 0) { + sPath = path.join(cwd, files[i]); + } else { + sPath = files[i]; + } + // If buffer + if (files[i].contents) { + name = path.relative(files[i].cwd, files[i].path); + buffer = files[i].contents; + } + // Else if it's a path and not a directory + else if (!fs.statSync(sPath).isDirectory()) { + if (cwd && files[i].indexOf && files[i].indexOf(cwd) === 0) { + name = files[i].substring(cwd.length); + } else { + name = files[i]; + } + buffer = fs.readFileSync(sPath); + } + // Else if it's a directory path + else { + zip.folder(sPath); + } + if (name) { + hasFiles = true; + zip.file(name, buffer); + } + } + if (hasFiles) { + var tempFile = temp.openSync({suffix: '.zip'}); + fs.outputFileSync(tempFile.path, zip.generate({type: 'nodebuffer'}), {encoding: 'base64'}); + files[0] = tempFile.path; + files.length = 1; + } else { + throw new Error('No source files found. If you intend to send a whole directory sufix your path with "**" (e.g. ./my-directory/**)'); + } + } + }, + /** + * It unzips a zip file to the given destination. + */ + unzipProject: function (zipFile, dest) { + var zip = new JSZip(zipFile); + var _size = size(zip.files); + for (var file in zip.files) { + if (!zip.files[file].options.dir) { + var buffer = zip.file(file).asNodeBuffer(); + if (typeof dest === 'function') { + dest(buffer, file); + } else if (dest) { + var lastDestChar = dest[dest.length - 1]; + var destPath; + if (_size === 1 && lastDestChar !== '/' && lastDestChar !== '\\') { + destPath = dest; + } else { + destPath = path.join(dest, file); + } + fs.outputFileSync(destPath, buffer); + } + } + } } }; diff --git a/lib/cli.js b/lib/cli.js new file mode 100644 index 0000000..c5207d6 --- /dev/null +++ b/lib/cli.js @@ -0,0 +1,56 @@ +'use strict'; + +var clone = require('lodash.clone'); +var snakeCase = require('snake-case'); + +module.exports = { + // Convert from command line option format to snake case for the JScrambler API. + // It also replaces truthy boolean flags with %DEFAULT% values + mergeAndParseParams: function (commander, params) { + params = clone(params || {}); + + // Override params file changes with any specified command line options + var name, is_bool_flag = { + assertsElimination: false, + browserOsLock: false, + constantFolding: true, + deadCode: true, + deadCodeElimination: true, + debuggingCodeElimination: false, + dictionaryCompression: true, + domainLock: false, + domainLockWarningFunction: false, + dotNotationElimination: true, + exceptionsList: false, + expirationDate: false, + expirationDateWarningFunction: false, + functionOutlining: true, + functionReorder: true, + ignoreFiles: false, + literalHooking: false, + literalDuplicates: true, + memberEnumeration: true, + mode: false, + namePrefix: false, + renameAll: false, + renameInclude: false, + renameLocal: true, + selfDefending: false, + stringSplitting: false, + whitespace: true, + preserveAnnotations: true + }; + + for (name in is_bool_flag) { + if (commander[name] !== undefined) { + if (is_bool_flag[name] === true) { + params[snakeCase(name)] = '%DEFAULT%'; + } else { + params[snakeCase(name)] = commander[name]; + } + } + } + + return params; + } +}; diff --git a/lib/config.js b/lib/config.js new file mode 100644 index 0000000..6cc915e --- /dev/null +++ b/lib/config.js @@ -0,0 +1,14 @@ +'use strict'; + +var rc = require('rc'); + +// Load RC configuration if present. Pass `[]` as last argument to avoid +// getting variables from `argv`. +var config = rc('jscrambler', { + keys: {}, + host: 'api.jscrambler.com', + port: 443, + apiVersion: 3 +}, []); + +module.exports = config; diff --git a/package.json b/package.json index 0896352..678e6dc 100644 --- a/package.json +++ b/package.json @@ -1,34 +1,55 @@ { - "name": "jscrambler", + "name": "jscrambler3", "description": "JScrambler API client.", - "version": "0.3.0", - "homepage": "https://github.com/auditmark/node-jscrambler", + "version": "0.7.5", + "homepage": "https://github.com/jscrambler/node-jscrambler", "author": { "name": "magalhas", "email": "magalhas@gmail.com" }, "repository": { "type": "git", - "url": "git://github.com:auditmark/node-jscrambler.git" + "url": "git://github.com:jscrambler/node-jscrambler.git" }, "bugs": { - "url": "https://github.com/auditmark/node-jscrambler/issues" + "url": "https://github.com/jscrambler/node-jscrambler/issues" }, "licenses": [ { "type": "MIT", - "url": "https://github.com/auditmark/node-jscrambler/blob/master/LICENSE-MIT" + "url": "https://github.com/jscrambler/node-jscrambler/blob/master/LICENSE-MIT" } ], + "scripts": { + "test": "grunt test 2> /dev/null" + }, "engines": { "node": ">= 0.8.0" }, "dependencies": { - "q": "^1.0.1", - "lodash": "^2.4.1", - "needle": "^0.6.6", - "commander": "^2.1.0", - "fs-extra": "^0.8.1" + "commander": "^2.8.1", + "flavored-path": "0.0.8", + "fs-extra": "^0.23.1", + "glob": "^5.0.14", + "jszip": "^2.5.0", + "lodash.assign": "^3.2.0", + "lodash.clone": "^3.0.3", + "lodash.defaults": "^3.1.2", + "lodash.keys": "^3.1.2", + "lodash.omit": "^3.1.0", + "lodash.pluck": "^3.1.2", + "lodash.size": "^3.0.2", + "needle": "^0.10.0", + "q": "^1.4.1", + "rc": "^1.1.0", + "snake-case": "^1.1.1", + "temp": "^0.8.3" + }, + "devDependencies": { + "grunt": "^0.4.5", + "grunt-cli": "^1.2.0", + "grunt-contrib-clean": "^0.6.0", + "grunt-jasmine-node": "^0.3.1" }, "main": "jscrambler", "bin": "bin/jscrambler" diff --git a/test/fixtures/multiple-files/hello-world.js b/test/fixtures/multiple-files/hello-world.js new file mode 100644 index 0000000..b9d3e23 --- /dev/null +++ b/test/fixtures/multiple-files/hello-world.js @@ -0,0 +1 @@ +console.log('Hello world!'); diff --git a/test/fixtures/multiple-files/index.html b/test/fixtures/multiple-files/index.html new file mode 100644 index 0000000..c7b67ea --- /dev/null +++ b/test/fixtures/multiple-files/index.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/test/fixtures/nested-files/index.html b/test/fixtures/nested-files/index.html new file mode 100644 index 0000000..c7b67ea --- /dev/null +++ b/test/fixtures/nested-files/index.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/test/fixtures/nested-files/lib/a/hello-world.js b/test/fixtures/nested-files/lib/a/hello-world.js new file mode 100644 index 0000000..b9d3e23 --- /dev/null +++ b/test/fixtures/nested-files/lib/a/hello-world.js @@ -0,0 +1 @@ +console.log('Hello world!'); diff --git a/test/fixtures/nested-files/lib/b/hello-world.js b/test/fixtures/nested-files/lib/b/hello-world.js new file mode 100644 index 0000000..b9d3e23 --- /dev/null +++ b/test/fixtures/nested-files/lib/b/hello-world.js @@ -0,0 +1 @@ +console.log('Hello world!'); diff --git a/test/fixtures/nested-files/lib/hello-world.js b/test/fixtures/nested-files/lib/hello-world.js new file mode 100644 index 0000000..b9d3e23 --- /dev/null +++ b/test/fixtures/nested-files/lib/hello-world.js @@ -0,0 +1 @@ +console.log('Hello world!'); diff --git a/test/fixtures/single-file/index.js b/test/fixtures/single-file/index.js new file mode 100644 index 0000000..b9d3e23 --- /dev/null +++ b/test/fixtures/single-file/index.js @@ -0,0 +1 @@ +console.log('Hello world!'); diff --git a/test/specs/jscrambler.js b/test/specs/jscrambler.js new file mode 100644 index 0000000..178c340 --- /dev/null +++ b/test/specs/jscrambler.js @@ -0,0 +1,144 @@ +/* global describe, beforeEach, it, expect, spyOn, Buffer, jasmine, console */ + +var fs = require('fs'); +var jScrambler = require('../../jscrambler'); +var jScramblerKeys = require('../../jscrambler_keys'); +var pluck = require('lodash.pluck'); +var util = require('util'); + +describe('JScrambler Client', function () { + var jScramblerClient, projectId, downloadedBuffer; + + beforeEach(function () { + jScramblerClient = new jScrambler.Client({ + accessKey: jScramblerKeys.accessKey, + secretKey: jScramblerKeys.secretKey + }); + jasmine.DEFAULT_TIMEOUT_INTERVAL = 20000; + }); + + test('Single file', [ + 'test/fixtures/single-file/index.js' + ]); + test('Multiple files', [ + 'test/fixtures/multiple-files/index.html', + 'test/fixtures/multiple-files/hello-world.js' + ]); + test('Nested files', [ + 'test/fixtures/nested-files/index.html', + 'test/fixtures/nested-files/lib/hello-world.js', + 'test/fixtures/nested-files/lib/a/hello-world.js', + 'test/fixtures/nested-files/lib/b/hello-world.js' + ]); + + function test (testName, files) { + it(testName + ': uploads code', function (done) { + var zipSpy = spyOn(jScrambler, 'zipProject').andCallThrough(); + + jScrambler + .uploadCode(jScramblerClient, { + files: files + }) + .then(function (res) { + expect(zipSpy).toHaveBeenCalled(); + expect(res.id).toBeDefined(); + expect(res.extension).toEqual('zip'); + + expect(res.sources.length).toEqual(files.length); + for (var i = 0, l = files.length; i < l; ++i) { + expect(pluck(res.sources, 'filename').indexOf(files[i]) !== -1).toBeTruthy(); + } + + projectId = res.id; + }) + .catch(function (error) { + console.log(util.inspect(error)); + }) + .fin(done); + }); + + it(testName + ': gets project info', function (done) { + expect(projectId).toBeDefined(); + + jScrambler + .getInfo(jScramblerClient, projectId) + .then(function (res) { + expect(res.error_id).toEqual(null); + expect(res.extension).toEqual('zip'); + + expect(res.sources.length).toEqual(files.length); + for (var i = 0, l = files.length; i < l; ++i) { + expect(pluck(res.sources, 'filename').indexOf(files[i]) !== -1).toBeTruthy(); + expect(res.sources[i].error_id).toEqual(null); + } + }) + .catch(function (error) { + console.log(util.inspect(error)); + }) + .fin(done); + }); + + it(testName + ': polls project', function (done) { + expect(projectId).toBeDefined(); + + jScrambler + .pollProject(jScramblerClient, projectId) + .then(function (res) { + expect(res.error_id).toEqual('0'); + expect(res.finished_at).toBeDefined(); + + expect(res.sources.length).toEqual(files.length); + for (var i = 0, l = files.length; i < l; ++i) { + expect(pluck(res.sources, 'filename').indexOf(files[i]) !== -1).toBeTruthy(); + } + + var finishedAt = new Date(res.finished_at); + expect(finishedAt instanceof Date).toBeTruthy(); + }) + .catch(function (error) { + console.log(util.inspect(error)); + }) + .fin(done); + }); + + it(testName + ': downloads code', function (done) { + expect(projectId).toBeDefined(); + + jScrambler + .downloadCode(jScramblerClient, projectId) + .then(function (res) { + expect(Buffer.isBuffer(res)).toBeTruthy(); + downloadedBuffer = res; + }) + .catch(function (error) { + console.log(util.inspect(error)); + }) + .fin(done); + }); + + it(testName + ': unzips the project', function () { + expect(downloadedBuffer).toBeDefined(); + + jScrambler.unzipProject(downloadedBuffer, './results/'); + for (var i = 0, l = files.length; i < l; ++i) { + expect(fs.existsSync('./results/' + files[i])).toBeTruthy(); + } + }); + + it(testName + ': processes', function (done) { + jScrambler + .process({ + filesSrc: files, + filesDest: './results', + keys: { + accessKey: jScramblerKeys.accessKey, + secretKey: jScramblerKeys.secretKey, + } + }) + .catch(function (error) { + console.log(util.inspect(error)); + }) + .fin(done); + }); + } +});