├── .gitignore ├── .versions ├── LICENSE ├── README.md ├── package.js └── server ├── main.js ├── shell_client.js └── shell_server.js /.gitignore: -------------------------------------------------------------------------------- 1 | .npm -------------------------------------------------------------------------------- /.versions: -------------------------------------------------------------------------------- 1 | babel-compiler@7.1.1 2 | babel-runtime@1.2.2 3 | dynamic-import@0.4.1 4 | ecmascript@0.11.1 5 | ecmascript-runtime@0.7.0 6 | ecmascript-runtime-client@0.7.1 7 | ecmascript-runtime-server@0.7.0 8 | http@1.4.1 9 | meteor@1.9.0 10 | modern-browsers@0.1.1 11 | modules@0.12.0 12 | modules-runtime@0.10.0 13 | promise@0.11.1 14 | qualia:prod-shell@0.0.3 15 | shell-server@0.3.1 16 | url@1.2.0 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Lucas Hansen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # qualia:prod-shell 2 | 3 | This package allows you to run the Meteor shell in production. Just SSH into the server and run `node ~/meteor-shell.js`. 4 | 5 | If the Meteor server is running as a user different than the SSH user, go to the home directory of that user, and run `node meteor-shell.js`. 6 | 7 | This package only exposes the Meteor shell to users with production SSH access. Security implications are minimal; if someone can SSH into your production server you have bigger problems. 8 | 9 | This package was developed as a part of the more ambitious `qualia:web-shell` package which allows access to the Meteor shell inside of the *browser* (both in dev and production)! 10 | 11 | ## Installing 12 | 13 | `meteor add qualia:prod-shell` 14 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'qualia:prod-shell', 3 | version: '0.0.3', 4 | summary: 'Enable Meteor shell in production', 5 | git: 'https://github.com/qualialabs/prod-shell', 6 | documentation: 'README.md', 7 | }); 8 | 9 | Package.onUse(function(api) { 10 | api.versionsFrom('METEOR@1.4'); 11 | 12 | api.use([ 13 | 'ecmascript', 14 | 'shell-server@0.2.1', 15 | ], 'server'); 16 | 17 | api.addAssets([ 18 | 'server/shell_client.js', 19 | ], 'server'); 20 | 21 | api.mainModule('server/main.js', 'server'); 22 | }); 23 | -------------------------------------------------------------------------------- /server/main.js: -------------------------------------------------------------------------------- 1 | import { MeteorShell } from './shell_server.js'; 2 | 3 | MeteorShell.ensureShellServer(); 4 | 5 | export { MeteorShell }; 6 | -------------------------------------------------------------------------------- /server/shell_client.js: -------------------------------------------------------------------------------- 1 | // Fork of https://github.com/meteor/meteor/devel/tools/shell-client.js 2 | 3 | // This allows us to access the Meteor shell without having Meteor 4 | // installed (as is the case in a deployed Meteor application). 5 | // All NPM dependencies have been removed. 6 | 7 | var assert = require("assert"); 8 | var fs = require("fs"); 9 | var path = require("path"); 10 | var net = require("net"); 11 | var EOL = require("os").EOL; 12 | 13 | // These two values (EXITING_MESSAGE and getInfoFile) must match the 14 | // values used by the shell-server package. 15 | var EXITING_MESSAGE = "Shell exiting..."; 16 | function getInfoFile(shellDir) { 17 | return path.join(shellDir, "info.json"); 18 | } 19 | 20 | function Client(shellDir) { 21 | var self = this; 22 | assert.ok(self instanceof Client); 23 | 24 | self.shellDir = shellDir; 25 | self.exitOnClose = false; 26 | self.firstTimeConnecting = true; 27 | self.connected = false; 28 | self.reconnectCount = 0; 29 | } 30 | 31 | var Cp = Client.prototype; 32 | 33 | Cp.reconnect = function reconnect(delay) { 34 | var self = this; 35 | 36 | // Display the "Server unavailable" warning only on the third attempt 37 | // to reconnect, so it doesn't get shown for successful reconnects. 38 | if (++self.reconnectCount === 3) { 39 | console.error("\x1b[33mServer unavailable (waiting to reconnect)\x1b[0m"); 40 | } 41 | 42 | if (!self.reconnectTimer) { 43 | self.reconnectTimer = setTimeout(function() { 44 | delete self.reconnectTimer; 45 | self.connect(); 46 | }, delay || 100); 47 | } 48 | }; 49 | 50 | Cp.connect = function connect() { 51 | var self = this; 52 | var infoFile = getInfoFile(self.shellDir); 53 | 54 | fs.readFile(infoFile, "utf8", function(err, json) { 55 | if (err) { 56 | return self.reconnect(); 57 | } 58 | 59 | try { 60 | var info = JSON.parse(json); 61 | } catch (err) { 62 | return self.reconnect(); 63 | } 64 | 65 | if (info.status !== "enabled") { 66 | if (self.firstTimeConnecting) { 67 | return self.reconnect(); 68 | } 69 | 70 | if (info.reason) { 71 | console.error(info.reason); 72 | } 73 | 74 | console.error(EXITING_MESSAGE); 75 | process.exit(0); 76 | } 77 | 78 | self.setUpSocket( 79 | net.connect(info.port, "127.0.0.1"), 80 | info.key 81 | ); 82 | }); 83 | }; 84 | 85 | Cp.setUpSocketForSingleUse = function (sock, key) { 86 | sock.on("connect", function () { 87 | const inputBuffers = []; 88 | process.stdin.on("data", buffer => inputBuffers.push(buffer)); 89 | process.stdin.on("end", () => { 90 | sock.write(JSON.stringify({ 91 | evaluateAndExit: { 92 | // Make sure the entire command is written as a string within a 93 | // JSON object, so that the server can easily tell when it has 94 | // received the whole command. 95 | command: Buffer.concat(inputBuffers).toString("utf8") 96 | }, 97 | terminal: false, 98 | key: key 99 | }) + "\n"); 100 | }); 101 | }); 102 | 103 | const outputBuffers = []; 104 | sock.on("data", buffer => outputBuffers.push(buffer)); 105 | sock.on("close", function () { 106 | var output = JSON.parse(Buffer.concat(outputBuffers)); 107 | if (output.error) { 108 | console.error(output.error); 109 | process.exit(output.code); 110 | } else { 111 | process.stdout.write(JSON.stringify(output.result) + "\n"); 112 | process.exit(0); 113 | } 114 | }); 115 | }; 116 | 117 | Cp.setUpSocket = function setUpSocket(sock, key) { 118 | const self = this; 119 | 120 | if (! process.stdin.isTTY) { 121 | return self.setUpSocketForSingleUse(sock, key); 122 | } 123 | 124 | // Put STDIN into "flowing mode": 125 | // http://nodejs.org/api/stream.html#stream_compatibility_with_older_node_versions 126 | process.stdin.resume(); 127 | 128 | function onConnect() { 129 | self.firstTimeConnecting = false; 130 | self.reconnectCount = 0; 131 | self.connected = true; 132 | 133 | // Sending a JSON-stringified options object (even just an empty 134 | // object) over the socket is required to start the REPL session. 135 | sock.write(JSON.stringify({ 136 | columns: process.stdout.columns, 137 | terminal: ! process.env.EMACS, 138 | key: key 139 | }) + "\n"); 140 | 141 | process.stdin.pipe(sock); 142 | if (process.stdin.setRawMode) { // https://github.com/joyent/node/issues/8204 143 | process.stdin.setRawMode(true); 144 | } 145 | } 146 | 147 | function onClose() { 148 | tearDown(); 149 | 150 | // If we received the special EXITING_MESSAGE just before the socket 151 | // closed, then exit the shell instead of reconnecting. 152 | if (self.exitOnClose) { 153 | process.exit(0); 154 | } else { 155 | self.reconnect(); 156 | } 157 | } 158 | 159 | function onError(err) { 160 | tearDown(); 161 | self.reconnect(); 162 | } 163 | 164 | function tearDown() { 165 | self.connected = false; 166 | if (process.stdin.setRawMode) { // https://github.com/joyent/node/issues/8204 167 | process.stdin.setRawMode(false); 168 | } 169 | process.stdin.unpipe(sock); 170 | sock.unpipe(process.stdout); 171 | sock.removeListener("connect", onConnect); 172 | sock.removeListener("close", onClose); 173 | sock.removeListener("error", onError); 174 | sock.end(); 175 | } 176 | 177 | sock.pipe(process.stdout); 178 | 179 | var tail = ""; 180 | sock.on("data", function(data) { 181 | tail = (tail + data).slice(-20); 182 | self.exitOnClose = tail.indexOf(EXITING_MESSAGE) >= 0; 183 | }); 184 | 185 | sock.on("connect", onConnect); 186 | sock.on("close", onClose); 187 | sock.on("error", onError); 188 | }; 189 | 190 | new Client(process.env.METEOR_SHELL_DIR).connect() 191 | -------------------------------------------------------------------------------- /server/shell_server.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import os from 'os'; 4 | 5 | let MeteorShell = { 6 | 7 | initialize() { 8 | return this; 9 | }, 10 | 11 | ensureShellServer() { 12 | try { 13 | if (process.env.METEOR_SHELL_DIR) { 14 | this.shellDir = process.env.METEOR_SHELL_DIR; 15 | } 16 | else { 17 | this.shellDir = process.env.METEOR_SHELL_DIR = this.makeShellDir(); 18 | this.listen(this.shellDir); 19 | } 20 | 21 | this.createShellClient(); 22 | } 23 | catch(e) { 24 | console.error('qualia:prod-shell - Failed to start Meteor shell.', e.stack || e); 25 | } 26 | }, 27 | 28 | makeShellDir() { 29 | let folderName = '.meteor-shell', 30 | folderPath = path.join(process.cwd(), folderName) 31 | ; 32 | 33 | if (!fs.existsSync(folderPath)) { 34 | fs.mkdirSync(folderPath); 35 | } 36 | 37 | return folderPath; 38 | }, 39 | 40 | // HACK: If the shell server isn't already running, we may need 41 | // to start it. However, the shell-server package doesn't export 42 | // a function for starting it. So... we dig into the implementation 43 | // of ES6 modules in Meteor to get ahold of it anyways. 44 | // See https://github.com/qualialabs/reval/blob/master/packages/qualia_reval/modules.js 45 | listen(shellDir) { 46 | let rootModule = module; 47 | while (rootModule.parent) { 48 | rootModule = rootModule.parent; 49 | } 50 | 51 | let { listen } = rootModule 52 | .children.find(m => m.id === '/node_modules/meteor/shell-server/main.js') 53 | .children.find(m => m.id === '/node_modules/meteor/shell-server/shell-server.js') 54 | .exports 55 | ; 56 | 57 | listen(shellDir); 58 | }, 59 | 60 | shellClientPath() { 61 | if (process.env.METEOR_SHELL_CLIENT) { 62 | return process.env.METEOR_SHELL_CLIENT; 63 | } 64 | 65 | let folder = Meteor.isDevelopment 66 | ? process.cwd() 67 | : os.homedir() 68 | ; 69 | 70 | return path.join( 71 | folder, 72 | 'meteor-shell.js' 73 | ); 74 | }, 75 | 76 | createShellClient() { 77 | let shellClientFile = Assets.getText('server/shell_client.js'); 78 | shellClientFile = `process.env.METEOR_SHELL_DIR = '${this.shellDir}';\n\n` + shellClientFile; 79 | shellClientFile = this.transpile(shellClientFile); 80 | 81 | fs.writeFileSync(this.shellClientPath(), shellClientFile); 82 | }, 83 | 84 | transpile(code) { 85 | process.env.BABEL_CACHE_DIR = process.env.BABEL_CACHE_DIR || process.cwd(); 86 | return Package.ecmascript.ECMAScript.compileForShell(code); 87 | }, 88 | 89 | }.initialize(); 90 | 91 | export { MeteorShell }; 92 | --------------------------------------------------------------------------------