url.js 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. const router = require('express').Router
  2. const request = require('request')
  3. const { URL } = require('url')
  4. const validator = require('validator')
  5. const { startDownUpload } = require('../helpers/upload')
  6. const { getURLMeta, getRedirectEvaluator, getProtectedHttpAgent } = require('../helpers/request')
  7. const logger = require('../logger')
  8. /**
  9. * Validates that the download URL is secure
  10. *
  11. * @param {string} url the url to validate
  12. * @param {boolean} ignoreTld whether to allow local addresses
  13. */
  14. const validateURL = (url, ignoreTld) => {
  15. if (!url) {
  16. return false
  17. }
  18. const validURLOpts = {
  19. protocols: ['http', 'https'],
  20. require_protocol: true,
  21. require_tld: !ignoreTld,
  22. }
  23. if (!validator.isURL(url, validURLOpts)) {
  24. return false
  25. }
  26. return true
  27. }
  28. /**
  29. * @callback downloadCallback
  30. * @param {Error} err
  31. * @param {string | Buffer | Buffer[]} chunk
  32. */
  33. /**
  34. * Downloads the content in the specified url, and passes the data
  35. * to the callback chunk by chunk.
  36. *
  37. * @param {string} url
  38. * @param {boolean} blockLocalIPs
  39. * @param {string} traceId
  40. * @returns {Promise}
  41. */
  42. const downloadURL = async (url, blockLocalIPs, traceId) => {
  43. const opts = {
  44. uri: url,
  45. method: 'GET',
  46. followRedirect: getRedirectEvaluator(url, blockLocalIPs),
  47. agentClass: getProtectedHttpAgent((new URL(url)).protocol, blockLocalIPs),
  48. }
  49. return new Promise((resolve, reject) => {
  50. const req = request(opts)
  51. .on('response', (resp) => {
  52. if (resp.statusCode >= 300) {
  53. req.abort() // No need to keep request
  54. reject(new Error(`URL server responded with status: ${resp.statusCode}`))
  55. return
  56. }
  57. // Don't allow any more data to flow yet.
  58. // https://github.com/request/request/issues/1990#issuecomment-184712275
  59. resp.pause()
  60. resolve(resp)
  61. })
  62. .on('error', (err) => {
  63. logger.error(err, 'controller.url.download.error', traceId)
  64. reject(err)
  65. })
  66. })
  67. }
  68. /**
  69. * Fteches the size and content type of a URL
  70. *
  71. * @param {object} req expressJS request object
  72. * @param {object} res expressJS response object
  73. */
  74. const meta = async (req, res) => {
  75. try {
  76. logger.debug('URL file import handler running', null, req.id)
  77. const { allowLocalUrls } = req.companion.options
  78. if (!validateURL(req.body.url, allowLocalUrls)) {
  79. logger.debug('Invalid request body detected. Exiting url meta handler.', null, req.id)
  80. return res.status(400).json({ error: 'Invalid request body' })
  81. }
  82. const urlMeta = await getURLMeta(req.body.url, !allowLocalUrls)
  83. return res.json(urlMeta)
  84. } catch (err) {
  85. logger.error(err, 'controller.url.meta.error', req.id)
  86. // @todo send more meaningful error message and status code to client if possible
  87. return res.status(err.status || 500).json({ message: 'failed to fetch URL metadata' })
  88. }
  89. }
  90. /**
  91. * Handles the reques of import a file from a remote URL, and then
  92. * subsequently uploading it to the specified destination.
  93. *
  94. * @param {object} req expressJS request object
  95. * @param {object} res expressJS response object
  96. */
  97. const get = async (req, res) => {
  98. logger.debug('URL file import handler running', null, req.id)
  99. const { allowLocalUrls } = req.companion.options
  100. if (!validateURL(req.body.url, allowLocalUrls)) {
  101. logger.debug('Invalid request body detected. Exiting url import handler.', null, req.id)
  102. res.status(400).json({ error: 'Invalid request body' })
  103. return
  104. }
  105. async function getSize () {
  106. const { size } = await getURLMeta(req.body.url, !allowLocalUrls)
  107. return size
  108. }
  109. async function download () {
  110. return downloadURL(req.body.url, !allowLocalUrls, req.id)
  111. }
  112. function onUnhandledError (err) {
  113. logger.error(err, 'controller.url.error', req.id)
  114. // @todo send more meaningful error message and status code to client if possible
  115. res.status(err.status || 500).json({ message: 'failed to fetch URL metadata' })
  116. }
  117. startDownUpload({ req, res, getSize, download, onUnhandledError })
  118. }
  119. module.exports = () => router()
  120. .post('/meta', meta)
  121. .post('/get', get)