diff --git a/lib/API.js b/lib/API.js index ff89d5e15..8424ef79a 100644 --- a/lib/API.js +++ b/lib/API.js @@ -1642,7 +1642,7 @@ require('./API/Extra.js')(API); require('./API/Interaction.js')(API); require('./API/Deploy.js')(API); require('./API/Modules/Modules.js')(API); -require('./API/Keymetrics/cli-api.js')(API); +require('./API/PM2/PM2IO.js')(API); require('./API/Configuration.js')(API); require('./API/Version.js')(API); require('./API/Startup.js')(API); diff --git a/lib/API/Keymetrics/cli-api.js b/lib/API/Keymetrics/cli-api.js deleted file mode 100644 index e6561e7dd..000000000 --- a/lib/API/Keymetrics/cli-api.js +++ /dev/null @@ -1,253 +0,0 @@ -var cst = require('../../../constants.js'); -var Common = require('../../Common.js'); -var UX = require('../CliUx'); -var chalk = require('chalk'); -var async = require('async'); -var path = require('path'); -var fs = require('fs'); -var KMDaemon = require('@pm2/agent/src/InteractorClient'); -var Table = require('cli-table-redemption'); -var open = require('../../tools/open.js'); -var promptly = require('promptly'); -var pkg = require('../../../package.json') - -module.exports = function(CLI) { - - CLI.prototype.openDashboard = function() { - var that = this; - - KMDaemon.getInteractInfo(this._conf, function(err, data) { - if (err) { - Common.printError(chalk.bold.white('Agent if offline, type `$ pm2 register` to log in')); - return that.exitCli(cst.ERROR_EXIT); - } - Common.printOut(chalk.bold('Opening Dashboard in Browser...')); - open('https://app.pm2.io/#/r/' + data.public_key); - setTimeout(function() { - that.exitCli(); - }, 200); - }); - }; - - CLI.prototype.loginToKM = function() { - printMotd(); - return loginPrompt(); - }; - - CLI.prototype.registerToKM = function() { - printMotd(); - - promptly.confirm(chalk.bold('Do you have a pm2.io account? (y/n)'), function (err, answer) { - if (answer == true) { - return loginPrompt(); - } - registerPrompt(); - }); - }; - - /** - * Monitor Selectively Processes (auto filter in interaction) - * @param String state 'monitor' or 'unmonitor' - * @param String target - * @param Function cb callback - */ - CLI.prototype.monitorState = function(state, target, cb) { - var that = this; - - if (process.env.NODE_ENV !== 'test') { - try { - fs.statSync(this._conf.INTERACTION_CONF); - } catch(e) { - printMotd(); - return registerPrompt(); - } - } - - if (!target) { - Common.printError(cst.PREFIX_MSG_ERR + 'Please specify an '); - return cb ? cb(new Error('argument missing')) : that.exitCli(cst.ERROR_EXIT); - } - - function monitor (pm_id, cb) { - // State can be monitor or unmonitor - that.Client.executeRemote(state, pm_id, cb); - } - if (target === 'all') { - that.Client.getAllProcessId(function (err, procs) { - if (err) { - Common.printError(err); - return cb ? cb(Common.retErr(err)) : that.exitCli(cst.ERROR_EXIT); - } - async.forEachLimit(procs, 1, monitor, function (err, res) { - return typeof cb === 'function' ? cb(err, res) : that.speedList(); - }); - }); - } else if (!Number.isInteger(parseInt(target))) { - this.Client.getProcessIdByName(target, true, function (err, procs) { - if (err) { - Common.printError(err); - return cb ? cb(Common.retErr(err)) : that.exitCli(cst.ERROR_EXIT); - } - async.forEachLimit(procs, 1, monitor, function (err, res) { - return typeof cb === 'function' ? cb(err, res) : that.speedList(); - }); - }); - } else { - monitor(parseInt(target), function (err, res) { - return typeof cb === 'function' ? cb(err, res) : that.speedList(); - }); - } - }; - - - /** - * Private Functions - */ - - function printMotd() { - var dt = fs.readFileSync(path.join(__dirname, 'motd')); - console.log(dt.toString()); - } - - function validateEmail(email) { - var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; - if (re.test(email) == false) - throw new Error('Not an email'); - return email; - } - - function validateUsername(value) { - if (value.length < 6) { - throw new Error('Min length of 6'); - } - return value; - }; - - - function linkOpenExit(target_bucket) { - KMDaemon.launchAndInteract(cst, { - public_key : target_bucket.public_id, - secret_key : target_bucket.secret_id, - pm2_version: pkg.version - }, function(err, dt) { - open('https://app.pm2.io/#/r/' + target_bucket.public_id); - setTimeout(function() { - process.exit(cst.SUCCESS_EXIT); - }, 100); - }); - } - - /** - * Login on Keymetrics - * Link to the only bucket or list bucket for selection - * Open Browser - */ - function loginPrompt(cb) { - var KM = require('./kmapi.js'); - console.log(chalk.bold('Log in to pm2.io')); - (function retry() { - promptly.prompt('Username or Email: ', function(err, username) { - promptly.password('Password: ', { replace : '*' }, function(err, password) { - KM.loginAndGetAccessToken({ username : username, password: password }, function(err) { - if (err) { - console.error(chalk.red.bold(err) + '\n'); - return retry(); - } - KM.getBuckets(function(err, buckets) { - if (err) { - console.error(chalk.red.bold(err) + '\n'); - return retry(); - } - - if (buckets.length > 1) { - console.log(chalk.bold('Bucket list')); - - var table = new Table({ - style : {'padding-left' : 1, head : ['cyan', 'bold'], compact : true}, - head : ['Bucket name', 'Plan type'] - }); - - buckets.forEach(function(bucket) { - table.push([bucket.name, bucket.credits.offer_type]); - }); - - console.log(table.toString()); - - (function retryInsertion() { - promptly.prompt('Type the bucket you want to link to: ', function(err, bucket_name) { - var target_bucket = null; - - buckets.some(function(bucket) { - if (bucket.name == bucket_name) { - target_bucket = bucket; - return true; - } - }); - - if (target_bucket == null) - return retryInsertion(); - linkOpenExit(target_bucket); - }); - })(); - } - else { - var target_bucket = buckets[0]; - console.log('Connecting local PM2 Runtime to pm2.io, bucket name= [%s]', target_bucket.name); - - KMDaemon.launchAndInteract(cst, { - public_key : target_bucket.public_id, - secret_key : target_bucket.secret_id, - pm2_version: pkg.version - }, function(err, dt) { - linkOpenExit(target_bucket); - }); - } - }); - }); - - }); - }) - })() - } - - /** - * Register on Keymetrics - * Create Bucket - * Auto Link local PM2 to new Bucket - * Open Browser for access to monitoring dashboard - */ - function registerPrompt() { - var KM = require('./kmapi.js'); - - console.log(chalk.bold('Now registering to pm2.io')); - promptly.prompt('Username: ', { - validator : validateUsername, - retry : true - }, function(err, username) { - promptly.prompt('Email: ', { - validator : validateEmail, - retry : true - }, function(err, email) { - promptly.password('Password: ', { replace : '*' }, function(err, password) { - process.stdout.write(chalk.bold('Creating account on pm2.io ...')); - var inter = setInterval(function() { - process.stdout.write('.'); - }, 300); - KM.fullCreationFlow({ - email : email, - password : password, - username : username - }, function(err, target_bucket) { - clearInterval(inter); - if (err) { - console.error('\n' + chalk.red.bold(err) + '\n'); - return registerPrompt(); - } - linkOpenExit(target_bucket); - }); - }); - }); - }) - } - -}; diff --git a/lib/API/Keymetrics/kmapi.js b/lib/API/Keymetrics/kmapi.js deleted file mode 100644 index f38431e9c..000000000 --- a/lib/API/Keymetrics/kmapi.js +++ /dev/null @@ -1,164 +0,0 @@ - -var fs = require('fs'); -var needle = require('needle'); -var url = require('url'); -var cst = require('../../../constants.js'); - -var KM = function() { - this.AUTH_URI = 'https://id.keymetrics.io'; - this.BASE_URI = 'https://app.keymetrics.io'; - this.CLIENT_ID = '938758711'; - this.CB_URI = 'https://app.keymetrics.io'; - this.ACCESS_TOKEN_FILE = cst.KM_ACCESS_TOKEN; - this.access_token = null; -} - -/** - * @param user_info.username - * @param user_info.password - * @return promise - */ -KM.prototype.loginAndGetAccessToken = function (user_info, cb) { - var querystring = require('querystring'); - - var that = this; - var URL_AUTH = '/api/oauth/authorize?response_type=token&scope=all&client_id=' + - that.CLIENT_ID + '&redirect_uri=' + that.CB_URI; - - needle.get(that.AUTH_URI + URL_AUTH, function(err, res) { - if (err) return cb(err); - - var cookie = res.cookies; - - needle.post(that.AUTH_URI + '/api/oauth/login', user_info, { - cookies : cookie - }, function(err, resp, body) { - if (err) return cb(err); - if (resp.statusCode != 200) return cb('Wrong credentials'); - - var location = resp.headers['x-redirect']; - var redirect = that.AUTH_URI + location; - - needle.get(redirect, { - cookies : cookie - }, function(err, res) { - if (err) return cb(err); - var refresh_token = querystring.parse(url.parse(res.headers.location).query).access_token; - - needle.post(that.AUTH_URI + '/api/oauth/token', { - client_id : that.CLIENT_ID, - grant_type : 'refresh_token', - refresh_token : refresh_token, - scope : 'all' - }, function(err, res, body) { - if (err) return cb(err); - that.access_token = body.access_token; - return cb(null, body.access_token); - }) - }); - }); - }); -} - -KM.prototype.getLocalAccessToken = function(cb) { - var that = this; - - fs.readFile(that.ACCESS_TOKEN_FILE, function(e, content) { - if (e) return cb(e); - cb(null, content.toString()); - }); -}; - -KM.prototype.saveLocalAccessToken = function(access_token, cb) { - var that = this; - fs.writeFile(that.ACCESS_TOKEN_FILE, access_token, function(e, content) { - if (e) return cb(e); - cb(); - }); -}; - -KM.prototype.getBuckets = function(cb) { - var that = this; - - needle.get(that.BASE_URI + '/api/bucket', { - headers : { - 'Authorization' : 'Bearer ' + that.access_token - }, - json : true - }, function(err, res, body) { - if (err) return cb(err); - return cb(null, body); - }); -} - -/** - * @param user_info.username - * @param user_info.password - * @param user_info.email - * @return promise - */ -KM.prototype.register = function(user_info, cb) { - var that = this; - - needle.post(that.BASE_URI + '/api/oauth/register', user_info, { - json: true, - headers: { - 'X-Register-Provider': 'pm2-register' - } - }, function (err, res, body) { - if (err) return cb(err); - if (body.email && body.email.message) return cb(body.email.message); - if (body.username && body.username.message) return cb(body.username.message); - - cb(null, { - token : body.access_token.token - }) - }); -}; - -KM.prototype.defaultNode = function(cb) { - var that = this; - - needle.get(that.BASE_URI + '/api/node/default', function(err, res, body) { - if (err) return cb(err); - cb(null, url.parse(body.endpoints.web).protocol + '//' + url.parse(body.endpoints.web).hostname); - }); -} - - -KM.prototype.createBucket = function(default_node, bucket_name, cb) { - var that = this; - - needle.post(default_node + '/api/bucket/create_classic', { - name : bucket_name - }, { - json : true, - headers : { - 'Authorization' : 'Bearer ' + that.access_token - } - }, function(err, res, body) { - if (err) return cb(err); - cb(null, body); - }); -} - -KM.prototype.fullCreationFlow = function(user_info, cb) { - var that = this; - - this.register(user_info, function(err, dt) { - if (err) return cb(err); - that.access_token = dt.token; - that.defaultNode(function(err, default_node) { - if (err) return cb(err); - that.createBucket(default_node, 'Node Monitoring', function(err, packet) { - if (err) return cb(err); - return cb(null, { - secret_id : packet.bucket.secret_id, - public_id : packet.bucket.public_id - }); - }); - }) - }); -} - -module.exports = new KM; diff --git a/lib/API/PM2/CliAuth.js b/lib/API/PM2/CliAuth.js new file mode 100644 index 000000000..17e74d8b6 --- /dev/null +++ b/lib/API/PM2/CliAuth.js @@ -0,0 +1,285 @@ + + +'use strict' + +const AuthStrategy = require('@pm2/js-api/src/auth_strategies/strategy') + +const http = require('http') +const fs = require('fs') +const url = require('url') +const exec = require('child_process').exec +const async = require('async') +const path = require('path') +const os = require('os') +const needle = require('needle'); +const chalk = require('chalk') +const cst = require('../../../constants.js'); + +module.exports = class CustomStrategy extends AuthStrategy { + // the client will try to call this but we handle this part ourselves + retrieveTokens (km, cb) { + this.authenticated = false + this.callback = cb + this.km = km + this.BASE_URI = 'https://app.keymetrics.io'; + } + + // so the cli know if we need to tell user to login/register + isAuthenticated () { + return new Promise((resolve, reject) => { + if (this.authenticated) return resolve(true) + + let tokensPath = cst.PM2_IO_ACCESS_TOKEN + fs.readFile(tokensPath, (err, tokens) => { + if (err && err.code === 'ENOENT') return resolve(false) + if (err) return reject(err) + + // verify that the token is valid + try { + tokens = JSON.parse(tokens || '{}') + } catch (err) { + fs.unlinkSync(tokensPath) + return resolve(false) + } + + // if the refresh tokens is here, the user could be automatically authenticated + return resolve(typeof tokens.refresh_token === 'string') + }) + }) + } + + verifyToken (refresh) { + return this.km.auth.retrieveToken({ + client_id: this.client_id, + refresh_token: refresh + }) + } + + // called when we are sure the user asked to be logged in + _retrieveTokens (optionalCallback) { + const km = this.km + const cb = this.callback + + async.tryEach([ + // try to find the token via the environement + (next) => { + if (!process.env.KM_TOKEN) { + return next(new Error('No token in env')) + } + this.verifyToken(process.env.KM_TOKEN) + .then((res) => { + return next(null, res.data) + }).catch(next) + }, + // try to find it in the file system + (next) => { + return next(new Error('nope')) + + fs.readFile(cst.PM2_IO_ACCESS_TOKEN, (err, tokens) => { + if (err) return next(err) + + // verify that the token is valid + tokens = JSON.parse(tokens || '{}') + if (new Date(tokens.expire_at) > new Date(new Date().toISOString())) { + return next(null, tokens) + } + + this.verifyToken(tokens.refresh_token) + .then((res) => { + return next(null, res.data) + }).catch(next) + }) + }, + // otherwise make the whole flow + (next) => { + return this.loginViaCLI((data) => { + // verify that the token is valid + this.verifyToken(data.refresh_token) + .then((res) => { + return next(null, res.data) + }).catch(next) + }) + } + ], (err, result) => { + // if present run the optional callback + if (typeof optionalCallback === 'function') { + optionalCallback(err, result) + } + + if (result.refresh_token) { + this.authenticated = true + let file = cst.PM2_IO_ACCESS_TOKEN + fs.writeFile(file, JSON.stringify(result), () => { + return cb(err, result) + }) + } else { + return cb(err, result) + } + }) + } + + loginViaCLI (cb) { + var promptly = require('promptly'); + + let retry = () => { + promptly.prompt('Username or Email: ', (err, username) => { + if (err) return retry(); + + promptly.password('Password: ', { replace : '*' }, (err, password) => { + if (err) return retry(); + + this._loginUser({ + username: username, + password: password + }, (err, data) => { + if (err) return retry() + cb(data) + }) + }) + }) + } + + retry() + } + + _loginUser (user_info, cb) { + const querystring = require('querystring'); + const AUTH_URI = 'https://id.keymetrics.io' + const URL_AUTH = '/api/oauth/authorize?response_type=token&scope=all&client_id=' + + this.client_id + '&redirect_uri=https://app.keymetrics.io'; + + console.log(chalk.bold('[-] Logging to pm2.io')) + + needle.get(AUTH_URI + URL_AUTH, (err, res) => { + if (err) return cb(err); + + var cookie = res.cookies; + + needle.post(AUTH_URI + '/api/oauth/login', user_info, { + cookies : cookie + }, (err, resp, body) => { + if (err) return cb(err); + if (resp.statusCode != 200) return cb('Wrong credentials'); + + var location = resp.headers['x-redirect']; + var redirect = AUTH_URI + location; + + needle.get(redirect, { + cookies : cookie + }, (err, res) => { + if (err) return cb(err); + var refresh_token = querystring.parse(url.parse(res.headers.location).query).access_token; + needle.post(AUTH_URI + '/api/oauth/token', { + client_id : this.client_id, + grant_type : 'refresh_token', + refresh_token : refresh_token, + scope : 'all' + }, (err, res, body) => { + if (err) return cb(err); + console.log(chalk.bold.green('[+] Logged in!')) + return cb(null, body); + }) + }); + }); + }); + } + + registerViaCLI (cb) { + var promptly = require('promptly'); + console.log(chalk.bold('[-] Registering to pm2.io')); + + var retry = () => { + promptly.prompt('Username: ', { + validator : this._validateUsername, + retry : true + }, (err, username) => { + promptly.prompt('Email: ', { + validator : this._validateEmail, + retry : true + },(err, email) => { + promptly.password('Password: ', { replace : '*' }, (err, password) => { + process.stdout.write('Creating account on pm2.io...'); + + var inter = setInterval(function() { + process.stdout.write('.'); + }, 200); + + this._registerUser({ + email : email, + password : password, + username : username + }, (err, data) => { + clearInterval(inter) + if (err) { + console.error() + console.error(chalk.bold.red(err)); + return retry() + } + console.log(chalk.green.bold('\n[+] Account created!')) + + this._loginUser({ + username: username, + password: password + }, (err, data) => { + this.callback(err, data) + return cb(err, data) + }) + }) + }); + }); + }) + } + retry(); + } + + /** + * Register function + * @param user_info.username + * @param user_info.password + * @param user_info.email + */ + _registerUser (user_info, cb) { + needle.post(this.BASE_URI + '/api/oauth/register', user_info, { + json: true, + headers: { + 'X-Register-Provider': 'pm2-register' + } + }, function (err, res, body) { + if (err) return cb(err); + if (body.email && body.email.message) return cb(body.email.message); + if (body.username && body.username.message) return cb(body.username.message); + if (!body.access_token) return cb(body.msg) + + cb(null, { + token : body.refresh_token.token + }) + }); + } + + _validateEmail (email) { + var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + if (re.test(email) == false) + throw new Error('Not an email'); + return email; + } + + _validateUsername (value) { + if (value.length < 6) { + throw new Error('Min length of 6'); + } + return value; + }; + + deleteTokens (km) { + return new Promise((resolve, reject) => { + // revoke the refreshToken + km.auth.revoke() + .then(res => { + // remove the token from the filesystem + let file = cst.PM2_IO_ACCESS_TOKEN + fs.unlinkSync(file) + return resolve(res) + }).catch(reject) + }) + } +} diff --git a/lib/API/PM2/PM2IO.js b/lib/API/PM2/PM2IO.js new file mode 100644 index 000000000..090480713 --- /dev/null +++ b/lib/API/PM2/PM2IO.js @@ -0,0 +1,245 @@ +'use strict' + +var cst = require('../../../constants.js'); +var Common = require('../../Common.js'); +var KMDaemon = require('@pm2/agent/src/InteractorClient'); + +const chalk = require('chalk'); +const async = require('async'); +const path = require('path'); +const fs = require('fs'); +const Table = require('cli-table-redemption'); +const open = require('../../tools/open.js'); +const pkg = require('../../../package.json') +const IOAPI = require('@pm2/js-api') + + +// const CustomStrategy = require('./custom_auth') +// const strategy = new CustomStrategy({ +// client_id: '7412235273' +// }) + +const CLIAuth = require('./CliAuth') + +const CLIAuthStrategy = new CLIAuth({ + client_id: '938758711' +}) + +const io = new IOAPI().use(CLIAuthStrategy) + +module.exports = function(CLI) { + + CLI.prototype.openDashboard = function() { + KMDaemon.getInteractInfo(this._conf, (err, data) => { + if (err) { + Common.printError(chalk.bold.white('Agent if offline, type `$ pm2 register` to log in')); + return this.exitCli(cst.ERROR_EXIT); + } + Common.printOut(chalk.bold('Opening Dashboard in Browser...')); + open('https://app.pm2.io/#/r/' + data.public_key); + setTimeout(_ => { + this.exitCli(); + }, 200); + }); + }; + + CLI.prototype.loginToKM = function() { + var promptly = require('promptly') + printMotd(); + + return CLIAuthStrategy._retrieveTokens((err, tokens) => { + if (err) { + console.error(`Oups, a error happened : ${err}`) + process.exit(1) + } + + // query both the user and all bucket + Promise.all([ io.user.retrieve(), io.bucket.retrieveAll() ]) + .then(results => { + let user = results[0].data + let buckets = results[1].data + + if (buckets.length > 1) { + var table = new Table({ + style : {'padding-left' : 1, head : ['cyan', 'bold'], compact : true}, + head : ['Bucket name', 'Plan type'] + }); + + buckets.forEach(function(bucket) { + table.push([bucket.name, bucket.credits.offer_type]); + }); + + console.log(table.toString()); + + (function retryInsertion() { + promptly.prompt('Type the bucket you want to link to: ', function(err, bucket_name) { + var target_bucket = null; + + buckets.some(function(bucket) { + if (bucket.name == bucket_name) { + target_bucket = bucket; + return true; + } + }); + + if (target_bucket == null) + return retryInsertion(); + linkOpenExit(target_bucket); + }); + })(); + } + else { + var target_bucket = buckets[0]; + linkOpenExit(target_bucket) + } + }).catch(err => { + console.error(chalk.bold.red(`Oups, a error happened : ${err}`)) + return process.exit(1) + }) + }) + }; + + CLI.prototype.registerToKM = function() { + var promptly = require('promptly'); + + promptly.confirm(chalk.bold('Do you have a pm2.io account? (y/n)'), (err, answer) => { + if (answer == true) { + return this.loginToKM(); + } + CLIAuthStrategy.registerViaCLI((err, data) => { + console.log('[-] Creating Bucket...') + + io.bucket.create({ + name: 'Node.JS Monitoring' + }).then(res => { + const bucket = res.data.bucket + console.log(chalk.bold.green('[+] Bucket created!')) + linkOpenExit(bucket) + }) + }) + }); + } + + CLI.prototype.logoutToKM = function () { + CLIAuthStrategy._retrieveTokens(_ => { + io.auth.logout() + .then(res => { + console.log(`- Logout successful`) + return process.exit(0) + }).catch(err => { + console.error(`Oups, a error happened : ${err.message}`) + return process.exit(1) + }) + }) + } + + CLI.prototype.connectToPM2IO = function() { + io.bucket.create({ + name: 'Node.JS Monitoring' + }).then(res => { + const bucket = res.data.bucket + console.log(`Succesfully created a bucket !`) + console.log(`To start using it, you should push data with : + pm2 link ${bucket.secret_id} ${bucket.public_id} + `) + console.log(`You can also access our dedicated UI by going here : + https://app.pm2.io/#/r/${bucket.public_id} + `) + + KMDaemon.launchAndInteract(cst, { + public_key : bucket.public_id, + secret_key : bucket.secret_id + }, function(err, dt) { + open(`https://app.pm2.io/#/r/${bucket.public_id}`); + setTimeout(_ => { + return process.exit(0) + }, 200) + }); + + }) + } + + /** + * Monitor Selectively Processes (auto filter in interaction) + * @param String state 'monitor' or 'unmonitor' + * @param String target + * @param Function cb callback + */ + CLI.prototype.monitorState = function(state, target, cb) { + var that = this; + + if (process.env.NODE_ENV !== 'test') { + try { + fs.statSync(this._conf.INTERACTION_CONF); + } catch(e) { + printMotd(); + return this.registerToKM(); + } + } + + if (!target) { + Common.printError(cst.PREFIX_MSG_ERR + 'Please specify an '); + return cb ? cb(new Error('argument missing')) : that.exitCli(cst.ERROR_EXIT); + } + + function monitor (pm_id, cb) { + // State can be monitor or unmonitor + that.Client.executeRemote(state, pm_id, cb); + } + if (target === 'all') { + that.Client.getAllProcessId(function (err, procs) { + if (err) { + Common.printError(err); + return cb ? cb(Common.retErr(err)) : that.exitCli(cst.ERROR_EXIT); + } + async.forEachLimit(procs, 1, monitor, function (err, res) { + return typeof cb === 'function' ? cb(err, res) : that.speedList(); + }); + }); + } else if (!Number.isInteger(parseInt(target))) { + this.Client.getProcessIdByName(target, true, function (err, procs) { + if (err) { + Common.printError(err); + return cb ? cb(Common.retErr(err)) : that.exitCli(cst.ERROR_EXIT); + } + async.forEachLimit(procs, 1, monitor, function (err, res) { + return typeof cb === 'function' ? cb(err, res) : that.speedList(); + }); + }); + } else { + monitor(parseInt(target), function (err, res) { + return typeof cb === 'function' ? cb(err, res) : that.speedList(); + }); + } + }; + + + function linkOpenExit(target_bucket) { + console.log('[-] Linking local PM2 to newly created bucket...') + KMDaemon.launchAndInteract(cst, { + public_key : target_bucket.public_id, + secret_key : target_bucket.secret_id, + pm2_version: pkg.version + }, function(err, dt) { + console.log(chalk.bold.green('[+] Local PM2 Connected!')) + + console.log('[-] Opening Monitoring Interface in Browser...') + + setTimeout(function() { + open('https://app.pm2.io/#/r/' + target_bucket.public_id); + console.log(chalk.bold.green('[+] Opened! Exiting now.')) + setTimeout(function() { + process.exit(cst.SUCCESS_EXIT); + }, 100); + }, 1000) + }); + } + + /** + * Private Functions + */ + function printMotd() { + var dt = fs.readFileSync(path.join(__dirname, 'motd')); + console.log(dt.toString()); + } +}; diff --git a/lib/API/PM2/WebAuth.js b/lib/API/PM2/WebAuth.js new file mode 100644 index 000000000..49a526316 --- /dev/null +++ b/lib/API/PM2/WebAuth.js @@ -0,0 +1,192 @@ + +'use strict' + +const cst = require('../../../constants.js'); + +const AuthStrategy = require('@pm2/js-api/src/auth_strategies/strategy') +const http = require('http') +const fs = require('fs') +const url = require('url') +const exec = require('child_process').exec +const async = require('async') +const path = require('path') +const os = require('os') +const needle = require('needle'); + +module.exports = class CustomStrategy extends AuthStrategy { + // the client will try to call this but we handle this part ourselves + retrieveTokens (km, cb) { + this.authenticated = false + this.callback = cb + this.km = km + } + + // so the cli know if we need to tell user to login/register + isAuthenticated () { + return new Promise((resolve, reject) => { + if (this.authenticated) return resolve(true) + + let tokensPath = cst.PM2_IO_ACCESS_TOKEN + fs.readFile(tokensPath, (err, tokens) => { + if (err && err.code === 'ENOENT') return resolve(false) + if (err) return reject(err) + + // verify that the token is valid + try { + tokens = JSON.parse(tokens || '{}') + } catch (err) { + fs.unlinkSync(tokensPath) + return resolve(false) + } + + // if the refresh tokens is here, the user could be automatically authenticated + return resolve(typeof tokens.refresh_token === 'string') + }) + }) + } + + // called when we are sure the user asked to be logged in + _retrieveTokens (optionalCallback) { + const km = this.km + const cb = this.callback + + let verifyToken = (refresh) => { + return km.auth.retrieveToken({ + client_id: this.client_id, + refresh_token: refresh + }) + } + async.tryEach([ + // try to find the token via the environement + (next) => { + if (!process.env.KM_TOKEN) { + return next(new Error('No token in env')) + } + verifyToken(process.env.KM_TOKEN) + .then((res) => { + return next(null, res.data) + }).catch(next) + }, + // try to find it in the file system + (next) => { + return next(new Error('nope')) + + fs.readFile(cst.PM2_IO_ACCESS_TOKEN, (err, tokens) => { + if (err) return next(err) + + // verify that the token is valid + tokens = JSON.parse(tokens || '{}') + if (new Date(tokens.expire_at) > new Date(new Date().toISOString())) { + return next(null, tokens) + } + + verifyToken(tokens.refresh_token) + .then((res) => { + return next(null, res.data) + }).catch(next) + }) + }, + // otherwise make the whole flow + (next) => { + return this.loginViaWeb((data) => { + // verify that the token is valid + verifyToken(data.access_token) + .then((res) => { + return next(null, res.data) + }).catch(next) + }) + } + ], (err, result) => { + // if present run the optional callback + if (typeof optionalCallback === 'function') { + optionalCallback(err, result) + } + + if (result.refresh_token) { + this.authenticated = true + let file = cst.PM2_IO_ACCESS_TOKEN + fs.writeFile(file, JSON.stringify(result), () => { + return cb(err, result) + }) + } else { + return cb(err, result) + } + }) + } + + loginViaWeb (cb) { + let shutdown = false + let server = http.createServer((req, res) => { + // only handle one request + if (shutdown === true) return res.end() + shutdown = true + + let query = url.parse(req.url, true).query + + res.write(` + + + + +

+ You can go back to your terminal now :) +

+ `) + res.end() + server.close() + return cb(query) + }) + server.listen(43532, () => { + this.open(`${this.oauth_endpoint}${this.oauth_query}`) + }) + } + + deleteTokens (km) { + return new Promise((resolve, reject) => { + // revoke the refreshToken + km.auth.revoke() + .then(res => { + // remove the token from the filesystem + let file = cst.PM2_IO_ACCESS_TOKEN + fs.unlinkSync(file) + return resolve(res) + }).catch(reject) + }) + } + + open (target, appName, callback) { + let opener + const escape = function (s) { + return s.replace(/"/g, '\\"') + } + + if (typeof (appName) === 'function') { + callback = appName + appName = null + } + + switch (process.platform) { + case 'darwin': { + opener = appName ? `open -a "${escape(appName)}"` : `open` + break + } + case 'win32': { + opener = appName ? `start "" ${escape(appName)}"` : `start ""` + break + } + default: { + opener = appName ? escape(appName) : `xdg-open` + break + } + } + + if (process.env.SUDO_USER) { + opener = 'sudo -u ' + process.env.SUDO_USER + ' ' + opener + } + return exec(`${opener} "${escape(target)}"`, callback) + } +} diff --git a/lib/API/Keymetrics/motd b/lib/API/PM2/motd similarity index 100% rename from lib/API/Keymetrics/motd rename to lib/API/PM2/motd diff --git a/package.json b/package.json index f504ef956..243a8988c 100644 --- a/package.json +++ b/package.json @@ -160,6 +160,7 @@ "dependencies": { "@pm2/agent": "latest", "@pm2/io": "latest", + "@pm2/js-api": "latest", "async": "^2.6", "blessed": "^0.1.81", "chalk": "^2.4.1", diff --git a/paths.js b/paths.js index c8bccb3e9..8830a7e08 100644 --- a/paths.js +++ b/paths.js @@ -45,7 +45,7 @@ module.exports = function(PM2_HOME) { DEFAULT_PID_PATH : p.resolve(PM2_HOME, 'pids'), DEFAULT_LOG_PATH : p.resolve(PM2_HOME, 'logs'), DEFAULT_MODULE_PATH : p.resolve(PM2_HOME, 'modules'), - KM_ACCESS_TOKEN : p.resolve(PM2_HOME, 'km-access-token'), + PM2_IO_ACCESS_TOKEN : p.resolve(PM2_HOME, 'pm2-io-token'), DUMP_FILE_PATH : p.resolve(PM2_HOME, 'dump.pm2'), DUMP_BACKUP_FILE_PATH : p.resolve(PM2_HOME, 'dump.pm2.bak'),