WebAuth.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. 'use strict'
  2. const cst = require('../../../../constants.js');
  3. const AuthStrategy = require('@pm2/js-api/src/auth_strategies/strategy')
  4. const http = require('http')
  5. const fs = require('fs')
  6. const url = require('url')
  7. const exec = require('child_process').exec
  8. const tryEach = require('async/tryEach');
  9. module.exports = class WebStrategy extends AuthStrategy {
  10. // the client will try to call this but we handle this part ourselves
  11. retrieveTokens (km, cb) {
  12. this.authenticated = false
  13. this.callback = cb
  14. this.km = km
  15. }
  16. // so the cli know if we need to tell user to login/register
  17. isAuthenticated () {
  18. return new Promise((resolve, reject) => {
  19. if (this.authenticated) return resolve(true)
  20. let tokensPath = cst.PM2_IO_ACCESS_TOKEN
  21. fs.readFile(tokensPath, (err, tokens) => {
  22. if (err && err.code === 'ENOENT') return resolve(false)
  23. if (err) return reject(err)
  24. // verify that the token is valid
  25. try {
  26. tokens = JSON.parse(tokens || '{}')
  27. } catch (err) {
  28. fs.unlinkSync(tokensPath)
  29. return resolve(false)
  30. }
  31. // if the refresh tokens is here, the user could be automatically authenticated
  32. return resolve(typeof tokens.refresh_token === 'string')
  33. })
  34. })
  35. }
  36. // called when we are sure the user asked to be logged in
  37. _retrieveTokens (optionalCallback) {
  38. const km = this.km
  39. const cb = this.callback
  40. let verifyToken = (refresh) => {
  41. return km.auth.retrieveToken({
  42. client_id: this.client_id,
  43. refresh_token: refresh
  44. })
  45. }
  46. tryEach([
  47. // try to find the token via the environment
  48. (next) => {
  49. if (!process.env.PM2_IO_TOKEN) {
  50. return next(new Error('No token in env'))
  51. }
  52. verifyToken(process.env.PM2_IO_TOKEN)
  53. .then((res) => {
  54. return next(null, res.data)
  55. }).catch(next)
  56. },
  57. // try to find it in the file system
  58. (next) => {
  59. fs.readFile(cst.PM2_IO_ACCESS_TOKEN, (err, tokens) => {
  60. if (err) return next(err)
  61. // verify that the token is valid
  62. tokens = JSON.parse(tokens || '{}')
  63. if (new Date(tokens.expire_at) > new Date(new Date().toISOString())) {
  64. return next(null, tokens)
  65. }
  66. verifyToken(tokens.refresh_token)
  67. .then((res) => {
  68. return next(null, res.data)
  69. }).catch(next)
  70. })
  71. },
  72. // otherwise make the whole flow
  73. (next) => {
  74. return this.loginViaWeb((data) => {
  75. // verify that the token is valid
  76. verifyToken(data.access_token)
  77. .then((res) => {
  78. return next(null, res.data)
  79. }).catch(err => next(err))
  80. })
  81. }
  82. ], (err, result) => {
  83. // if present run the optional callback
  84. if (typeof optionalCallback === 'function') {
  85. optionalCallback(err, result)
  86. }
  87. if (result.refresh_token) {
  88. this.authenticated = true
  89. let file = cst.PM2_IO_ACCESS_TOKEN
  90. fs.writeFile(file, JSON.stringify(result), () => {
  91. return cb(err, result)
  92. })
  93. } else {
  94. return cb(err, result)
  95. }
  96. })
  97. }
  98. loginViaWeb (cb) {
  99. const redirectURL = `${this.oauth_endpoint}${this.oauth_query}`
  100. console.log(`${cst.PM2_IO_MSG} Please follow the popup or go to this URL :`, '\n', ' ', redirectURL)
  101. let shutdown = false
  102. let server = http.createServer((req, res) => {
  103. // only handle one request
  104. if (shutdown === true) return res.end()
  105. shutdown = true
  106. let query = url.parse(req.url, true).query
  107. res.write(`
  108. <head>
  109. <script>
  110. </script>
  111. </head>
  112. <body>
  113. <h2 style="text-align: center">
  114. You can go back to your terminal now :)
  115. </h2>
  116. </body>`)
  117. res.end()
  118. server.close()
  119. return cb(query)
  120. })
  121. server.listen(43532, () => {
  122. this.open(redirectURL)
  123. })
  124. }
  125. deleteTokens (km) {
  126. return new Promise((resolve, reject) => {
  127. // revoke the refreshToken
  128. km.auth.revoke()
  129. .then(res => {
  130. // remove the token from the filesystem
  131. let file = cst.PM2_IO_ACCESS_TOKEN
  132. fs.unlinkSync(file)
  133. return resolve(res)
  134. }).catch(reject)
  135. })
  136. }
  137. open (target, appName, callback) {
  138. let opener
  139. const escape = function (s) {
  140. return s.replace(/"/g, '\\"')
  141. }
  142. if (typeof (appName) === 'function') {
  143. callback = appName
  144. appName = null
  145. }
  146. switch (process.platform) {
  147. case 'darwin': {
  148. opener = appName ? `open -a "${escape(appName)}"` : `open`
  149. break
  150. }
  151. case 'win32': {
  152. opener = appName ? `start "" ${escape(appName)}"` : `start ""`
  153. break
  154. }
  155. default: {
  156. opener = appName ? escape(appName) : `xdg-open`
  157. break
  158. }
  159. }
  160. if (process.env.SUDO_USER) {
  161. opener = 'sudo -u ' + process.env.SUDO_USER + ' ' + opener
  162. }
  163. return exec(`${opener} "${escape(target)}"`, callback)
  164. }
  165. }