diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..6a268ef6 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,16 @@ +{ + "parser": "babel-eslint", + "env": { + "commonjs": true, + "es2021": true, + "node": true, + "browser": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": 12 + }, + "rules": { + "no-unused-vars": ["warn", { "args": "after-used", "argsIgnorePattern": "^_" }] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..6c1524dc --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules/ + +# don't commit session data +sessions/* + +# but commit the empty folders +!sessions/.gitkeep \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..62458491 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "printWidth": 120, + "trailingComma": "none", + "singleQuote": true, + "tabWidth": 4 +} diff --git a/README.md b/README.md new file mode 100644 index 00000000..01b782a9 --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# rammerhead-proxy + +> proxy based on testcafe-hammerhead + +## Who is this package for + +Package is for those who +- don't want to + +## Features of proxy + + + +## Effectiveness of proxy + +This proxy supports proxying +- react + +## Installing and running + +Assuming you have node already installed, clone the repo, then run `npm install`. + +After, configure your settings in [src/config.js](src/config.js) and [src/config2.js](src/config2.js). + +Finally, there are two options in starting rammerhead: + +- `node src/server.js` + - this starts the server as normal +- `node src/multi-server.js` + - this spawns N workers and load balances automatically among them, where N is the number of CPU threads in the system. configure number of workers settings in [src/multi-config.js](src/multi-config.js) + - try not to use this because race conditions occur when workers try to read and delete files from the session store at the same time. this can lead to unexpected behavior, like cookies randomly deciding to not work. Also, see [RammerheadSessionFilePersistentStore.js](src/RammerheadSessionFilePersistentStore.js) for a more in-depth description on the drawbacks of using this setup. + +## Supporting me and contributing + +Server infrastructure costs money, so I would appreciate it greatly if you become a Patreon member. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..560c791a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2230 @@ +{ + "name": "rammerhead", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/generator": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.15.0.tgz", + "integrity": "sha512-eKl4XdMrbpYvuB505KTta4AV9g+wWzmVBW69tX0H2NwKVKd2YJbKgyK6M8j/rgLbmHOYJn6rUklV677nOyJrEQ==", + "dev": true, + "requires": { + "@babel/types": "^7.15.0", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/helper-function-name": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.14.5.tgz", + "integrity": "sha512-Gjna0AsXWfFvrAuX+VKcN/aNNWonizBj39yGwUzVDVTlMYJMK2Wp6xdpy72mfArFq5uK+NOuexfzZlzI1z9+AQ==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.14.5", + "@babel/template": "^7.14.5", + "@babel/types": "^7.14.5" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.14.5.tgz", + "integrity": "sha512-I1Db4Shst5lewOM4V+ZKJzQ0JGGaZ6VY1jYvMghRjqs6DWgxLCIyFt30GlnKkfUeFLpJt2vzbMVEXVSXlIFYUg==", + "dev": true, + "requires": { + "@babel/types": "^7.14.5" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.14.5.tgz", + "integrity": "sha512-R1PXiz31Uc0Vxy4OEOm07x0oSjKAdPPCh3tPivn/Eo8cvz6gveAeuyUUPB21Hoiif0uoPQSSdhIPS3352nvdyQ==", + "dev": true, + "requires": { + "@babel/types": "^7.14.5" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.14.5.tgz", + "integrity": "sha512-hprxVPu6e5Kdp2puZUmvOGjaLv9TCe58E/Fl6hRq4YiVQxIcNvuq6uTM2r1mT/oPskuS9CgR+I94sqAYv0NGKA==", + "dev": true, + "requires": { + "@babel/types": "^7.14.5" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.14.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz", + "integrity": "sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==", + "dev": true + }, + "@babel/highlight": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + } + } + }, + "@babel/parser": { + "version": "7.15.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.3.tgz", + "integrity": "sha512-O0L6v/HvqbdJawj0iBEfVQMc3/6WP+AeOsovsIgBFyJaG+W2w7eqvZB7puddATmWuARlm1SX7DwxJ/JJUnDpEA==", + "dev": true + }, + "@babel/template": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.14.5.tgz", + "integrity": "sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.14.5", + "@babel/parser": "^7.14.5", + "@babel/types": "^7.14.5" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", + "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.14.5" + } + } + } + }, + "@babel/traverse": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.15.0.tgz", + "integrity": "sha512-392d8BN0C9eVxVWd8H6x9WfipgVH5IaIoLp23334Sc1vbKKWINnvwRpb4us0xtPaCumlwbTtIYNA0Dv/32sVFw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.14.5", + "@babel/generator": "^7.15.0", + "@babel/helper-function-name": "^7.14.5", + "@babel/helper-hoist-variables": "^7.14.5", + "@babel/helper-split-export-declaration": "^7.14.5", + "@babel/parser": "^7.15.0", + "@babel/types": "^7.15.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", + "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.14.5" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.0.tgz", + "integrity": "sha512-OBvfqnllOIdX4ojTHpwZbpvz4j3EWyjkZEdmjH0/cgsd6QOdSgU8rLSk6ard/pcW7rlmjdVSX/AWOaORR1uNOQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.9", + "to-fast-properties": "^2.0.0" + } + }, + "@eslint/eslintrc": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", + "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^13.9.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + } + }, + "@humanwhocodes/config-array": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", + "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.0", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + } + }, + "@humanwhocodes/object-schema": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz", + "integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==", + "dev": true + }, + "@types/estree": { + "version": "0.0.46", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.46.tgz", + "integrity": "sha512-laIjwTQaD+5DukBZaygQ79K1Z0jb1bPEMRrkXSLjtCcZm+abyp5YbrqpSLzD42FwWW6gK/aS4NYpJ804nG2brg==" + }, + "@types/glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-w+LsMxKyYQm347Otw+IfBXOv9UWVjpHpCDdbBMt8Kz/xbvCYNjP+0qPh91Km3iKfSRLBB0P7fAMf0KHrPu+MyA==", + "optional": true, + "requires": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "optional": true + }, + "@types/node": { + "version": "16.7.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.7.0.tgz", + "integrity": "sha512-e66BrnjWQ3BRBZ2+iA5e85fcH9GLNe4S0n1H0T3OalK2sXg5XWEFTO4xvmGrYQ3edy+q6fdOh5t0/HOY8OAqBg==", + "optional": true + }, + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + }, + "acorn-hammerhead": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/acorn-hammerhead/-/acorn-hammerhead-0.5.0.tgz", + "integrity": "sha512-TI9TFfJBfduhcM2GggayNhdYvdJ3UgS/Bu3sB7FB2AUmNCmCJ+TSOT6GXu+bodG5/xL74D5zE4XRaqyjgjsYVQ==", + "requires": { + "@types/estree": "0.0.46" + } + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" + }, + "ansi": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/ansi/-/ansi-0.3.1.tgz", + "integrity": "sha1-DELU+xcWDVqa8eSEus4cZpIsGyE=", + "dev": true + }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, + "ansi-escapes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", + "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", + "dev": true + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "dev": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "asar": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/asar/-/asar-2.1.0.tgz", + "integrity": "sha512-d2Ovma+bfqNpvBzY/KU8oPY67ZworixTpkjSx0PCXnQi67c2cXmssaTxpFDUM0ttopXoGx/KRxNg/GDThYbXQA==", + "requires": { + "@types/glob": "^7.1.1", + "chromium-pickle-js": "^0.2.0", + "commander": "^2.20.0", + "cuint": "^0.2.2", + "glob": "^7.1.3", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "tmp-promise": "^1.0.5" + } + }, + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true + }, + "async": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", + "integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=", + "dev": true + }, + "async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==" + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" + }, + "babel-eslint": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", + "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.7.0", + "@babel/traverse": "^7.7.0", + "@babel/types": "^7.7.0", + "eslint-visitor-keys": "^1.0.0", + "resolve": "^1.12.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" + }, + "bowser": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-1.6.0.tgz", + "integrity": "sha1-N/w4e2Fstq7zcNq01r1AK3TFxU0=" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "brotli": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.2.tgz", + "integrity": "sha1-UlqcrU/LqWR119OI9q7LE+7VL0Y=", + "requires": { + "base64-js": "^1.1.2" + } + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "caporal": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/caporal/-/caporal-1.4.0.tgz", + "integrity": "sha512-3pWfIwKVdIbB/gWmpLloO6iGAXTRi9mcTinPOwvHfzH3BYjOhLgq2XRG3hKtp+F6vBcBXxMgCobUzBAx1d8T4A==", + "dev": true, + "requires": { + "bluebird": "^3.4.7", + "cli-table3": "^0.5.0", + "colorette": "^1.0.1", + "fast-levenshtein": "^2.0.6", + "lodash": "^4.17.14", + "micromist": "1.1.0", + "prettyjson": "^1.2.1", + "tabtab": "^2.2.2", + "winston": "^2.3.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha1-BKEGZywYsIWrd02YPfo+oTjyIgU=" + }, + "cli-cursor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", + "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", + "dev": true, + "requires": { + "restore-cursor": "^1.0.1" + } + }, + "cli-table3": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.5.1.tgz", + "integrity": "sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==", + "dev": true, + "requires": { + "colors": "^1.1.2", + "object-assign": "^4.1.0", + "string-width": "^2.1.1" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "cli-width": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", + "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", + "dev": true + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "colorette": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.3.0.tgz", + "integrity": "sha512-ecORCqbSFP7Wm8Y6lyqMJjexBQqXSF7SSeaTyGGphogUjBlFP9m9o08wy86HL2uB7fMTxtOUzLMk7ogKcxMg1w==", + "dev": true + }, + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "crypto-md5": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/crypto-md5/-/crypto-md5-1.0.0.tgz", + "integrity": "sha1-zMjadQx1PH7curxUKWdHKjhOhrs=" + }, + "css": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/css/-/css-2.2.3.tgz", + "integrity": "sha512-0W171WccAjQGGTKLhw4m2nnl0zPHUlTO/I8td4XzJgIB8Hg3ZZx71qT4G4eX8OVsSiaAKiUMy73E3nsbPlg2DQ==", + "requires": { + "inherits": "^2.0.1", + "source-map": "^0.1.38", + "source-map-resolve": "^0.5.1", + "urix": "^0.1.0" + } + }, + "cuint": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz", + "integrity": "sha1-QICG1AlVDCYxFVYZ6fp7ytw7mRs=" + }, + "cycle": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", + "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=", + "dev": true + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.1" + } + }, + "es-check": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/es-check/-/es-check-6.0.0.tgz", + "integrity": "sha512-FwWQ03GgWL8HplV7gWMDtpR5I+n0K/JYLnLoMDw2DrdsfxF2jV+dtpqo0sIVFeoHgYjEeWkb0ntZvEah7R7T1w==", + "dev": true, + "requires": { + "acorn": "^8.4.1", + "caporal": "^1.4.0", + "glob": "^7.1.7" + }, + "dependencies": { + "acorn": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.4.1.tgz", + "integrity": "sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==", + "dev": true + } + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "eslint": { + "version": "7.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", + "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", + "dev": true, + "requires": { + "@babel/code-frame": "7.12.11", + "@eslint/eslintrc": "^0.4.3", + "@humanwhocodes/config-array": "^0.5.0", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.1.2", + "globals": "^13.6.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.9", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + }, + "esotope-hammerhead": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/esotope-hammerhead/-/esotope-hammerhead-0.6.1.tgz", + "integrity": "sha512-RG4orJ1xy+zD6fTEKuDYaqCuL1ymYa1/Bp+j9c7b/u7B8yI6+Qgg8o4lT1EDAOG9eBzBtwtTWR0chqt3hr0hZw==", + "requires": { + "@types/estree": "0.0.46" + } + }, + "espree": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "dev": true, + "requires": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "exit-hook": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", + "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", + "dev": true + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "external-editor": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-1.1.1.tgz", + "integrity": "sha1-Etew24UPf/fnCBuvQAVwAGDEYAs=", + "dev": true, + "requires": { + "extend": "^3.0.0", + "spawn-sync": "^1.0.15", + "tmp": "^0.0.29" + }, + "dependencies": { + "tmp": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.29.tgz", + "integrity": "sha1-8lEl/w3Z2jzLDC3Tce4SiLuRKMA=", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.1" + } + } + } + }, + "eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5", + "object-assign": "^4.1.0" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "dependencies": { + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "flatted": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.2.tgz", + "integrity": "sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "gauge": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-1.2.7.tgz", + "integrity": "sha1-6c7FSD09TuDvRLYKfZnkk14TbZM=", + "dev": true, + "requires": { + "ansi": "^0.3.0", + "has-unicode": "^2.0.0", + "lodash.pad": "^4.1.0", + "lodash.padend": "^4.1.0", + "lodash.padstart": "^4.1.0" + } + }, + "glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.11.0.tgz", + "integrity": "sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + } + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true + }, + "http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + }, + "iconv-lite": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.5.1.tgz", + "integrity": "sha512-ONHr16SQvKZNSqjQT9gy5z24Jw+uqfO02/ngBSBoqChZ+W8qXX7GPRa1RoUnzGADw8K63R1BXUMzarCVQBpY8Q==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "inquirer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-1.2.3.tgz", + "integrity": "sha1-TexvMvN+97sLLtPx0aXD9UUHSRg=", + "dev": true, + "requires": { + "ansi-escapes": "^1.1.0", + "chalk": "^1.0.0", + "cli-cursor": "^1.0.1", + "cli-width": "^2.0.0", + "external-editor": "^1.1.0", + "figures": "^1.3.5", + "lodash": "^4.3.0", + "mute-stream": "0.0.6", + "pinkie-promise": "^2.0.0", + "run-async": "^2.2.0", + "rx": "^4.1.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "is-core-module": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.6.0.tgz", + "integrity": "sha512-wShG8vs60jKfPWpF2KZRaAtvt3a20OAn7+IJ6hLPECpSABLcKtFKTTI4ZtH5QcBruBHlq+WsdHWyz0BCZW7svQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-date-parser": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-date-parser/-/json-date-parser-1.0.1.tgz", + "integrity": "sha1-tw60fImPOxZkOtY/+42WIP14T4U=" + }, + "json-format": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-format/-/json-format-1.0.1.tgz", + "integrity": "sha1-FD9n5irxKda//tKIpGJl6iPQ3ww=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", + "dev": true + }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", + "dev": true + }, + "lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=", + "dev": true + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "lodash.pad": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/lodash.pad/-/lodash.pad-4.5.1.tgz", + "integrity": "sha1-QzCUmoM6fI2iLMIPaibE1Z3runA=", + "dev": true + }, + "lodash.padend": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/lodash.padend/-/lodash.padend-4.6.1.tgz", + "integrity": "sha1-U8y6BH0G4VjTEfRdpiX05J5vFm4=", + "dev": true + }, + "lodash.padstart": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/lodash.padstart/-/lodash.padstart-4.6.1.tgz", + "integrity": "sha1-0uPuv/DZ05rVD1y9G1KnvOa7YRs=", + "dev": true + }, + "lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", + "dev": true + }, + "lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", + "dev": true + }, + "lru-cache": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.6.3.tgz", + "integrity": "sha1-UczQtPwMhDWH16VwnOTTt2Kb7cU=" + }, + "match-url-wildcard": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/match-url-wildcard/-/match-url-wildcard-0.0.4.tgz", + "integrity": "sha512-R1XhQaamUZPWLOPtp4ig5j+3jctN+skhgRmEQTUamMzmNtRG69QEirQs0NZKLtHMR7tzWpmtnS4Eqv65DcgXUA==", + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "merge-stream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz", + "integrity": "sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=", + "requires": { + "readable-stream": "^2.0.1" + } + }, + "micromist": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromist/-/micromist-1.1.0.tgz", + "integrity": "sha512-+CQ76pabE9egniSEdmDuH+j2cYyIBKP97kujG8ZLZyLCRq5ExwtIy4DPHPFrq4jVbhMRBnyjuH50KU9Ohs8QCg==", + "dev": true, + "requires": { + "lodash.camelcase": "^4.3.0" + } + }, + "mime": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "requires": { + "minimist": "^1.2.5" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "mustache": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-2.3.2.tgz", + "integrity": "sha512-KpMNwdQsYz3O/SBS1qJ/o3sqUJ5wSb8gb0pul8CO0S56b9Y2ALm8zCfsjPXsqGFfoNBkDwZuZIAjhsZI03gYVQ==" + }, + "mute-stream": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.6.tgz", + "integrity": "sha1-SJYrGeFp/R38JAs/HnMXYnu8R9s=", + "dev": true + }, + "nanoid": { + "version": "3.1.25", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.25.tgz", + "integrity": "sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==" + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "npm-force-resolutions": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/npm-force-resolutions/-/npm-force-resolutions-0.0.10.tgz", + "integrity": "sha512-Jscex+xIU6tw3VsyrwxM1TeT+dd9Fd3UOMAjy6J1TMpuYeEqg4LQZnATQO5vjPrsARm3und6zc6Dii/GUyRE5A==", + "dev": true, + "requires": { + "json-format": "^1.0.1", + "source-map-support": "^0.5.5", + "xmlhttprequest": "^1.8.0" + } + }, + "npmlog": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-2.0.4.tgz", + "integrity": "sha1-mLUlMPJRTKkNCexbIsiEZyI3VpI=", + "dev": true, + "requires": { + "ansi": "~0.3.1", + "are-we-there-yet": "~1.1.2", + "gauge": "~1.2.5" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", + "dev": true + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "os-family": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/os-family/-/os-family-1.1.0.tgz", + "integrity": "sha512-E3Orl5pvDJXnVmpaAA2TeNNpNhTMl4o5HghuWhOivBjEiTnJSrMYSa5uZMek1lBEvu8kKEsa2YgVcGFVDqX/9w==" + }, + "os-shim": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/os-shim/-/os-shim-0.1.3.tgz", + "integrity": "sha1-a2LDeRz3kJ6jXtRuF2WLtBfLORc=", + "dev": true + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-2.2.3.tgz", + "integrity": "sha1-DE/EHBAAxea5PUiwP4CDg3g06fY=" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "prettier": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.2.tgz", + "integrity": "sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ==", + "dev": true + }, + "prettyjson": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prettyjson/-/prettyjson-1.2.1.tgz", + "integrity": "sha1-/P+rQdGcq0365eV15kJGYZsS0ok=", + "dev": true, + "requires": { + "colors": "^1.1.2", + "minimist": "^1.2.0" + } + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "proxy-deep": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/proxy-deep/-/proxy-deep-3.1.1.tgz", + "integrity": "sha512-kppbvLUNJ4IOMZds9/4gz/rtT5OFiesy3XosLsgMKlF3vb6GA5Y3ptyDlzKLcOcUBW+zaY+RiMINTsgE+O6e+Q==" + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "rate-limiter-flexible": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-2.2.4.tgz", + "integrity": "sha512-8u4k5b1afuBcfydX0L0l3J2PNjgcuo3zua8plhvIisyDqOBldrCwfSFut/Fj00LAB1nxJYVM9jeszr2rZyDhQw==" + }, + "read-file-relative": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/read-file-relative/-/read-file-relative-1.2.0.tgz", + "integrity": "sha1-mPfZbqoh0rTHov69Y9L8jPNen5s=", + "requires": { + "callsite": "^1.0.0" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, + "resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" + }, + "restore-cursor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", + "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", + "dev": true, + "requires": { + "exit-hook": "^1.0.0", + "onetime": "^1.0.0" + } + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, + "run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true + }, + "rx": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz", + "integrity": "sha1-pfE/957zt0D+MKqAP7CfmIBdR4I=", + "dev": true + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + } + } + }, + "source-map": { + "version": "0.1.43", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", + "requires": { + "amdefine": ">=0.0.4" + } + }, + "source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "requires": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "source-map-url": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", + "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==" + }, + "spawn-sync": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/spawn-sync/-/spawn-sync-1.0.15.tgz", + "integrity": "sha1-sAeZVX63+wyDdsKdROih6mfldHY=", + "dev": true, + "requires": { + "concat-stream": "^1.4.7", + "os-shim": "^0.1.2" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", + "dev": true + }, + "string-width": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "table": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/table/-/table-6.7.1.tgz", + "integrity": "sha512-ZGum47Yi6KOOFDE8m223td53ath2enHcYLgOCjGr5ngu8bdIARQk6mN/wRMv4yMRcHnCSnHbCEha4sobQx5yWg==", + "dev": true, + "requires": { + "ajv": "^8.0.1", + "lodash.clonedeep": "^4.5.0", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.6.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz", + "integrity": "sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + } + } + }, + "tabtab": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tabtab/-/tabtab-2.2.2.tgz", + "integrity": "sha1-egR/FDsBC0y9MfhX6ClhUSy/ThQ=", + "dev": true, + "requires": { + "debug": "^2.2.0", + "inquirer": "^1.0.2", + "lodash.difference": "^4.5.0", + "lodash.uniq": "^4.5.0", + "minimist": "^1.2.0", + "mkdirp": "^0.5.1", + "npmlog": "^2.0.3", + "object-assign": "^4.1.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "testcafe-hammerhead": { + "version": "24.5.1", + "resolved": "https://registry.npmjs.org/testcafe-hammerhead/-/testcafe-hammerhead-24.5.1.tgz", + "integrity": "sha512-upLFJbEH3unQ8dq65AZdevYgWaJtWiALKW5YO5uWbx7lLvMMeQbyA7gnwrlGbIaepUdi+PlW4dORjncrU+lY/Q==", + "requires": { + "acorn-hammerhead": "0.5.0", + "asar": "^2.0.1", + "bowser": "1.6.0", + "brotli": "^1.3.1", + "crypto-md5": "^1.0.0", + "css": "2.2.3", + "debug": "4.3.1", + "esotope-hammerhead": "0.6.1", + "http-cache-semantics": "^4.1.0", + "iconv-lite": "0.5.1", + "lodash": "^4.17.20", + "lru-cache": "2.6.3", + "match-url-wildcard": "0.0.4", + "merge-stream": "^1.0.1", + "mime": "~1.4.1", + "mustache": "^2.1.1", + "nanoid": "^3.1.12", + "os-family": "^1.0.0", + "parse5": "2.2.3", + "pinkie": "2.0.4", + "read-file-relative": "^1.2.0", + "semver": "5.5.0", + "tough-cookie": "2.3.3", + "tunnel-agent": "0.6.0", + "webauth": "^1.1.0" + }, + "dependencies": { + "mime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" + } + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "tmp": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz", + "integrity": "sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==", + "requires": { + "rimraf": "^2.6.3" + }, + "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "tmp-promise": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-1.1.0.tgz", + "integrity": "sha512-8+Ah9aB1IRXCnIOxXZ0uFozV1nMU5xiu7hhFVUSxZ3bYu+psD4TzagCzVbexUCgNNGJnsmNDQlS4nG3mTyoNkw==", + "requires": { + "bluebird": "^3.5.0", + "tmp": "0.1.0" + } + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, + "tough-cookie": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", + "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=", + "requires": { + "punycode": "^1.4.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + } + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, + "v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, + "webauth": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/webauth/-/webauth-1.1.0.tgz", + "integrity": "sha1-ZHBPa4AmmGYFvDymKZUubib90QA=" + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "winston": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.5.tgz", + "integrity": "sha512-TWoamHt5yYvsMarGlGEQE59SbJHqGsZV8/lwC+iCcGeAe0vUaOh+Lv6SYM17ouzC/a/LB1/hz/7sxFBtlu1l4A==", + "dev": true, + "requires": { + "async": "~1.0.0", + "colors": "1.0.x", + "cycle": "1.0.x", + "eyes": "0.1.x", + "isstream": "0.1.x", + "stack-trace": "0.0.x" + }, + "dependencies": { + "colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", + "dev": true + } + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "ws": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.0.tgz", + "integrity": "sha512-uYhVJ/m9oXwEI04iIVmgLmugh2qrZihkywG9y5FfZV2ATeLIzHf93qs+tUNqlttbQK957/VX3mtwAS+UfIwA4g==" + }, + "xmlhttprequest": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz", + "integrity": "sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw=", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..025a400a --- /dev/null +++ b/package.json @@ -0,0 +1,45 @@ +{ + "name": "rammerhead", + "version": "1.0.0", + "description": "User friendly web proxy powered by testcafe-hammerhead", + "main": "src/index.js", + "scripts": { + "preinstall": "npx npm-force-resolutions", + "start": "node src/server.js", + "test": "npm run lint && npm run clientes5", + "lint": "eslint -c .eslintrc.json --ext .js src", + "format": "prettier --write 'src/**/*.js'", + "clientes5": "es-check es5 src/client/*.js public/**/*.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/binary-person/rammerhead.git" + }, + "author": "Simon Cheng (https://github.com/binary-person)", + "license": "MIT", + "bugs": { + "url": "https://github.com/binary-person/rammerhead/issues" + }, + "homepage": "https://github.com/binary-person/rammerhead#readme", + "dependencies": { + "async-exit-hook": "^2.0.1", + "json-date-parser": "^1.0.1", + "mime": "^2.5.2", + "proxy-deep": "^3.1.1", + "rate-limiter-flexible": "^2.2.4", + "rimraf": "^3.0.2", + "testcafe-hammerhead": "24.5.1", + "uuid": "^8.3.2", + "ws": "^8.2.0" + }, + "devDependencies": { + "babel-eslint": "^10.1.0", + "es-check": "^6.0.0", + "eslint": "^7.32.0", + "npm-force-resolutions": "0.0.10", + "prettier": "^2.3.2" + }, + "resolutions": { + "tmp": "0.2.1" + } +} diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 00000000..f2253505 Binary files /dev/null and b/public/favicon.png differ diff --git a/public/index.html b/public/index.html new file mode 100644 index 00000000..f2fe4974 --- /dev/null +++ b/public/index.html @@ -0,0 +1,80 @@ + + + + + + + + + + + Rammerhead Proxy + + + +
+ +

Rammerhead Proxy Beta

+
+
+ +

Notice: inactive sessions will be deleted after 1 day and all sessions will be + deleted max 4 days after their creation.

+

Join the Discord server for updates or just chat about life

+

Notice 2: Treat every session id like an isolated incognito browser that + belongs only to you. DO NOT SHARE THE SESSION ID. All logins that you make with the session id will be saved + in that session. Anyone that has your session id or session url CAN ACCESS your logged in sites.

+
+
+ Enter password +
+ +
+
+
+ Enter URL +
+ +
+ +
+
+
+
+ Session ID +
+ +
+ +
+
+ + +
+
+ + + + + + + + + +
Session IDCreated on
+
+ + + + \ No newline at end of file diff --git a/public/logo.png b/public/logo.png new file mode 100644 index 00000000..5b823be6 Binary files /dev/null and b/public/logo.png differ diff --git a/public/script.js b/public/script.js new file mode 100644 index 00000000..45cfde47 --- /dev/null +++ b/public/script.js @@ -0,0 +1,227 @@ +(function () { + function setError(err) { + var element = document.getElementById('error-text'); + if (err) { + element.style.display = 'block'; + element.textContent = 'An error occurred: ' + err; + } else { + element.style.display = 'none'; + element.textContent = ''; + } + } + function getPassword() { + return document.getElementById('session-password').value; + } + function get(url, callback, shush = false) { + var pwd = getPassword(); + if (pwd) { + // really cheap way of adding a query parameter + if (url.includes('?')) { + url += '&pwd=' + pwd; + } else { + url += '?pwd=' + pwd; + } + } + + var request = new XMLHttpRequest(); + request.open('GET', url, true); + request.send(); + + request.onerror = function () { + if (!shush) setError('Cannot communicate with the server'); + }; + request.onload = function () { + if (request.status === 200) { + callback(request.responseText); + } else { + if (!shush) + setError( + 'unexpected server response to not match "200". Server says "' + request.responseText + '"' + ); + } + }; + } + + var api = { + newsession(callback) { + get('/newsession', callback); + }, + editsession(id, httpProxy, callback) { + get( + '/editsession?id=' + + encodeURIComponent(id) + + (httpProxy ? '&httpProxy=' + encodeURIComponent(httpProxy) : ''), + function (res) { + if (res !== 'Success') return setError('unexpected response from server. received ' + res); + callback(); + } + ); + }, + sessionexists(id, callback) { + get('/sessionexists?id=' + encodeURIComponent(id), function (res) { + if (res === 'exists') return callback(true); + if (res === 'not found') return callback(false); + setError('unexpected response from server. received' + res); + }); + }, + deletesession(id, callback) { + api.sessionexists(id, function (exists) { + if (exists) { + get('/deletesession?id=' + id, function (res) { + if (res !== 'Success' && res !== 'not found') + return setError('unexpected response from server. received ' + res); + callback(); + }); + } else { + callback(); + } + }); + } + }; + + var localStorageKey = 'rammerhead_sessionids'; + var localStorageKeyDefault = 'rammerhead_default_sessionid'; + var sessionIdsStore = { + get() { + var rawData = localStorage.getItem(localStorageKey); + if (!rawData) return []; + try { + var data = JSON.parse(rawData); + if (!Array.isArray(data)) throw 'getout'; + return data; + } catch (e) { + return []; + } + }, + set(data) { + if (!data || !Array.isArray(data)) throw new TypeError('must be array'); + localStorage.setItem(localStorageKey, JSON.stringify(data)); + }, + getDefault() { + var sessionId = localStorage.getItem(localStorageKeyDefault); + if (sessionId) { + var data = sessionIdsStore.get(); + data.filter((e) => e.id === sessionId); + if (data.length) return data[0]; + } + return null; + }, + setDefault(id) { + localStorage.setItem(localStorageKeyDefault, id); + } + }; + + function renderSessionTable(data) { + var tbody = document.querySelector('tbody'); + while (tbody.firstChild && !tbody.firstChild.remove()); + for (var i = 0; i < data.length; i++) { + var tr = document.createElement('tr'); + appendIntoTr(data[i].id); + appendIntoTr(data[i].createdOn); + + var fillInBtn = document.createElement('button'); + fillInBtn.textContent = 'Fill in existing session ID'; + fillInBtn.className = 'btn btn-outline-primary'; + fillInBtn.onclick = index(i, function (idx) { + setError(); + sessionIdsStore.setDefault(data[idx].id); + loadSettings(data[idx]); + }); + appendIntoTr(fillInBtn); + + var deleteBtn = document.createElement('button'); + deleteBtn.textContent = 'Delete'; + deleteBtn.className = 'btn btn-outline-danger'; + deleteBtn.onclick = index(i, function (idx) { + setError(); + api.deletesession(data[idx].id, function () { + data.splice(idx, 1)[0]; + sessionIdsStore.set(data); + renderSessionTable(data); + }); + }); + appendIntoTr(deleteBtn); + + tbody.appendChild(tr); + } + function appendIntoTr(stuff) { + var td = document.createElement('td'); + if (typeof stuff === 'object') { + td.appendChild(stuff); + } else { + td.textContent = stuff; + } + tr.appendChild(td); + } + function index(i, func) { + return func.bind(null, i); + } + } + function loadSettings(session) { + document.getElementById('session-id').value = session.id; + document.getElementById('session-httpproxy').value = session.httpproxy || ''; + } + function loadSessions() { + var sessions = sessionIdsStore.get(); + var defaultSession = sessionIdsStore.getDefault(); + if (defaultSession) loadSettings(defaultSession); + renderSessionTable(sessions); + } + function addSession(id) { + var data = sessionIdsStore.get(); + data.unshift({ id: id, createdOn: new Date().toLocaleString() }); + sessionIdsStore.set(data); + renderSessionTable(data); + } + function editSession(id, httpproxy) { + var data = sessionIdsStore.get(); + for (var i = 0; i < data.length; i++) { + if (data[i].id === id) { + data[i].httpproxy = httpproxy; + sessionIdsStore.set(data); + return; + } + } + throw new TypeError('cannot find ' + id); + } + + window.addEventListener('load', function () { + loadSessions(); + + var showingAdvancedOptions = false; + document.getElementById('session-advanced-toggle').onclick = function () { + // eslint-disable-next-line no-cond-assign + document.getElementById('session-advanced-container').style.display = (showingAdvancedOptions = + !showingAdvancedOptions) + ? 'block' + : 'none'; + }; + + document.getElementById('session-create-btn').onclick = function () { + setError(); + api.newsession(function (id) { + addSession(id); + document.getElementById('session-id').value = id; + document.getElementById('session-httpproxy').value = ''; + }); + }; + function go() { + setError(); + var id = document.getElementById('session-id').value; + var httpproxy = document.getElementById('session-httpproxy').value; + var url = document.getElementById('session-url').value || 'https://www.google.com/'; + if (!id) return setError('must generate a session id first'); + api.sessionexists(id, function (value) { + if (!value) return setError('session does not exist. try deleting or generating a new session'); + api.editsession(id, httpproxy, function () { + editSession(id, httpproxy); + window.open('/' + id + '/' + url); + }); + }); + } + document.getElementById('session-go').onclick = go; + document.getElementById('session-url').onkeydown = function (event) { + if (event.key === 'Enter') go(); + }; + }); +})(); diff --git a/public/style.css b/public/style.css new file mode 100644 index 00000000..236b4178 --- /dev/null +++ b/public/style.css @@ -0,0 +1,28 @@ +body { + background-color: rgb(255, 239, 231); + font-family: Arial, Helvetica, sans-serif; +} +header { + text-align: center; +} +header h1 { + margin-top: 0; + font-weight: bold; +} +header img { + width: 200px; +} +.input-group-text, .disable-text { + cursor: default; + user-select: none; +} + +table.table-bordered { + border: 1px solid #c1c1c1; +} +table.table-bordered > thead > tr > th { + border: 1px solid #c1c1c1; +} +table.table-bordered > tbody > tr > td { + border: 1px solid #c1c1c1; +} diff --git a/sessions/.gitkeep b/sessions/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/classes/RammerheadLogging.js b/src/classes/RammerheadLogging.js new file mode 100644 index 00000000..7417ad20 --- /dev/null +++ b/src/classes/RammerheadLogging.js @@ -0,0 +1,80 @@ +/** + * @typedef {'disabled'|'debug'|'traffic'|'info'|'warn'|'error'} LoggingLevels + */ + +const LOG_LEVELS = ['disabled', 'debug', 'traffic', 'info', 'warn', 'error']; + +function defaultGeneratePrefix(level) { + return `[${new Date().toISOString()}] [${level.toUpperCase()}] `; +} + +class RammerheadLogging { + /** + * @param {object} options + * @param {LoggingLevels} options.logLevel - logLevel to initialize the logger with + * @param {(data: string) => void} options.logger - expects the logger to automatically add a newline, just like what console.log does + * @param {*} options.loggerThis - logger will be called with loggerThis binded + * @param {(level: LoggingLevels) => string} options.generatePrefix - generates a prefix before every log. set to null to disable + */ + constructor({ + logLevel = 'info', + logger = console.log, + loggerThis = console, + generatePrefix = defaultGeneratePrefix + } = {}) { + this.logger = logger; + this.loggerThis = loggerThis; + this.generatePrefix = generatePrefix; + + /** + * @private + */ + this._logRank = this._getLogRank(logLevel); + } + + get logLevel() { + return LOG_LEVELS[this._logRank]; + } + /** + * logger() will be called based on this log level + * @param {LoggingLevels} level + */ + set logLevel(level) { + this._logRank = this._getLogRank(level); + } + callLogger(data) { + this.logger.call(this.loggerThis, data); + } + /** + * @param {LoggingLevels} level + * @param {string} data + */ + log(level, data) { + const rank = this._getLogRank(level); + // the higher the log level, the more important it is. + // ensure it's not disabled + if (rank && this._logRank <= rank) { + this.callLogger((this.generatePrefix ? this.generatePrefix(level) : '') + data); + } + } + debug = (data) => this.log('debug', data); + traffic = (data) => this.log('traffic', data); + info = (data) => this.log('info', data); + warn = (data) => this.log('warn', data); + error = (data) => this.log('error', data); + + /** + * @private + * @param {LoggingLevels} level + * @returns {number} + */ + _getLogRank(level) { + const index = LOG_LEVELS.indexOf(level); + if (index === -1) { + throw new TypeError(`Invalid log level '${level}'. Valid log levels: ${LOG_LEVELS.join(', ')}`); + } + return index; + } +} + +module.exports = RammerheadLogging; diff --git a/src/classes/RammerheadProxy.js b/src/classes/RammerheadProxy.js new file mode 100644 index 00000000..d6e837e3 --- /dev/null +++ b/src/classes/RammerheadProxy.js @@ -0,0 +1,517 @@ +const http = require('http'); +const https = require('https'); +const stream = require('stream'); +const fs = require('fs'); +const path = require('path'); +const { getPathname } = require('testcafe-hammerhead/lib/utils/url'); +const { Proxy } = require('testcafe-hammerhead'); +const WebSocket = require('ws'); +const logger = require('../util/logger'); +const httpResponse = require('../util/httpResponse'); +const streamToString = require('../util/streamToString'); +const URLPath = require('../util/URLPath'); + +require('../util/fixCorsHeader'); + +/** + * taken directly from + * https://github.com/DevExpress/testcafe-hammerhead/blob/a9fbf7746ff347f7bdafe1f80cf7135eeac21e34/src/typings/proxy.d.ts#L1 + * @typedef {object} ServerInfo + * @property {string} hostname + * @property {number} port + * @property {number} crossDomainPort + * @property {string} protocol + * @property {string} domain + * @property {boolean} cacheRequests + */ + +/** + * @typedef {object} RammerheadServerInfo + * @property {string} hostname + * @property {number} port + * @property {'https:'|'http:'} protocol + */ + +/** + * @private + * @typedef {import('./RammerheadSession')} RammerheadSession + */ + +/** + * wrapper for hammerhead's Proxy + */ +class RammerheadProxy extends Proxy { + /** + * + * @param {object} options + * @param {(req: http.IncomingMessage) => string} options.loggerGetIP - use custom logic to get IP, either from headers or directly + * @param {string} options.bindingAddress - hostname for proxy to bind to + * @param {number} options.port - port for proxy to listen to + * @param {number|null} options.crossDomainPort - crossDomain port to simulate cross origin requests. set to null + * to disable using this. highly not recommended to disable this because it breaks sites that check for the origin header + * @param {http.ServerOptions} options.ssl - set to null to disable ssl + * @param {(req: http.IncomingMessage) => RammerheadServerInfo} options.getServerInfo - force hammerhead to rewrite using specified + * server info (server info includes hostname, port, and protocol). Useful for a reverse proxy setup like nginx where you + * need to rewrite the hostname/port/protocol + */ + constructor({ + loggerGetIP = (req) => req.socket.remoteAddress, + bindingAddress = '127.0.0.1', + port = 8080, + crossDomainPort = 8081, + ssl = null, + getServerInfo = (req) => { + const { hostname, port } = new URL('http://' + req.headers.host); + return { + hostname, + port, + protocol: req.socket.encrypted ? 'https:' : 'http:' + }; + } + } = {}) { + if (!crossDomainPort) { + const httpOrHttps = ssl ? https : http; + const proxyHttpOrHttps = http; + const originalProxyCreateServer = proxyHttpOrHttps.createServer; + const originalCreateServer = httpOrHttps.createServer; // handle recursion case if proxyHttpOrHttps and httpOrHttps are the same + let onlyOneHttpServer = null; + + // a hack to force testcafe-hammerhead's proxy library into using only one http port. + // a downside to using only one proxy server is that crossdomain requests + // will not be simulated correctly + proxyHttpOrHttps.createServer = function (...args) { + const emptyFunc = () => {}; + if (onlyOneHttpServer) { + // createServer for server1 already called. now we return a mock http server for server2 + return { on: emptyFunc, listen: emptyFunc, close: emptyFunc }; + } + if (args.length !== 2) throw new Error('unexpected argument length coming from hammerhead'); + return (onlyOneHttpServer = originalCreateServer(...args)); + }; + + // now, we force the server to listen to a specific port and a binding address, regardless of what + // hammerhead server.listen(anything) + const originalListen = http.Server.prototype.listen; + http.Server.prototype.listen = function (_proxyPort) { + originalListen.call(this, port, bindingAddress); + }; + + // actual proxy initialization + // the values don't matter (except for developmentMode), since we'll be rewriting serverInfo anyway + super('hostname', 'port', 'port', { + ssl, + developmentMode: false, + cache: true + }); + + // restore hooked functions to their original state + proxyHttpOrHttps.createServer = originalProxyCreateServer; + http.Server.prototype.listen = originalListen; + } else { + // just initialize the proxy as usual, since we don't need to do hacky stuff like the above. + // we still need to make sure the proxy binds to the correct address though + const originalListen = http.Server.prototype.listen; + http.Server.prototype.listen = function (portArg) { + originalListen.call(this, portArg, bindingAddress); + }; + super('doesntmatter', port, crossDomainPort, { + ssl, + developmentMode: false, + cache: true + }); + this.crossDomainPort = crossDomainPort; + http.Server.prototype.listen = originalListen; + } + + this._setupLocalStorageServiceRoutes(); + + this.onRequestPipeline = []; + this.onUpgradePipeline = []; + this.websocketRoutes = []; + this.rewriteServerHeaders = { + 'permissions-policy': (headerValue) => headerValue && headerValue.replace(/sync-xhr/g, 'sync-yes'), + 'feature-policy': (headerValue) => headerValue && headerValue.replace(/sync-xhr/g, 'sync-yes'), + 'referrer-policy': () => 'no-referrer-when-downgrade' + }; + + this.getServerInfo = getServerInfo; + this.serverInfo1 = null; // make sure no one uses these serverInfo + this.serverInfo2 = null; + + this.loggerGetIP = loggerGetIP; + } + + // add WS routing + /** + * since we have .GET and .POST, why not add in a .WS also + * @param {string|RegExp} route - can be '/route/to/things' or /^\\/route\\/(this)|(that)\\/things$/ + * @param {(ws: WebSocket, req: http.IncomingMessage) => WebSocket} handler - ws is the connection between the client and the server + * @param {object} websocketOptions - read https://www.npmjs.com/package/ws for a list of Websocket.Server options. Note that + * the { noServer: true } will always be applied + * @returns {WebSocket.Server} + */ + WS(route, handler, websocketOptions = {}) { + if (this.checkIsRoute(route)) { + throw new TypeError('WS route already exists'); + } + + const wsServer = new WebSocket.Server({ + ...websocketOptions, + noServer: true + }); + this.websocketRoutes.push({ route, handler, wsServer }); + + return wsServer; + } + unregisterWS(route) { + if (!this.getWSRoute(route, true)) { + throw new TypeError('websocket route does not exist'); + } + } + /** + * @param {string} path + * @returns {{ route: string|RegExp, handler: (ws: WebSocket, req: http.IncomingMessage) => WebSocket, wsServer: WebSocket.Server}|null} + */ + getWSRoute(path, doDelete = false) { + for (let i = 0; i < this.websocketRoutes.length; i++) { + if ( + (typeof this.websocketRoutes[i].route === 'string' && this.websocketRoutes[i].route === path) || + (this.websocketRoutes[i] instanceof RegExp && this.websocketRoutes[i].route.test(path)) + ) { + const route = this.websocketRoutes[i]; + if (doDelete) { + this.websocketRoutes.splice(i, 1); + i--; + } + return route; + } + } + return null; + } + /** + * @private + */ + _WSRouteHandler(req, socket, head) { + const route = this.getWSRoute(req.url); + if (route) { + // RH stands for rammerhead. RHROUTE is a custom implementation by rammerhead that is + // unrelated to hammerhead + logger.traffic(`WSROUTE UPGRADE ${this.loggerGetIP(req)} ${req.url}`); + route.wsServer.handleUpgrade(req, socket, head, (client, req) => { + logger.traffic(`WSROUTE OPEN ${this.loggerGetIP(req)} ${req.url}`); + client.once('close', () => { + logger.traffic(`WSROUTE CLOSE ${this.loggerGetIP(req)} ${req.url}`); + }); + route.handler(client, req); + }); + return true; + } + } + + // manage pipelines // + /** + * @param {(req: http.IncomingMessage, + * res: http.ServerResponse, + * serverInfo: ServerInfo, + * isRoute: boolean, + * isWebsocket: boolean) => Promise} onRequest - return true to terminate handoff to proxy. + * There is an isWebsocket even though there is an onUpgrade pipeline already. This is because hammerhead + * processes the onUpgrade and then passes it directly to onRequest, but without the "head" Buffer argument. + * The onUpgrade pipeline is to solve that lack of the "head" argument issue in case one needs it. + * @param {boolean} beginning - whether to add it to the beginning of the pipeline + */ + addToOnRequestPipeline(onRequest, beginning = false) { + if (beginning) { + this.onRequestPipeline.push(onRequest); + } else { + this.onRequestPipeline.unshift(onRequest); + } + } + /** + * @param {(req: http.IncomingMessage, + * socket: stream.Duplex, + * head: Buffer, + * serverInfo: ServerInfo, + * isRoute: boolean) => Promise} onUpgrade - return true to terminate handoff to proxy + * @param {boolean} beginning - whether to add it to the beginning of the pipeline + */ + addToOnUpgradePipeline(onUpgrade, beginning = false) { + if (beginning) { + this.onUpgradePipeline.push(onUpgrade); + } else { + this.onUpgradePipeline.unshift(onUpgrade); + } + } + + // override hammerhead's proxy functions to use the pipeline // + checkIsRoute(req) { + if (req instanceof RegExp) { + return !!this.getWSRoute(req); + } + // code modified from + // https://github.com/DevExpress/testcafe-hammerhead/blob/879d6ae205bb711dfba8c1c88db635e8803b8840/src/proxy/router.ts#L95 + const routerQuery = `${req.method} ${getPathname(req.url || '')}`; + const route = this.routes.get(routerQuery); + if (route) { + return true; + } + for (const routeWithParams of this.routesWithParams) { + const routeMatch = routerQuery.match(routeWithParams.re); + if (routeMatch) { + return true; + } + } + return !!this.getWSRoute(req.url); + } + /** + * @param {http.IncomingMessage} req + * @param {http.ServerResponse} res + * @param {ServerInfo} serverInfo + */ + async _onRequest(req, res, serverInfo) { + serverInfo = this._rewriteServerInfo(req); + + // strip server headers + const originalWriteHead = res.writeHead; + const self = this; + res.writeHead = function (statusCode, statusMessage, headers) { + if (!headers) { + headers = statusMessage; + statusMessage = undefined; + } + + if (headers) { + const alreadyRewrittenHeaders = []; + if (Array.isArray(headers)) { + // [content-type, text/html, headerKey, headerValue, ...] + for (let i = 0; i < headers.length - 1; i += 2) { + const header = headers[i].toLowerCase(); + if (header in self.rewriteServerHeaders) { + alreadyRewrittenHeaders.push(header); + headers[i + 1] = + self.rewriteServerHeaders[header] && self.rewriteServerHeaders[header](headers[i + 1]); + if (!headers[i + 1]) { + headers.splice(i, 2); + i -= 2; + } + } + } + for (const header in self.rewriteServerHeaders) { + if (alreadyRewrittenHeaders.includes(header)) continue; + // if user wants to add headers, they can do that here + const value = self.rewriteServerHeaders[header] && self.rewriteServerHeaders[header](); + if (value) { + headers.push(header, value); + } + } + } else { + for (const header in headers) { + if (header in self.rewriteServerHeaders) { + alreadyRewrittenHeaders.push(header); + headers[header] = self.rewriteServerHeaders[header] && self.rewriteServerHeaders[header](); + if (!headers[header]) { + delete headers[header]; + } + } + } + for (const header in self.rewriteServerHeaders) { + if (alreadyRewrittenHeaders.includes(header)) continue; + const value = self.rewriteServerHeaders[header] && self.rewriteServerHeaders[header](); + if (value) { + headers[header] = value; + } + } + } + } + + if (statusMessage) { + originalWriteHead.call(this, statusCode, statusMessage, headers); + } else { + originalWriteHead.call(this, statusCode, headers); + } + }; + + const isWebsocket = res instanceof stream.Duplex; + const isRoute = this.checkIsRoute(req); + const ip = this.loggerGetIP(req); + + logger.traffic(`${isRoute ? 'ROUTE ' : ''}${ip} ${req.url}`); + for (const handler of this.onRequestPipeline) { + if ((await handler.call(this, req, res, serverInfo, isRoute, isWebsocket)) === true) { + return; + } + } + // hammerhead's routing does not support websockets. Allowing it + // will result in an error thrown + if (isRoute && isWebsocket) { + httpResponse.badRequest(req, res, ip, 'Rejected unsupported websocket request'); + return; + } + super._onRequest(req, res, serverInfo); + } + /** + * @param {http.IncomingMessage} req + * @param {stream.Duplex} socket + * @param {Buffer} head + * @param {ServerInfo} serverInfo + */ + async _onUpgradeRequest(req, socket, head, serverInfo) { + serverInfo = this._rewriteServerInfo(req); + for (const handler of this.onUpgradePipeline) { + const isRoute = this.checkIsRoute(req); + if ((await handler.call(this, req, socket, head, serverInfo, isRoute)) === true) { + return; + } + } + if (this._WSRouteHandler(req, socket, head)) return; + super._onUpgradeRequest(req, socket, head, serverInfo); + } + + /** + * @private + * @param {http.IncomingMessage} req + * @returns {ServerInfo} + */ + _rewriteServerInfo(req) { + const serverInfo = this.getServerInfo(req); + return { + hostname: serverInfo.hostname, + port: serverInfo.port, + crossDomainPort: serverInfo.crossDomainPort || this.crossDomainPort || serverInfo.port, + protocol: serverInfo.protocol, + domain: `${serverInfo.protocol}//${serverInfo.hostname}:${serverInfo.port}`, + cacheRequests: true + }; + } + /** + * @private + */ + _setupLocalStorageServiceRoutes() { + this.GET('/rammerhead.js', { + content: fs.readFileSync(path.join(__dirname, '../client/rammerhead.js')), + contentType: 'application/x-javascript' + }); + this.POST('/syncLocalStorage', async (req, res) => { + const badRequest = (msg) => httpResponse.badRequest(req, res, this.loggerGetIP(req), msg); + const respondJson = (obj) => res.end(JSON.stringify(obj)); + const { sessionId, origin } = new URLPath(req.url).getParams(); + + if (!sessionId || !this.openSessions.has(sessionId)) { + return badRequest('Invalid session id'); + } + if (!origin) { + return badRequest('Invalid origin'); + } + + let parsed; + try { + parsed = JSON.parse(await streamToString(req)); + } catch (e) { + return badRequest('bad client body'); + } + + const now = Date.now(); + const session = this.openSessions.get(sessionId, false); + if (!session.data.localStorage) session.data.localStorage = {}; + + switch (parsed.type) { + case 'sync': + if (parsed.fetch) { + // client is syncing for the first time + if (!session.data.localStorage[origin]) { + // server does not have any data on origin, so create an empty record + // and send an empty object back + session.data.localStorage[origin] = { data: {}, timestamp: now }; + return respondJson({ + timestamp: now, + data: {} + }); + } else { + // server does have data, so send data back + return respondJson({ + timestamp: session.data.localStorage[origin].timestamp, + data: session.data.localStorage[origin].data + }); + } + } else { + // sync server and client localStorage + + parsed.timestamp = parseInt(parsed.timestamp); + if (isNaN(parsed.timestamp)) return badRequest('must specify valid timestamp'); + if (parsed.timestamp > now) return badRequest('cannot specify timestamp in the future'); + if (!parsed.data || typeof parsed.data !== 'object') + return badRequest('data must be an object'); + + for (const prop in parsed.data) { + if (typeof parsed.data[prop] !== 'string') { + return badRequest('data[prop] must be a string'); + } + } + + if (!session.data.localStorage[origin]) { + // server does not have data, so use client's + session.data.localStorage[origin] = { data: parsed.data, timestamp: now }; + return respondJson({}); + } else if (session.data.localStorage[origin].timestamp <= parsed.timestamp) { + // server data is either the same as client or outdated, but we + // sync even if timestamps are the same in case the client changed the localStorage + // without updating + session.data.localStorage[origin].data = parsed.data; + session.data.localStorage[origin].timestamp = parsed.timestamp; + return respondJson({}); + } else { + // client data is stale + return respondJson({ + timestamp: session.data.localStorage[origin].timestamp, + data: session.data.localStorage[origin].data + }); + } + } + case 'update': + if (!session.data.localStorage[origin]) + return badRequest('must perform sync first on a new origin'); + if (!parsed.updateData || typeof parsed.updateData !== 'object') + return badRequest('updateData must be an object'); + for (const prop in parsed.updateData) { + if (!parsed.updateData[prop] || typeof parsed.updateData[prop] !== 'string') + return badRequest('updateData[prop] must be a non-empty string'); + } + for (const prop in parsed.updateData) { + session.data.localStorage[origin].data[prop] = parsed.updateData[prop]; + } + session.data.localStorage[origin].timestamp = now; + return respondJson({ + timestamp: now + }); + default: + return badRequest('unknown type ' + parsed.type); + } + }); + } + + openSession() { + throw new TypeError('unimplemented. please use a RammerheadSessionStore and use their .add() method'); + } + close() { + super.close(); + this.openSessions.close(); + } + + // the following is to fix hamerhead's typescript definitions + /** + * @param {string} route + * @param {StaticContent | (req: http.IncomingMessage, res: http.ServerResponse) => void} handler + */ + GET(route, handler) { + super.GET(route, handler); + } + /** + * @param {string} route + * @param {StaticContent | (req: http.IncomingMessage, res: http.ServerResponse) => void} handler + */ + POST(route, handler) { + super.POST(route, handler); + } +} + +module.exports = RammerheadProxy; diff --git a/src/classes/RammerheadRateLimiter.js b/src/classes/RammerheadRateLimiter.js new file mode 100644 index 00000000..1e454026 --- /dev/null +++ b/src/classes/RammerheadRateLimiter.js @@ -0,0 +1,78 @@ +const { RateLimiterMemory, BurstyRateLimiter, RateLimiterCluster } = require('rate-limiter-flexible'); +const logger = require('../util/logger'); + +class RammerheadRateLimiter { + /** + * @param {object} options + * @param {number} options.requestsPerSecond + * @param {number} options.burst - this setting is an nginx style rate limiting. How this setting works is this + * that if the user goes over the requestsPerSecond, it uses up the burst. if the "bucket" is depleted, + * then the user is blocked. the bucket charges at a rate of requestsPerSecond + * @param {string|null|undefined} options.useClusterStore - if true, it will use RateLimiterCluster for storing data + * @param {(req: IncomingMessage) => string} options.getIP - the rate limiter depends on this to distinguish between users + */ + constructor({ + requestsPerSecond = 100, + burst = 500, + useClusterStore = false, + getIP = (req) => req.socket.remoteAddress + }) { + const MemoryOrClusterStorage = useClusterStore ? RateLimiterCluster : RateLimiterMemory; + + this.getIP = getIP; + this.limiter = new BurstyRateLimiter( + new MemoryOrClusterStorage({ + keyPrefix: 'cluster', + points: requestsPerSecond, + duration: 1 + }), + new MemoryOrClusterStorage({ + keyPrefix: 'clusterburst', + points: burst, + duration: burst / requestsPerSecond + }) + ); + } + /** + * @param {import('./RammerheadProxy')} proxy + */ + attachToProxy(proxy) { + if (proxy.rateLimiterActive) throw new TypeError('already added rate limiter'); + proxy.rateLimiterActive = true; + + proxy.addToOnUpgradePipeline(async (req, socket) => { + if (await this._handler(req)) { + socket.end('HTTP/1.1 429 Too Many Requests\n\n'); + return true; + } + }); + proxy.addToOnRequestPipeline(async (req, res, _serverInfo, _isRoute, isWebsocket) => { + if (!isWebsocket && (await this._handler(req))) { + // already handled by the above + res.writeHead(429); + res.end('429 Too many requests'); + return true; + } + }); + } + /** + * @private + * @param {import('http').IncomingMessage} req + * @returns {Promise} - true for throttling + */ + async _handler(req) { + const ip = this.getIP(req); + try { + await this.limiter.consume(this.getIP(req)); + return false; + } catch (e) { + if (!('msBeforeNext' in e)) { + throw e; + } + logger.warn(`(RateLimiter) throttling ${ip}`); + return true; + } + } +} + +module.exports = RammerheadRateLimiter; diff --git a/src/classes/RammerheadSession.js b/src/classes/RammerheadSession.js new file mode 100644 index 00000000..b0863d3c --- /dev/null +++ b/src/classes/RammerheadSession.js @@ -0,0 +1,106 @@ +const { Session } = require('testcafe-hammerhead'); +const UploadStorage = require('testcafe-hammerhead/lib/upload/storage'); +const generateId = require('../util/generateId'); + +// disable UploadStorage, a testcafe testing feature we do not need +const emptyFunc = () => {}; +UploadStorage.prototype.copy = emptyFunc; +UploadStorage.prototype.get = emptyFunc; +UploadStorage.prototype.store = emptyFunc; + +/** + * wrapper for initializing Session with saving capabilities + */ +class RammerheadSession extends Session { + data = {}; + createdAt = Date.now(); + lastUsed = Date.now(); + + /** + * @param {object} options + * @param {string} options.id + * @param {boolean} options.dontConnectToData - used when we want to connect to data later (or simply don't wnat to) + */ + constructor({ id = generateId(), dontConnectToData = false } = {}) { + super(['blah/blah'], { + allowMultipleWindows: true, + disablePageCaching: false + }); + + // necessary abstract methods for Session + this.getIframePayloadScript = async () => ''; + this.getPayloadScript = async () => ''; + this.getAuthCredentials = () => ({}); + this.handleFileDownload = () => void 0; + this.handlePageError = () => void 0; + // this.handlePageError = (ctx, err) => { + // console.error(ctx.req.url); + // console.error(err); + // }; + + // support localStorage // + this.injectable.scripts.push('/rammerhead.js'); + + this.id = id; + if (!dontConnectToData) { + this.connectHammerheadToData(); + } + } + /** + * @param {boolean} dontCookie - set this to true if the store is using a more reliable approach to + * saving the cookies (like in serializeSession) + */ + connectHammerheadToData(dontCookie = false) { + this._connectObjectToHook(this, 'createdAt'); + this._connectObjectToHook(this, 'lastUsed'); + this._connectObjectToHook(this, 'injectable'); + this._connectObjectToHook(this, 'externalProxySettings'); + if (!dontCookie) this._connectObjectToHook(this.cookies._cookieJar.store, 'idx', 'cookies'); + } + + updateLastUsed() { + this.lastUsed = Date.now(); + } + serializeSession() { + return JSON.stringify({ + data: this.data, + serializedCookieJar: this.cookies.serializeJar() + }); + } + // hook system and serializing are for two different store systems + static DeserializeSession(id, serializedSession) { + const parsed = JSON.parse(serializedSession); + if (!parsed.data) throw new Error('expected serializedSession to contain data object'); + if (!parsed.serializedCookieJar) + throw new Error('expected serializedSession to contain serializedCookieJar object'); + + const session = new RammerheadSession({ id, dontConnectToData: true }); + session.data = parsed.data; + session.connectHammerheadToData(true); + session.cookies.setJar(parsed.serializedCookieJar); + return session; + } + + hasRequestEventListeners() { + // force forceProxySrcForImage to be true + // see https://github.com/DevExpress/testcafe-hammerhead/blob/a9fbf7746ff347f7bdafe1f80cf7135eeac21e34/src/session/index.ts#L180 + return true; + } + /** + * @private + */ + _connectObjectToHook(obj, prop, dataProp = prop) { + const originalValue = obj[prop]; + Object.defineProperty(obj, prop, { + get: () => this.data[dataProp], + set: (value) => { + this.data[dataProp] = value; + } + }); + if (!(dataProp in this.data)) { + this.data[dataProp] = originalValue; + } + } +} + +module.exports = RammerheadSession; diff --git a/src/classes/RammerheadSessionAbstractStore.js b/src/classes/RammerheadSessionAbstractStore.js new file mode 100644 index 00000000..faad6f0e --- /dev/null +++ b/src/classes/RammerheadSessionAbstractStore.js @@ -0,0 +1,92 @@ +/* eslint-disable no-unused-vars */ + +/** + * @private + * @typedef {import("./RammerheadSession")} RammerheadSession + */ + +/** + * this is the minimum in order to have a fully working versatile session store. Though it is an abstract + * class and should be treated as such, it includes default functions deemed necessary that are not + * particular to different implementations + * @abstract + */ +class RammerheadSessionAbstractStore { + constructor() { + if (this.constructor === RammerheadSessionAbstractStore) { + throw new Error('abstract classes cannot be instantiated'); + } + } + + /** + * + * @param {import('./RammerheadProxy')} proxy - this will overwrite proxy.openSessions with this class instance and + * adds a request handler that calls loadSessionToMemory + * @param {boolean} removeExistingSessions - whether to remove all sessions before overwriting proxy.openSessions + */ + attachToProxy(proxy, removeExistingSessions = true) { + if (proxy.openSessions === this) throw new TypeError('already attached to proxy'); + + if (removeExistingSessions) { + for (const [, session] of proxy.openSessions.entries()) { + proxy.closeSession(session); + } + } + proxy.openSessions = this; + } + + /** + * @private + */ + _mustImplement() { + throw new Error('must be implemented'); + } + + /** + * @abstract + * @returns {string[]} - list of session ids in store + */ + keys() { + this._mustImplement(); + } + /** + * @abstract + * @param {string} id + */ + has(id) { + this._mustImplement(); + } + /** + * @abstract + * @param {string} id + * @param {boolean} updateActiveTimestamp + * @returns {RammerheadSession|undefined} + */ + get(id, updateActiveTimestamp = true) { + this._mustImplement(); + } + /** + * the implemented method here will use the dataOperation option in RammerheadSession however they + * see fit + * @abstract + * @param {string} id + * @returns {RammerheadSession} + */ + add(id) { + this._mustImplement(); + } + /** + * @abstract + * @param {string} id + * @returns {boolean} - returns true when a delete operation is performed + */ + delete(id) { + this._mustImplement(); + } + /** + * optional abstract method + */ + close() {} +} + +module.exports = RammerheadSessionAbstractStore; diff --git a/src/classes/RammerheadSessionFilePersistentStore.js b/src/classes/RammerheadSessionFilePersistentStore.js new file mode 100644 index 00000000..eacdec37 --- /dev/null +++ b/src/classes/RammerheadSessionFilePersistentStore.js @@ -0,0 +1,174 @@ +const fs = require('fs'); +const path = require('path'); +const rimraf = require('rimraf'); +const RammerheadSessionAbstractStore = require('./RammerheadSessionAbstractStore'); +const RammerheadSession = require('./RammerheadSession'); +const ObjectFileStore = require('../util/ObjectFileDatabase'); +const logger = require('../util/logger'); + +const sessionFolderExtension = '.rhsession'; + +/** + * perfect for load balancing because they share the same session data. + * not good for servers that have slow disk iops speeds. A downside to this is + * there will be race conditions that will happen when the workers access the data. So + * occasionally, there will inevitably be thrown errors here and there. + * Also, while testing, "expiryTime is not a function" has been encountered. Although this is still speculation and + * further investigation is required, this is subject to the limitations of ObjectFileStore, which don't instantiate + * an object class correctly. Though this is likely not the case since the memstore of tough-cookie uses it like a regular object, + * (see https://github.com/salesforce/tough-cookie/blob/1b25269dbb0478232f910c26386b3cac4ec9d857/lib/memstore.js#L42) + * further work needs to be put in. tl;dr: this sessionstore class is unreliable and can break randomly. + */ +class RammerheadSessionFilePersistentStore extends RammerheadSessionAbstractStore { + /** + * + * @param {object} options + * @param {string} options.saveDirectory - all sessions will be saved in this folder + * to avoid storing all the sessions in the memory. + * @param {number} options.unloadMemoryTimeout - timeout before unloading cached session (cached meaning cached folder structure) + * @param {number} options.unloadMemoryInterval - timeout between unloadMemory runs + * @param {object|null} options.cleanupOptions - set to null to disable cleaning up stale sessions + * @param {number|null} options.cleanupOptions.staleTimeout - stale sessions that are inside saveDirectory that go over + * this timeout will be deleted. Set to null to disable. + * @param {number|null} options.cleanupOptions.maxToLive - any created sessions that maxToLive < (now - createdAt) will be deleted. + * Set to null to disable. + * @param {number} options.cleanupOptions.cleanupInterval - timeout between staleTimeout cleanup runs. + */ + constructor({ + saveDirectory = path.join(__dirname, '../sessions'), + unloadMemoryTimeout = 1000 * 60 * 20, // 20 minutes + unloadMemoryInterval = 1000 * 60 * 5, // 5 minutes + cleanupOptions = { + staleTimeout: 1000 * 60 * 60 * 24 * 1, // 1 day + maxToLive: 1000 * 60 * 60 * 24 * 4, // four days + cleanupInterval: 1000 * 60 * 60 * 1 // 1 hour + } + } = {}) { + super(); + this.saveDirectory = saveDirectory; + this.cachedSessions = new Map(); + setInterval(() => this._unloadMemoryRun(unloadMemoryTimeout), unloadMemoryInterval).unref(); + if (cleanupOptions) { + setInterval( + () => this._cleanupRun(cleanupOptions.staleTimeout, cleanupOptions.maxToLive), + cleanupOptions.cleanupInterval + ).unref(); + } + } + + keys() { + return fs + .readdirSync(this.saveDirectory) + .filter((folder) => folder.endsWith(sessionFolderExtension)) + .map((folder) => folder.slice(0, -sessionFolderExtension.length)); + } + has(id) { + return fs.existsSync(this._getSessionFolderPath(id)); + } + get(id, updateActiveTimestamp = true) { + if (!this.has(id)) { + logger.debug(`(FilePersistentStore.get) ${id} does not exist`); + return; + } + + logger.debug(`(FilePersistentStore.get) ${id}`); + if (this.cachedSessions.has(id)) { + logger.debug(`(FilePersistentStore.get) returning memory cached session ${id}`); + return this.cachedSessions.get(id); + } + + const fileDatabase = new ObjectFileStore({ + handler: this._getSessionFolderPath(id) + }); + const session = new RammerheadSession({ id, dontConnectToData: true }); + session.data = fileDatabase.fileObject; + session.connectHammerheadToData(); + + if (updateActiveTimestamp) { + logger.debug(`(FilePersistentStore.get) ${id} update active timestamp`); + session.updateLastUsed(); + } + + this.cachedSessions.set(id, session); + logger.debug(`(FilePersistentStore.get) saved ${id} into cache memory`); + + return session; + } + add(id) { + if (this.has(id)) throw new Error(`session ${id} already exists`); + + fs.mkdirSync(this._getSessionFolderPath(id)); + + logger.debug(`FilePersistentStore.add ${id}`); + + return this.get(id); + } + delete(id) { + logger.debug(`(FilePersistentStore.delete) deleting ${id}`); + if (this.has(id)) { + rimraf.sync(this._getSessionFolderPath(id)); + this.cachedSessions.delete(id); + logger.debug(`(FilePersistentStore.delete) deleted ${id}`); + return true; + } + if (this.cachedSessions.has(id)) { + this.cachedSessions.delete(id); + logger.debug(`(FilePersistentStore.delete) removed ${id} from cache memory`); + } + logger.debug(`(FilePersistentStore.delete) ${id} does not exist`); + return false; + } + + /** + * @private + * @param {string} id + * @returns {string} - generated folder path to session + */ + _getSessionFolderPath(id) { + return path.join(this.saveDirectory, id.replace(/\/|\\/g, '') + sessionFolderExtension); + } + /** + * @private + * @param {number|null} staleTimeout + * @param {number|null} maxToLive + */ + _cleanupRun(staleTimeout, maxToLive) { + const sessionIds = this.keys(); + let deleteCount = 0; + logger.debug(`(FilePersistentStore._cleanupRun) Need to go through ${sessionIds.length} sessions`); + + const now = Date.now(); + for (const id of sessionIds) { + const session = this.get(id, false); + if ( + (staleTimeout && now - session.lastUsed > staleTimeout) || + (maxToLive && now - session.createdAt > maxToLive) + ) { + this.delete(id); + deleteCount++; + } + } + + logger.debug(`(FilePersistentStore._cleanupRun) Deleted ${deleteCount} sessions from store`); + } + /** + * @private + * @param {number} unloadMemoryTimeout + */ + _unloadMemoryRun(unloadMemoryTimeout) { + logger.debug(`(FilePersistentStore._unloadMemoryRun) need to go through ${this.cachedSessions.size} sessions`); + + const now = Date.now(); + for (const [sessionId, session] of this.cachedSessions) { + // sometimes the session data is already gone, so check for that first + if (!this.has(sessionId) || now - session.lastUsed > unloadMemoryTimeout) { + this.cachedSessions.delete(sessionId); + logger.debug(`(FilePersistentStore._unloadMemoryRun) removed ${sessionId} from memory`); + } + } + + logger.debug(`(FilePersistentStore._unloadMemoryRun) finished run`); + } +} + +module.exports = RammerheadSessionFilePersistentStore; diff --git a/src/classes/RammerheadSessionMemoryCacheFileStore.js b/src/classes/RammerheadSessionMemoryCacheFileStore.js new file mode 100644 index 00000000..c12f7202 --- /dev/null +++ b/src/classes/RammerheadSessionMemoryCacheFileStore.js @@ -0,0 +1,179 @@ +const fs = require('fs'); +const path = require('path'); +const RammerheadSessionAbstractStore = require('./RammerheadSessionAbstractStore'); +const RammerheadSession = require('./RammerheadSession'); +const logger = require('../util/logger'); + +// rh = rammerhead. extra f to distinguish between rhsession (folder) and rhfsession (file) +const sessionFileExtension = '.rhfsession'; + +/** + * This is a compromise between not using that much memory and not needing a high + * disk IOPs. Essentially gaining the benefit of memory read speeds and while decreasing + * the usage of memory (basically a cache). + */ +class RammerheadSessionMemoryCacheFileStore extends RammerheadSessionAbstractStore { + /** + * + * @param {object} options + * @param {string} options.saveDirectory - all unloadMemoryTimeouted sessions will be saved in this folder + * to avoid storing all the sessions in the memory. + * @param {number} options.unloadMemoryTimeout - timeout before unloading cached session + * @param {number} options.unloadMemoryInterval - timeout between unloadMemory runs + * @param {object|null} options.cleanupOptions - set to null to disable cleaning up stale sessions + * @param {number|null} options.cleanupOptions.staleTimeout - stale sessions that are inside saveDirectory that go over + * this timeout will be deleted. Set to null to disable. + * @param {number|null} options.cleanupOptions.maxToLive - any created sessions that maxToLive < (now - createdAt) will be deleted. + * Set to null to disable. + * @param {number} options.cleanupOptions.cleanupInterval - timeout between staleTimeout cleanup runs. + */ + constructor({ + saveDirectory = path.join(__dirname, '../sessions'), + unloadMemoryTimeout = 1000 * 60 * 20, // 20 minutes + unloadMemoryInterval = 1000 * 60 * 10, // 10 minutes + cleanupOptions = { + staleTimeout: 1000 * 60 * 60 * 24 * 1, // 1 day + maxToLive: 1000 * 60 * 60 * 24 * 4, // four days + cleanupInterval: 1000 * 60 * 60 * 1 // 1 hour + } + } = {}) { + super(); + this.saveDirectory = saveDirectory; + /** + * @type {Map.} + */ + this.cachedSessions = new Map(); + setInterval(() => this._unloadMemoryRun(unloadMemoryTimeout), unloadMemoryInterval).unref(); + if (cleanupOptions) { + setInterval( + () => this._cleanupRun(cleanupOptions.staleTimeout, cleanupOptions.maxToLive), + cleanupOptions.cleanupInterval + ).unref(); + } + } + + keysStore() { + return fs + .readdirSync(this.saveDirectory) + .filter((file) => file.endsWith(sessionFileExtension)) + .map((file) => file.slice(0, -sessionFileExtension.length)); + } + keys() { + let arr = this.keysStore(); + for (const id of this.cachedSessions.keys()) { + if (!arr.includes(id)) arr.push(id); + } + return arr; + } + has(id) { + return this.cachedSessions.has(id) || fs.existsSync(this._getSessionFilePath(id)); + } + get(id, updateActiveTimestamp = true, cacheToMemory = true) { + if (!this.has(id)) { + logger.debug(`(MemoryCacheFileStore.get) ${id} does not exist`); + return; + } + + logger.debug(`(MemoryCacheFileStore.get) ${id}`); + if (this.cachedSessions.has(id)) { + logger.debug(`(MemoryCacheFileStore.get) returning memory cached session ${id}`); + return this.cachedSessions.get(id); + } + + const session = RammerheadSession.DeserializeSession(id, fs.readFileSync(this._getSessionFilePath(id))); + + if (updateActiveTimestamp) { + logger.debug(`(MemoryCacheFileStore.get) ${id} update active timestamp`); + session.updateLastUsed(); + } + + if (cacheToMemory) { + this.cachedSessions.set(id, session); + logger.debug(`(MemoryCacheFileStore.get) saved ${id} into cache memory`); + } + + return session; + } + add(id) { + if (this.has(id)) throw new Error(`session ${id} already exists`); + + fs.writeFileSync(this._getSessionFilePath(id), new RammerheadSession().serializeSession()); + + logger.debug(`MemoryCacheFileStore.add ${id}`); + + return this.get(id); + } + delete(id) { + logger.debug(`(MemoryCacheFileStore.delete) deleting ${id}`); + if (this.has(id)) { + fs.unlinkSync(this._getSessionFilePath(id)); + this.cachedSessions.delete(id); + logger.debug(`(MemoryCacheFileStore.delete) deleted ${id}`); + return true; + } + logger.debug(`(MemoryCacheFileStore.delete) ${id} does not exist`); + return false; + } + close() { + logger.debug(`(MemoryCacheFileStore.close) calling _unloadMemoryRun`); + this._unloadMemoryRun(-1); + } + + /** + * @private + * @param {string} id + * @returns {string} - generated file path to session + */ + _getSessionFilePath(id) { + return path.join(this.saveDirectory, id.replace(/\/|\\/g, '') + sessionFileExtension); + } + /** + * @private + * @param {number|null} staleTimeout + * @param {number|null} maxToLive + */ + _cleanupRun(staleTimeout, maxToLive) { + const sessionIds = this.keysStore(); + let deleteCount = 0; + logger.debug(`(MemoryCacheFileStore._cleanupRun) Need to go through ${sessionIds.length} sessions in store`); + + const now = Date.now(); + for (const id of sessionIds) { + const session = this.get(id, false); + if ( + (staleTimeout && now - session.lastUsed > staleTimeout) || + (maxToLive && now - session.createdAt > maxToLive) + ) { + this.delete(id); + deleteCount++; + logger.debug(`(MemoryCacheFileStore._cleanupRun) deleted ${id}`); + } + } + + logger.debug(`(MemoryCacheFileStore._cleanupRun) Deleted ${deleteCount} sessions from store`); + } + /** + * @private + * @param {number} unloadMemoryTimeout + */ + _unloadMemoryRun(unloadMemoryTimeout) { + let deleteCount = 0; + logger.debug(`(MemoryCacheFileStore._unloadMemoryRun) need to go through ${this.cachedSessions.size} sessions`); + + const now = Date.now(); + for (const [sessionId, session] of this.cachedSessions) { + if (now - session.lastUsed > unloadMemoryTimeout) { + fs.writeFileSync(this._getSessionFilePath(sessionId), session.serializeSession()); + this.cachedSessions.delete(sessionId); + deleteCount++; + logger.debug( + `(MemoryCacheFileStore._unloadMemoryRun) removed ${sessionId} from memory and saved to store` + ); + } + } + + logger.debug(`(MemoryCacheFileStore._unloadMemoryRun) Removed ${deleteCount} sessions from memory`); + } +} + +module.exports = RammerheadSessionMemoryCacheFileStore; diff --git a/src/classes/RammerheadSessionMemoryStore.js b/src/classes/RammerheadSessionMemoryStore.js new file mode 100644 index 00000000..0b181012 --- /dev/null +++ b/src/classes/RammerheadSessionMemoryStore.js @@ -0,0 +1,77 @@ +const RammerheadSession = require('./RammerheadSession'); +const RammerheadSessionAbstractStore = require('./RammerheadSessionAbstractStore'); +const logger = require('../util/logger'); + +/** + * perfect for slow disk servers but high amounts of ram. + * not good for load balancing. + */ +class RammerheadSessionMemoryStore extends RammerheadSessionAbstractStore { + /** + * @param {object} options + * @param {number|null} options.staleTimeout - if inactivity goes beyond this, then the session is deleted. null to disable + * @param {number|null} options.maxToLive - if now - createdAt surpasses maxToLive, then the session is deleted. null to disable + * @param {number} options.cleanupInterval - every cleanupInterval ms will run a cleanup check + */ + constructor({ + staleTimeout = 1000 * 60 * 30, // 30 minutes + maxToLive = 1000 * 60 * 60 * 4, // 4 hours + cleanupInterval = 1000 * 60 * 1 // 1 minute + } = {}) { + super(); + this.mapStore = new Map(); + setInterval(() => this._cleanupRun(staleTimeout, maxToLive), cleanupInterval).unref(); + } + + keys() { + return Array.from(this.mapStore.keys()); + } + has(id) { + const exists = this.mapStore.has(id); + logger.debug(`(MemoryStore.has) ${id} ${exists}`); + return exists; + } + get(id, updateActiveTimestamp = true) { + if (!this.has(id)) return; + logger.debug(`(MemoryStore.get) ${id} ${updateActiveTimestamp}`); + + const session = this.mapStore.get(id); + if (updateActiveTimestamp) session.updateLastUsed(); + + return session; + } + add(id) { + if (this.has(id)) throw new Error('the following session already exists: ' + id); + logger.debug(`(MemoryStore.add) ${id}`); + const session = new RammerheadSession({ id }); + this.mapStore.set(id, session); + return session; + } + delete(id) { + return this.mapStore.delete(id); + } + + /** + * @private + * @param {number|null} staleTimeout + * @param {number|null} maxToLive + */ + _cleanupRun(staleTimeout, maxToLive) { + logger.debug(`(MemoryStore._cleanupRun) cleanup run. Need to go through ${this.mapStore.size} sessions`); + + const now = Date.now(); + for (const [sessionId, session] of this.mapStore) { + if ( + (staleTimeout && now - session.lastUsed > staleTimeout) || + (maxToLive && now - session.createdAt > maxToLive) + ) { + this.mapStore.delete(sessionId); + logger.debug(`(MemoryStore._cleanupRun) delete ${sessionId}`); + } + } + + logger.debug('(MemoryStore._cleanupRun) finished cleanup run'); + } +} + +module.exports = RammerheadSessionMemoryStore; diff --git a/src/client/rammerhead.js b/src/client/rammerhead.js new file mode 100644 index 00000000..1aac443e --- /dev/null +++ b/src/client/rammerhead.js @@ -0,0 +1,150 @@ +// since hammerhead is es5, we'll follow that also to avoid losing compatibility +(function () { + var hammerhead = window['%hammerhead%']; + if (!hammerhead) throw new Error('hammerhead not loaded yet'); + if (hammerhead.settings._settings.sessionId) { + // task.js already loaded. this will likely never happen though since this file loads before task.js + main(); + } else { + // wait for task.js to load + hookHammerheadStartOnce(main); + } + + function main() { + // consts + var timestampKey = 'rammerhead_synctimestamp'; + var updateInterval = 5000; + var isSyncing = false; + + var proxiedLocalStorage = localStorage; + var realLocalStorage = proxiedLocalStorage.internal.nativeStorage; + var sessionId = hammerhead.settings._settings.sessionId; + var origin = window.__get$(window, 'location').origin; + var keyChanges = []; + + syncLocalStorage(); + proxiedLocalStorage.addChangeEventListener(function (event) { + if (isSyncing) return; + if (keyChanges.indexOf(event.key) === -1) keyChanges.push(event.key); + }); + setInterval(function () { + var update = compileUpdate(); + if (!update) return; + localStorageRequest({ type: 'update', updateData: update }, function (data) { + updateTimestamp(data.timestamp); + }); + + keyChanges = []; + }, updateInterval); + document.addEventListener('visibilitychange', function() { + if (document.visibilityState === 'hidden') { + var update = compileUpdate(); + if (update) { + // even though we'll never get the timestamp, it's fine. this way, + // the data is safer + hammerhead.nativeMethods.sendBeacon.call(window.navigator, getSyncStorageEndpoint(), JSON.stringify({ + type: 'update', + updateData: update + })); + } + } + }); + + function syncLocalStorage() { + isSyncing = true; + var timestamp = getTimestamp(); + var response; + if (!timestamp) { + // first time syncing + response = localStorageRequest({ type: 'sync', fetch: true }); + if (response.timestamp) { + updateTimestamp(response.timestamp); + overwriteLocalStorage(response.data); + } + } else { + // resync + response = localStorageRequest({ type: 'sync', timestamp: timestamp, data: proxiedLocalStorage }); + if (response.timestamp) { + updateTimestamp(response.timestamp); + overwriteLocalStorage(response.data); + } + } + isSyncing = false; + + function overwriteLocalStorage(data) { + if (!data || typeof data !== 'object') throw new TypeError('data must be an object'); + proxiedLocalStorage.clear(); + for (var prop in data) { + proxiedLocalStorage[prop] = data[prop]; + } + } + } + function updateTimestamp(timestamp) { + if (!timestamp) throw new TypeError('timestamp must be defined'); + if (isNaN(parseInt(timestamp))) throw new TypeError('timestamp must be a number. received' + timestamp); + realLocalStorage[timestampKey] = timestamp; + } + function getTimestamp() { + var rawTimestamp = realLocalStorage[timestampKey]; + var timestamp = parseInt(rawTimestamp); + if (isNaN(timestamp)) { + if (rawTimestamp) { + console.warn('invalid timestamp retrieved from storage: ' + rawTimestamp); + } + return null; + } + return timestamp; + } + function getSyncStorageEndpoint() { + return '/syncLocalStorage?sessionId=' + encodeURIComponent(sessionId) + '&origin=' + encodeURIComponent(origin); + } + function localStorageRequest(data, callback) { + if (!data || typeof data !== 'object') throw new TypeError('data must be an object'); + + var request = hammerhead.createNativeXHR(); + // make synchronous if there is no callback + request.open( + 'POST', + getSyncStorageEndpoint(), + !!callback + ); + request.setRequestHeader('content-type', 'application/json'); + request.send(JSON.stringify(data)); + function check() { + if (request.status !== 200) + throw new Error( + 'server sent a non 200 code. got ' + request.status + '. Response: ' + request.responseText + ); + } + if (!callback) { + check(); + return JSON.parse(request.responseText); + } else { + request.onload = function () { + check(); + callback(JSON.parse(request.responseText)); + }; + } + } + function compileUpdate() { + if (!keyChanges.length) return null; + + var updates = {}; + for (var i = 0; i < keyChanges.length; i++) { + updates[keyChanges[i]] = proxiedLocalStorage[keyChanges[i]]; + } + + keyChanges = []; + return updates; + } + } + + function hookHammerheadStartOnce(callback) { + var originalStart = hammerhead.__proto__.start; + hammerhead.__proto__.start = function () { + originalStart.apply(this, arguments); + hammerhead.__proto__.start = originalStart; + callback(); + }; + } +})(); diff --git a/src/config.js b/src/config.js new file mode 100644 index 00000000..d4534207 --- /dev/null +++ b/src/config.js @@ -0,0 +1,65 @@ +const cluster = require('cluster'); +const path = require('path'); + +module.exports = { + // valid values: 'disabled', 'debug', 'traffic', 'info', 'warn', 'error' + logLevel: 'debug', + generatePrefix: (level) => `[${new Date().toISOString()}] [${level.toUpperCase()}] `, + + // if rammerhead is sitting behind a reverse proxy like nginx, then the logger and + // the rate limiter will see the IP as 127.0.0.1. This option is to solve that issue. + // the following is for hosting this directly + getIP: (req) => req.socket.remoteAddress, + // the following is for hosting it behind nginx. make sure it controls the header + // IPs to avoid spoofing (in this case, 'x-forwarded-for'). customize the function as needed + // loggerGetIP: req => (req.headers['x-forwarded-for'] || req.connection.remoteAddress || '').split(',')[0].trim(), + + bindingAddress: '127.0.0.1', + port: 8080, + crossDomainPort: 8081, + publicDir: path.join(__dirname, '../public'), // set to null to disable + + // ssl object is either null or { key: fs.readFileSync('path/to/key'), cert: fs.readFileSync('path/to/cert') } + // for more info, see https://nodejs.org/api/https.html#https_https_createserver_options_requestlistener + ssl: null, + + // optional: enforce a password for creating new sessions + password: 'lol', + + // when running this behind a reverse proxy like cloudflare or nginx, they add unnecessary headers + // that get sent to the proxied target. this is to remove such headers. + // cloudflare example: + // stripClientHeaders: ['cf-ipcountry', 'cf-ray', 'x-forwarded-proto', 'cf-visitor', 'cf-connecting-ip', 'cdn-loop'], + stripClientHeaders: [], + // sometimes, you want to embed the proxy in an iframe, so you would want to remove the x-frame-options header like so + // rewriteServerHeaders: { + // // you can also specify a function to modify/add the header using the original value (undefined if adding the header) + // // 'x-frame-options': (originalHeaderValue) => '', + // 'x-frame-options': null, // set to null to tell rammerhead that you want to delete it + // }, + rewriteServerHeaders: {}, + + // set to null to disable + rateLimitOptions: { + requestsPerSecond: 80, + burst: 600, // takes burst/requestPerSecond to fill the burst bucket. if this is depleted, we throw an error + useClusterStore: cluster.isWorker // if false, this will disable rate limit clustering and use own process memory + }, + + // this function's return object will determine how the client url rewriting will work. + // set them differently from bindingAddress and port if rammerhead is being served + // from a reverse proxy. + // the following example is if you disabled the crossDomainPort + // getServerInfo: (req) => { + // const { hostname, port } = new URL('http://' + req.headers.host); + // return { + // hostname, + // port, + // protocol: req.socket.encrypted ? 'https:' : 'http:' + // }; + // } + // another example is setting the serverInfo to a certain value. this is especially + // useful if you are using crossDomainPort, because you cannot rely on the Host header's port + // value to always be what you expect (meaning the port could be a normal port or a crossDomainPort) + getServerInfo: () => ({ hostname: 'localhost', port: 8080, crossDomainPort: 8081, protocol: 'http:' }) +}; diff --git a/src/config2.js b/src/config2.js new file mode 100644 index 00000000..302959e2 --- /dev/null +++ b/src/config2.js @@ -0,0 +1,31 @@ +const cluster = require('cluster'); +const path = require('path'); +const RammerheadSessionFilePersistentStore = require('./classes/RammerheadSessionFilePersistentStore'); +const RammerheadSessionMemoryCacheFileStore = require('./classes/RammerheadSessionMemoryCacheFileStore'); + +// reason for the existence of another config file is to avoid +// the circular dependency problem. if this were to be put in config.js, then the following +// will happen (format: moduleA requires moduleB = moduleA -> moduleB): +// server -> config.js -> MemoryStore -> logger -> config.js -> MemoryStore -> logger,... +// if we were to separate the require portion away from config.js, all is fine +// server -> config.js and server -> config-store.js -> MemoryStore -> logger -> config.js + +const saveDirectory = path.join(__dirname, '../sessions'); +module.exports = { + // by default, use file storage if running using multi-server.js to share the session and limiter states + // across workers. see jsdoc for each storage class for more info on the options + sessionStore: cluster.isWorker + ? new RammerheadSessionFilePersistentStore({ + saveDirectory: saveDirectory + // set cleanupOptions object to null if you don't want to clean up + // cleanupOptions: null + }) + : new RammerheadSessionMemoryCacheFileStore({ + saveDirectory: saveDirectory + }), + + // when user generates an ID and never uses it, we want to clean that up (this is implemented directly in server.js) + // set either to null to disable + unusedTimeout: 1000 * 60 * 20, // 20 minutes + unusedInterval: 1000 * 60 * 10 // 10 minutes +}; diff --git a/src/multi-config.js b/src/multi-config.js new file mode 100644 index 00000000..69cbcd41 --- /dev/null +++ b/src/multi-config.js @@ -0,0 +1,6 @@ +const os = require('os'); + +module.exports = { + // rest of the config should be configured in config.js and config2.js + workers: os.cpus().length +}; diff --git a/src/multi-server.js b/src/multi-server.js new file mode 100644 index 00000000..053adf39 --- /dev/null +++ b/src/multi-server.js @@ -0,0 +1,35 @@ +const cluster = require('cluster'); +const exitHook = require('async-exit-hook'); +const { RateLimiterClusterMaster } = require('rate-limiter-flexible'); +const logger = require('./util/logger'); +const startServer = require('./server'); +const config = require('./config'); +const config2 = require('./config2'); +const multiConfig = require('./multi-config'); + +const generateGeneratePrefix = (prefix) => (level) => + `[${new Date().toISOString()}] (${prefix}) [${level.toUpperCase()}] `; + +if (cluster.isMaster) { + logger.generatePrefix = generateGeneratePrefix('master'); + + if (config.rateLimitOptions) { + new RateLimiterClusterMaster(); + } + + logger.info(`spawning ${multiConfig.workers} workers`); + for (let i = 0; i < multiConfig.workers; i++) { + cluster.fork(); + } + + exitHook((callback) => { + logger.info('Shutting down workers'); + cluster.disconnect(() => { + logger.info('Done'); + callback(); + }); + }); +} else { + logger.generatePrefix = generateGeneratePrefix(`worker ${cluster.worker.id}`); + startServer(config, config2); +} diff --git a/src/server.js b/src/server.js new file mode 100644 index 00000000..d6ce2fc7 --- /dev/null +++ b/src/server.js @@ -0,0 +1,148 @@ +const exitHook = require('async-exit-hook'); +const RammerheadProxy = require('./classes/RammerheadProxy'); +const addStaticDirToProxy = require('./util/addStaticDirToProxy'); +const logger = require('./util/logger'); +const RammerheadRateLimiter = require('./classes/RammerheadRateLimiter'); +const generateId = require('./util/generateId'); +const URLPath = require('./util/URLPath'); +const httpResponse = require('./util/httpResponse'); +const config = require('./config'); +const config2 = require('./config2'); + +/** + * @param {typeof config} config + * @param {typeof config2} config2 + */ +function startServer(config, config2) { + const proxyServer = new RammerheadProxy({ + logger, + loggerGetIP: config.getIP, + bindingAddress: config.bindingAddress, + port: config.port, + crossDomainPort: config.crossDomainPort, + ssl: config.ssl, + getServerInfo: config.getServerInfo + }); + + // public static directory + if (config.publicDir) { + addStaticDirToProxy(proxyServer, config.publicDir); + } + + // session store + const sessionStore = config2.sessionStore; + sessionStore.attachToProxy(proxyServer); + + // rate limiter + if (config.rateLimitOptions) { + const ratelimiter = new RammerheadRateLimiter(Object.assign({ getIP: config.getIP }, config.rateLimitOptions)); + ratelimiter.attachToProxy(proxyServer); + } + + // remove headers defined in config.js + proxyServer.addToOnRequestPipeline((req) => { + for (const eachHeader of config.stripClientHeaders) { + delete req.headers[eachHeader]; + } + }); + Object.assign(proxyServer.rewriteServerHeaders, config.rewriteServerHeaders); + + // setup routes // + const isNotAuthorized = (req, res) => { + if (!config.password) return; + const { pwd } = new URLPath(req.url).getParams(); + if (config.password !== pwd) { + httpResponse.accessForbidden(req, res, config.getIP(req), 'bad password'); + return true; + } + return false; + }; + proxyServer.GET('/newsession', (req, res) => { + if (isNotAuthorized(req, res)) return; + + const id = generateId(); + sessionStore.add(id); + res.end(id); + }); + proxyServer.GET('/editsession', (req, res) => { + if (isNotAuthorized(req, res)) return; + + let { id, httpProxy } = new URLPath(req.url).getParams(); + + if (!id || !sessionStore.has(id)) { + return httpResponse.badRequest(req, res, config.getIP(req), 'not found'); + } + + const session = sessionStore.get(id); + + if (httpProxy) { + if (httpProxy.startsWith('http://')) { + httpProxy = httpProxy.slice(7); + } + session.setExternalProxySettings(httpProxy); + } + + res.end('Success'); + }); + proxyServer.GET('/deletesession', (req, res) => { + if (isNotAuthorized(req, res)) return; + + const { id } = new URLPath(req.url).getParams(); + + if (!id || !sessionStore.has(id)) { + res.end('not found'); + } + + sessionStore.delete(id); + res.end('Success'); + }); + proxyServer.GET('/sessionexists', (req, res) => { + const id = new URLPath(req.url).get('id'); + if (!id) { + httpResponse.badRequest(req, res, config.getIP(req), 'Must specify id parameter'); + } else { + res.end(sessionStore.has(id) ? 'exists' : 'not found'); + } + }); + + // cleanup unused session ids + if (config2.unusedTimeout && config2.unusedInterval) { + setInterval(() => { + const list = sessionStore.keys(); + let deleteCount = 0; + logger.debug(`(server) Going through ${list.length} sessions, cleaning all unused ones`); + + const now = Date.now(); + for (const sessionId of list) { + const session = sessionStore.get(sessionId, false, false); + if (session.lastUsed === session.createdAt && now - session.lastUsed > config2.unusedTimeout) { + sessionStore.delete(sessionId); + deleteCount++; + logger.debug(`(server) Deleted unused session ${sessionId}`); + } + } + + logger.debug(`(server) cleaned ${deleteCount} unused sessions`); + }, config2.unusedInterval).unref(); + } + + // nicely close proxy server and save sessions to store before we exit + exitHook(() => { + logger.info(`(server) Received exit signal, closing proxy server`); + proxyServer.close(); + logger.info('(server) Closed proxy server'); + }); + + const formatUrl = (secure, hostname, port) => `${secure ? 'https' : 'http'}://${hostname}:${port}`; + logger.info( + `(server) Rammerhead proxy is listening on ${formatUrl(config.ssl, config.bindingAddress, config.port)}` + ); + + return proxyServer; +} + +module.exports = startServer; + +if (require.main === module) { + startServer(config, config2); +} diff --git a/src/util/ObjectFileDatabase.js b/src/util/ObjectFileDatabase.js new file mode 100644 index 00000000..1b668da4 --- /dev/null +++ b/src/util/ObjectFileDatabase.js @@ -0,0 +1,174 @@ +const fs = require('fs'); +const path = require('path'); +const DeepProxy = require('proxy-deep'); +const jsonDateParser = require('json-date-parser'); +const rimraf = require('rimraf'); +const logger = require('./logger'); + +/** + * In noSQL terms, a "file" is a docment and a "folder" is a collection. + * @typedef {'read'|'write'|'delete'|'has'|'list'|'createFolder'|'isFolder'} DataOperations + */ + +/** + * Any simple objects will be saved to a folder/file style database. If data.prop = {}, it will create a + * new folder called "prop." If data.prop = [] or data.folder.file = 'data', then it will create a new file named + * 'file' in the folder 'folder' + * + * Note: Yes, a filesystem is a database. We're just using it because there's no other viable + * synchronous database options + */ +class ObjectFileStore { + // these are used as object references (`===` comparisons) + + /** + * @param {string} filePath + * @param {(path: string) => string} friendlyPath + */ + static DefaultHandler(filePath, friendlyPath) { + /** + * @private + * @param {DataOperations} operation + * @param {string[]} pathArr + * @param {any} value + * @returns {string|boolean|undefined} + */ + function handler(operation, pathArr, value) { + pathArr = pathArr.map((e) => friendlyPath(e)); // take care of slashes in the path + const fullPath = path.join(filePath, ...pathArr); + logger.debug(`(ObjectFileDatabase.DefaultHandler) ${operation} on ${fullPath}`); + + switch (operation) { + case 'read': + return JSON.parse(fs.readFileSync(fullPath, 'utf8'), jsonDateParser).data; + case 'write': + return fs.writeFileSync(fullPath, JSON.stringify({ data: value }), 'utf8'); + case 'delete': + return rimraf.sync(fullPath); + case 'has': + return fs.existsSync(fullPath); + case 'list': + return fs.readdirSync(fullPath); + case 'createFolder': + return fs.mkdirSync(fullPath, { recursive: true }); + case 'isFolder': + return fs.lstatSync(fullPath).isDirectory(); + default: + throw new TypeError('unknown operation: ' + operation); + } + } + return handler; + } + + /** + * @param {object} options + * @param {string|(operation: DataOperations, path: string[], value: any) => any} options.handler - when + * data is read/write, it calls this function. if it is a write operation, value will not be undefined. If this is a string, + * then it will treat the string as a file path and use the default handler + * @param {(path: string) => string} options.handlerFriendlyPath - convert illegal filenames like '/' to something else. only used + * if options.handler is a string. by default, this uses encodeURIComponent + * @param {number} options.maxFolderDepth - stop recursion errors early when implementing the handler. set to -1 to disable + * @param {(path: string[], value) => boolean} options.shouldRecursive - for determining whether to traverse further or not + * @param {(obj) => any} options.convertObjectToFile - all objects passed to the handler will go through this first + * @param {(file) => any} options.convertFileToObject - all objects received from the handler will go through this first + */ + constructor({ + handler = ObjectFileStore.DefaultHandler, + handlerFriendlyPath = encodeURIComponent, + maxFolderDepth = 20, + shouldRecursive = () => true, + convertObjectToFile = (obj) => obj, + convertFileToObject = (file) => file + }) { + this.maxFolderDepth = maxFolderDepth; + this.shouldRecursive = shouldRecursive; + this.convertObjectToFile = convertObjectToFile; + this.convertFileToObject = convertFileToObject; + if (typeof handler === 'string') { + this.handler = ObjectFileStore.DefaultHandler(handler, handlerFriendlyPath); + } else if (typeof handler === 'function') { + this.handler = handler; + } else { + throw new TypeError( + `handler must be either a string (filepath) or a function handler. received type ${typeof handler}` + ); + } + + const getProxiedObj = (path) => { + let obj = this.fileObject; + for (const eachProp of path) obj = obj[eachProp]; + return obj; + }; + const self = this; + this.fileObject = DeepProxy( + {}, + { + has(_target, prop) { + return self.handler('has', this.path.concat([prop])); + }, + get(target, prop) { + const fullPath = this.path.concat([prop]); + + if (self.handler('has', fullPath)) { + target[prop] = {}; + if (self.handler('isFolder', fullPath)) { + return this.nest(target[prop]); + } else { + return self.convertFileToObject(self.handler('read', fullPath)); + } + } + + delete target[prop]; + return undefined; + }, + set(target, prop, value) { + value = self.convertObjectToFile(value); + target[prop] = {}; + const fullPath = this.path.concat([prop]); + if (self.maxFolderDepth !== -1 && fullPath.length > self.maxFolderDepth) { + throw new TypeError('max folder depth exceeded'); + } + if ( + typeof value === 'object' && + value !== null && + !Array.isArray(value) && + !(value instanceof Date) && + self.shouldRecursive(fullPath, value) + ) { + self.handler('delete', fullPath); + self.handler('createFolder', fullPath); + const proxiedObj = getProxiedObj(fullPath); + for (const eachProp in value) { + if ( + typeof value[eachProp] !== 'function' && + self.shouldRecursive(fullPath, value[eachProp]) + ) { + proxiedObj[eachProp] = value[eachProp]; + } + } + } else { + if (self.handler('has', fullPath)) { + self.handler('delete', fullPath); + } + self.handler('write', fullPath, value); + } + return true; + }, + deleteProperty(target, prop) { + const fullPath = this.path.concat([prop]); + if (self.handler('has', fullPath)) { + self.handler('delete', fullPath); + delete target[prop]; + return true; + } + return false; + }, + ownKeys(_target) { + return self.handler('list', this.path); + } + } + ); + } +} + +module.exports = ObjectFileStore; diff --git a/src/util/URLPath.js b/src/util/URLPath.js new file mode 100644 index 00000000..98446454 --- /dev/null +++ b/src/util/URLPath.js @@ -0,0 +1,24 @@ +/** + * for lazy people who don't want to type out `new URL('http://blah' + req.url).searchParams.get(ugh)` all the time + */ +module.exports = class URLPath extends URL { + /** + * @param {string} path - /site/path + */ + constructor(path) { + super(path, 'http://foobar'); + } + /** + * @param {string} param - ?param=value + * @returns {string|null} + */ + get(param) { + return this.searchParams.get(param); + } + /** + * @returns {{[param: string]: string}} + */ + getParams() { + return Object.fromEntries(this.searchParams); + } +}; diff --git a/src/util/addStaticDirToProxy.js b/src/util/addStaticDirToProxy.js new file mode 100644 index 00000000..5e12e1b3 --- /dev/null +++ b/src/util/addStaticDirToProxy.js @@ -0,0 +1,61 @@ +const mime = require('mime'); +const fs = require('fs'); +const path = require('path'); + +// these routes are reserved by hammerhead and rammerhead +const forbiddenRoutes = [ + '/rammerhead.js', + '/hammerhead.js', + '/task.js', + '/iframe-task.js', + '/messaging', + '/transport-worker.js', + '/worker-hammerhead.js' +]; + +const isDirectory = (dir) => fs.lstatSync(dir).isDirectory(); + +/** + * + * @param {import('testcafe-hammerhead').Proxy} proxy + * @param {string} staticDir - all of the files and folders in the specified directory will be served + * publicly. /index.html will automatically link to / + * @param {string} rootPath - all the files that will be served under rootPath + */ +function addStaticFilesToProxy(proxy, staticDir, rootPath = '/') { + if (!isDirectory(staticDir)) { + throw new TypeError('specified folder path is not a directory'); + } + + if (!rootPath.endsWith('/')) rootPath = rootPath + '/'; + if (!rootPath.startsWith('/')) rootPath = '/' + rootPath; + + const files = fs.readdirSync(staticDir); + + files.map((file) => { + if (isDirectory(path.join(staticDir, file))) { + addStaticFilesToProxy(proxy, path.join(staticDir, file), rootPath + file + '/'); + return; + } + + const pathToFile = path.join(staticDir, file); + const staticContent = { + content: fs.readFileSync(pathToFile), + contentType: mime.getType(file) + }; + const route = rootPath + file; + + if (forbiddenRoutes.includes(route)) { + throw new TypeError( + `route clashes with hammerhead. problematic route: ${route}. problematic static file: ${pathToFile}` + ); + } + + proxy.GET(rootPath + file, staticContent); + if (file === 'index.html') { + proxy.GET(rootPath, staticContent); + } + }); +} + +module.exports = addStaticFilesToProxy; diff --git a/src/util/fixCorsHeader.js b/src/util/fixCorsHeader.js new file mode 100644 index 00000000..07917071 --- /dev/null +++ b/src/util/fixCorsHeader.js @@ -0,0 +1,22 @@ +const urlUtils = require('testcafe-hammerhead/lib/utils/url'); +const RequestPipelineContext = require('testcafe-hammerhead/lib/request-pipeline/context'); + +/** + * if a non-crossdomain origin makes a request to a crossdomain port, the ports are flipped. this is to fix that issue. + * original: https://github.com/DevExpress/testcafe-hammerhead/blob/f5b0508d10614bf39a75c772dc6bd01c24f29417/src/request-pipeline/context.ts#L436 + */ +RequestPipelineContext.prototype.getProxyOrigin = function getProxyOrigin(isCrossDomain = false) { + return urlUtils.getDomain({ + protocol: this.serverInfo.protocol, + hostname: this.serverInfo.hostname, + // if we receive a request that has a proxy origin header, (ctx.getProxyOrigin(!!ctx.dest.reqOrigin), + // https://github.com/DevExpress/testcafe-hammerhead/blob/f5b0508d10614bf39a75c772dc6bd01c24f29417/src/request-pipeline/header-transforms/transforms.ts#L128), + // then we must return the other port over. however, the issue with this is we don't know if the incoming request is actually a + // crossdomain port (a simple check for reqOrigin cannot suffice, as a request from a non-crossdomain origin to a crossdomain port and + // vice versa can happen), + // so this will fix the issue from non-crossdomain port to crossdomain-port but will NOT fix crosdomain-port to non-crossdomain port. + // However, the latter case will never happen because hammerhead made all client rewriting cross-domain requests to always use the + // cross-domain ports, even if the origin is from a cross-domain port + port: isCrossDomain ? this.serverInfo.port : this.serverInfo.crossDomainPort // <-- changed + }); +}; diff --git a/src/util/generateId.js b/src/util/generateId.js new file mode 100644 index 00000000..a4475c2f --- /dev/null +++ b/src/util/generateId.js @@ -0,0 +1,3 @@ +const uuid = require('uuid').v4; + +module.exports = () => uuid().replace(/-/g, ''); diff --git a/src/util/httpResponse.js b/src/util/httpResponse.js new file mode 100644 index 00000000..37178e2c --- /dev/null +++ b/src/util/httpResponse.js @@ -0,0 +1,21 @@ +const logger = require('./logger'); + +/** + * @typedef {'badRequest'|'accessForbidden'} httpResponseTypes + */ + +/** + * @type {{[key in httpResponseTypes]: (req: import('http').IncomingMessage, res: import('http').ServerResponse, ip: string, msg: string) => void}} + */ +module.exports = { + badRequest: (req, res, ip, msg) => { + logger.error(`(httpResponse.badRequest) ${ip} ${req.url} ${msg}`); + res.writeHead(400); + res.end(msg); + }, + accessForbidden: (req, res, ip, msg) => { + logger.error(`(httpResponse.badRequest) ${ip} ${req.url} ${msg}`); + res.writeHead(403); + res.end(msg); + } +}; diff --git a/src/util/logger.js b/src/util/logger.js new file mode 100644 index 00000000..4537de31 --- /dev/null +++ b/src/util/logger.js @@ -0,0 +1,7 @@ +const RammerheadLogging = require('../classes/RammerheadLogging'); +const config = require('../config'); + +module.exports = new RammerheadLogging({ + logLevel: config.logLevel, + generatePrefix: config.generatePrefix +}); diff --git a/src/util/streamToString.js b/src/util/streamToString.js new file mode 100644 index 00000000..5972a63d --- /dev/null +++ b/src/util/streamToString.js @@ -0,0 +1,8 @@ +module.exports = function streamToString(stream) { + const chunks = []; + return new Promise((resolve, reject) => { + stream.on('data', (chunk) => chunks.push(Buffer.from(chunk))); + stream.on('error', (err) => reject(err)); + stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))); + }); +};