├── .eslintrc ├── .github └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── TODO.md ├── babel.config.js ├── dist ├── index.html ├── monkey.js ├── monkey.js.LICENSE.txt ├── monkey.js.map ├── server.js ├── server.js.LICENSE.txt └── server.js.map ├── do-release ├── doc ├── development │ ├── contributing.md │ └── release.md └── usage │ ├── client.md │ ├── server.md │ ├── ssh.md │ └── user-management.md ├── package-lock.json ├── package.json ├── src ├── client │ ├── convert-styles.js │ ├── index.html │ ├── index.js │ └── utils.js ├── lib │ ├── common-utils.js │ └── cycle.js └── server │ ├── bunyan-stream.js │ ├── command-interface.js │ ├── command-manager.js │ ├── index.js │ ├── setup-server.js │ ├── setup-socket.js │ ├── ssh-manager.js │ ├── user-auth.js │ ├── user-manager.js │ └── utils.js └── webpack.config.babel.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:prettier/recommended"], 3 | "plugins": ["prettier"], 4 | "parser": "babel-eslint", 5 | "rules": { 6 | "prettier/prettier": "error" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | branches: [master] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [master] 14 | schedule: 15 | - cron: '0 0 * * 6' 16 | 17 | jobs: 18 | analyze: 19 | name: Analyze 20 | runs-on: ubuntu-latest 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | # Override automatic language detection by changing the below list 26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 27 | language: ['javascript'] 28 | # Learn more... 29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v2 34 | with: 35 | # We must fetch at least the immediate parents so that if this is 36 | # a pull request then we can checkout the head. 37 | fetch-depth: 2 38 | 39 | # If this run was triggered by a pull request event, then checkout 40 | # the head of the pull request instead of the merge commit. 41 | - run: git checkout HEAD^2 42 | if: ${{ github.event_name == 'pull_request' }} 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | .* 3 | !dist/**/* 4 | !doc/**/* 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "semi": false, 4 | "trailingComma": "all", 5 | "arrowParens": "always" 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnPaste": true, 3 | "editor.formatOnSave": true 4 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Version 1.1.5 2 | 3 | - Security updates 4 | 5 | ## Version 1.1.4 6 | 7 | - Security updates 8 | 9 | ## Version 1.1.3 10 | 11 | - Update ALL package-lock dependencies to resolve Github dependabot alert 12 | 13 | ## Version 1.1.2 14 | 15 | - Update dependencies to address security warnings. Everything _should_ work the same as before but there were definitely updates with breaking changes in dependencies that could be lurking. Any breakage is a bug. 16 | 17 | ## Version 1.1.1 18 | 19 | - Update dependencies to fix latest security vulnerability warnings in deps 20 | - Update docs to use modern best practices with `import` and `const` 21 | - Fix bug ensuring only one command manager instance is used and instead runs commands with separate command interfaces 22 | 23 | ## Version 1.0.0.beta.3 24 | 25 | - Improve documentation 26 | - Fix bug with attachConsole() causing improper behavior and extraneous output 27 | 28 | ## Version 1.0.0.beta.2 29 | 30 | - Allow more advanced command functionality and prompting for user input 31 | 32 | ## Version 1.0.0-beta.1 33 | 34 | - Complete rewrite. Not remotely backward compatible. 35 | - Implemented user authentication for running in production environments 36 | - Introduced new SSH interface to get a remote shell into your app 37 | 38 | ## Version 0.2.8 39 | 40 | - Fix crashing bug in Windows (workaround for socket.io bug) 41 | - Convert bash formatting escape sequences to matching JS console styles (disable with new `convertStyles` option) 42 | 43 | ## Version 0.2.7 44 | 45 | - New `overrideConsole` option to disable overriding the console functions when starting NodeMonkey 46 | - Allow configuring the `host` the NodeMonkey client should connect to and not just the `port`. This is useful when the server is running on a different subdomain or other hostname than the one in the URL. 47 | - Get rid of buffering message spam in the console where NodeMonkey is running 48 | - Use Lo-Dash instead of Underscore 49 | - Support `console.dir()` 50 | 51 | ## Version 0.2.6 52 | 53 | - Fixed references to cycle.js and underscore-min.js 54 | - Using underscore templates to serve up the client.js file with the correct configured port for socket.io to connect to 55 | 56 | ## Version 0.2.5 57 | 58 | - Fixed bug where results of commands weren't being decycled before being sent to the client 59 | - Allow second argument to nomo.cmd(...) on client to be optional - if no args are used or required, just pass the callback as the second argument instead 60 | 61 | ## Version 0.2.4 62 | 63 | - Fixed a bug with cycle.js causing it to filter out functions any potentially other data types 64 | - Now sends full object representation including functions, which are normally stripped out by converting to JSON 65 | 66 | ## Version 0.2.3 67 | 68 | - Added missing 'profiler.getData' and 'profiler.clearData' commands to client side 69 | - Changed the way commands work for added security and to make the command interface available the application developer for any desired use 70 | - Added support for logging cyclical objects using cycle.js from (Thanks Douglas Crockford) 71 | - Cleaned up code a bit including moving all code related files (except index.js) to `src/` directory and breaking client HTML file into 72 | a separate Underscore template file 73 | - Renamed global object from 'nm' to 'nomo' 74 | - Added 'revertConsole()' method 75 | - Removed `active` config option for profiler and any documentation referencing configuring the profiler until there is something to configure 76 | - Standardized a method of documentation and documented everything well 77 | 78 | ## Version 0.2.0 79 | 80 | - Added profiling functionality 81 | - Added ability to send commands to the Node.js server from the web browser 82 | 83 | ## Version 0.1.2 84 | 85 | - Fixed a bug causing NodeMonkey to crash the app it's included in 86 | 87 | ## Version 0.1.1 88 | 89 | - Changed default port to 50500 90 | - Fixed logging issue causing messages to only be sent to the client on initial connection 91 | - Fixed websocket reconnection problem 92 | - Added buffering on Firefox if Firebug isn't open on initial page load so messages can be displayed once it is 93 | - Dumps instructions to the console when started. Added `silent` option to disable this behavior. 94 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2022 Justin Warkentin 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node Monkey 2 | 3 | A tool for inspecting, debugging and commanding Node.js applications through a web browser or SSH interface into your app (with your own custom commands). 4 | 5 | Node Monkey runs a simple server (or attaches to your existing server) and uses [Socket.IO](https://github.com/LearnBoost/socket.io) to create a websocket connection between the browser and server. Its primary feature captures anything that would normally be logged to the terminal and passes it to the browser for inspection. 6 | 7 | It is incredibly easy to get started (see [Quick Usage](#quick-usage) below) but Node Monkey also provides additional features and significant flexibility for more advanced usage. You can actually SSH into your app where Node Monkey will provide a command line interface to execute your own custom commands. This can be very useful for debugging, monitoring or otherwise controlling your application while it is running. It provides authentication for security in production applications. 8 | 9 | ## Contents 10 | 11 | - [Motivation](#motivation) 12 | - [Features](#features) 13 | - [Installation](#installation) 14 | - [Quick Usage](#quick-usage) 15 | - [Server](doc/usage/server.md) 16 | - [Provide your own](doc/usage/server.md#provide-your-own) 17 | - [Options](doc/usage/server.md#options) 18 | - [Properties](doc/usage/server.md#properties) 19 | - [Methods](doc/usage/server.md#methods) 20 | - [Client (browser)](doc/usage/client.md) 21 | - [Properties](doc/usage/client.md#properties) 22 | - [Methods](doc/usage/client.md#methods) 23 | - [SSH](doc/usage/ssh.md) 24 | - [Setup](doc/usage/ssh.md#setup) 25 | - [Usage](doc/usage/ssh.md#usage) 26 | - [User Management](doc/usage/user-management.md) 27 | - [Contributing](doc/usage/contributing.md) 28 | - [Changelog](CHANGELOG.md) 29 | - [MIT License](LICENSE.md) 30 | 31 | ## Motivation 32 | 33 | The motivation for this project came from trying to debug a Node.js server I wrote that used websockets. I found it problematic trying to inspect objects with the terminal because the output was too large and not browsable. I tried using the built-in debugging that works with the [Chrome Developer Tools plugin](https://github.com/joyent/node/wiki/using-eclipse-as-node-applications-debugger) for Eclipse. Unfortunately, I ran into a problem where setting breakpoints to inspect objects would cause the server to stop responding to heartbeats thus causing the client to disconnect. This would entirely mess up my debugging efforts. All I really needed to do was have a good way to inspect objects. 34 | 35 | I searched Google and found projects like [node-inspector](https://github.com/dannycoates/node-inspector), which didn't work with the latest versions of Node, and [node-codein](http://thomashunter.name/blog/nodejs-console-object-debug-inspector/) which had many bugs. And neither worked with Firefox. So, Node Monkey was born! 36 | 37 | ## Features 38 | 39 | - Log console output from your app to a browser console for easier inspection 40 | - Provides a stream for those using Bunyan (see [here](doc/usage/server.md#nodemonkeybunyan_stream)) 41 | - Provides SSH capability so you can get a shell into your app for inspection, debugging or controlling your app 42 | - Register commands for your application that can be executed from the browser console or the SSH interface 43 | 44 | ## Installation 45 | 46 | ``` 47 | npm install --save node-monkey 48 | ``` 49 | 50 | If you're interested in testing experimental and upcoming features, run this instead: 51 | 52 | ``` 53 | npm install --save node-monkey@next 54 | ``` 55 | 56 | ## Quick Usage 57 | 58 | Although Node Monkey supports many features, getting started is designed to be extremely easy. All you have to do is include a line or two in your application. Anything that is logged to the console after this will show up in the browser console once connected. It captures the output to most `console.*` function calls and forwards the output to the browser for inspection. 59 | 60 | The simplest usage looks like this: 61 | 62 | ```js 63 | const NodeMonkey = require("node-monkey") 64 | NodeMonkey() 65 | ``` 66 | 67 | Node Monkey also supports many configuration [options](doc/usage/server.md#options) and named instances. The call takes the form `NodeMonkey([options[, name])`. So, for example, to suppress local console output and only see output in your connected browser or terminal you might do something like this: 68 | 69 | ```js 70 | const NodeMonkey = require("node-monkey") 71 | const monkey = NodeMonkey({ 72 | server: { 73 | disableLocalOutput: true, 74 | }, 75 | }) 76 | ``` 77 | 78 | You can include Node Monkey in all the files within your app that you want and if used like the examples above, each call to `NodeMonkey()` will always return the same instance you first constructed, ignoring any options passed on subsequent calls. However, you may want to construct new instances with different options. To do so, give your instance a name: 79 | 80 | ```js 81 | const NodeMonkey = require("node-monkey") 82 | const monkey1 = NodeMonkey() // Creates an instance named 'default' 83 | const monkey2 = NodeMonkey("george") // Creates a new instance with default options 84 | const monkey3 = NodeMonkey( 85 | { 86 | // Creates a new instance with custom options named 'ninja' 87 | server: { 88 | silent: true, 89 | }, 90 | }, 91 | "ninja", 92 | ) 93 | ``` 94 | 95 | If you don't specify a port for additional instances it will automatically be set for you and will just increment from the default (e.g. 50502, 50504 for the websocket server and 50503, 50505 for the SSH server). 96 | 97 | To get an already constructed instance in another file just call it with the name again: 98 | 99 | ```js 100 | const NodeMonkey = require("node-monkey") 101 | const monkey3 = NodeMonkey("ninja") 102 | ``` 103 | 104 | When you start your app you will see the following output: 105 | 106 | ``` 107 | Node Monkey listening at http://0.0.0.0:50500 108 | ``` 109 | 110 | To connect your browser simply go to the address it shows in your web browser (`http://0.0.0.0:50500` in this case). If you change the default `host` and `port` bindings or pass in your own server be sure to adjust your URL accordingly. It will prompt you for a username and password. Until you setup a user the default is `guest` and `guest`. 111 | 112 | If you provide your own server you can view output in the console of your own web application instead. To see how to provide your own server check out the [documentation](doc/usage/server.md#provide-your-own). You will need to include the following ` 116 | ``` 117 | 118 | **NOTE**: You do NOT have to refresh the page when you restart your Node.js application to continue to receive output. Node Monkey will automatically reconnect. 119 | 120 | --- 121 | 122 | ### LICENSE: [MIT](LICENSE.md) 123 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO List 2 | 3 | ### General 4 | 5 | - Add documentation and examples for command management 6 | - Make sure CORS situation works with default server 7 | - Finish documenting how to serve files from `getServerPaths()` with custom server 8 | - Think of new name for project 9 | - Consider building with a postinstall script and using the 'env' preset to determine environment: 10 | - Consider setting up a default data directory in `/tmp` named based on hash of path to application root 11 | - This would make SSH require no setup out of the box allowing it to be more useful and exciting to the uninitiated 12 | - This would allow creating signed tokens for browser auth to allow re-connecting within a session time window without re-authenticating even after refreshing the page. 13 | - This would eliminate the need to store credentials even in memory in the browser thus improving security. 14 | - This would create a mechanism that could be used to expire sessions if, for example, a developer connected from a user's machine to debug and then walked away and forgot to disconnect, thus leaving a knowledgable user with full access to the internals of the server. 15 | 16 | ### Documentation 17 | 18 | - Show good examples (video?) of wrapping an existing server socket with Node Monkey websocket and including script on page 19 | - Show examples of how to implement custom SSH commands under server `addCmd()` documentation 20 | - Clarify that "Client" refers to the browser in the documentation 21 | 22 | ### Message tagging and filtering 23 | 24 | - Allow tagging messages for filtering 25 | - Pass user's tagging function context such as name of module/file and function where call was made from 26 | - Consider ways to add an extra context object or array when log calls are made that is compatible with existing console log calls (would have to be a noop when Node Monkey isn't running/attached) 27 | - Allow routing messages based on tags to browser and/or console 28 | - Allow filtering server side based on tags to restrict what users can see which messages 29 | - Allow setting include/exclude filter regexp's on the client to only show messages matching filters 30 | 31 | ### Command Manager 32 | 33 | - Implement command abort functionality 34 | - runCmd() call should return an object with an `abort()` method 35 | - Commands should be provided a context object with an EventEmitter prototype that they can bind to for handling aborts 36 | - Should it be a fatal error if there is no event listener after the command has started? 37 | - Even though some commands aren't really cancelable it they can still bind a noop callback. It just would force implementers to think about cancellation to try and prevent uninteruptible commands. 38 | - Make SSH module's `CTRL_C` call `abort()` 39 | 40 | ### SSH 41 | 42 | - Allow an application name to be set (default: 'Node Monkey') which gets interpolated into the prompt text 43 | - Refactor to improve organization and keep terminal events, data and management separate. Also need to expose more useful API. 44 | - Consider how to make authentication and command management pluggable in preparation for moving to its own module 45 | - Break out into separate module 46 | 47 | ### Browser 48 | 49 | - Hide reconnection errors and just show 'Reconnecting...' instead. Once it connects, show a simple horizontal rule to break up the output. 50 | - Consider possibility of implementing a simple CLI emulator 51 | - Create shortcut in browser (`Alt+R`?) to pop up prompt for command and then show the output in an alert box instead of the terminal 52 | - Figure out how to properly collect command output and display alert dialogs with respons output and errors 53 | 54 | ### Server 55 | 56 | - Add option to show console log call sources in the terminal, not just in the browser 57 | - Implement `console.trace()` capturing and sending to browser 58 | - Implement `console.time()` and `console.timeEnd()` capturing and sending to browser 59 | - Catch unhandled exceptions and pass them through to remote before dying 60 | - Replace object's functions with a call that sends a command, runs the function and returns the result 61 | - Is it possible to rebuild full objects including inheritance? 62 | - Implement a way to document command help info and view existing commands as well as individual command documentation 63 | - NOTE: The full set of available commands can already be seen by pressing `TAB` to auto-complete with nothing entered over SSH, but this doesn't solve the problem for the browser. Probably just need to add a command to list commands (possibly with an auto-complete-like prefix filter). 64 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (api) => { 2 | const webpackTarget = api.caller((caller) => caller && caller.target) 3 | const targets = webpackTarget === "node" ? { node: true, esmodules: true } : "defaults" 4 | 5 | const presets = [ 6 | [ 7 | "@babel/preset-env", 8 | { 9 | targets, 10 | // useBuiltIns: webpackTarget === "web" && "entry", 11 | }, 12 | ], 13 | ] 14 | const plugins = ["@babel/plugin-transform-runtime", "@babel/proposal-class-properties"] 15 | api.cache(true) 16 | 17 | return { 18 | presets, 19 | plugins, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Open your console to see output 6 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /dist/monkey.js: -------------------------------------------------------------------------------- 1 | /*! For license information please see monkey.js.LICENSE.txt */ 2 | !function(e,o){"object"==typeof exports&&"object"==typeof module?module.exports=o():"function"==typeof define&&define.amd?define([],o):"object"==typeof exports?exports.NodeMonkey=o():e.NodeMonkey=o()}(self,(()=>(()=>{"use strict";var __webpack_modules__={"./src/client/convert-styles.js":(e,o,t)=>{t.r(o),t.d(o,{default:()=>l});var n=t("./src/client/utils.js"),r={"":"text-decoration: none","":"font-weight: normal","":"font-weight: bold","":"font-style: italic","":"text-decoration: underline","":"font-style: normal","":"color: ","":"color: white","":"color: grey","":"color: black","":"color: magenta","":"color: yellow","":"color: red","":"color: cyan","":"color: blue","":"color: green"},c=/(\u001b\[.*?m)+/g,_=/(?:^|[^%])%(s|d|i|o|f|c)/g;const l=function(e,o){e.length||e.push("");var t,l=1,i=[];n.default.isObject(e[0])&&(e.splice(1,0,e[0]),e[0]="%o");for(var a=e[0];t=_.exec(a);)"o"==t[1]&&(e[0]=e[0].replace(t[0],t[0].slice(0,t[0].length-2)+"%o")),l++;if(e.length>l)for(var u=l;u{t.r(o),t.d(o,{default:()=>r});var n=t("./src/lib/common-utils.js");const r=Object.assign({getClientHost:function(){for(var e=document.getElementsByTagName("script"),o=/\/monkey\.js/,t=null,n=e.length-1;n>=0;--n)if(o.test(e[n].src)){t=e[n];break}if(t){var r=document.createElement("a");return r.href=t.src,"".concat(r.protocol,"//").concat(r.host)}return"".concat(location.protocol,"//").concat(location.host)},addHeadScript:function(e){var o=document.createElement("script");return o.type="text/javascript",o.src=e,document.getElementsByTagName("head")[0].appendChild(o),o}},n.default)},"./src/lib/common-utils.js":(e,o,t)=>{t.r(o),t.d(o,{default:()=>r});var n=t("./node_modules/@babel/runtime/helpers/esm/typeof.js");const r={isObject:function(e){var o=(0,n.default)(e);return!!e&&("object"==o||"function"==o)},invert:function(e){var o={};for(var t in e)e.hasOwnProperty(t)&&(o[e[t]]=t);return o}}},"./src/lib/cycle.js":(__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,{default:()=>__WEBPACK_DEFAULT_EXPORT__});var _babel_runtime_helpers_typeof__WEBPACK_IMPORTED_MODULE_0__=__webpack_require__("./node_modules/@babel/runtime/helpers/esm/typeof.js"),origJSON=__webpack_require__.g.JSON,JSON={};const __WEBPACK_DEFAULT_EXPORT__=JSON;"function"!=typeof JSON.decycle&&(JSON.decycle=function(e,o){var t=[],n=[];return function e(r,c){var _,l;return void 0!==o&&(r=o(r)),"object"!==(0,_babel_runtime_helpers_typeof__WEBPACK_IMPORTED_MODULE_0__.default)(r)||null===r||r instanceof Boolean||r instanceof Date||r instanceof Number||r instanceof RegExp||r instanceof String?r:(_=t.indexOf(r))>=0?{$ref:n[_]}:(t.push(r),n.push(c),Array.isArray(r)?(l=[],r.forEach((function(o,t){l[t]=e(o,c+"["+t+"]")}))):(l={},Object.keys(r).forEach((function(o){l[o]=e(r[o],c+"["+JSON.stringify(o)+"]")}))),l)}(e,"$")}),"function"!=typeof JSON.retrocycle&&(JSON.retrocycle=function retrocycle($){var px=/^\$(?:\[(?:\d+|\"(?:[^\\\"\u0000-\u001f]|\\([\\\"\/bfnrt]|u[0-9a-zA-Z]{4}))*\")\])*$/;return function rez(value){value&&"object"===(0,_babel_runtime_helpers_typeof__WEBPACK_IMPORTED_MODULE_0__.default)(value)&&(Array.isArray(value)?value.forEach((function(element,i){if("object"===(0,_babel_runtime_helpers_typeof__WEBPACK_IMPORTED_MODULE_0__.default)(element)&&null!==element){var path=element.$ref;"string"==typeof path&&px.test(path)?value[i]=eval(path):rez(element)}})):Object.keys(value).forEach((function(name){var item=value[name];if("object"===(0,_babel_runtime_helpers_typeof__WEBPACK_IMPORTED_MODULE_0__.default)(item)&&null!==item){var path=item.$ref;"string"==typeof path&&px.test(path)?value[name]=eval(path):rez(item)}})))}($),$}),JSON=origJSON},"./node_modules/@babel/runtime/helpers/esm/typeof.js":(e,o,t)=>{function n(e){return n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},n(e)}t.r(o),t.d(o,{default:()=>n})}},__webpack_module_cache__={};function __webpack_require__(e){var o=__webpack_module_cache__[e];if(void 0!==o)return o.exports;var t=__webpack_module_cache__[e]={exports:{}};return __webpack_modules__[e](t,t.exports,__webpack_require__),t.exports}__webpack_require__.d=(e,o)=>{for(var t in o)__webpack_require__.o(o,t)&&!__webpack_require__.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:o[t]})},__webpack_require__.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),__webpack_require__.o=(e,o)=>Object.prototype.hasOwnProperty.call(e,o),__webpack_require__.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var __webpack_exports__={};return(()=>{__webpack_require__.r(__webpack_exports__);var e=__webpack_require__("./src/client/utils.js"),o=__webpack_require__("./src/lib/cycle.js"),t=__webpack_require__("./src/client/convert-styles.js"),n=!1,r=window.monkey={cmdId:0,runningCmd:{},connect:null,disconnect:null,init:function(){if(!n)return n=!0,new Promise((function(n,c){e.default.addHeadScript("".concat(e.default.getClientHost(),"/monkey.io-client/socket.io.js")).addEventListener("load",(function(){new Promise((function(e,o){var t=io("".concat(location.origin,"/nm"),{path:"/monkey.io"});t.on("connect",(function(){})),t.on("error",(function(e){console.error(e)})),t.on("connect_error",(function(e){console.error(e)})),t.on("reconnect_error",(function(e){console.error(e)})),t.on("connect_timeout",(function(){console.error(new Error("Socket.IO connection timed out"))})),e(t)})).then((function(e){var c=0,_=null,l=t.default,i={convertStyles:!0};r.client=e,r.connect=e.connect.bind(e),r.disconnect=function(){c=0,_=null,e.disconnect.call(e)};var a=function(){var o,t;c>2?r.disconnect():(_||(o=prompt("Node Monkey username"),t=prompt("Node Monkey password"),_={username:o,password:t}),++c,e.emit("auth",_))};e.on("cmdResponse",(function(e,o,t){if(r.runningCmd[e]){var n=r.runningCmd[e],c=n.resolve,_=n.reject;delete r.runningCmd[e],o?_(o):c(t)}})),e.on("settings",(function(e){Object.assign(i,e),i.convertStyles||(l=function(e,o){return e.concat([o])})})),e.on("auth",a),e.on("authResponse",(function(e,o){e||(_=null,console.warn("Auth failed:",o),a())})),e.on("console",(function(e){var t,n=(e=o.default.retrocycle(e)).callerInfo;n&&(t=" -- Called from "+n.file+":"+n.line+":"+n.column+(n.caller?"(function "+n.caller+")":"")),"dir"===e.method?(console.dir(e.args[0]),t&&console.log.apply(console,l(["^^^"],t))):console[e.method].apply(console,l(e.args,t))})),e.on("prompt",(function(o,t,n){n||(n={}),e.emit("promptResponse",o,prompt(t))})),n()})).catch(c)}))}));r.connect()},cmd:function(e,o){if(r.client){var t=new Promise((function(o,t){var n=r.cmdId++;r.client.emit("cmd",n,e),r.runningCmd[n]={resolve:o,reject:t}}));return o||t.then((function(e){return null!==e&&console.log(e)})).catch((function(e){null!==e&&(console.error(e),alert(e.message))})),t}console.error("Must be connected to a server to execute a command")}}})(),__webpack_exports__=__webpack_exports__.default,__webpack_exports__})())); 3 | //# sourceMappingURL=monkey.js.map -------------------------------------------------------------------------------- /dist/monkey.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! ../lib/common-utils */ 2 | 3 | /*! ../lib/cycle */ 4 | 5 | /*! ./convert-styles */ 6 | 7 | /*! ./utils */ 8 | 9 | /*! @babel/runtime/helpers/typeof */ 10 | 11 | /*!**************************!*\ 12 | !*** ./src/lib/cycle.js ***! 13 | \**************************/ 14 | 15 | /*!*****************************!*\ 16 | !*** ./src/client/index.js ***! 17 | \*****************************/ 18 | 19 | /*!*****************************!*\ 20 | !*** ./src/client/utils.js ***! 21 | \*****************************/ 22 | 23 | /*!*********************************!*\ 24 | !*** ./src/lib/common-utils.js ***! 25 | \*********************************/ 26 | 27 | /*!**************************************!*\ 28 | !*** ./src/client/convert-styles.js ***! 29 | \**************************************/ 30 | 31 | /*!***********************************************************!*\ 32 | !*** ./node_modules/@babel/runtime/helpers/esm/typeof.js ***! 33 | \***********************************************************/ 34 | -------------------------------------------------------------------------------- /dist/monkey.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"monkey.js","mappings":"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","sources":["webpack://NodeMonkey/webpack/universalModuleDefinition","webpack://NodeMonkey/./src/client/convert-styles.js","webpack://NodeMonkey/./src/client/utils.js","webpack://NodeMonkey/./src/lib/common-utils.js","webpack://NodeMonkey/./src/lib/cycle.js","webpack://NodeMonkey/./node_modules/@babel/runtime/helpers/esm/typeof.js","webpack://NodeMonkey/webpack/bootstrap","webpack://NodeMonkey/webpack/runtime/define property getters","webpack://NodeMonkey/webpack/runtime/global","webpack://NodeMonkey/webpack/runtime/hasOwnProperty shorthand","webpack://NodeMonkey/webpack/runtime/make namespace object","webpack://NodeMonkey/./src/client/index.js"],"sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"NodeMonkey\"] = factory();\n\telse\n\t\troot[\"NodeMonkey\"] = factory();\n})(self, () => {\nreturn ","import utils from \"./utils\"\n\nlet styleMap = {\n // Styles\n \"\\u001b[24m\": \"text-decoration: none\",\n \"\\u001b[22m\": \"font-weight: normal\",\n \"\\u001b[1m\": \"font-weight: bold\",\n \"\\u001b[3m\": \"font-style: italic\",\n \"\\u001b[4m\": \"text-decoration: underline\",\n \"\\u001b[23m\": \"font-style: normal\",\n\n // Colors\n \"\\u001b[39m\": \"color: \",\n \"\\u001b[37m\": \"color: white\",\n \"\\u001b[90m\": \"color: grey\",\n \"\\u001b[30m\": \"color: black\",\n \"\\u001b[35m\": \"color: magenta\",\n \"\\u001b[33m\": \"color: yellow\",\n \"\\u001b[31m\": \"color: red\",\n \"\\u001b[36m\": \"color: cyan\",\n \"\\u001b[34m\": \"color: blue\",\n \"\\u001b[32m\": \"color: green\",\n },\n // Styles for the caller data.\n traceStyle = \"color: grey; font-family: Helvetica, Arial, sans-serif\",\n // RegExp pattern for styles\n stylePattern = /(\\u001b\\[.*?m)+/g,\n // RegExp pattern for format specifiers (like '%o', '%s')\n formatPattern = /(?:^|[^%])%(s|d|i|o|f|c)/g\n\nfunction stylize(data, cdata) {\n if (!data.length) {\n data.push(\"\")\n }\n\n // If `data` has multiple arguments, we are going to merge everything into\n // the first argument, so style-specifiers can be used throughout all arguments.\n\n let cap,\n mergeArgsStart = 1,\n formatSpecifiers = []\n\n // If the first argument is an object, we need to replace it with `%o`\n // (always preemptively reset the color)\n if (utils.isObject(data[0])) {\n data.splice(1, 0, data[0])\n data[0] = \"%o\"\n }\n\n // Count all format specifiers in the first argument to see from where we need to\n // start merging\n let txt = data[0]\n while ((cap = formatPattern.exec(txt))) {\n if (cap[1] == \"o\") {\n // Insert color resetter\n data[0] = data[0].replace(cap[0], cap[0].slice(0, cap[0].length - 2) + \"\\u001b[39m%o\")\n }\n mergeArgsStart++\n }\n\n // Start merging...\n if (data.length > mergeArgsStart) {\n for (let i = mergeArgsStart; i < data.length; i++) {\n let arg = data[i],\n specifier\n\n if (typeof arg == \"string\") {\n // Since this argument is a string and may be styled as well, put it right in...\n specifier = \" \" + arg\n // ...and remove the argument...\n data.splice(i, 1)\n // ...and adapt the iterator.\n i--\n } else {\n // Otherwise use the '%o'-specifier (preemptively reset color)\n specifier = \" \\u001b[39m%o\"\n }\n\n data[0] += specifier\n }\n }\n\n // Now let's collect all format specifiers and their positions as well,\n // so we know where to put our style-specifiers.\n while ((cap = formatPattern.exec(data[0]))) {\n formatSpecifiers.push(cap)\n }\n\n let added = 0\n txt = data[0]\n\n // Let's do some styling...\n while ((cap = stylePattern.exec(txt))) {\n let styles = [],\n capsplit = cap[0].split(\"m\")\n\n // Get the needed styles\n for (let j = 0; j < capsplit.length; j++) {\n let s\n if ((s = styleMap[capsplit[j] + \"m\"])) styles.push(s)\n }\n\n // Check if the style must be added before other specifiers\n if (styles.length) {\n let k\n for (k = 0; k < formatSpecifiers.length; k++) {\n let sp = formatSpecifiers[k]\n if (cap[\"index\"] < sp[\"index\"]) {\n break\n }\n }\n\n // Add them at the right position\n let pos = k + 1 + added\n data.splice(pos, 0, styles.join(\";\"))\n added++\n\n // Replace original with `%c`-specifier\n data[0] = data[0].replace(cap[0], \"%c\")\n }\n }\n // ...done!\n\n // At last, add caller data, if present.\n if (cdata) {\n data[0] += \"%c\" + cdata\n data.push(traceStyle)\n }\n\n return data\n}\n\nexport default stylize\n","import commonUtils from \"../lib/common-utils\"\n\nexport default Object.assign(\n {\n getClientHost() {\n let scripts = document.getElementsByTagName(\"script\"),\n scriptRe = /\\/monkey\\.js/,\n script = null\n\n // Loop in reverse since the correct script will be the last one except when the `async` attribute is set on the script\n for (let i = scripts.length - 1; i >= 0; --i) {\n if (scriptRe.test(scripts[i].src)) {\n script = scripts[i]\n break\n }\n }\n\n if (script) {\n let parser = document.createElement(\"a\")\n parser.href = script.src\n\n return `${parser.protocol}//${parser.host}`\n }\n\n return `${location.protocol}//${location.host}`\n },\n\n addHeadScript(src) {\n let script = document.createElement(\"script\")\n script.type = \"text/javascript\"\n script.src = src\n document.getElementsByTagName(\"head\")[0].appendChild(script)\n\n return script\n },\n },\n commonUtils,\n)\n","export default {\n isObject(value) {\n let type = typeof value\n return !!value && (type == \"object\" || type == \"function\")\n },\n\n invert(obj) {\n let inverted = {}\n for (let k in obj) {\n if (obj.hasOwnProperty(k)) {\n inverted[obj[k]] = k\n }\n }\n\n return inverted\n },\n}\n","/*\n cycle.js\n 2016-05-01\n\n Public Domain.\n\n NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.\n\n This code should be minified before deployment.\n See http://javascript.crockford.com/jsmin.html\n\n USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO\n NOT CONTROL.\n*/\n\n/*jslint eval, for */\n\n/*property\n $ref, decycle, forEach, isArray, keys, length, push, retrocycle, stringify,\n test\n*/\n\nlet origJSON = global.JSON,\n JSON = {}\nexport default JSON\n\nif (typeof JSON.decycle !== \"function\") {\n JSON.decycle = function decycle(object, replacer) {\n \"use strict\"\n\n // Make a deep copy of an object or array, assuring that there is at most\n // one instance of each object or array in the resulting structure. The\n // duplicate references (which might be forming cycles) are replaced with\n // an object of the form\n\n // {\"$ref\": PATH}\n\n // where the PATH is a JSONPath string that locates the first occurance.\n\n // So,\n\n // var a = [];\n // a[0] = a;\n // return JSON.stringify(JSON.decycle(a));\n\n // produces the string '[{\"$ref\":\"$\"}]'.\n\n // If a replacer function is provided, then it will be called for each value.\n // A replacer function receives a value and returns a replacement value.\n\n // JSONPath is used to locate the unique object. $ indicates the top level of\n // the object or array. [NUMBER] or [STRING] indicates a child element or\n // property.\n\n var objects = [] // Keep a reference to each unique object or array\n var paths = [] // Keep the path to each unique object or array\n\n return (function derez(value, path) {\n // The derez function recurses through the object, producing the deep copy.\n\n var i // The loop counter\n var nu // The new object or array\n\n // If a replacer function was provided, then call it to get a replacement value.\n\n if (replacer !== undefined) {\n value = replacer(value)\n }\n\n // typeof null === \"object\", so go on if this value is really an object but not\n // one of the weird builtin objects.\n\n if (\n typeof value === \"object\" &&\n value !== null &&\n !(value instanceof Boolean) &&\n !(value instanceof Date) &&\n !(value instanceof Number) &&\n !(value instanceof RegExp) &&\n !(value instanceof String)\n ) {\n // If the value is an object or array, look to see if we have already\n // encountered it. If so, return a {\"$ref\":PATH} object. This is a hard\n // linear search that will get slower as the number of unique objects grows.\n // Someday, this should be replaced with an ES6 WeakMap.\n\n i = objects.indexOf(value)\n if (i >= 0) {\n return { $ref: paths[i] }\n }\n\n // Otherwise, accumulate the unique value and its path.\n\n objects.push(value)\n paths.push(path)\n\n // If it is an array, replicate the array.\n\n if (Array.isArray(value)) {\n nu = []\n value.forEach(function (element, i) {\n nu[i] = derez(element, path + \"[\" + i + \"]\")\n })\n } else {\n // If it is an object, replicate the object.\n\n nu = {}\n Object.keys(value).forEach(function (name) {\n nu[name] = derez(value[name], path + \"[\" + JSON.stringify(name) + \"]\")\n })\n }\n return nu\n }\n return value\n })(object, \"$\")\n }\n}\n\nif (typeof JSON.retrocycle !== \"function\") {\n JSON.retrocycle = function retrocycle($) {\n \"use strict\"\n\n // Restore an object that was reduced by decycle. Members whose values are\n // objects of the form\n // {$ref: PATH}\n // are replaced with references to the value found by the PATH. This will\n // restore cycles. The object will be mutated.\n\n // The eval function is used to locate the values described by a PATH. The\n // root object is kept in a $ variable. A regular expression is used to\n // assure that the PATH is extremely well formed. The regexp contains nested\n // * quantifiers. That has been known to have extremely bad performance\n // problems on some browsers for very long strings. A PATH is expected to be\n // reasonably short. A PATH is allowed to belong to a very restricted subset of\n // Goessner's JSONPath.\n\n // So,\n // var s = '[{\"$ref\":\"$\"}]';\n // return JSON.retrocycle(JSON.parse(s));\n // produces an array containing a single element which is the array itself.\n\n var px = /^\\$(?:\\[(?:\\d+|\\\"(?:[^\\\\\\\"\\u0000-\\u001f]|\\\\([\\\\\\\"\\/bfnrt]|u[0-9a-zA-Z]{4}))*\\\")\\])*$/\n\n ;(function rez(value) {\n // The rez function walks recursively through the object looking for $ref\n // properties. When it finds one that has a value that is a path, then it\n // replaces the $ref object with a reference to the value that is found by\n // the path.\n\n if (value && typeof value === \"object\") {\n if (Array.isArray(value)) {\n value.forEach(function (element, i) {\n if (typeof element === \"object\" && element !== null) {\n var path = element.$ref\n if (typeof path === \"string\" && px.test(path)) {\n value[i] = eval(path)\n } else {\n rez(element)\n }\n }\n })\n } else {\n Object.keys(value).forEach(function (name) {\n var item = value[name]\n if (typeof item === \"object\" && item !== null) {\n var path = item.$ref\n if (typeof path === \"string\" && px.test(path)) {\n value[name] = eval(path)\n } else {\n rez(item)\n }\n }\n })\n }\n }\n })($)\n return $\n }\n}\n\nJSON = origJSON\n","export default function _typeof(obj) {\n \"@babel/helpers - typeof\";\n\n return _typeof = \"function\" == typeof Symbol && \"symbol\" == typeof Symbol.iterator ? function (obj) {\n return typeof obj;\n } : function (obj) {\n return obj && \"function\" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj;\n }, _typeof(obj);\n}","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.g = (function() {\n\tif (typeof globalThis === 'object') return globalThis;\n\ttry {\n\t\treturn this || new Function('return this')();\n\t} catch (e) {\n\t\tif (typeof window === 'object') return window;\n\t}\n})();","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","import utils from \"./utils\"\nimport cycle from \"../lib/cycle\"\nimport convertStyles from \"./convert-styles\"\n\nlet initialized = false\nconst monkey = (window.monkey = {\n cmdId: 0,\n runningCmd: {},\n connect: null,\n disconnect: null,\n\n init() {\n if (initialized) {\n monkey.connect()\n return\n }\n initialized = true\n\n return new Promise((resolve, reject) => {\n utils.addHeadScript(`${utils.getClientHost()}/monkey.io-client/socket.io.js`).addEventListener(\"load\", () => {\n initClient()\n .then((client) => {\n let authAttempts = 0,\n creds = null,\n stylize = convertStyles,\n settings = {\n convertStyles: true,\n }\n\n monkey.client = client\n monkey.connect = client.connect.bind(client)\n monkey.disconnect = () => {\n authAttempts = 0\n creds = null\n client.disconnect.call(client)\n }\n\n let doAuth = () => {\n if (authAttempts > 2) {\n monkey.disconnect()\n return\n }\n\n let username, password\n if (!creds) {\n username = prompt(\"Node Monkey username\")\n password = prompt(\"Node Monkey password\")\n creds = { username, password }\n }\n\n ++authAttempts\n client.emit(\"auth\", creds)\n }\n\n client.on(\"cmdResponse\", (cmdId, error, output) => {\n if (monkey.runningCmd[cmdId]) {\n let { resolve, reject } = monkey.runningCmd[cmdId]\n delete monkey.runningCmd[cmdId]\n\n if (error) {\n reject(error)\n } else {\n resolve(output)\n }\n }\n })\n\n client.on(\"settings\", (data) => {\n Object.assign(settings, data)\n\n if (!settings.convertStyles) {\n stylize = function (args, trace) {\n return args.concat([trace])\n }\n }\n })\n\n client.on(\"auth\", doAuth)\n\n client.on(\"authResponse\", (result, err) => {\n if (!result) {\n creds = null\n console.warn(\"Auth failed:\", err)\n doAuth()\n }\n })\n\n client.on(\"console\", (data) => {\n data = cycle.retrocycle(data)\n\n let trace,\n cdata = data.callerInfo\n if (cdata) {\n trace =\n \" -- Called from \" +\n cdata.file +\n \":\" +\n cdata.line +\n \":\" +\n cdata.column +\n (cdata.caller ? \"(function \" + cdata.caller + \")\" : \"\")\n }\n if (data.method === \"dir\") {\n console.dir(data.args[0])\n if (trace) {\n console.log.apply(console, stylize([\"^^^\"], trace))\n }\n } else {\n console[data.method].apply(console, stylize(data.args, trace))\n }\n })\n\n client.on(\"prompt\", (promptId, promptTxt, opts) => {\n opts || (opts = {})\n\n client.emit(\"promptResponse\", promptId, prompt(promptTxt))\n })\n\n resolve()\n })\n .catch(reject)\n })\n })\n },\n\n cmd(command, noOutput) {\n if (!monkey.client) {\n console.error(`Must be connected to a server to execute a command`)\n return\n }\n\n let p = new Promise((resolve, reject) => {\n let cmdId = monkey.cmdId++\n monkey.client.emit(\"cmd\", cmdId, command)\n monkey.runningCmd[cmdId] = { resolve, reject }\n })\n\n if (!noOutput) {\n p.then((output) => output !== null && console.log(output)).catch((error) => {\n if (error !== null) {\n console.error(error)\n alert(error.message)\n }\n })\n }\n\n return p\n },\n})\n\nfunction initClient() {\n return new Promise((resolve, reject) => {\n let client = io(`${location.origin}/nm`, {\n path: \"/monkey.io\"\n })\n\n client.on(\"connect\", function () {})\n\n client.on(\"error\", (err) => {\n console.error(err)\n })\n\n client.on(\"connect_error\", (err) => {\n console.error(err)\n })\n\n client.on(\"reconnect_error\", (err) => {\n console.error(err)\n })\n\n client.on(\"connect_timeout\", () => {\n console.error(new Error(\"Socket.IO connection timed out\"))\n })\n\n resolve(client)\n })\n}\n"],"names":["root","factory","exports","module","define","amd","self","styleMap","stylePattern","formatPattern","data","cdata","length","push","cap","mergeArgsStart","formatSpecifiers","utils","splice","txt","exec","replace","slice","i","arg","specifier","added","styles","capsplit","split","j","s","k","sp","pos","join","Object","assign","getClientHost","scripts","document","getElementsByTagName","scriptRe","script","test","src","parser","createElement","href","protocol","host","location","addHeadScript","type","appendChild","commonUtils","isObject","value","invert","obj","inverted","hasOwnProperty","origJSON","global","JSON","decycle","object","replacer","objects","paths","derez","path","nu","undefined","Boolean","Date","Number","RegExp","String","indexOf","$ref","Array","isArray","forEach","element","keys","name","stringify","retrocycle","$","px","rez","eval","item","_typeof","Symbol","iterator","constructor","prototype","__webpack_module_cache__","__webpack_require__","moduleId","cachedModule","__webpack_modules__","d","definition","key","o","defineProperty","enumerable","get","g","globalThis","this","Function","e","window","prop","call","r","toStringTag","initialized","monkey","cmdId","runningCmd","connect","disconnect","init","Promise","resolve","reject","addEventListener","client","io","origin","on","err","console","error","Error","then","authAttempts","creds","stylize","convertStyles","settings","bind","doAuth","username","password","prompt","emit","output","args","trace","concat","result","warn","cycle","callerInfo","file","line","column","caller","method","dir","log","apply","promptId","promptTxt","opts","catch","cmd","command","noOutput","p","alert","message"],"sourceRoot":""} -------------------------------------------------------------------------------- /dist/server.js: -------------------------------------------------------------------------------- 1 | /*! For license information please see server.js.LICENSE.txt */ 2 | require("source-map-support").install(),function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.NodeMonkey=t():e.NodeMonkey=t()}(global,(()=>(()=>{"use strict";var __webpack_modules__={"./src/lib/common-utils.js":(e,t,r)=>{r.r(t),r.d(t,{default:()=>i});var n=r("@babel/runtime/helpers/typeof"),s=r.n(n);const i={isObject:function(e){var t=s()(e);return!!e&&("object"==t||"function"==t)},invert:function(e){var t={};for(var r in e)e.hasOwnProperty(r)&&(t[e[r]]=r);return t}}},"./src/lib/cycle.js":(__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,{default:()=>__WEBPACK_DEFAULT_EXPORT__});var _babel_runtime_helpers_typeof__WEBPACK_IMPORTED_MODULE_0__=__webpack_require__("@babel/runtime/helpers/typeof"),_babel_runtime_helpers_typeof__WEBPACK_IMPORTED_MODULE_0___default=__webpack_require__.n(_babel_runtime_helpers_typeof__WEBPACK_IMPORTED_MODULE_0__),origJSON=global.JSON,JSON={};const __WEBPACK_DEFAULT_EXPORT__=JSON;"function"!=typeof JSON.decycle&&(JSON.decycle=function(e,t){var r=[],n=[];return function e(s,i){var a,o;return void 0!==t&&(s=t(s)),"object"!==_babel_runtime_helpers_typeof__WEBPACK_IMPORTED_MODULE_0___default()(s)||null===s||s instanceof Boolean||s instanceof Date||s instanceof Number||s instanceof RegExp||s instanceof String?s:(a=r.indexOf(s))>=0?{$ref:n[a]}:(r.push(s),n.push(i),Array.isArray(s)?(o=[],s.forEach((function(t,r){o[r]=e(t,i+"["+r+"]")}))):(o={},Object.keys(s).forEach((function(t){o[t]=e(s[t],i+"["+JSON.stringify(t)+"]")}))),o)}(e,"$")}),"function"!=typeof JSON.retrocycle&&(JSON.retrocycle=function retrocycle($){var px=/^\$(?:\[(?:\d+|\"(?:[^\\\"\u0000-\u001f]|\\([\\\"\/bfnrt]|u[0-9a-zA-Z]{4}))*\")\])*$/;return function rez(value){value&&"object"===_babel_runtime_helpers_typeof__WEBPACK_IMPORTED_MODULE_0___default()(value)&&(Array.isArray(value)?value.forEach((function(element,i){if("object"===_babel_runtime_helpers_typeof__WEBPACK_IMPORTED_MODULE_0___default()(element)&&null!==element){var path=element.$ref;"string"==typeof path&&px.test(path)?value[i]=eval(path):rez(element)}})):Object.keys(value).forEach((function(name){var item=value[name];if("object"===_babel_runtime_helpers_typeof__WEBPACK_IMPORTED_MODULE_0___default()(item)&&null!==item){var path=item.$ref;"string"==typeof path&&px.test(path)?value[name]=eval(path):rez(item)}})))}($),$}),JSON=origJSON},"./src/server/bunyan-stream.js":(e,t,r)=>{r.r(t),r.d(t,{default:()=>i});var n={trace:10,debug:20,info:30,warn:40,error:50,fatal:60},s=r("./src/server/utils.js").default.invert(n);const i=function(e){return{write:function(t){t=JSON.parse(t),e._sendMessage({method:s[t.level]||"info",args:[t.msg,t]})}}}},"./src/server/command-interface.js":(e,t,r)=>{r.r(t),r.d(t,{default:()=>c});var n=r("@babel/runtime/helpers/createClass"),s=r.n(n),i=r("@babel/runtime/helpers/classCallCheck"),a=r.n(i),o=r("@babel/runtime/helpers/defineProperty"),u=r.n(o);const c=s()((function e(t,r,n,s,i){a()(this,e),u()(this,"commandManager",null),u()(this,"write",(function(e,t){console.log(e)})),u()(this,"writeLn",(function(e,t){console.log(e)})),u()(this,"error",(function(e,t){console.error(e)})),u()(this,"prompt",(function(e,t,r){"function"==typeof t&&(t,t=void 0),t||(t={}),console.warn("Prompt not implemented")})),this.commandManager=t,this.write=r,this.writeLn=n,this.error=s,this.prompt=i}))},"./src/server/command-manager.js":(e,t,r)=>{r.r(t),r.d(t,{default:()=>d});var n=r("@babel/runtime/helpers/asyncToGenerator"),s=r.n(n),i=r("@babel/runtime/helpers/createClass"),a=r.n(i),o=r("@babel/runtime/helpers/classCallCheck"),u=r.n(o),c=r("@babel/runtime/helpers/defineProperty"),l=r.n(c),p=r("@babel/runtime/regenerator"),h=r.n(p),f=(r("lodash"),r("./src/server/utils.js")),_=r("minimist"),m=r.n(_);const d=a()((function e(){var t=this;u()(this,e),l()(this,"commands",{}),l()(this,"addCmd",(function(e,r,n){if(t.commands[e])throw new Error("'".concat(e,"' is already registered as a command"));"function"==typeof r&&(n=r,r={}),t.commands[e]={opts:r,exec:n}})),l()(this,"runCmd",function(){var e=s()(h().mark((function e(r,n,s){var i,a,o,u,c,l;return h().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(i=f.default.parseCommand(r),a=i[0],o=t.commands[a],n){e.next=5;break}throw new Error("Missing user context for command '".concat(a,"'"));case 5:if(o){e.next=7;break}throw new Error("Command not found: '".concat(a,"'"));case 7:return u=m()(i.slice(1)),c=f.default.getPromiseObj(),l=o.exec({args:u,username:n},{write:s.write,writeLn:s.writeLn,error:s.error,prompt:s.prompt},c.resolve),e.abrupt("return",l.then?l:c.promise);case 11:case"end":return e.stop()}}),e)})));return function(t,r,n){return e.apply(this,arguments)}}())}))},"./src/server/setup-server.js":(e,t,r)=>{r.r(t),r.d(t,{default:()=>_});var n=r("@babel/runtime/helpers/asyncToGenerator"),s=r.n(n),i=r("@babel/runtime/regenerator"),a=r.n(i),o=r("path"),u=r("http"),c=r("fs/promises"),l={html:"text/html",js:"application/javascript",json:"application/json",map:"application/json"},p=new Map;function h(e){return f.apply(this,arguments)}function f(){return(f=s()(a().mark((function e(t){return a().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(p.has(t)){e.next=7;break}return e.t0=p,e.t1=t,e.next=5,(0,c.readFile)(t);case 5:e.t2=e.sent,e.t0.set.call(e.t0,e.t1,e.t2);case 7:return e.abrupt("return",p.get(t));case 8:case"end":return e.stop()}}),e)})))).apply(this,arguments)}const _=function(e){var t=e.filePaths||{},r=(0,u.createServer)(function(){var e=s()(a().mark((function e(r,n){var s,i,u;return a().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(!(s=t[r.url])){e.next=11;break}return i=l[((0,o.extname)(s)||"").slice(1)]||"text/plain",e.next=5,h(s);case 5:u=e.sent,n.setHeader("Content-Type",i),n.writeHead(200),n.end(u),e.next=13;break;case 11:n.writeHead(404),n.end();case 13:case"end":return e.stop()}}),e)})));return function(t,r){return e.apply(this,arguments)}}());return r}},"./src/server/setup-socket.js":(e,t,r)=>{r.r(t),r.d(t,{default:()=>u});var n=r("lodash"),s=r.n(n),i=r("socket.io"),a=r.n(i),o=r("./src/server/command-interface.js");const u=function(e){var t=a()();t.attach(e.server,{path:"/monkey.io",autoUnref:!0});var r=t.of("/nm");return r.on("connection",(function(t){var r=null;t.emit("settings",e.clientSettings),t.emit("auth"),t.on("auth",(function(r){e.userManager.verifyUser(r.username,r.password).then((function(n){t.emit("authResponse",n,n?void 0:"Incorrect password"),n&&(t.username=r.username,t.join("authed"),e.onAuth&&e.onAuth(t))})).catch((function(e){t.emit("authResponse",!1,e)}))})),t.on("cmd",(function(n,s){t.username?(r||(r=function(e,t){var r=0,n={},s=function(e,r){e&&t.emit("console",{method:"log",args:[e]})},i=function(e,r){e&&t.emit("console",{method:"error",args:[e]})},a=function(e,s,i){"function"==typeof s&&(i=s,s=void 0),s||(s={});var a=r++;t.emit("prompt",a,e,s),n[a]=i};return t.on("promptResponse",(function(e,t){var r=n[e];r&&r(null,t)})),new o.default(e,s,s,i,a)}(e.cmdManager,t)),e.cmdManager.runCmd(s,t.username,r).then((function(e){t.emit("cmdResponse",n,null,e)})).catch((function(e){t.emit("cmdResponse",n,e&&e.message||e,null)}))):t.emit("cmdResponse",n,"You are not authorized to run commands")}))})),s().each(e.handlers,(function(e,r){t.on(r,e)})),r}},"./src/server/ssh-manager.js":(e,t,r)=>{r.r(t),r.d(t,{default:()=>x});var n=r("@babel/runtime/helpers/classCallCheck"),s=r.n(n),i=r("@babel/runtime/helpers/createClass"),a=r.n(i),o=r("@babel/runtime/helpers/defineProperty"),u=r.n(o),c=r("fs"),l=r.n(c),p=r("tty"),h=r.n(p),f=r("node-pty"),_=r("ssh2"),m=r.n(_),d=r("terminal-kit"),v=r.n(d),y=r("./src/server/command-interface.js");function b(e,t){var r="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(!r){if(Array.isArray(e)||(r=function(e,t){if(!e)return;if("string"==typeof e)return w(e,t);var r=Object.prototype.toString.call(e).slice(8,-1);"Object"===r&&e.constructor&&(r=e.constructor.name);if("Map"===r||"Set"===r)return Array.from(e);if("Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return w(e,t)}(e))||t&&e&&"number"==typeof e.length){r&&(e=r);var n=0,s=function(){};return{s,n:function(){return n>=e.length?{done:!0}:{done:!1,value:e[n++]}},e:function(e){throw e},f:s}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var i,a=!0,o=!1;return{s:function(){r=r.call(e)},n:function(){var e=r.next();return a=e.done,e},e:function(e){o=!0,i=e},f:function(){try{a||null==r.return||r.return()}finally{if(o)throw i}}}}function w(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);r0&&void 0!==arguments[0]?arguments[0]:"",r=arguments.length>1?arguments[1]:void 0,n=arguments.length>2?arguments[2]:void 0;"function"==typeof r&&(n=r,r=void 0),r||(r={});var s={};r.hideInput&&(s.echo=!1),e.term(t),e.term.inputField(s,n)}))}},{key:"write",value:function(e,t){var r=t.style,n=void 0===r?void 0:r;this.term&&(n?this.term[n](e):this.term(e))}},{key:"close",value:function(){this.stream&&this.stream.end(),this.onClose()}},{key:"onAuth",value:function(e){var t=this;"password"==e.method?this.userManager.verifyUser(e.username,e.password).then((function(r){r?(t.username=e.username,e.accept()):e.reject()})).catch((function(t){e.reject()})):(e.method,e.reject())}},{key:"onReady",value:function(){var e=this;this.client.on("session",(function(t,r){e.session=t(),e.session.once("pty",(function(t,r,n){e.ptyInfo=n,t&&t()})).on("window-change",(function(t,r,n){Object.assign(e.ptyInfo,n),e._resize(),t&&t()})).once("shell",(function(t,r){e.stream=t(),e._initCmdMan(),e._initStream(),e._initPty(),e._initTerm()}))}))}},{key:"onClose",value:function(){var e=this.options.onClose;e&&e()}},{key:"onKey",value:function(e,t,r){var n=this;if("CTRL_L"===e)this.clearScreen();else if("CTRL_C"===e)this.inputActive=!1,this.inputField.abort(),this.term("\n^^C\n"),this.prompt();else if("CTRL_D"===e){this.inputField.getInput().length||(this.term.nextLine(),setTimeout((function(){n.close()}),0))}}},{key:"_resize",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this,t=e.term;t&&t.stdout.emit("resize")}},{key:"_initStream",value:function(){var e=this.stream;e.name=this.title,e.isTTY=!0,e.setRawMode=function(){},e.on("error",(function(e){console.error("SSH stream error:",e.message)}))}},{key:"_initPty",value:function(){var e=this,t=f.native.open(this.ptyInfo.cols,this.ptyInfo.rows);this.pty={master_fd:t.master,slave_fd:t.slave,master:new(h().WriteStream)(t.master),slave:new(h().ReadStream)(t.slave)},Object.defineProperty(this.pty.slave,"columns",{enumerable:!0,get:function(){return e.ptyInfo.cols}}),Object.defineProperty(this.pty.slave,"rows",{enumerable:!0,get:function(){return e.ptyInfo.rows}}),this.stream.stdin.pipe(this.pty.master),this.pty.master.pipe(this.stream.stdout)}},{key:"_initTerm",value:function(){var e=this.term=v().createTerminal({stdin:this.pty.slave,stdout:this.pty.slave,stderr:this.pty.slave,generic:this.ptyInfo.term,appName:this.title,isSSH:!0,isTTY:!0});e.on("key",this.onKey.bind(this)),e.windowTitle(this._interpolate(this.title)),this.clearScreen()}},{key:"_interpolate",value:function(e){for(var t,r=/{@(.+?)}/g,n={username:this.username};t=r.exec(e);)n[t[1]]&&(e=e.replace(t[0],n[t[1]]));return e}},{key:"clearScreen",value:function(){this.term.clear(),this.prompt()}},{key:"prompt",value:function(){var e=this,t=this.term;t.windowTitle(this._interpolate(this.title)),t.bold(this._interpolate(this.promptTxt)),this.inputActive||(this.inputActive=!0,this.inputField=t.inputField({history:this.cmdHistory,autoComplete:Object.keys(this.options.cmdManager.commands),autoCompleteHint:!0,autoCompleteMenu:!0},(function(r,n){return e.inputActive=!1,t.nextLine(),r?t.error(r.message||r):n?(" "!==n[0]&&e.cmdHistory.push(n),void("exit"===n?setTimeout(e.close.bind(e)):"clear"===n?e.clearScreen():n?e.options.cmdManager.runCmd(n,e.username,e.cmdInterface).then((function(t){"string"!=typeof t&&(t=JSON.stringify(t,null," ")),e.term(t),e.term.nextLine(),e.prompt()})).catch((function(t){"string"!=typeof t&&(t=t.message||JSON.stringify(t,null," ")),e.term.red.error(t),e.term.nextLine(),e.prompt()})):e.prompt())):e.prompt()})))}}]),e}();const x=g},"./src/server/user-manager.js":(e,t,r)=>{r.r(t),r.d(t,{default:()=>v});var n=r("@babel/runtime/helpers/asyncToGenerator"),s=r.n(n),i=r("@babel/runtime/helpers/classCallCheck"),a=r.n(i),o=r("@babel/runtime/helpers/createClass"),u=r.n(o),c=r("@babel/runtime/helpers/defineProperty"),l=r.n(c),p=r("@babel/runtime/regenerator"),h=r.n(p),f=r("fs"),_=r.n(f),m=r("scrypt-kdf"),d=r.n(m);const v=function(){function e(t){a()(this,e),l()(this,"userFile",void 0),l()(this,"userFileCache",null),l()(this,"userFileCreated",!1),this.userFile=t.userFile,t.silent||this.getUsers().then((function(e){var t=Object.keys(e);t.length?1===t.length&&"guest"===t[0]&&console.warn("[WARN] No users detected. You can login with default user 'guest' and password 'guest' when prompted.\nThis user will be disabled when you create a user account.\n"):"production"===process.env.NODE_ENV?console.warn("No users have been created and you are running in production mode so you will not be able to login.\n"):console.warn("It seems there are no users and you are not running in production mode so you will not be able to login. This is probably a bug. Please report it!\n")}))}var t,r,n,i,o,c,p,f;return u()(e,[{key:"getUsers",value:(f=s()(h().mark((function e(){var t,r=this;return h().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(!this.userFileCache){e.next=2;break}return e.abrupt("return",this.userFileCache);case 2:if(e.prev=2,this.userFile){e.next=7;break}throw(t=new Error("No user file specified")).code="ENOENT",t;case 7:return this.userFileCache=JSON.parse(_().readFileSync(this.userFile).toString("base64")),this.userFileCreated=!0,setTimeout((function(){r.userFileCache=null}),5e3),e.abrupt("return",this.userFileCache);case 13:if(e.prev=13,e.t0=e.catch(2),"ENOENT"!==e.t0.code){e.next=17;break}return e.abrupt("return","production"===process.env.NODE_ENV?{}:{guest:{password:"c2NyeXB0AA8AAAAIAAAAAc8D4r96lep3aBQSBeAqf0a+9MX6KyB6zKTF9Nk3ruTPIXrzy8IM7vjSLpIKuVZMNTZZ72CMqKp/PQmnyXmf7wGup1bWBGSwoV5ymA72ZzZg"}});case 17:throw e.t0;case 18:case"end":return e.stop()}}),e,this,[[2,13]])}))),function(){return f.apply(this,arguments)})},{key:"_writeFile",value:function(e){this.userFileCache=null,_().writeFileSync(this.userFile,JSON.stringify(e,null," ")),this.userFileCreated=!0}},{key:"_hashPassword",value:(p=s()(h().mark((function e(t){return h().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,d().kdf(t,{logN:15,r:8,p:1});case 2:return e.abrupt("return",e.sent.toString("base64"));case 3:case"end":return e.stop()}}),e)}))),function(e){return p.apply(this,arguments)})},{key:"_verifyPassword",value:(c=s()(h().mark((function e(t,r){return h().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",d().verify(Buffer.from(t,"base64"),r));case 1:case"end":return e.stop()}}),e)}))),function(e,t){return c.apply(this,arguments)})},{key:"createUser",value:(o=s()(h().mark((function e(t,r){var n;return h().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(this.userFile){e.next=2;break}throw new Error("No user file found. Did you forget to set the 'dataDir' option?");case 2:return e.next=4,this.getUsers();case 4:if(!(n=e.sent)[t]){e.next=7;break}throw new Error("User '".concat(t,"' already exists"));case 7:return this.userFileCreated||delete n.guest,e.next=10,this._hashPassword(r);case 10:e.t0=e.sent,n[t]={password:e.t0},this._writeFile(n);case 13:case"end":return e.stop()}}),e,this)}))),function(e,t){return o.apply(this,arguments)})},{key:"deleteUser",value:(i=s()(h().mark((function e(t){var r;return h().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(this.userFile){e.next=2;break}throw new Error("No user file found. Did you forget to set the 'dataDir' option?");case 2:return e.next=4,this.getUsers();case 4:if((r=e.sent)[t]){e.next=7;break}throw new Error("User '".concat(t,"' does not exist"));case 7:if(this.userFileCreated){e.next=9;break}throw new Error("User file has not been created");case 9:delete r[t],this._writeFile(r);case 11:case"end":return e.stop()}}),e,this)}))),function(e){return i.apply(this,arguments)})},{key:"setPassword",value:(n=s()(h().mark((function e(t,r){var n;return h().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(this.userFile){e.next=2;break}throw new Error("No user file found. Did you forget to set the 'dataDir' option?");case 2:return e.next=4,this.getUsers();case 4:return n=e.sent,e.next=7,this._hashPassword(r);case 7:n[t].password=e.sent,this._writeFile(n);case 9:case"end":return e.stop()}}),e,this)}))),function(e,t){return n.apply(this,arguments)})},{key:"getUserData",value:(r=s()(h().mark((function e(t){var r;return h().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,this.getUsers();case 2:if((r=e.sent)[t]){e.next=5;break}throw new Error("User '".concat(t,"' does not exist"));case 5:return e.abrupt("return",r[t]);case 6:case"end":return e.stop()}}),e,this)}))),function(e){return r.apply(this,arguments)})},{key:"verifyUser",value:(t=s()(h().mark((function e(t,r){var n;return h().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,this.getUserData(t);case 2:return n=e.sent,e.abrupt("return",this._verifyPassword(n.password,r));case 4:case"end":return e.stop()}}),e,this)}))),function(e,r){return t.apply(this,arguments)})}]),e}()},"./src/server/utils.js":(e,t,r)=>{r.r(t),r.d(t,{default:()=>a});var n=r("./src/lib/common-utils.js"),s=r("source-map-support"),i=r.n(s);const a=Object.assign({parseCommand:function(e){var t,r=/"(.*?)"|'(.*?)'|`(.*?)`|([^\s"]+)/gi,n=[];do{null!==(t=r.exec(e))&&n.push(t[1]||t[2]||t[3]||t[4])}while(null!==t);return n},getStack:function(){var e=Error.prepareStackTrace,t=Error.stackTraceLimit;Error.prepareStackTrace=function(e,t){return t.map(i().wrapCallSite)},Error.stackTraceLimit=30;var r=(new Error).stack;return Error.prepareStackTrace=e,Error.stackTraceLimit=t,r.slice(1)},getPromiseObj:function(){var e={};return e.promise=new Promise((function(t,r){Object.assign(e,{resolve:t,reject:r})})),e}},n.default)},"@babel/runtime/helpers/asyncToGenerator":e=>{e.exports=require("@babel/runtime/helpers/asyncToGenerator")},"@babel/runtime/helpers/classCallCheck":e=>{e.exports=require("@babel/runtime/helpers/classCallCheck")},"@babel/runtime/helpers/createClass":e=>{e.exports=require("@babel/runtime/helpers/createClass")},"@babel/runtime/helpers/defineProperty":e=>{e.exports=require("@babel/runtime/helpers/defineProperty")},"@babel/runtime/helpers/typeof":e=>{e.exports=require("@babel/runtime/helpers/typeof")},"@babel/runtime/regenerator":e=>{e.exports=require("@babel/runtime/regenerator")},events:e=>{e.exports=require("events")},keypair:e=>{e.exports=require("keypair")},lodash:e=>{e.exports=require("lodash")},minimist:e=>{e.exports=require("minimist")},"node-pty":e=>{e.exports=require("node-pty")},"scrypt-kdf":e=>{e.exports=require("scrypt-kdf")},"socket.io":e=>{e.exports=require("socket.io")},"source-map-support":e=>{e.exports=require("source-map-support")},ssh2:e=>{e.exports=require("ssh2")},"terminal-kit":e=>{e.exports=require("terminal-kit")},fs:e=>{e.exports=require("fs")},"fs/promises":e=>{e.exports=require("fs/promises")},http:e=>{e.exports=require("http")},os:e=>{e.exports=require("os")},path:e=>{e.exports=require("path")},tty:e=>{e.exports=require("tty")}},__webpack_module_cache__={};function __webpack_require__(e){var t=__webpack_module_cache__[e];if(void 0!==t)return t.exports;var r=__webpack_module_cache__[e]={exports:{}};return __webpack_modules__[e](r,r.exports,__webpack_require__),r.exports}__webpack_require__.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return __webpack_require__.d(t,{a:t}),t},__webpack_require__.d=(e,t)=>{for(var r in t)__webpack_require__.o(t,r)&&!__webpack_require__.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},__webpack_require__.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),__webpack_require__.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var __webpack_exports__={};return(()=>{__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,{NodeMonkey:()=>L,default:()=>R});var e=__webpack_require__("@babel/runtime/helpers/asyncToGenerator"),t=__webpack_require__.n(e),r=__webpack_require__("@babel/runtime/helpers/classCallCheck"),n=__webpack_require__.n(r),s=__webpack_require__("@babel/runtime/helpers/createClass"),i=__webpack_require__.n(s),a=__webpack_require__("@babel/runtime/helpers/defineProperty"),o=__webpack_require__.n(a),u=__webpack_require__("@babel/runtime/regenerator"),c=__webpack_require__.n(u),l=__webpack_require__("os"),p=__webpack_require__.n(l),h=__webpack_require__("fs"),f=__webpack_require__.n(h),_=__webpack_require__("path"),m=__webpack_require__.n(_),d=__webpack_require__("events"),v=__webpack_require__("lodash"),y=__webpack_require__.n(v),b=__webpack_require__("keypair"),w=__webpack_require__.n(b),g=__webpack_require__("./src/lib/cycle.js"),k=__webpack_require__("./src/server/bunyan-stream.js"),x=__webpack_require__("./src/server/setup-server.js"),S=__webpack_require__("./src/server/setup-socket.js"),C=__webpack_require__("./src/server/ssh-manager.js"),j=__webpack_require__("./src/server/command-manager.js"),M=__webpack_require__("./src/server/user-manager.js"),q=__webpack_require__("./src/server/utils.js");function O(e,t){var r="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(!r){if(Array.isArray(e)||(r=function(e,t){if(!e)return;if("string"==typeof e)return E(e,t);var r=Object.prototype.toString.call(e).slice(8,-1);"Object"===r&&e.constructor&&(r=e.constructor.name);if("Map"===r||"Set"===r)return Array.from(e);if("Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return E(e,t)}(e))||t&&e&&"number"==typeof e.length){r&&(e=r);var n=0,s=function(){};return{s,n:function(){return n>=e.length?{done:!0}:{done:!1,value:e[n++]}},e:function(e){throw e},f:s}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var i,a=!0,o=!1;return{s:function(){r=r.call(e)},n:function(){var e=r.next();return a=e.done,e},e:function(e){o=!0,i=e},f:function(){try{a||null==r.return||r.return()}finally{if(o)throw i}}}}function E(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);rthis.options.server.bufferSize&&this.msgBuffer.shift(),this._sendMessages()}},{key:"_sendMessages",value:function(){var e=this.remoteClients;y().size(e.adapter.rooms.get("authed"))&&(y().each(this.msgBuffer,(function(t){e.to("authed").emit("console",g.default.decycle(t))})),this.msgBuffer=[])}},{key:"_createLocal",value:function(){this.local=P}},{key:"_createRemote",value:function(){var e=this,t=this.remote={};I.forEach((function(r){e.remote[r]=function(){for(var t=arguments.length,n=new Array(t),s=0;s1&&void 0!==arguments[1]?arguments[1]:"default";if("string"==typeof e&&(t=e,e=void 0),!D[t]){e||(e={});var r=y().get(e,"server.port");r?U=+r:(y().set(e,"server.port",++U),y().set(e,"ssh.port",++U)),D[t]=new L(e)}return D[t]}})(),__webpack_exports__=__webpack_exports__.default,__webpack_exports__})())); 3 | //# sourceMappingURL=server.js.map -------------------------------------------------------------------------------- /dist/server.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! ../lib/common-utils */ 2 | 3 | /*! ../lib/cycle */ 4 | 5 | /*! ./bunyan-stream */ 6 | 7 | /*! ./command-interface */ 8 | 9 | /*! ./command-manager */ 10 | 11 | /*! ./setup-server */ 12 | 13 | /*! ./setup-socket */ 14 | 15 | /*! ./ssh-manager */ 16 | 17 | /*! ./user-manager */ 18 | 19 | /*! ./utils */ 20 | 21 | /*! @babel/runtime/helpers/asyncToGenerator */ 22 | 23 | /*! @babel/runtime/helpers/classCallCheck */ 24 | 25 | /*! @babel/runtime/helpers/createClass */ 26 | 27 | /*! @babel/runtime/helpers/defineProperty */ 28 | 29 | /*! @babel/runtime/helpers/typeof */ 30 | 31 | /*! @babel/runtime/regenerator */ 32 | 33 | /*! events */ 34 | 35 | /*! fs */ 36 | 37 | /*! fs/promises */ 38 | 39 | /*! http */ 40 | 41 | /*! keypair */ 42 | 43 | /*! lodash */ 44 | 45 | /*! minimist */ 46 | 47 | /*! node-pty */ 48 | 49 | /*! os */ 50 | 51 | /*! path */ 52 | 53 | /*! scrypt-kdf */ 54 | 55 | /*! socket.io */ 56 | 57 | /*! source-map-support */ 58 | 59 | /*! ssh2 */ 60 | 61 | /*! terminal-kit */ 62 | 63 | /*! tty */ 64 | 65 | /*!*********************!*\ 66 | !*** external "fs" ***! 67 | \*********************/ 68 | 69 | /*!*********************!*\ 70 | !*** external "os" ***! 71 | \*********************/ 72 | 73 | /*!**********************!*\ 74 | !*** external "tty" ***! 75 | \**********************/ 76 | 77 | /*!***********************!*\ 78 | !*** external "http" ***! 79 | \***********************/ 80 | 81 | /*!***********************!*\ 82 | !*** external "path" ***! 83 | \***********************/ 84 | 85 | /*!***********************!*\ 86 | !*** external "ssh2" ***! 87 | \***********************/ 88 | 89 | /*!*************************!*\ 90 | !*** external "events" ***! 91 | \*************************/ 92 | 93 | /*!*************************!*\ 94 | !*** external "lodash" ***! 95 | \*************************/ 96 | 97 | /*!**************************!*\ 98 | !*** ./src/lib/cycle.js ***! 99 | \**************************/ 100 | 101 | /*!**************************!*\ 102 | !*** external "keypair" ***! 103 | \**************************/ 104 | 105 | /*!***************************!*\ 106 | !*** external "minimist" ***! 107 | \***************************/ 108 | 109 | /*!***************************!*\ 110 | !*** external "node-pty" ***! 111 | \***************************/ 112 | 113 | /*!****************************!*\ 114 | !*** external "socket.io" ***! 115 | \****************************/ 116 | 117 | /*!*****************************!*\ 118 | !*** ./src/server/index.js ***! 119 | \*****************************/ 120 | 121 | /*!*****************************!*\ 122 | !*** ./src/server/utils.js ***! 123 | \*****************************/ 124 | 125 | /*!*****************************!*\ 126 | !*** external "scrypt-kdf" ***! 127 | \*****************************/ 128 | 129 | /*!******************************!*\ 130 | !*** external "fs/promises" ***! 131 | \******************************/ 132 | 133 | /*!*******************************!*\ 134 | !*** external "terminal-kit" ***! 135 | \*******************************/ 136 | 137 | /*!*********************************!*\ 138 | !*** ./src/lib/common-utils.js ***! 139 | \*********************************/ 140 | 141 | /*!***********************************!*\ 142 | !*** ./src/server/ssh-manager.js ***! 143 | \***********************************/ 144 | 145 | /*!************************************!*\ 146 | !*** ./src/server/setup-server.js ***! 147 | \************************************/ 148 | 149 | /*!************************************!*\ 150 | !*** ./src/server/setup-socket.js ***! 151 | \************************************/ 152 | 153 | /*!************************************!*\ 154 | !*** ./src/server/user-manager.js ***! 155 | \************************************/ 156 | 157 | /*!*************************************!*\ 158 | !*** ./src/server/bunyan-stream.js ***! 159 | \*************************************/ 160 | 161 | /*!*************************************!*\ 162 | !*** external "source-map-support" ***! 163 | \*************************************/ 164 | 165 | /*!***************************************!*\ 166 | !*** ./src/server/command-manager.js ***! 167 | \***************************************/ 168 | 169 | /*!*****************************************!*\ 170 | !*** ./src/server/command-interface.js ***! 171 | \*****************************************/ 172 | 173 | /*!*********************************************!*\ 174 | !*** external "@babel/runtime/regenerator" ***! 175 | \*********************************************/ 176 | 177 | /*!************************************************!*\ 178 | !*** external "@babel/runtime/helpers/typeof" ***! 179 | \************************************************/ 180 | 181 | /*!*****************************************************!*\ 182 | !*** external "@babel/runtime/helpers/createClass" ***! 183 | \*****************************************************/ 184 | 185 | /*!********************************************************!*\ 186 | !*** external "@babel/runtime/helpers/classCallCheck" ***! 187 | \********************************************************/ 188 | 189 | /*!********************************************************!*\ 190 | !*** external "@babel/runtime/helpers/defineProperty" ***! 191 | \********************************************************/ 192 | 193 | /*!**********************************************************!*\ 194 | !*** external "@babel/runtime/helpers/asyncToGenerator" ***! 195 | \**********************************************************/ 196 | -------------------------------------------------------------------------------- /do-release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # function echo_run { 4 | # echo "> $1" 5 | # eval $1 6 | # } 7 | 8 | echo "Enter the release type [ | major | minor | patch | premajor | preminor | prepatch | prerelease | from-git]" 9 | read -p '> ' RELTYPE 10 | 11 | # `set -x` will echo commands before running them 12 | set -x 13 | 14 | npm version "$RELTYPE" 15 | git push --follow-tags 16 | RELVER="$(node -pe "require('./package.json').version")" 17 | npm publish "https://github.com/jwarkentin/node-monkey/archive/refs/tags/v$RELVER.tar.gz" 18 | -------------------------------------------------------------------------------- /doc/development/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Node Monkey is built using webpack and babel. To setup your environment for development, follow these instructions. 4 | 5 | Run the following commands to setup the project: 6 | ``` 7 | git clone git@github.com:jwarkentin/node-monkey.git 8 | cd node-monkey 9 | npm install 10 | ``` 11 | 12 | Then while you're developing you'll want to run this in the root of the `node-monkey` project directory: 13 | ``` 14 | npm run webpack 15 | ``` 16 | 17 | ### Standards 18 | 19 | Please follow the code and formatting style of the project. -------------------------------------------------------------------------------- /doc/development/release.md: -------------------------------------------------------------------------------- 1 | # How to cut a release 2 | 3 | > **IMPORTANT: Make sure documentation and changelog are up-to-date** 4 | 5 | 1. Do a production build: 6 | 7 | ```sh 8 | NODE_ENV=production npm run build 9 | ``` 10 | 11 | 2. Draft the new release on Github: 12 | 13 | 3. Publish to npm (defaults to `latest` tag): 14 | 15 | Just use the `do-release` script in the project root: 16 | 17 | ```sh 18 | $ ./do-release 19 | Enter the release type [ | major | minor | patch | premajor | preminor | prepatch | prerelease | from-git] 20 | > patch 21 | ... 22 | ``` 23 | 24 | These are the manual steps: 25 | 26 | ```sh 27 | npm version [major|minor|patch|] 28 | git push --follow-tags 29 | npm publish https://github.com/jwarkentin/node-monkey/archive/refs/tags/v.tar.gz 30 | ``` 31 | 32 | > Note: See [here](https://jbavari.github.io/blog/2015/10/16/using-npm-tags/) for more details on tagging releases. 33 | > 34 | > Previousely the `next` tag was being used for pre-releases but currently there isn't enough demand for the added process complexity. If the community grows and needs better production guarantees and support it could be reinroduced. 35 | -------------------------------------------------------------------------------- /doc/usage/client.md: -------------------------------------------------------------------------------- 1 | # Client 2 | 3 | The Node Monkey client is exposed in the global scope and is available in your console as `monkey`. 4 | 5 | ## Properties 6 | 7 | ### monkey#client 8 | 9 | This generally shouldn't need to be touched but it gives you access to the socket.io client socket. See [here](server.md#nodemonkeyremoteclients) for more on why and how you might use this. 10 | 11 | 12 | ## Methods 13 | 14 | ### monkey#init() 15 | 16 | When you want to connect the client to the server, which you will probably want to do in production environments for debugging, you must call `monkey.init()` to kick things off. When you do, you will be prompted to enter a username and password to authenticate and any authentication errors will display in the console. 17 | 18 | You may disconnect at any time after which point calling `monkey.init()` will just invoke `monkey.connect()` so they are equivalent. You will be prompted for authentication again. 19 | 20 | 21 | ### monkey#connect() 22 | 23 | Re-establish a connection to the server after you've called `monkey.disconnect()`. Calling `monkey.init()` a second time will also call this. 24 | 25 | 26 | ### monkey#disconnect() 27 | 28 | Disconnect from the server. 29 | 30 | 31 | ### monkey#cmd(\command[, noOutput]) 32 | 33 | * `command`: The full command string to execute (e.g. `adduser bob -p password123`) 34 | * `noOutput`: By default the result of the command you run will be logged to the console. If you set this to `true` it will not log the result. 35 | 36 | **Return** 37 | 38 | A promise that will resolve with successful output or reject with the error output. 39 | 40 | **Example** 41 | 42 | ```js 43 | monkey.cmd('adduser bob') 44 | 45 | monkey.cmd('adduser bob').then(function(output) { 46 | /* do something */ 47 | }).catch(function(error) { 48 | /* do something else */ 49 | }) 50 | ``` -------------------------------------------------------------------------------- /doc/usage/server.md: -------------------------------------------------------------------------------- 1 | # Server 2 | 3 | ## Provide your own 4 | 5 | Node Monkey allows you to provide your own server if you want and it will just attach to it. However, if you do so you have to setup a route to serve the Node Monkey client file at a bare minimum. Examples for how to do this below. 6 | 7 | Note that with both of these examples it doesn't matter when you call `listen()`. If your server is already listening Node Monkey will still print out a message with the URL to access it unless you set `server.silent` to `true` when instantiating Node Monkey. 8 | 9 | **Example with restify** 10 | 11 | ```js 12 | const restify = require('restify') 13 | const app = restify.createServer() 14 | const monkey = require('node-monkey')({ 15 | server: { 16 | server: app.server 17 | } 18 | }) 19 | 20 | const monkeyFiles = monkey.getServerPaths() 21 | app.get(/\/monkey\.js$/, restify.serveStatic({ 22 | directory: monkeyFiles.basePath, 23 | file: monkeyFiles.client 24 | })) 25 | 26 | app.get(/\/monkey\/?$/, restify.serveStatic({ 27 | directory: monkeyFiles.basePath, 28 | file: monkeyFiles.index 29 | })) 30 | 31 | app.listen(80, '0.0.0.0') 32 | ``` 33 | 34 | **Example with express** 35 | 36 | ```js 37 | const app = require('express')() 38 | const server = require('http').Server(app) 39 | const monkey = require('node-monkey')({ 40 | server: { 41 | server: server 42 | } 43 | }) 44 | 45 | const monkeyFiles = monkey.getServerPaths() 46 | app.get('/monkey.js', function(req, res, next) { 47 | res.sendFile(`${monkeyFiles.basePath}/${monkeyFiles.client}`) 48 | }) 49 | 50 | app.get('/monkey', function(req, res, next) { 51 | res.sendFile(`${monkeyFiles.basePath}/${monkeyFiles.index}`) 52 | }) 53 | 54 | server.listen(80, '0.0.0.0') 55 | ``` 56 | 57 | ## Options 58 | 59 | The `options` object you can provide to Node Monkey is nested and hopefully somewhat intuitive. 60 | 61 | * `server` 62 | * `server`: An [http](https://nodejs.org/api/http.html) (or https) server instance for Node Monkey to attach to instead of creating its own. When this is passed the `host` and `port` options aren't used. 63 | * `host`: Host interface to bind to. The default `0.0.0.0` listens on all host IPs. 64 | * `port`: The port to listen on for Node Monkey client connections. 65 | * `silent`: When `true`, the start-up messages Node Monkey normally prints will be silenced. 66 | * `bufferSize`: When there are no clients connected to receive messages Node Monkey buffers them until at least one client connects to receive them. This is the number of messages it will buffer before the oldest ones start to drop off. 67 | * `attachOnStart`: Whether Node Monkey should automatically call `attachConsole()` for you when it initializes. 68 | * `disableLocalOutput`: When `true` and `attachConsole()` has been called all local console output will be silenced and logged output will only be visible in the browser console. Can be overridden on each individual call to `attachConsole()` (see [docs](nodemonkeyattachconsole)). 69 | 70 | * `client` 71 | * `showCallerInfo`: When `true` all browser console output will show at least the file and line number where the call was made that logged the output. There is a generally negligible performance penalty (microseconds) for showing call traces. The only time it would matter is during rapid, continuous execution of a function that triggers a call trace. 72 | * `convertStyles`: Sometimes terminal output contains special codes that create colored output in the terminal. When true, this attempts to convert the terminal output styles to the equivalent browser console styles. 73 | 74 | * `ssh` 75 | * `enabled`: When `true` the SSH app interface is enabled. This allows you to actually SSH in to your application to run custom commands for whatever purposes you may have. The `dataDir` option is required to enable this. 76 | * `host`: The SSH host interface to listen on (see `server.host` above). 77 | * `port`: The port to listen on for SSH connections. 78 | * `title`: You can customize what the terminal window title says by setting this option. It allows variable interpolation with `{@myvar}` syntax. For now there is only one possible variable: `username`. 79 | * `prompt`: You can customize what the terminal prompt says by setting this option. It allows variable interpolation with `{@myvar}` syntax. For now there is only one possible variable: `username`. 80 | 81 | * `dataDir`: To enable user accounts and SSH functionality Node Monkey needs a directory to store a few files in. Without this there is a default `guest` user with password `guest` but SSH cannot be enabled. Generally you will want to commit this directory with the code base of your project that uses Node Monkey. 82 | 83 | **Defaults** 84 | 85 | ```js 86 | { 87 | server: { 88 | // You can provide your own server and Node Monkey will use it instead of creating its own. 89 | // However, this MUST be the underlying http server instance, not the express/restify/whatever app. 90 | server: null, 91 | 92 | host: '0.0.0.0', 93 | port: 50500, 94 | silent: false, 95 | bufferSize: 50, 96 | attachOnStart: true, 97 | 98 | // Only takes effect when Node Monkey is attached to the console 99 | disableLocalOutput: false 100 | }, 101 | client: { 102 | showCallerInfo: NODE_ENV === 'production' ? false : true, 103 | convertStyles: true 104 | }, 105 | ssh: { 106 | enabled: false, 107 | host: '0.0.0.0', 108 | port: 50501, 109 | title: `Node Monkey on ${os.hostname()}`, 110 | prompt: `[Node Monkey] {@username}@${os.hostname()}:` 111 | }, 112 | 113 | // Needed for storing things like user files and SSH host keys 114 | dataDir: null 115 | } 116 | ``` 117 | 118 | If you provide your own server you can view output in the console of your own web application instead. To see how to provide your own server check out the [documentation](doc/server.md#provide-your-own). You will need to include the following ` 122 | ``` 123 | 124 | ## Properties 125 | 126 | ### NodeMonkey#BUNYAN_STREAM 127 | 128 | If you use the awesome [Bunyan](https://github.com/trentm/node-bunyan) library for logging, you can add Node Monkey as a Bunyan log stream. It is highly recommended that you don't pass `src: true` when creating the logger since there is a performance penalty and it will be ignored since Node Monkey already performs its own optional call traces. 129 | 130 | **Example** 131 | 132 | ```js 133 | const monkey = require('node-monkey')() 134 | const bunyan = require('bunyan') 135 | 136 | const logger = bunyan.createLogger({ 137 | name: 'app', 138 | streams: [ 139 | { 140 | level: 'info', 141 | stream: monkey.BUNYAN_STREAM 142 | } 143 | ] 144 | }) 145 | 146 | logger.info('something happened!') 147 | logger.error('something bad happened!') 148 | ``` 149 | 150 | ### NodeMonkey#server 151 | 152 | You generally shouldn't need to use this property but it gives you access to whatever the web server is that's being used to serve Node Monkey clients. 153 | 154 | ### NodeMonkey#remoteClients 155 | 156 | You generally shouldn't need to use this but it gives you access to the underlying namespaced socket.io instance managing connected sockets. So, if you wanted you could send a message to all authorized clients like so: 157 | 158 | ```js 159 | monkey.remoteClients.to('authed').emit('mychannel', something, somethingElse, etc) 160 | ``` 161 | 162 | And then to receive that on the client side you'd do: 163 | 164 | ```js 165 | monkey.client.on('mychannel', function(arg1, arg2, arg3) { 166 | console.log(arguments) 167 | }) 168 | ``` 169 | 170 | ## Methods 171 | 172 | ### NodeMonkey#constructor([\options[, \name]]) 173 | 174 | Instantiates an instance of Node Monkey. 175 | 176 | By default, this will automatically attach to the `console` object and send log messages to both the terminal and your browser console. This behavior can be customized by the [options](#options) you can pass in. 177 | 178 | The constructed instance contains two useful properties named `local` and `remote`. If you want to only log something to either the local terminal or only to the remote browser console and not both, despite Node Monkey being attached to the `console` object, you can do so using these objects as demonstrated below. 179 | 180 | **Example** 181 | 182 | ``` 183 | const NodeMonkey = require('node-monkey') 184 | const monkey = NodeMonkey() 185 | 186 | // With the default options this will show in the browser console and in your terminal 187 | console.log('Hello world!') 188 | 189 | // This will only appear in your terminal 190 | monkey.local.log('Local!') 191 | 192 | // This will only appear in your attached browser console 193 | monkey.remote.log('Remote!') 194 | ``` 195 | 196 | #### Named Instances 197 | 198 | You can include Node Monkey in all the files within your app that you want and if used like the examples above, each call to `NodeMonkey()` will always return the same instance you first constructed, ignoring any options passed on subsequent calls. However, you may want to construct new instances with different options. To do so, give your instance a name: 199 | 200 | ```js 201 | const NodeMonkey = require('node-monkey') 202 | const monkey1 = NodeMonkey() // Creates an instance named 'default' 203 | const monkey2 = NodeMonkey('george') // Creates a new instance with default options 204 | const monkey3 = NodeMonkey({ // Creates a new instance with custom options named 'ninja' 205 | server: { 206 | silent: true 207 | } 208 | }, 'ninja') 209 | ``` 210 | 211 | If you don't specify a port for additional instances it will automatically be set for you and will just increment from the default (e.g. 50502, 50504 for the websocket server and 50503, 50505 for the SSH server). 212 | 213 | To get an already constructed instance in another file just call it with the name again: 214 | 215 | ```js 216 | const NodeMonkey = require('node-monkey') 217 | const monkey3 = NodeMonkey('ninja') 218 | ``` 219 | 220 | ### NodeMonkey#getServerPaths() 221 | 222 | Use this when instantiating Node Monkey with your own web server. At a minimum the `monkey.js` client file must be loaded to connect to Node Monkey. This function returns an object with the `basePath` directory which contains all Node Monkey static files as well as the names of the `client` and `index` files served out-of-the-box by Node Monkey when it provides the server. 223 | 224 | **Return** 225 | 226 | **Example response object** 227 | 228 | ```js 229 | { 230 | basePath: '/srv/myapp/node_modules/node-monkey/dist', 231 | client: 'monkey.js', 232 | index: 'index.html' 233 | } 234 | ``` 235 | 236 | ### NodeMonkey#attachConsole([\disableLocal]) 237 | 238 | When called this attaches to the built-in `console` object so all calls are handled by Node Monkey. If set, the optional `disableLocal` flag will stop local console output so all output is only visible in your attached browser console. You can also set `server.disableLocal` in the options passed when instantiating Node Monkey instead. 239 | 240 | ### NodeMonkey#detachConsole() 241 | 242 | Detaches Node Monkey from the built-in `console` object. 243 | 244 | ### NodeMonkey#addCmd(\cmdName[, \opts], \exec) 245 | 246 | Adds a custom command that can be called remotely from the browser or SSH command line if it's enabled. When the command is called from either interface your `exec` callback will be called with four arguments, as follows: 247 | 248 | _exec(opts, term, callback)_: 249 | 250 | * `opts`: An object containing two properties currently available to commands to use as needed 251 | * `args`: Any parsed arguments given on the command line. They are parsed using [minimist](https://github.com/substack/minimist) so see the documentation there for further details. 252 | * `username`: The username of the user that is executing the command 253 | * `term`: An object containing a few functions for working with input and output 254 | * `write`: Writes whatever is given to the terminal or web console without a newline following. Not that when your command finishes a newline will be inserted automatically before the prompt is displayed so your final output does not need to have a newline. 255 | * _write(\output, \options)_ 256 | `options` accepts `newline` and `bold` 257 | * `writeLn`: Calls `write()` but automatically adds a newline. 258 | * _writeLn(\output, \options)_ 259 | `options` accepts `bold` 260 | * `error`: Writes red error text to the console. 261 | * _error(\output, \options)_ 262 | `options` accepts `newline` 263 | * `prompt`: Prompt the user for input 264 | * _prompt(\promptTxt[, \options], \callback)_ 265 | `prompTxt` is self explanatory 266 | `options` currently accepts the option `hideInput` which will hide keyboard input from being displayed which is useful when prompting for passwords. 267 | `callback` should accept two arguments: `error` and `input`. 268 | * `done`: You must call this when your command function has finished executing. 269 | 270 | ### NodeMonkey#runCmd(\rawCommand, \asUser) 271 | 272 | * `rawCommand`: The full command you want to be parsed and executed (e.g. `addUser bob -p password`) 273 | * `asUser`: The user to run the command as 274 | 275 | **Return** 276 | 277 | A promise that will resolve with successful command output or reject with an error. 278 | -------------------------------------------------------------------------------- /doc/usage/ssh.md: -------------------------------------------------------------------------------- 1 | # SSH 2 | 3 | ## Setup 4 | 5 | For the SSH interface to be activated it needs to generate host keys. The first time you run Node Monkey with SSH enabled it will automatically generate the host keys. This means you need to specify a data directory for it to save the keys in. If you are running it in production you will want to commit your data directory with your code. 6 | 7 | **Enabling SSH** 8 | 9 | ```js 10 | import NodeMonkey from "node-monkey" 11 | 12 | const monkey = NodeMonkey({ 13 | dataDir: `${__dirname}/monkey-data`, 14 | ssh: { 15 | enabled: true 16 | } 17 | }) 18 | ``` 19 | 20 | ## Usage 21 | 22 | **SSH connection example** 23 | 24 | If you were running your server on `localhost` on the default port and without any accounts created, you would run this command: 25 | 26 | ``` 27 | ssh guest@localhost -p 50501 28 | ``` 29 | 30 | You would then be prompted for a password. The default password for the guest account is 'guest'. Once authenticated, you will be presented with a command prompt provided by your application with any custom commands you've registered that looks like this (by default): 31 | 32 | ``` 33 | [Node Monkey] guest@YourHostName: 34 | ``` 35 | 36 | Pressing `TAB` at an empty prompt will show you all available commands and allow you to select one using `TAB` or the left and right arrow keys. 37 | 38 | #### Features currently supported 39 | 40 | * Tab autocomplete for commands 41 | * Command history (with up/down arrow keys) 42 | * Space at start of command prevents it from being recorded in command history 43 | * Holding CTRL while pressing the right and left arrow keys jumps the cursor by whole words 44 | * CTRL+D exits 45 | * CTRL+L clears the screen 46 | * CTRL+K clears from the cursor to the end of the line 47 | * CTRL+U clears from the cursor to the beginning of the line 48 | * CTRL+W clears the previous word 49 | * ALT+D clears the next word 50 | 51 | #### Built-in commands 52 | 53 | * clear 54 | * exit 55 | * adduser 56 | * deluser 57 | * showusers 58 | * passwd 59 | 60 | #### Notable missing features 61 | 62 | * CTRL+C to abort running commands 63 | -------------------------------------------------------------------------------- /doc/usage/user-management.md: -------------------------------------------------------------------------------- 1 | # User Management 2 | 3 | Node Monkey requies authentication to connect via the web client or an SSH client. When you first start it you will authenticate with username `guest` and password `guest`. To create a user you must specify a `dataDir` as described in the [options](server.md#options). 4 | 5 | Passwords are stored with strong scrypt hashes so you don't have to worry about someone gaining access to the users file and obtaining the hashes. 6 | 7 | If you have to manually edit or replace the `users.json` file that stores the accounts, the changes you make will automatically take effect within a few seconds at most. 8 | 9 | ## Creating users 10 | 11 | ``` 12 | adduser [username] 13 | ``` 14 | 15 | You will then be prompted twice for a password and it will create the user if they match. 16 | 17 | 18 | ## Changing passwords 19 | 20 | You can only change your own password. 21 | 22 | ``` 23 | passwd 24 | ``` 25 | 26 | You will be prompted to confirm your current password and then to enter your new password twice to confirm. 27 | 28 | 29 | ## Deleting users 30 | 31 | ``` 32 | deluser [username] 33 | ``` 34 | 35 | Currently anyone can delete a user. This will change when permission management is added. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-monkey", 3 | "version": "1.1.5", 4 | "description": "A tool for inspecting, debugging and commanding Node applications through a web browser or SSH interface into your app", 5 | "keywords": [ 6 | "inspect", 7 | "debug", 8 | "debugging", 9 | "console", 10 | "log", 11 | "profile", 12 | "profiler" 13 | ], 14 | "main": "./dist/server.js", 15 | "scripts": { 16 | "build": "webpack", 17 | "build:dev": "webpack --watch", 18 | "start": "babel-node --es-module-specifier-resolution=node src/server/index.js", 19 | "lint": "eslint src/**/*.js", 20 | "lint:fix": "npm run lint -- --fix" 21 | }, 22 | "author": "Justin Warkentin ", 23 | "license": "MIT", 24 | "homepage": "https://github.com/jwarkentin/node-monkey", 25 | "bugs": "https://github.com/jwarkentin/node-monkey/issues", 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/jwarkentin/node-monkey.git" 29 | }, 30 | "dependencies": { 31 | "@babel/runtime": "^7.17.9", 32 | "keypair": "^1.0.4", 33 | "lodash": "^4.17.21", 34 | "minimist": "^1.2.6", 35 | "node-pty": "^0.10.1", 36 | "scrypt-kdf": "^2.0.1", 37 | "socket.io": "^4.4.1", 38 | "source-map-support": "^0.5.21", 39 | "ssh2": "^1.9.0", 40 | "terminal-kit": "^2.4.0" 41 | }, 42 | "devDependencies": { 43 | "@babel/core": "^7.17.9", 44 | "@babel/node": "^7.16.8", 45 | "@babel/plugin-proposal-class-properties": "^7.16.7", 46 | "@babel/plugin-transform-runtime": "^7.17.0", 47 | "@babel/preset-env": "^7.16.11", 48 | "@babel/register": "^7.17.7", 49 | "babel-eslint": "^10.1.0", 50 | "babel-loader": "^8.2.4", 51 | "babel-plugin-source-map-support": "^2.1.1", 52 | "bunyan": "^1.8.15", 53 | "eslint": "^8.13.0", 54 | "eslint-config-prettier": "^8.5.0", 55 | "eslint-plugin-prettier": "^4.0.0", 56 | "html-webpack-plugin": "^5.5.0", 57 | "ignore-loader": "^0.1.2", 58 | "prettier": "^2.6.2", 59 | "terser-webpack-plugin": "^5.3.1", 60 | "webpack": "^5.72.0", 61 | "webpack-cli": "^4.9.2", 62 | "webpack-node-externals": "^3.0.0" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/client/convert-styles.js: -------------------------------------------------------------------------------- 1 | import utils from "./utils" 2 | 3 | let styleMap = { 4 | // Styles 5 | "\u001b[24m": "text-decoration: none", 6 | "\u001b[22m": "font-weight: normal", 7 | "\u001b[1m": "font-weight: bold", 8 | "\u001b[3m": "font-style: italic", 9 | "\u001b[4m": "text-decoration: underline", 10 | "\u001b[23m": "font-style: normal", 11 | 12 | // Colors 13 | "\u001b[39m": "color: ", 14 | "\u001b[37m": "color: white", 15 | "\u001b[90m": "color: grey", 16 | "\u001b[30m": "color: black", 17 | "\u001b[35m": "color: magenta", 18 | "\u001b[33m": "color: yellow", 19 | "\u001b[31m": "color: red", 20 | "\u001b[36m": "color: cyan", 21 | "\u001b[34m": "color: blue", 22 | "\u001b[32m": "color: green", 23 | }, 24 | // Styles for the caller data. 25 | traceStyle = "color: grey; font-family: Helvetica, Arial, sans-serif", 26 | // RegExp pattern for styles 27 | stylePattern = /(\u001b\[.*?m)+/g, 28 | // RegExp pattern for format specifiers (like '%o', '%s') 29 | formatPattern = /(?:^|[^%])%(s|d|i|o|f|c)/g 30 | 31 | function stylize(data, cdata) { 32 | if (!data.length) { 33 | data.push("") 34 | } 35 | 36 | // If `data` has multiple arguments, we are going to merge everything into 37 | // the first argument, so style-specifiers can be used throughout all arguments. 38 | 39 | let cap, 40 | mergeArgsStart = 1, 41 | formatSpecifiers = [] 42 | 43 | // If the first argument is an object, we need to replace it with `%o` 44 | // (always preemptively reset the color) 45 | if (utils.isObject(data[0])) { 46 | data.splice(1, 0, data[0]) 47 | data[0] = "%o" 48 | } 49 | 50 | // Count all format specifiers in the first argument to see from where we need to 51 | // start merging 52 | let txt = data[0] 53 | while ((cap = formatPattern.exec(txt))) { 54 | if (cap[1] == "o") { 55 | // Insert color resetter 56 | data[0] = data[0].replace(cap[0], cap[0].slice(0, cap[0].length - 2) + "\u001b[39m%o") 57 | } 58 | mergeArgsStart++ 59 | } 60 | 61 | // Start merging... 62 | if (data.length > mergeArgsStart) { 63 | for (let i = mergeArgsStart; i < data.length; i++) { 64 | let arg = data[i], 65 | specifier 66 | 67 | if (typeof arg == "string") { 68 | // Since this argument is a string and may be styled as well, put it right in... 69 | specifier = " " + arg 70 | // ...and remove the argument... 71 | data.splice(i, 1) 72 | // ...and adapt the iterator. 73 | i-- 74 | } else { 75 | // Otherwise use the '%o'-specifier (preemptively reset color) 76 | specifier = " \u001b[39m%o" 77 | } 78 | 79 | data[0] += specifier 80 | } 81 | } 82 | 83 | // Now let's collect all format specifiers and their positions as well, 84 | // so we know where to put our style-specifiers. 85 | while ((cap = formatPattern.exec(data[0]))) { 86 | formatSpecifiers.push(cap) 87 | } 88 | 89 | let added = 0 90 | txt = data[0] 91 | 92 | // Let's do some styling... 93 | while ((cap = stylePattern.exec(txt))) { 94 | let styles = [], 95 | capsplit = cap[0].split("m") 96 | 97 | // Get the needed styles 98 | for (let j = 0; j < capsplit.length; j++) { 99 | let s 100 | if ((s = styleMap[capsplit[j] + "m"])) styles.push(s) 101 | } 102 | 103 | // Check if the style must be added before other specifiers 104 | if (styles.length) { 105 | let k 106 | for (k = 0; k < formatSpecifiers.length; k++) { 107 | let sp = formatSpecifiers[k] 108 | if (cap["index"] < sp["index"]) { 109 | break 110 | } 111 | } 112 | 113 | // Add them at the right position 114 | let pos = k + 1 + added 115 | data.splice(pos, 0, styles.join(";")) 116 | added++ 117 | 118 | // Replace original with `%c`-specifier 119 | data[0] = data[0].replace(cap[0], "%c") 120 | } 121 | } 122 | // ...done! 123 | 124 | // At last, add caller data, if present. 125 | if (cdata) { 126 | data[0] += "%c" + cdata 127 | data.push(traceStyle) 128 | } 129 | 130 | return data 131 | } 132 | 133 | export default stylize 134 | -------------------------------------------------------------------------------- /src/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Open your console to see output 6 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/client/index.js: -------------------------------------------------------------------------------- 1 | import utils from "./utils" 2 | import cycle from "../lib/cycle" 3 | import convertStyles from "./convert-styles" 4 | 5 | let initialized = false 6 | const monkey = (window.monkey = { 7 | cmdId: 0, 8 | runningCmd: {}, 9 | connect: null, 10 | disconnect: null, 11 | 12 | init() { 13 | if (initialized) { 14 | monkey.connect() 15 | return 16 | } 17 | initialized = true 18 | 19 | return new Promise((resolve, reject) => { 20 | utils.addHeadScript(`${utils.getClientHost()}/monkey.io-client/socket.io.js`).addEventListener("load", () => { 21 | initClient() 22 | .then((client) => { 23 | let authAttempts = 0, 24 | creds = null, 25 | stylize = convertStyles, 26 | settings = { 27 | convertStyles: true, 28 | } 29 | 30 | monkey.client = client 31 | monkey.connect = client.connect.bind(client) 32 | monkey.disconnect = () => { 33 | authAttempts = 0 34 | creds = null 35 | client.disconnect.call(client) 36 | } 37 | 38 | let doAuth = () => { 39 | if (authAttempts > 2) { 40 | monkey.disconnect() 41 | return 42 | } 43 | 44 | let username, password 45 | if (!creds) { 46 | username = prompt("Node Monkey username") 47 | password = prompt("Node Monkey password") 48 | creds = { username, password } 49 | } 50 | 51 | ++authAttempts 52 | client.emit("auth", creds) 53 | } 54 | 55 | client.on("cmdResponse", (cmdId, error, output) => { 56 | if (monkey.runningCmd[cmdId]) { 57 | let { resolve, reject } = monkey.runningCmd[cmdId] 58 | delete monkey.runningCmd[cmdId] 59 | 60 | if (error) { 61 | reject(error) 62 | } else { 63 | resolve(output) 64 | } 65 | } 66 | }) 67 | 68 | client.on("settings", (data) => { 69 | Object.assign(settings, data) 70 | 71 | if (!settings.convertStyles) { 72 | stylize = function (args, trace) { 73 | return args.concat([trace]) 74 | } 75 | } 76 | }) 77 | 78 | client.on("auth", doAuth) 79 | 80 | client.on("authResponse", (result, err) => { 81 | if (!result) { 82 | creds = null 83 | console.warn("Auth failed:", err) 84 | doAuth() 85 | } 86 | }) 87 | 88 | client.on("console", (data) => { 89 | data = cycle.retrocycle(data) 90 | 91 | let trace, 92 | cdata = data.callerInfo 93 | if (cdata) { 94 | trace = 95 | " -- Called from " + 96 | cdata.file + 97 | ":" + 98 | cdata.line + 99 | ":" + 100 | cdata.column + 101 | (cdata.caller ? "(function " + cdata.caller + ")" : "") 102 | } 103 | if (data.method === "dir") { 104 | console.dir(data.args[0]) 105 | if (trace) { 106 | console.log.apply(console, stylize(["^^^"], trace)) 107 | } 108 | } else { 109 | console[data.method].apply(console, stylize(data.args, trace)) 110 | } 111 | }) 112 | 113 | client.on("prompt", (promptId, promptTxt, opts) => { 114 | opts || (opts = {}) 115 | 116 | client.emit("promptResponse", promptId, prompt(promptTxt)) 117 | }) 118 | 119 | resolve() 120 | }) 121 | .catch(reject) 122 | }) 123 | }) 124 | }, 125 | 126 | cmd(command, noOutput) { 127 | if (!monkey.client) { 128 | console.error(`Must be connected to a server to execute a command`) 129 | return 130 | } 131 | 132 | let p = new Promise((resolve, reject) => { 133 | let cmdId = monkey.cmdId++ 134 | monkey.client.emit("cmd", cmdId, command) 135 | monkey.runningCmd[cmdId] = { resolve, reject } 136 | }) 137 | 138 | if (!noOutput) { 139 | p.then((output) => output !== null && console.log(output)).catch((error) => { 140 | if (error !== null) { 141 | console.error(error) 142 | alert(error.message) 143 | } 144 | }) 145 | } 146 | 147 | return p 148 | }, 149 | }) 150 | 151 | function initClient() { 152 | return new Promise((resolve, reject) => { 153 | let client = io(`${location.origin}/nm`, { 154 | path: "/monkey.io" 155 | }) 156 | 157 | client.on("connect", function () {}) 158 | 159 | client.on("error", (err) => { 160 | console.error(err) 161 | }) 162 | 163 | client.on("connect_error", (err) => { 164 | console.error(err) 165 | }) 166 | 167 | client.on("reconnect_error", (err) => { 168 | console.error(err) 169 | }) 170 | 171 | client.on("connect_timeout", () => { 172 | console.error(new Error("Socket.IO connection timed out")) 173 | }) 174 | 175 | resolve(client) 176 | }) 177 | } 178 | -------------------------------------------------------------------------------- /src/client/utils.js: -------------------------------------------------------------------------------- 1 | import commonUtils from "../lib/common-utils" 2 | 3 | export default Object.assign( 4 | { 5 | getClientHost() { 6 | let scripts = document.getElementsByTagName("script"), 7 | scriptRe = /\/monkey\.js/, 8 | script = null 9 | 10 | // Loop in reverse since the correct script will be the last one except when the `async` attribute is set on the script 11 | for (let i = scripts.length - 1; i >= 0; --i) { 12 | if (scriptRe.test(scripts[i].src)) { 13 | script = scripts[i] 14 | break 15 | } 16 | } 17 | 18 | if (script) { 19 | let parser = document.createElement("a") 20 | parser.href = script.src 21 | 22 | return `${parser.protocol}//${parser.host}` 23 | } 24 | 25 | return `${location.protocol}//${location.host}` 26 | }, 27 | 28 | addHeadScript(src) { 29 | let script = document.createElement("script") 30 | script.type = "text/javascript" 31 | script.src = src 32 | document.getElementsByTagName("head")[0].appendChild(script) 33 | 34 | return script 35 | }, 36 | }, 37 | commonUtils, 38 | ) 39 | -------------------------------------------------------------------------------- /src/lib/common-utils.js: -------------------------------------------------------------------------------- 1 | export default { 2 | isObject(value) { 3 | let type = typeof value 4 | return !!value && (type == "object" || type == "function") 5 | }, 6 | 7 | invert(obj) { 8 | let inverted = {} 9 | for (let k in obj) { 10 | if (obj.hasOwnProperty(k)) { 11 | inverted[obj[k]] = k 12 | } 13 | } 14 | 15 | return inverted 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /src/lib/cycle.js: -------------------------------------------------------------------------------- 1 | /* 2 | cycle.js 3 | 2016-05-01 4 | 5 | Public Domain. 6 | 7 | NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. 8 | 9 | This code should be minified before deployment. 10 | See http://javascript.crockford.com/jsmin.html 11 | 12 | USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO 13 | NOT CONTROL. 14 | */ 15 | 16 | /*jslint eval, for */ 17 | 18 | /*property 19 | $ref, decycle, forEach, isArray, keys, length, push, retrocycle, stringify, 20 | test 21 | */ 22 | 23 | let origJSON = global.JSON, 24 | JSON = {} 25 | export default JSON 26 | 27 | if (typeof JSON.decycle !== "function") { 28 | JSON.decycle = function decycle(object, replacer) { 29 | "use strict" 30 | 31 | // Make a deep copy of an object or array, assuring that there is at most 32 | // one instance of each object or array in the resulting structure. The 33 | // duplicate references (which might be forming cycles) are replaced with 34 | // an object of the form 35 | 36 | // {"$ref": PATH} 37 | 38 | // where the PATH is a JSONPath string that locates the first occurance. 39 | 40 | // So, 41 | 42 | // var a = []; 43 | // a[0] = a; 44 | // return JSON.stringify(JSON.decycle(a)); 45 | 46 | // produces the string '[{"$ref":"$"}]'. 47 | 48 | // If a replacer function is provided, then it will be called for each value. 49 | // A replacer function receives a value and returns a replacement value. 50 | 51 | // JSONPath is used to locate the unique object. $ indicates the top level of 52 | // the object or array. [NUMBER] or [STRING] indicates a child element or 53 | // property. 54 | 55 | var objects = [] // Keep a reference to each unique object or array 56 | var paths = [] // Keep the path to each unique object or array 57 | 58 | return (function derez(value, path) { 59 | // The derez function recurses through the object, producing the deep copy. 60 | 61 | var i // The loop counter 62 | var nu // The new object or array 63 | 64 | // If a replacer function was provided, then call it to get a replacement value. 65 | 66 | if (replacer !== undefined) { 67 | value = replacer(value) 68 | } 69 | 70 | // typeof null === "object", so go on if this value is really an object but not 71 | // one of the weird builtin objects. 72 | 73 | if ( 74 | typeof value === "object" && 75 | value !== null && 76 | !(value instanceof Boolean) && 77 | !(value instanceof Date) && 78 | !(value instanceof Number) && 79 | !(value instanceof RegExp) && 80 | !(value instanceof String) 81 | ) { 82 | // If the value is an object or array, look to see if we have already 83 | // encountered it. If so, return a {"$ref":PATH} object. This is a hard 84 | // linear search that will get slower as the number of unique objects grows. 85 | // Someday, this should be replaced with an ES6 WeakMap. 86 | 87 | i = objects.indexOf(value) 88 | if (i >= 0) { 89 | return { $ref: paths[i] } 90 | } 91 | 92 | // Otherwise, accumulate the unique value and its path. 93 | 94 | objects.push(value) 95 | paths.push(path) 96 | 97 | // If it is an array, replicate the array. 98 | 99 | if (Array.isArray(value)) { 100 | nu = [] 101 | value.forEach(function (element, i) { 102 | nu[i] = derez(element, path + "[" + i + "]") 103 | }) 104 | } else { 105 | // If it is an object, replicate the object. 106 | 107 | nu = {} 108 | Object.keys(value).forEach(function (name) { 109 | nu[name] = derez(value[name], path + "[" + JSON.stringify(name) + "]") 110 | }) 111 | } 112 | return nu 113 | } 114 | return value 115 | })(object, "$") 116 | } 117 | } 118 | 119 | if (typeof JSON.retrocycle !== "function") { 120 | JSON.retrocycle = function retrocycle($) { 121 | "use strict" 122 | 123 | // Restore an object that was reduced by decycle. Members whose values are 124 | // objects of the form 125 | // {$ref: PATH} 126 | // are replaced with references to the value found by the PATH. This will 127 | // restore cycles. The object will be mutated. 128 | 129 | // The eval function is used to locate the values described by a PATH. The 130 | // root object is kept in a $ variable. A regular expression is used to 131 | // assure that the PATH is extremely well formed. The regexp contains nested 132 | // * quantifiers. That has been known to have extremely bad performance 133 | // problems on some browsers for very long strings. A PATH is expected to be 134 | // reasonably short. A PATH is allowed to belong to a very restricted subset of 135 | // Goessner's JSONPath. 136 | 137 | // So, 138 | // var s = '[{"$ref":"$"}]'; 139 | // return JSON.retrocycle(JSON.parse(s)); 140 | // produces an array containing a single element which is the array itself. 141 | 142 | var px = /^\$(?:\[(?:\d+|\"(?:[^\\\"\u0000-\u001f]|\\([\\\"\/bfnrt]|u[0-9a-zA-Z]{4}))*\")\])*$/ 143 | 144 | ;(function rez(value) { 145 | // The rez function walks recursively through the object looking for $ref 146 | // properties. When it finds one that has a value that is a path, then it 147 | // replaces the $ref object with a reference to the value that is found by 148 | // the path. 149 | 150 | if (value && typeof value === "object") { 151 | if (Array.isArray(value)) { 152 | value.forEach(function (element, i) { 153 | if (typeof element === "object" && element !== null) { 154 | var path = element.$ref 155 | if (typeof path === "string" && px.test(path)) { 156 | value[i] = eval(path) 157 | } else { 158 | rez(element) 159 | } 160 | } 161 | }) 162 | } else { 163 | Object.keys(value).forEach(function (name) { 164 | var item = value[name] 165 | if (typeof item === "object" && item !== null) { 166 | var path = item.$ref 167 | if (typeof path === "string" && px.test(path)) { 168 | value[name] = eval(path) 169 | } else { 170 | rez(item) 171 | } 172 | } 173 | }) 174 | } 175 | } 176 | })($) 177 | return $ 178 | } 179 | } 180 | 181 | JSON = origJSON 182 | -------------------------------------------------------------------------------- /src/server/bunyan-stream.js: -------------------------------------------------------------------------------- 1 | import utils from "./utils" 2 | 3 | const BUNYAN_TRACE = 10 4 | const BUNYAN_DEBUG = 20 5 | const BUNYAN_INFO = 30 6 | const BUNYAN_WARN = 40 7 | const BUNYAN_ERROR = 50 8 | const BUNYAN_FATAL = 60 9 | 10 | let levelFromName = { 11 | trace: BUNYAN_TRACE, 12 | debug: BUNYAN_DEBUG, 13 | info: BUNYAN_INFO, 14 | warn: BUNYAN_WARN, 15 | error: BUNYAN_ERROR, 16 | fatal: BUNYAN_FATAL, 17 | } 18 | let nameFromLevel = utils.invert(levelFromName) 19 | 20 | export default (inst) => { 21 | return { 22 | write: function (rec) { 23 | rec = JSON.parse(rec) 24 | inst._sendMessage({ 25 | method: nameFromLevel[rec.level] || "info", 26 | args: [rec.msg, rec], 27 | }) 28 | }, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/server/command-interface.js: -------------------------------------------------------------------------------- 1 | class CommandInterface { 2 | commandManager = null 3 | 4 | write = (val, opts) => { 5 | console.log(val) 6 | } 7 | 8 | writeLn = (val, opts) => { 9 | console.log(val) 10 | } 11 | 12 | error = (val, opts) => { 13 | console.error(val) 14 | } 15 | 16 | prompt = (promptTxt, opts, cb) => { 17 | if (typeof opts === "function") { 18 | cb = opts 19 | opts = undefined 20 | } 21 | opts || (opts = {}) 22 | 23 | console.warn("Prompt not implemented") 24 | } 25 | 26 | constructor(commandManager, writeFn, writeLnFn, errorFn, promptFn) { 27 | this.commandManager = commandManager 28 | this.write = writeFn 29 | this.writeLn = writeLnFn 30 | this.error = errorFn 31 | this.prompt = promptFn 32 | } 33 | } 34 | 35 | export default CommandInterface 36 | -------------------------------------------------------------------------------- /src/server/command-manager.js: -------------------------------------------------------------------------------- 1 | import _ from "lodash" 2 | import utils from "./utils" 3 | import minimist from "minimist" 4 | 5 | class CommandManager { 6 | commands = {} 7 | 8 | addCmd = (cmdName, opts, exec) => { 9 | if (this.commands[cmdName]) { 10 | throw new Error(`'${cmdName}' is already registered as a command`) 11 | } 12 | 13 | if (typeof opts === "function") { 14 | exec = opts 15 | opts = {} 16 | } 17 | 18 | this.commands[cmdName] = { 19 | opts, 20 | exec, 21 | } 22 | } 23 | 24 | runCmd = async (rawCommand, asUser, io) => { 25 | const parsed = utils.parseCommand(rawCommand) 26 | const cmdName = parsed[0] 27 | const cmd = this.commands[cmdName] 28 | 29 | if (!asUser) { 30 | throw new Error(`Missing user context for command '${cmdName}'`) 31 | } 32 | 33 | if (!cmd) { 34 | throw new Error(`Command not found: '${cmdName}'`) 35 | } 36 | 37 | const args = minimist(parsed.slice(1)) 38 | const doneP = utils.getPromiseObj() 39 | const result = cmd.exec( 40 | { 41 | args, 42 | username: asUser, 43 | }, 44 | { 45 | write: io.write, 46 | writeLn: io.writeLn, 47 | error: io.error, 48 | prompt: io.prompt, 49 | }, 50 | doneP.resolve, 51 | ) 52 | 53 | return result.then ? result : doneP.promise 54 | } 55 | } 56 | 57 | export default CommandManager 58 | -------------------------------------------------------------------------------- /src/server/index.js: -------------------------------------------------------------------------------- 1 | import os from "os" 2 | import fs from "fs" 3 | import path from "path" 4 | import { EventEmitter } from "events" 5 | import _ from "lodash" 6 | import keypair from "keypair" 7 | import cycle from "../lib/cycle" 8 | import bunyanStream from "./bunyan-stream" 9 | import setupServer from "./setup-server" 10 | import setupSocket from "./setup-socket" 11 | import SSHMan from "./ssh-manager" 12 | import CommandManager from "./command-manager" 13 | import UserManager from "./user-manager" 14 | import utils from "./utils" 15 | 16 | const NODE_ENV = process.env.NODE_ENV 17 | const DEFAULT_PORT = 50500 18 | const CONSOLE = _.mapValues(console) 19 | const ConsoleEvent = new EventEmitter() 20 | const HANDLE_TYPES = ["log", "info", "warn", "error", "dir"] 21 | 22 | let attachedCount = 0 23 | 24 | class NodeMonkey { 25 | msgBuffer = [] 26 | BUNYAN_STREAM = bunyanStream(this) 27 | _attached = false 28 | _typeHandlers = {} 29 | 30 | constructor(opts) { 31 | const options = (this.options = _.merge( 32 | { 33 | server: { 34 | // You can provide your own server and Node Monkey will use it instead of creating its own. 35 | // However, this MUST be the underlying http server instance, not the express/restify/whatever app. 36 | server: null, 37 | 38 | host: "0.0.0.0", 39 | port: DEFAULT_PORT, 40 | silent: false, 41 | bufferSize: 50, 42 | attachOnStart: true, 43 | 44 | // Only takes effect when Node Monkey is attached to the console 45 | disableLocalOutput: false, 46 | }, 47 | client: { 48 | showCallerInfo: NODE_ENV === "production" ? false : true, 49 | convertStyles: true, 50 | }, 51 | ssh: { 52 | enabled: false, 53 | host: "0.0.0.0", 54 | port: DEFAULT_PORT + 1, 55 | title: `Node Monkey on ${os.hostname()}`, 56 | prompt: `[Node Monkey] {@username}@${os.hostname()}:`, 57 | }, 58 | 59 | // Needed for storing things like user files and SSH host keys 60 | dataDir: null, 61 | }, 62 | opts, 63 | )) 64 | 65 | this._createLocal() 66 | this._createRemote() 67 | this._setupCmdMan() 68 | this._setupUserManager() 69 | this._setupServer() 70 | this._setupSSH() 71 | 72 | if (options.server.attachOnStart) { 73 | this.attachConsole() 74 | } 75 | 76 | // TODO: Deprecated. Remove everything after this line by v1.0.0 77 | console.local = _.mapValues(this.local, (fn, method) => { 78 | const localFn = (...args) => { 79 | return fn(...args) 80 | } 81 | Object.defineProperty(localFn, "name", { value: method }) 82 | return localFn 83 | }) 84 | console.remote = _.mapValues(this.remote, (fn, method) => { 85 | const remoteFn = (...args) => { 86 | return fn({ callerStackDistance: 2 }, ...args) 87 | } 88 | Object.defineProperty(remoteFn, "name", { value: method }) 89 | return remoteFn 90 | }) 91 | } 92 | 93 | _getServerProtocol(server) { 94 | if (server._events && server._events.tlsClientError) { 95 | return "https" 96 | } 97 | return "http" 98 | } 99 | 100 | getServerPaths() { 101 | const resolve = __non_webpack_require__ ? __non_webpack_require__.resolve : resolve 102 | const basePath = path.normalize(`${__dirname}/../dist`) 103 | const sioBasePath = path.normalize(`${path.dirname(resolve("socket.io"))}/../client-dist`) 104 | 105 | const files = { 106 | "/": `${basePath}/index.html`, 107 | "/monkey.js": `${basePath}/monkey.js`, 108 | "/monkey.js.map": `${basePath}/monkey.js.map`, 109 | } 110 | 111 | fs.readdirSync(sioBasePath).forEach(fileName => { 112 | files[`/monkey.io-client/${fileName}`] = `${sioBasePath}/${fileName}` 113 | }) 114 | 115 | return files 116 | } 117 | 118 | _displayServerWelcome() { 119 | if (!this.options.server.silent) { 120 | const server = this.serverApp 121 | const address = typeof server.address === "function" && server.address() 122 | if (server.listening && address) { 123 | const proto = this._getServerProtocol(server) 124 | const { address, port } = server.address() 125 | this.local.log(`Node Monkey listening at ${proto}://${address}:${port}`) 126 | } else { 127 | server.on("listening", this._displayServerWelcome.bind(this)) 128 | } 129 | } 130 | } 131 | 132 | _setupCmdMan() { 133 | this._cmdMan = new CommandManager({ 134 | write: (val, opts) => { 135 | console.log(val) 136 | }, 137 | writeLn: (val, opts) => { 138 | console.log(val) 139 | }, 140 | error: (val, opts) => { 141 | console.error(val) 142 | }, 143 | prompt: (promptTxt, opts, cb) => { 144 | if (typeof opts === "function") { 145 | cb = opts 146 | opts = undefined 147 | } 148 | opts || (opts = {}) 149 | 150 | console.warn("Prompt not implemented") 151 | }, 152 | }) 153 | 154 | this.addCmd = this._cmdMan.addCmd 155 | this.runCmd = this._cmdMan.runCmd 156 | } 157 | 158 | _setupUserManager() { 159 | const dataDir = this.options.dataDir 160 | const userMan = (this.userManager = new UserManager({ 161 | userFile: dataDir ? `${dataDir}/users.json` : undefined, 162 | silent: this.options.server.silent, 163 | })) 164 | 165 | this.addCmd("showusers", async (opts, term) => { 166 | const users = await userMan.getUsers() 167 | term.writeLn(Object.keys(users).join("\n")) 168 | }) 169 | 170 | this.addCmd("adduser", (opts, term, done) => { 171 | const args = opts.args 172 | const username = args._[0] 173 | 174 | if (!username) { 175 | term.error(`You must specify a username`) 176 | return done() 177 | } 178 | 179 | term.prompt("Password: ", { hideInput: true }, (error, password) => { 180 | term.writeLn() 181 | term.prompt("Again: ", { hideInput: true }, (error, passwordAgain) => { 182 | term.writeLn() 183 | if (password === passwordAgain) { 184 | userMan 185 | .createUser(username, password) 186 | .then(() => term.write(`Created user '${username}'`)) 187 | .catch(term.error) 188 | .then(done) 189 | } else { 190 | term.error("Passwords do not match") 191 | done() 192 | } 193 | }) 194 | }) 195 | }) 196 | 197 | this.addCmd("deluser", (opts, term, done) => { 198 | const args = opts.args 199 | const username = args._[0] 200 | 201 | if (!username) { 202 | term.error(`You must specify a username`) 203 | return done() 204 | } 205 | 206 | userMan 207 | .deleteUser(username) 208 | .then(() => term.write(`Deleted user '${username}'`)) 209 | .catch(term.error) 210 | .then(done) 211 | }) 212 | 213 | this.addCmd("passwd", (opts, term, done) => { 214 | const args = opts.args 215 | const user = opts.username 216 | 217 | term.prompt("Current password: ", { hideInput: true }, (error, curpwd) => { 218 | term.writeLn() 219 | userMan.verifyUser(user, curpwd).then((matches) => { 220 | if (matches) { 221 | term.prompt("Password: ", { hideInput: true }, (error, password) => { 222 | term.writeLn() 223 | term.prompt("Again: ", { hideInput: true }, (error, passwordAgain) => { 224 | term.writeLn() 225 | if (password === passwordAgain) { 226 | userMan 227 | .setPassword(user, password) 228 | .then(() => term.write(`Updated password for ${user}`)) 229 | .catch(term.error) 230 | .then(done) 231 | } else { 232 | term.error("Passwords do not match") 233 | done() 234 | } 235 | }) 236 | }) 237 | } else { 238 | term.error("Incorrect password") 239 | done() 240 | } 241 | }) 242 | }) 243 | }) 244 | } 245 | 246 | _setupServer() { 247 | const options = this.options 248 | const srvOpts = options.server 249 | 250 | if (srvOpts.server) { 251 | this.serverApp = srvOpts.server 252 | } else { 253 | this.serverApp = setupServer({ 254 | filePaths: this.getServerPaths(), 255 | }) 256 | this.serverApp.listen(srvOpts.port, srvOpts.host) 257 | } 258 | 259 | this._displayServerWelcome() 260 | this.remoteClients = setupSocket({ 261 | server: this.serverApp, 262 | cmdManager: this._cmdMan, 263 | userManager: this.userManager, 264 | onAuth: this._sendMessages.bind(this), 265 | clientSettings: options.client, 266 | }) 267 | } 268 | 269 | _setupSSH() { 270 | const sshOpts = this.options.ssh 271 | if (sshOpts.enabled) { 272 | const dataDir = this.options.dataDir 273 | if (!dataDir) { 274 | throw new Error(`Options 'dataDir' is required to enable SSH`) 275 | } 276 | 277 | // Get host keys 278 | const files = fs.readdirSync(dataDir) 279 | const keyRe = /\.key$/ 280 | let hostKeys = [] 281 | for (let file of files) { 282 | if (keyRe.test(file)) { 283 | hostKeys.push(`${dataDir}/${file}`) 284 | } 285 | } 286 | 287 | if (!hostKeys.length) { 288 | console.log("No SSH host key found. Generating new host key...") 289 | let keys = keypair() 290 | fs.writeFileSync(`${dataDir}/rsa.key`, keys.private) 291 | fs.writeFileSync(`${dataDir}/rsa.key.pub`, keys.public) 292 | hostKeys = [`${dataDir}/rsa.key`] 293 | } 294 | 295 | this.SSHMan = new SSHMan({ 296 | monkey: this, 297 | userManager: this.userManager, 298 | cmdManager: this._cmdMan, 299 | silent: this.options.server.silent, 300 | host: sshOpts.host, 301 | port: sshOpts.port, 302 | title: _.result(sshOpts, "title"), 303 | prompt: _.result(sshOpts, "prompt"), 304 | hostKeys, 305 | }) 306 | } 307 | } 308 | 309 | // TODO: This whole process of trying to identify the true source of the call is so fucking messy and fragile. Need to think 310 | // of a better way to identify the call source and rewrite all this shitty code handling it right now. 311 | _getCallerInfo(callerStackDistance) { 312 | if (this.options.client.showCallerInfo) { 313 | const stack = utils.getStack().map((frame) => { 314 | return { 315 | functionName: frame.getFunctionName(), 316 | methodName: frame.getMethodName(), 317 | fileName: frame.getFileName(), 318 | lineNumber: frame.getLineNumber(), 319 | columnNumber: frame.getColumnNumber(), 320 | } 321 | }) 322 | 323 | let caller = stack.find((frame, index, stack) => { 324 | // We're either looking for a console method call or a bunyan log call. This logic will break down if method names change. 325 | const twoBack = stack[index - 2] 326 | const sixBack = stack[index - 4] 327 | if (twoBack && twoBack.functionName === "Logger._emit" && /\/bunyan\.js$/.test(twoBack.fileName)) { 328 | return true 329 | } else if (twoBack && sixBack && twoBack.methodName === "emit" && sixBack.methodName === "_sendMessage") { 330 | return true 331 | } 332 | }) 333 | 334 | if (!caller && typeof callerStackDistance === "number") { 335 | caller = stack[callerStackDistance] 336 | } 337 | 338 | if (caller) { 339 | return { 340 | caller: caller.functionName || caller.methodName, 341 | file: caller.fileName, 342 | line: caller.lineNumber, 343 | column: caller.columnNumber, 344 | } 345 | } 346 | } 347 | } 348 | 349 | _sendMessage(info, callerStackDistance) { 350 | this.msgBuffer.push({ 351 | method: info.method, 352 | args: info.args, 353 | callerInfo: info.callerInfo || this._getCallerInfo(callerStackDistance + 1), 354 | }) 355 | if (this.msgBuffer.length > this.options.server.bufferSize) { 356 | this.msgBuffer.shift() 357 | } 358 | this._sendMessages() 359 | } 360 | 361 | _sendMessages() { 362 | const remoteClients = this.remoteClients 363 | if (_.size(remoteClients.adapter.rooms.get("authed"))) { 364 | _.each(this.msgBuffer, (info) => { 365 | remoteClients.to("authed").emit("console", cycle.decycle(info)) 366 | }) 367 | 368 | this.msgBuffer = [] 369 | } 370 | } 371 | 372 | _createLocal() { 373 | // NOTE: The console functions here should not be wrapped since these values are used to restore the defaults 374 | // when `detachConsole()` is called. 375 | this.local = CONSOLE 376 | } 377 | 378 | _createRemote() { 379 | let remote = (this.remote = {}) 380 | HANDLE_TYPES.forEach((method) => { 381 | this.remote[method] = (...args) => { 382 | const stackdist = args[0] && args[0].callerStackDistance 383 | this._sendMessage( 384 | { 385 | method, 386 | args: stackdist ? args.slice(1) : args, 387 | }, 388 | stackdist ? stackdist + 1 : 2, 389 | ) 390 | } 391 | Object.defineProperty(remote[method], "name", { value: method }) 392 | }) 393 | } 394 | 395 | attachConsole(disableLocalOutput) { 396 | if (this._attached) { 397 | return 398 | } 399 | 400 | if (!attachedCount) { 401 | // If this function is in the process of handling the log call we will try and prevent potential infinite recursion 402 | let handlersActive = 0 403 | HANDLE_TYPES.forEach((method) => { 404 | console[method] = (...args) => { 405 | if (handlersActive) { 406 | return this.local[method](...args) 407 | } 408 | 409 | ++handlersActive 410 | ConsoleEvent.emit(method, ...args) 411 | --handlersActive 412 | } 413 | Object.defineProperty(console[method], "name", { value: method }) 414 | }) 415 | } 416 | 417 | ++attachedCount 418 | 419 | const serverOptions = this.options.server 420 | disableLocalOutput = disableLocalOutput !== undefined ? disableLocalOutput : serverOptions.disableLocalOutput 421 | 422 | _.each(this.remote, (fn, method) => { 423 | const handler = (this._typeHandlers[method] = (...args) => { 424 | fn({ callerStackDistance: 4 }, ...args) 425 | 426 | if (!disableLocalOutput) { 427 | this.local[method](...args) 428 | } 429 | }) 430 | Object.defineProperty(handler, "name", { value: method }) 431 | 432 | ConsoleEvent.on(method, handler) 433 | }) 434 | 435 | this._attached = true 436 | } 437 | 438 | detachConsole() { 439 | Object.assign(console, this.local) 440 | this._attached = false 441 | --attachedCount 442 | 443 | HANDLE_TYPES.forEach((method) => { 444 | ConsoleEvent.removeListener(method, this._typeHandlers[method]) 445 | delete this._typeHandlers[method] 446 | }) 447 | } 448 | 449 | stop() { 450 | this.serverApp.close() 451 | } 452 | } 453 | 454 | const instances = {} 455 | let lastPort = DEFAULT_PORT - 1 456 | export default function createInst(options, name = "default") { 457 | if (typeof options === "string") { 458 | name = options 459 | options = undefined 460 | } 461 | 462 | if (!instances[name]) { 463 | options || (options = {}) 464 | let port = _.get(options, "server.port") 465 | if (port) { 466 | lastPort = +port 467 | } else { 468 | _.set(options, "server.port", ++lastPort) 469 | _.set(options, "ssh.port", ++lastPort) 470 | } 471 | instances[name] = new NodeMonkey(options) 472 | } 473 | 474 | return instances[name] 475 | } 476 | 477 | // Just exporting in case someone needs to wrap this or access the internals for some reason 478 | export { NodeMonkey } 479 | -------------------------------------------------------------------------------- /src/server/setup-server.js: -------------------------------------------------------------------------------- 1 | import { extname, } from "path" 2 | import { createServer, } from "http" 3 | import { readFile, } from "fs/promises" 4 | 5 | const extMap = { 6 | html: "text/html", 7 | js: "application/javascript", 8 | json: "application/json", 9 | map: "application/json", 10 | } 11 | 12 | const loadedFiles = new Map() 13 | async function loadFile(file) { 14 | if (!loadedFiles.has(file)) { 15 | loadedFiles.set(file, await readFile(file)) 16 | } 17 | return loadedFiles.get(file) 18 | } 19 | 20 | export default (options) => { 21 | const filePaths = options.filePaths || {} 22 | const server = createServer(async (req, res) => { 23 | const filePath = filePaths[req.url] 24 | if (filePath) { 25 | const contentType = extMap[(extname(filePath) || "").slice(1)] || "text/plain" 26 | const content = await loadFile(filePath) 27 | 28 | res.setHeader("Content-Type", contentType) 29 | res.writeHead(200) 30 | res.end(content) 31 | } else { 32 | res.writeHead(404) 33 | res.end() 34 | } 35 | }) 36 | 37 | return server 38 | } 39 | -------------------------------------------------------------------------------- /src/server/setup-socket.js: -------------------------------------------------------------------------------- 1 | import _ from "lodash" 2 | import socketio from "socket.io" 3 | import CommandInterface from "./command-interface" 4 | 5 | export default (options) => { 6 | const io = socketio() 7 | io.attach(options.server, { 8 | path: "/monkey.io", 9 | autoUnref: true, 10 | }) 11 | 12 | const ns = io.of("/nm") 13 | ns.on("connection", (socket) => { 14 | let cmdInterface = null 15 | socket.emit("settings", options.clientSettings) 16 | socket.emit("auth") 17 | 18 | socket.on("auth", (creds) => { 19 | options.userManager 20 | .verifyUser(creds.username, creds.password) 21 | .then((result) => { 22 | socket.emit("authResponse", result, result ? undefined : "Incorrect password") 23 | if (result) { 24 | socket.username = creds.username 25 | socket.join("authed") 26 | if (options.onAuth) { 27 | options.onAuth(socket) 28 | } 29 | } 30 | }) 31 | .catch((err) => { 32 | socket.emit("authResponse", false, err) 33 | }) 34 | }) 35 | 36 | socket.on("cmd", (cmdId, command) => { 37 | if (!socket.username) { 38 | socket.emit("cmdResponse", cmdId, `You are not authorized to run commands`) 39 | return 40 | } 41 | 42 | if (!cmdInterface) { 43 | cmdInterface = createCmdInterface(options.cmdManager, socket) 44 | } 45 | 46 | options.cmdManager 47 | .runCmd(command, socket.username, cmdInterface) 48 | .then((output) => { 49 | socket.emit("cmdResponse", cmdId, null, output) 50 | }) 51 | .catch((err) => { 52 | socket.emit("cmdResponse", cmdId, (err && err.message) || err, null) 53 | }) 54 | }) 55 | }) 56 | 57 | _.each(options.handlers, function (handler, event) { 58 | io.on(event, handler) 59 | }) 60 | 61 | return ns 62 | } 63 | 64 | function createCmdInterface(cmdManager, socket) { 65 | let promptId = 0 66 | const prompts = {} 67 | 68 | const writeFn = (val, opts) => { 69 | if (!val) return 70 | 71 | socket.emit("console", { 72 | method: "log", 73 | args: [val], 74 | }) 75 | } 76 | 77 | const errorFn = (val, opts) => { 78 | if (!val) return 79 | 80 | socket.emit("console", { 81 | method: "error", 82 | args: [val], 83 | }) 84 | } 85 | 86 | const promptFn = (promptTxt, opts, cb) => { 87 | if (typeof opts === "function") { 88 | cb = opts 89 | opts = undefined 90 | } 91 | opts || (opts = {}) 92 | 93 | let pid = promptId++ 94 | socket.emit("prompt", pid, promptTxt, opts) 95 | 96 | prompts[pid] = cb 97 | } 98 | 99 | socket.on("promptResponse", (promptId, response) => { 100 | const cb = prompts[promptId] 101 | if (cb) { 102 | cb(null, response) 103 | } 104 | }) 105 | 106 | return new CommandInterface(cmdManager, writeFn, writeFn, errorFn, promptFn) 107 | } 108 | -------------------------------------------------------------------------------- /src/server/ssh-manager.js: -------------------------------------------------------------------------------- 1 | import fs from "fs" 2 | import tty from "tty" 3 | import { native as nativePty } from "node-pty" 4 | import ssh2 from "ssh2" 5 | import termkit from "terminal-kit" 6 | import CommandInterface from "./command-interface" 7 | 8 | class SSHManager { 9 | options = { 10 | host: "127.0.0.1", 11 | port: 50501, 12 | title: "Node Monkey", 13 | prompt: "Node Monkey:", 14 | silent: false, 15 | } 16 | server 17 | clients = new Set() 18 | 19 | constructor(options) { 20 | options = Object.assign(this.options, options) 21 | 22 | this.server = new ssh2.Server( 23 | { 24 | hostKeys: options.hostKeys.map((file) => { 25 | return fs.readFileSync(file) 26 | }), 27 | }, 28 | this.onClient.bind(this), 29 | ) 30 | 31 | const monkey = this.options.monkey 32 | this.server.listen(options.port, options.host, function () { 33 | options.silent || monkey.local.log(`SSH listening on ${this.address().port}`) 34 | }) 35 | } 36 | 37 | shutdown() { 38 | const clients = this.clients 39 | for (const c of clients) { 40 | c.write("\nShutting down") 41 | c.close() 42 | } 43 | } 44 | 45 | onClient(client) { 46 | const { cmdManager, userManager, title, prompt } = this.options 47 | 48 | this.clients.add( 49 | new SSHClient({ 50 | client, 51 | cmdManager, 52 | userManager, 53 | title, 54 | prompt, 55 | onClose: () => this.clients.delete(client), 56 | }), 57 | ) 58 | } 59 | } 60 | 61 | class SSHClient { 62 | constructor(options) { 63 | this.options = options 64 | this.client = options.client 65 | this.cmdInterface = null 66 | this.userManager = options.userManager 67 | this.session = null 68 | this.stream = null 69 | this.pty = null 70 | this.term = null 71 | this.ptyInfo = null 72 | 73 | this.title = options.title 74 | this.promptTxt = `${options.prompt} ` 75 | this.inputActive = false 76 | this.cmdHistory = [] 77 | 78 | this.username = null 79 | 80 | this.client.on("authentication", this.onAuth.bind(this)) 81 | this.client.on("ready", this.onReady.bind(this)) 82 | this.client.on("end", this.onClose.bind(this)) 83 | } 84 | 85 | _initCmdMan() { 86 | const writeFn = (val, opts) => { 87 | opts || (opts = {}) 88 | val || (val = "") 89 | 90 | if (opts.bold) { 91 | this.term.bold(val) 92 | } else { 93 | this.term(val) 94 | } 95 | 96 | if (opts.newline) { 97 | this.term.nextLine() 98 | } 99 | } 100 | 101 | const writeLnFn = (val, opts) => { 102 | opts || (opts = {}) 103 | opts.newline = true 104 | writeFn(val, opts) 105 | } 106 | 107 | const errorFn = (val, opts) => { 108 | opts || (opts = {}) 109 | 110 | // TODO: Apparently by sending this to stdout there is a timing issue and anything sent to 111 | // stdout appears before this value is sent to stderr for some reason. 112 | // this.term.red.error(val) 113 | this.term.red(val) 114 | 115 | if (opts.newline) { 116 | this.term.nextLine() 117 | } 118 | } 119 | 120 | const promptFn = (promptTxt = "", opts, cb) => { 121 | if (typeof opts === "function") { 122 | cb = opts 123 | opts = undefined 124 | } 125 | opts || (opts = {}) 126 | 127 | let inputOpts = {} 128 | if (opts.hideInput) { 129 | inputOpts.echo = false 130 | } 131 | 132 | this.term(promptTxt) 133 | this.term.inputField(inputOpts, cb) 134 | } 135 | 136 | this.cmdInterface = new CommandInterface(this.options.cmdManager, writeFn, writeLnFn, errorFn, promptFn) 137 | } 138 | 139 | write(msg, { style = undefined }) { 140 | if (this.term) { 141 | if (style) { 142 | this.term[style](msg) 143 | } else { 144 | this.term(msg) 145 | } 146 | } 147 | } 148 | 149 | close() { 150 | if (this.stream) { 151 | this.stream.end() 152 | } 153 | this.onClose() 154 | } 155 | 156 | onAuth(ctx) { 157 | if (ctx.method == "password") { 158 | this.userManager 159 | .verifyUser(ctx.username, ctx.password) 160 | .then((result) => { 161 | if (result) { 162 | this.username = ctx.username 163 | ctx.accept() 164 | } else { 165 | ctx.reject() 166 | } 167 | }) 168 | .catch((err) => { 169 | ctx.reject() 170 | }) 171 | } else if (ctx.method == "publickey") { 172 | ctx.reject() 173 | } else { 174 | ctx.reject() 175 | } 176 | } 177 | 178 | onReady() { 179 | this.client.on("session", (accept, reject) => { 180 | this.session = accept() 181 | 182 | this.session 183 | .once("pty", (accept, reject, info) => { 184 | this.ptyInfo = info 185 | accept && accept() 186 | }) 187 | .on("window-change", (accept, reject, info) => { 188 | Object.assign(this.ptyInfo, info) 189 | this._resize() 190 | accept && accept() 191 | }) 192 | .once("shell", (accept, reject) => { 193 | this.stream = accept() 194 | this._initCmdMan() 195 | this._initStream() 196 | this._initPty() 197 | this._initTerm() 198 | }) 199 | }) 200 | } 201 | 202 | onClose() { 203 | let onClose = this.options.onClose 204 | onClose && onClose() 205 | } 206 | 207 | onKey(name, matches, data) { 208 | if (name === "CTRL_L") { 209 | this.clearScreen() 210 | } else if (name === "CTRL_C") { 211 | this.inputActive = false 212 | this.inputField.abort() 213 | this.term("\n^^C\n") 214 | this.prompt() 215 | } else if (name === "CTRL_D") { 216 | let input = this.inputField.getInput() 217 | if (!input.length) { 218 | this.term.nextLine() 219 | setTimeout(() => { 220 | this.close() 221 | }, 0) 222 | } 223 | } 224 | } 225 | 226 | _resize({ term } = this) { 227 | if (term) { 228 | term.stdout.emit("resize") 229 | } 230 | } 231 | 232 | _initStream() { 233 | const stream = this.stream 234 | stream.name = this.title 235 | stream.isTTY = true 236 | stream.setRawMode = () => {} 237 | stream.on("error", (error) => { 238 | console.error("SSH stream error:", error.message) 239 | }) 240 | } 241 | 242 | _initPty() { 243 | const newPty = nativePty.open(this.ptyInfo.cols, this.ptyInfo.rows) 244 | this.pty = { 245 | master_fd: newPty.master, 246 | slave_fd: newPty.slave, 247 | master: new tty.WriteStream(newPty.master), 248 | slave: new tty.ReadStream(newPty.slave), 249 | } 250 | 251 | Object.defineProperty(this.pty.slave, "columns", { 252 | enumerable: true, 253 | get: () => this.ptyInfo.cols, 254 | }) 255 | Object.defineProperty(this.pty.slave, "rows", { 256 | enumerable: true, 257 | get: () => this.ptyInfo.rows, 258 | }) 259 | 260 | this.stream.stdin.pipe(this.pty.master) 261 | this.pty.master.pipe(this.stream.stdout) 262 | } 263 | 264 | _initTerm() { 265 | const term = (this.term = termkit.createTerminal({ 266 | stdin: this.pty.slave, 267 | stdout: this.pty.slave, 268 | stderr: this.pty.slave, 269 | generic: this.ptyInfo.term, 270 | appName: this.title, 271 | isSSH: true, 272 | isTTY: true, 273 | })) 274 | 275 | term.on("key", this.onKey.bind(this)) 276 | term.windowTitle(this._interpolate(this.title)) 277 | this.clearScreen() 278 | } 279 | 280 | _interpolate(str) { 281 | let varRe = /{@(.+?)}/g 282 | let vars = { 283 | username: this.username, 284 | } 285 | 286 | let match 287 | while ((match = varRe.exec(str))) { 288 | if (vars[match[1]]) { 289 | str = str.replace(match[0], vars[match[1]]) 290 | } 291 | } 292 | 293 | return str 294 | } 295 | 296 | clearScreen() { 297 | this.term.clear() 298 | this.prompt() 299 | } 300 | 301 | prompt() { 302 | const { term } = this 303 | term.windowTitle(this._interpolate(this.title)) 304 | term.bold(this._interpolate(this.promptTxt)) 305 | 306 | if (!this.inputActive) { 307 | this.inputActive = true 308 | this.inputField = term.inputField( 309 | { 310 | history: this.cmdHistory, 311 | autoComplete: Object.keys(this.options.cmdManager.commands), 312 | autoCompleteHint: true, 313 | autoCompleteMenu: true, 314 | }, 315 | (error, input) => { 316 | this.inputActive = false 317 | term.nextLine() 318 | 319 | if (error) { 320 | return term.error(error.message || error) 321 | } 322 | 323 | if (!input) { 324 | return this.prompt() 325 | } 326 | input[0] !== " " && this.cmdHistory.push(input) 327 | 328 | if (input === "exit") { 329 | // This is delayed briefly so the newline can be echoed to the client, creating cleaner output when exiting 330 | setTimeout(this.close.bind(this)) 331 | } else if (input === "clear") { 332 | this.clearScreen() 333 | } else if (input) { 334 | this.options.cmdManager 335 | .runCmd(input, this.username, this.cmdInterface) 336 | .then((output) => { 337 | if (typeof output !== "string") { 338 | output = JSON.stringify(output, null, " ") 339 | } 340 | this.term(output) 341 | this.term.nextLine() 342 | this.prompt() 343 | }) 344 | .catch((err) => { 345 | if (typeof err !== "string") { 346 | err = err.message || JSON.stringify(err, null, " ") 347 | } 348 | this.term.red.error(err) 349 | this.term.nextLine() 350 | this.prompt() 351 | }) 352 | } else { 353 | this.prompt() 354 | } 355 | }, 356 | ) 357 | } 358 | } 359 | } 360 | 361 | export default SSHManager 362 | -------------------------------------------------------------------------------- /src/server/user-auth.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwarkentin/node-monkey/256e7b37746b030965b49a2ccbb435ed29f7128a/src/server/user-auth.js -------------------------------------------------------------------------------- /src/server/user-manager.js: -------------------------------------------------------------------------------- 1 | import fs from "fs" 2 | import scrypt from "scrypt-kdf" 3 | 4 | class UserManager { 5 | userFile 6 | userFileCache = null 7 | userFileCreated = false 8 | 9 | constructor(options) { 10 | this.userFile = options.userFile 11 | 12 | if (!options.silent) { 13 | this.getUsers().then((users) => { 14 | const usernames = Object.keys(users) 15 | if (!usernames.length) { 16 | if (process.env.NODE_ENV === "production") { 17 | console.warn( 18 | `No users have been created and you are running in production mode so you will not be able to login.\n`, 19 | ) 20 | } else { 21 | console.warn( 22 | `It seems there are no users and you are not running in production mode so you will not be able to login. This is probably a bug. Please report it!\n`, 23 | ) 24 | } 25 | } else if (usernames.length === 1 && usernames[0] === "guest") { 26 | console.warn( 27 | `[WARN] No users detected. You can login with default user 'guest' and password 'guest' when prompted.\n` + 28 | `This user will be disabled when you create a user account.\n`, 29 | ) 30 | } 31 | }) 32 | } 33 | } 34 | 35 | async getUsers() { 36 | if (this.userFileCache) { 37 | return this.userFileCache 38 | } 39 | 40 | try { 41 | if (!this.userFile) { 42 | let err = new Error(`No user file specified`) 43 | err.code = "ENOENT" 44 | throw err 45 | } 46 | 47 | this.userFileCache = JSON.parse(fs.readFileSync(this.userFile).toString("base64")) 48 | this.userFileCreated = true 49 | setTimeout(() => { 50 | this.userFileCache = null 51 | }, 5000) 52 | 53 | return this.userFileCache 54 | } catch (err) { 55 | if (err.code === "ENOENT") { 56 | return process.env.NODE_ENV === "production" 57 | ? {} 58 | : { 59 | guest: { 60 | password: 61 | "c2NyeXB0AA8AAAAIAAAAAc8D4r96lep3aBQSBeAqf0a+9MX6KyB6zKTF9Nk3ruTPIXrzy8IM7vjSLpIKuVZMNTZZ72CMqKp/PQmnyXmf7wGup1bWBGSwoV5ymA72ZzZg", 62 | }, 63 | } 64 | } 65 | throw err 66 | } 67 | } 68 | 69 | _writeFile(data) { 70 | this.userFileCache = null 71 | fs.writeFileSync(this.userFile, JSON.stringify(data, null, " ")) 72 | this.userFileCreated = true 73 | } 74 | 75 | async _hashPassword(passwd) { 76 | return (await scrypt.kdf(passwd, { logN: 15, r: 8, p: 1 })).toString("base64") 77 | } 78 | 79 | async _verifyPassword(actualPasswd, testPasswd) { 80 | return scrypt.verify(Buffer.from(actualPasswd, "base64"), testPasswd) 81 | } 82 | 83 | async createUser(username, password) { 84 | if (!this.userFile) { 85 | throw new Error(`No user file found. Did you forget to set the 'dataDir' option?`) 86 | } 87 | 88 | const users = await this.getUsers() 89 | if (users[username]) { 90 | throw new Error(`User '${username}' already exists`) 91 | } 92 | 93 | if (!this.userFileCreated) { 94 | delete users["guest"] 95 | } 96 | 97 | users[username] = { 98 | password: await this._hashPassword(password), 99 | } 100 | this._writeFile(users) 101 | } 102 | 103 | async deleteUser(username) { 104 | if (!this.userFile) { 105 | throw new Error(`No user file found. Did you forget to set the 'dataDir' option?`) 106 | } 107 | 108 | const users = await this.getUsers() 109 | if (!users[username]) { 110 | throw new Error(`User '${username}' does not exist`) 111 | } 112 | 113 | if (!this.userFileCreated) { 114 | throw new Error(`User file has not been created`) 115 | } 116 | 117 | delete users[username] 118 | this._writeFile(users) 119 | } 120 | 121 | async setPassword(username, password) { 122 | if (!this.userFile) { 123 | throw new Error(`No user file found. Did you forget to set the 'dataDir' option?`) 124 | } 125 | 126 | const users = await this.getUsers() 127 | users[username].password = await this._hashPassword(password) 128 | this._writeFile(users) 129 | } 130 | 131 | async getUserData(username) { 132 | const users = await this.getUsers() 133 | if (!users[username]) { 134 | throw new Error(`User '${username}' does not exist`) 135 | } 136 | 137 | return users[username] 138 | } 139 | 140 | async verifyUser(username, passwd) { 141 | const userData = await this.getUserData(username) 142 | return this._verifyPassword(userData.password, passwd) 143 | } 144 | } 145 | 146 | export default UserManager 147 | -------------------------------------------------------------------------------- /src/server/utils.js: -------------------------------------------------------------------------------- 1 | import commonUtils from "../lib/common-utils" 2 | import sourceMapSupport from "source-map-support" 3 | 4 | export default Object.assign( 5 | { 6 | parseCommand(str) { 7 | const reg = /"(.*?)"|'(.*?)'|`(.*?)`|([^\s"]+)/gi 8 | const arr = [] 9 | let match 10 | 11 | do { 12 | match = reg.exec(str) 13 | if (match !== null) { 14 | arr.push(match[1] || match[2] || match[3] || match[4]) 15 | } 16 | } while (match !== null) 17 | 18 | return arr 19 | }, 20 | 21 | getStack() { 22 | let prep = Error.prepareStackTrace 23 | let limit = Error.stackTraceLimit 24 | Error.prepareStackTrace = (error, trace) => trace.map(sourceMapSupport.wrapCallSite) 25 | Error.stackTraceLimit = 30 26 | 27 | let stack = new Error().stack 28 | Error.prepareStackTrace = prep 29 | Error.stackTraceLimit = limit 30 | 31 | return stack.slice(1) 32 | }, 33 | 34 | getPromiseObj() { 35 | const pobj = {} 36 | pobj.promise = new Promise((resolve, reject) => { 37 | Object.assign(pobj, { 38 | resolve, 39 | reject, 40 | }) 41 | }) 42 | return pobj 43 | }, 44 | }, 45 | commonUtils, 46 | ) 47 | -------------------------------------------------------------------------------- /webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import webpack from "webpack" 2 | import nodeExternals from "webpack-node-externals" 3 | import HtmlWebpackPlugin from "html-webpack-plugin" 4 | import TerserPlugin from "terser-webpack-plugin" 5 | 6 | export default [ 7 | { 8 | mode: process.env.NODE_ENV || "development", 9 | entry: "./src/server/index.js", 10 | output: { 11 | path: `${__dirname}/dist`, 12 | filename: `server.js`, 13 | library: "NodeMonkey", 14 | libraryExport: "default", 15 | libraryTarget: "umd", 16 | }, 17 | 18 | optimization: { 19 | minimize: true, 20 | nodeEnv: false, 21 | minimizer: [new TerserPlugin()], 22 | }, 23 | 24 | target: "node", 25 | externals: [nodeExternals()], 26 | node: { 27 | __filename: false, 28 | __dirname: false, 29 | }, 30 | 31 | module: { 32 | rules: [ 33 | { 34 | test: /\.m?js$/, 35 | exclude: /node_modules/, 36 | use: ["babel-loader"], 37 | }, 38 | ], 39 | }, 40 | 41 | plugins: [ 42 | new webpack.BannerPlugin({ 43 | banner: "require('source-map-support').install();", 44 | raw: true, 45 | }), 46 | ], 47 | 48 | devtool: "source-map", 49 | }, 50 | { 51 | mode: process.env.NODE_ENV || "development", 52 | entry: "./src/client/index.js", 53 | output: { 54 | path: `${__dirname}/dist`, 55 | filename: `monkey.js`, 56 | library: "NodeMonkey", 57 | libraryExport: "default", 58 | libraryTarget: "umd", 59 | }, 60 | 61 | optimization: { 62 | minimize: true, 63 | minimizer: [new TerserPlugin()], 64 | }, 65 | 66 | module: { 67 | rules: [ 68 | { 69 | test: /\.m?js$/, 70 | exclude: /node_modules/, 71 | use: ["babel-loader"], 72 | }, 73 | ], 74 | }, 75 | 76 | plugins: [ 77 | new HtmlWebpackPlugin({ 78 | title: "NodeMonkey Client Test", 79 | inject: "head", 80 | scriptLoading: "blocking", 81 | template: `${__dirname}/src/client/index.html`, 82 | }), 83 | ], 84 | 85 | devtool: "source-map", 86 | }, 87 | ] 88 | --------------------------------------------------------------------------------