url.js 4.3 KB

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