middlewares.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. const cors = require('cors')
  2. const promBundle = require('express-prom-bundle')
  3. // @ts-ignore
  4. const { version } = require('../../package.json')
  5. const tokenService = require('./helpers/jwt')
  6. const logger = require('./logger')
  7. const getS3Client = require('./s3-client')
  8. const { getURLBuilder } = require('./helpers/utils')
  9. const { isOAuthProvider } = require('./provider/Provider')
  10. exports.hasSessionAndProvider = (req, res, next) => {
  11. if (!req.session) {
  12. logger.debug('No session attached to req object. Exiting dispatcher.', null, req.id)
  13. return res.sendStatus(400)
  14. }
  15. if (!req.companion.provider) {
  16. logger.debug('No provider/provider-handler found. Exiting dispatcher.', null, req.id)
  17. return res.sendStatus(400)
  18. }
  19. return next()
  20. }
  21. const isOAuthProviderReq = (req) => isOAuthProvider(req.companion.providerClass.oauthProvider)
  22. const isSimpleAuthProviderReq = (req) => !!req.companion.providerClass.hasSimpleAuth
  23. /**
  24. * Middleware can be used to verify that the current request is to an OAuth provider
  25. * This is because not all requests are supported by non-oauth providers (formerly known as SearchProviders)
  26. */
  27. exports.hasOAuthProvider = (req, res, next) => {
  28. if (!isOAuthProviderReq(req)) {
  29. logger.debug('Provider does not support OAuth.', null, req.id)
  30. return res.sendStatus(400)
  31. }
  32. return next()
  33. }
  34. exports.hasSimpleAuthProvider = (req, res, next) => {
  35. if (!isSimpleAuthProviderReq(req)) {
  36. logger.debug('Provider does not support simple auth.', null, req.id)
  37. return res.sendStatus(400)
  38. }
  39. return next()
  40. }
  41. exports.hasBody = (req, res, next) => {
  42. if (!req.body) {
  43. logger.debug('No body attached to req object. Exiting dispatcher.', null, req.id)
  44. return res.sendStatus(400)
  45. }
  46. return next()
  47. }
  48. exports.hasSearchQuery = (req, res, next) => {
  49. if (typeof req.query.q !== 'string') {
  50. logger.debug('search request has no search query', 'search.query.check', req.id)
  51. return res.sendStatus(400)
  52. }
  53. return next()
  54. }
  55. exports.verifyToken = (req, res, next) => {
  56. if (isOAuthProviderReq(req) || isSimpleAuthProviderReq(req)) {
  57. // For OAuth / simple auth provider, we find the encrypted auth token from the header:
  58. const token = req.companion.authToken
  59. if (token == null) {
  60. logger.info('cannot auth token', 'token.verify.unset', req.id)
  61. res.sendStatus(401)
  62. return
  63. }
  64. const { providerName } = req.params
  65. try {
  66. const payload = tokenService.verifyEncryptedAuthToken(token, req.companion.options.secret, providerName)
  67. req.companion.providerUserSession = payload[providerName]
  68. } catch (err) {
  69. logger.error(err.message, 'token.verify.error', req.id)
  70. res.sendStatus(401)
  71. return
  72. }
  73. next()
  74. return
  75. }
  76. // for non auth providers, we just load the static key from options
  77. if (!isOAuthProviderReq(req)) {
  78. const { providerOptions } = req.companion.options
  79. const { providerName } = req.params
  80. const key = providerOptions[providerName]?.key;
  81. if (!key) {
  82. logger.info(`unconfigured credentials for ${providerName}`, 'non.oauth.token.load.unset', req.id)
  83. res.sendStatus(501)
  84. return
  85. }
  86. req.companion.providerUserSession = {
  87. accessToken: key,
  88. }
  89. next()
  90. }
  91. }
  92. // does not fail if token is invalid
  93. exports.gentleVerifyToken = (req, res, next) => {
  94. const { providerName } = req.params
  95. if (req.companion.authToken) {
  96. try {
  97. const payload = tokenService.verifyEncryptedAuthToken(
  98. req.companion.authToken, req.companion.options.secret, providerName,
  99. )
  100. req.companion.providerUserSession = payload[providerName]
  101. } catch (err) {
  102. logger.error(err.message, 'token.gentle.verify.error', req.id)
  103. }
  104. }
  105. next()
  106. }
  107. exports.cookieAuthToken = (req, res, next) => {
  108. req.companion.authToken = req.cookies[`uppyAuthToken--${req.companion.providerClass.oauthProvider}`]
  109. return next()
  110. }
  111. exports.cors = (options = {}) => (req, res, next) => {
  112. // HTTP headers are not case sensitive, and express always handles them in lower case, so that's why we lower case them.
  113. // I believe that HTTP verbs are case sensitive, and should be uppercase.
  114. const existingExposeHeaders = res.get('Access-Control-Expose-Headers')
  115. const exposeHeadersSet = new Set(existingExposeHeaders?.split(',')?.map((method) => method.trim().toLowerCase()))
  116. if (options.sendSelfEndpoint) exposeHeadersSet.add('i-am')
  117. // Needed for basic operation: https://github.com/transloadit/uppy/issues/3021
  118. const allowedHeaders = [
  119. 'uppy-auth-token',
  120. 'uppy-credentials-params',
  121. 'authorization',
  122. 'origin',
  123. 'content-type',
  124. 'accept',
  125. ]
  126. const existingAllowHeaders = res.get('Access-Control-Allow-Headers')
  127. const allowHeadersSet = new Set(existingAllowHeaders
  128. ? existingAllowHeaders
  129. .split(',')
  130. .map((method) => method.trim().toLowerCase())
  131. .concat(allowedHeaders)
  132. : allowedHeaders)
  133. const existingAllowMethods = res.get('Access-Control-Allow-Methods')
  134. const allowMethodsSet = new Set(existingAllowMethods?.split(',')?.map((method) => method.trim().toUpperCase()))
  135. // Needed for basic operation:
  136. allowMethodsSet.add('GET').add('POST').add('OPTIONS').add('DELETE')
  137. // If endpoint urls are specified, then we only allow those endpoints.
  138. // Otherwise, we allow any client url to access companion.
  139. // Must be set to at least true (origin "*" with "credentials: true" will cause error in many browsers)
  140. // https://github.com/expressjs/cors/issues/119
  141. // allowedOrigins can also be any type supported by https://github.com/expressjs/cors#configuration-options
  142. const { corsOrigins: origin = true } = options
  143. // Because we need to merge with existing headers, we need to call cors inside our own middleware
  144. return cors({
  145. credentials: true,
  146. origin,
  147. methods: Array.from(allowMethodsSet),
  148. allowedHeaders: Array.from(allowHeadersSet).join(','),
  149. exposedHeaders: Array.from(exposeHeadersSet).join(','),
  150. })(req, res, next)
  151. }
  152. exports.metrics = ({ path = undefined } = {}) => {
  153. const metricsMiddleware = promBundle({ includeMethod: true, metricsPath: path ? `${path}/metrics` : undefined })
  154. // @ts-ignore Not in the typings, but it does exist
  155. const { promClient } = metricsMiddleware
  156. const { collectDefaultMetrics } = promClient
  157. collectDefaultMetrics({ register: promClient.register })
  158. // Add version as a prometheus gauge
  159. const versionGauge = new promClient.Gauge({ name: 'companion_version', help: 'npm version as an integer' })
  160. const numberVersion = Number(version.replace(/\D/g, ''))
  161. versionGauge.set(numberVersion)
  162. return metricsMiddleware
  163. }
  164. /**
  165. *
  166. * @param {object} options
  167. */
  168. exports.getCompanionMiddleware = (options) => {
  169. /**
  170. * @param {object} req
  171. * @param {object} res
  172. * @param {Function} next
  173. */
  174. const middleware = (req, res, next) => {
  175. req.companion = {
  176. options,
  177. s3Client: getS3Client(options, false),
  178. s3ClientCreatePresignedPost: getS3Client(options, true),
  179. authToken: req.header('uppy-auth-token') || req.query.uppyAuthToken,
  180. buildURL: getURLBuilder(options),
  181. }
  182. next()
  183. }
  184. return middleware
  185. }