uppy.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. const express = require('express')
  2. // @ts-ignore
  3. const Grant = require('grant-express')
  4. const grantConfig = require('./config/grant')()
  5. const providerManager = require('./server/provider')
  6. const controllers = require('./server/controllers')
  7. const s3 = require('./server/controllers/s3')
  8. const url = require('./server/controllers/url')
  9. const SocketServer = require('ws').Server
  10. const emitter = require('./server/emitter')
  11. const merge = require('lodash.merge')
  12. const redis = require('./server/redis')
  13. const cookieParser = require('cookie-parser')
  14. const { jsonStringify, getURLBuilder } = require('./server/helpers/utils')
  15. const jobs = require('./server/jobs')
  16. const interceptor = require('express-interceptor')
  17. const logger = require('./server/logger')
  18. const { STORAGE_PREFIX } = require('./server/Uploader')
  19. const middlewares = require('./server/middlewares')
  20. const providers = providerManager.getDefaultProviders()
  21. const defaultOptions = {
  22. server: {
  23. protocol: 'http',
  24. path: ''
  25. },
  26. providerOptions: {
  27. s3: {
  28. acl: 'public-read',
  29. endpoint: 'https://{service}.{region}.amazonaws.com',
  30. conditions: [],
  31. getKey: (req, filename) => filename
  32. }
  33. },
  34. debug: true
  35. }
  36. /**
  37. * Entry point into initializing the Companion app.
  38. *
  39. * @param {object} options
  40. */
  41. module.exports.app = (options = {}) => {
  42. options = merge({}, defaultOptions, options)
  43. providerManager.addProviderOptions(options, grantConfig)
  44. const customProviders = options.customProviders
  45. if (customProviders) {
  46. providerManager.addCustomProviders(customProviders, providers, grantConfig)
  47. }
  48. // create singleton redis client
  49. if (options.redisUrl) {
  50. redis.client(merge({ url: options.redisUrl }, options.redisOptions || {}))
  51. }
  52. emitter(options.multipleInstances && options.redisUrl)
  53. const app = express()
  54. app.use(cookieParser()) // server tokens are added to cookies
  55. app.use(interceptGrantErrorResponse)
  56. app.use(new Grant(grantConfig))
  57. app.use((req, res, next) => {
  58. res.header(
  59. 'Access-Control-Allow-Headers',
  60. [res.get('Access-Control-Allow-Headers'), 'uppy-auth-token', 'uppy-client'].join(', ')
  61. )
  62. next()
  63. })
  64. if (options.sendSelfEndpoint) {
  65. app.use('*', (req, res, next) => {
  66. const { protocol } = options.server
  67. res.header('i-am', `${protocol}://${options.sendSelfEndpoint}`)
  68. // add it to the exposed custom headers.
  69. res.header('Access-Control-Expose-Headers', [res.get('Access-Control-Expose-Headers'), 'i-am'].join(', '))
  70. next()
  71. })
  72. }
  73. // add uppy options to the request object so it can be accessed by subsequent handlers.
  74. app.use('*', getOptionsMiddleware(options))
  75. app.use('/s3', s3(options.providerOptions.s3))
  76. app.use('/url', url())
  77. app.get('/:providerName/callback', middlewares.hasSessionAndProvider, controllers.callback)
  78. app.get('/:providerName/connect', middlewares.hasSessionAndProvider, controllers.connect)
  79. app.get('/:providerName/redirect', middlewares.hasSessionAndProvider, controllers.redirect)
  80. app.get('/:providerName/logout', middlewares.hasSessionAndProvider, middlewares.gentleVerifyToken, controllers.logout)
  81. app.get('/:providerName/send-token', middlewares.hasSessionAndProvider, middlewares.verifyToken, controllers.sendToken)
  82. app.get('/:providerName/list/:id?', middlewares.hasSessionAndProvider, middlewares.verifyToken, controllers.list)
  83. app.post('/:providerName/get/:id', middlewares.hasSessionAndProvider, middlewares.verifyToken, controllers.get)
  84. app.get('/:providerName/thumbnail/:id', middlewares.hasSessionAndProvider, middlewares.cookieAuthToken, middlewares.verifyToken, controllers.thumbnail)
  85. app.param('providerName', providerManager.getProviderMiddleware(providers))
  86. if (app.get('env') !== 'test') {
  87. jobs.startCleanUpJob(options.filePath)
  88. }
  89. return app
  90. }
  91. /**
  92. * the socket is used to send progress events during an upload
  93. *
  94. * @param {object} server
  95. */
  96. module.exports.socket = (server) => {
  97. const wss = new SocketServer({ server })
  98. const redisClient = redis.client()
  99. // A new connection is usually created when an upload begins,
  100. // or when connection fails while an upload is on-going and,
  101. // client attempts to reconnect.
  102. wss.on('connection', (ws) => {
  103. // @ts-ignore
  104. const fullPath = ws.upgradeReq.url
  105. // the token identifies which ongoing upload's progress, the socket
  106. // connection wishes to listen to.
  107. const token = fullPath.replace(/^.*\/api\//, '')
  108. logger.info(`connection received from ${token}`, 'socket.connect')
  109. /**
  110. *
  111. * @param {{action: string, payload: object}} data
  112. */
  113. function sendProgress (data) {
  114. ws.send(jsonStringify(data), (err) => {
  115. if (err) logger.error(err, 'socket.progress.error')
  116. })
  117. }
  118. // if the redisClient is available, then we attempt to check the storage
  119. // if we have any already stored progress data on the upload.
  120. if (redisClient) {
  121. redisClient.get(`${STORAGE_PREFIX}:${token}`, (err, data) => {
  122. if (err) logger.error(err, 'socket.redis.error')
  123. if (data) {
  124. const dataObj = JSON.parse(data.toString())
  125. if (dataObj.action) sendProgress(dataObj)
  126. }
  127. })
  128. }
  129. emitter().emit(`connection:${token}`)
  130. emitter().on(token, sendProgress)
  131. ws.on('message', (jsonData) => {
  132. const data = JSON.parse(jsonData.toString())
  133. // whitelist triggered actions
  134. if (data.action === 'pause' || data.action === 'resume') {
  135. emitter().emit(`${data.action}:${token}`)
  136. }
  137. })
  138. ws.on('close', () => {
  139. emitter().removeListener(token, sendProgress)
  140. })
  141. })
  142. }
  143. // intercepts grantJS' default response error when something goes
  144. // wrong during oauth process.
  145. const interceptGrantErrorResponse = interceptor((req, res) => {
  146. return {
  147. isInterceptable: () => {
  148. // match grant.js' callback url
  149. return /^\/connect\/\w+\/callback/.test(req.path)
  150. },
  151. intercept: (body, send) => {
  152. const unwantedBody = 'error=Grant%3A%20missing%20session%20or%20misconfigured%20provider'
  153. if (body === unwantedBody) {
  154. logger.error(`grant.js responded with error: ${body}`, 'grant.oauth.error')
  155. send([
  156. 'Companion was unable to complete the OAuth process :(',
  157. '(Hint, try clearing your cookies and try again)'
  158. ].join('\n'))
  159. } else {
  160. send(body)
  161. }
  162. }
  163. }
  164. })
  165. /**
  166. *
  167. * @param {object} options
  168. */
  169. const getOptionsMiddleware = (options) => {
  170. let s3Client = null
  171. if (options.providerOptions.s3) {
  172. const S3 = require('aws-sdk/clients/s3')
  173. const AWS = require('aws-sdk')
  174. const config = options.providerOptions.s3
  175. // Use credentials to allow assumed roles to pass STS sessions in.
  176. // If the user doesn't specify key and secret, the default credentials (process-env)
  177. // will be used by S3 in calls below.
  178. let credentials
  179. if (config.key && config.secret) {
  180. credentials = new AWS.Credentials(config.key, config.secret, config.sessionToken)
  181. }
  182. s3Client = new S3({
  183. region: config.region,
  184. endpoint: config.endpoint,
  185. credentials,
  186. signatureVersion: 'v4'
  187. })
  188. }
  189. /**
  190. *
  191. * @param {object} req
  192. * @param {object} res
  193. * @param {function} next
  194. */
  195. const middleware = (req, res, next) => {
  196. req.uppy = {
  197. options,
  198. s3Client,
  199. authToken: req.header('uppy-auth-token') || req.query.uppyAuthToken,
  200. clientVersion: req.header('uppy-versions') || req.query.uppyVersions,
  201. buildURL: getURLBuilder(options)
  202. }
  203. next()
  204. }
  205. return middleware
  206. }