ws.js 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. const crypto = require('crypto');
  2. const storage = require('../storage');
  3. const config = require('../config');
  4. const mozlog = require('../log');
  5. const Limiter = require('../limiter');
  6. const fxa = require('../fxa');
  7. const { encryptedSize } = require('../../app/utils');
  8. const { Transform } = require('stream');
  9. const log = mozlog('send.upload');
  10. module.exports = function(ws, req) {
  11. let fileStream;
  12. ws.on('close', e => {
  13. if (e !== 1000 && fileStream !== undefined) {
  14. fileStream.destroy();
  15. }
  16. });
  17. ws.once('message', async function(message) {
  18. try {
  19. const newId = crypto.randomBytes(8).toString('hex');
  20. const owner = crypto.randomBytes(10).toString('hex');
  21. const fileInfo = JSON.parse(message);
  22. const timeLimit = fileInfo.timeLimit || config.default_expire_seconds;
  23. const dlimit = fileInfo.dlimit || config.default_downloads;
  24. const metadata = fileInfo.fileMetadata;
  25. const auth = fileInfo.authorization;
  26. const user = await fxa.verify(fileInfo.bearer);
  27. const maxFileSize = config.max_file_size;
  28. const maxExpireSeconds = config.max_expire_seconds;
  29. const maxDownloads = config.max_downloads;
  30. if (config.fxa_required && !user) {
  31. ws.send(
  32. JSON.stringify({
  33. error: 401
  34. })
  35. );
  36. return ws.close();
  37. }
  38. if (
  39. !metadata ||
  40. !auth ||
  41. timeLimit <= 0 ||
  42. timeLimit > maxExpireSeconds ||
  43. dlimit > maxDownloads
  44. ) {
  45. ws.send(
  46. JSON.stringify({
  47. error: 400
  48. })
  49. );
  50. return ws.close();
  51. }
  52. const meta = {
  53. owner,
  54. metadata,
  55. dlimit,
  56. auth: auth.split(' ')[1],
  57. nonce: crypto.randomBytes(16).toString('base64')
  58. };
  59. const url = `${config.deriveBaseUrl(req)}/download/${newId}/`;
  60. ws.send(
  61. JSON.stringify({
  62. url,
  63. ownerToken: meta.owner,
  64. id: newId
  65. })
  66. );
  67. const limiter = new Limiter(encryptedSize(maxFileSize));
  68. const eof = new Transform({
  69. transform: function(chunk, encoding, callback) {
  70. if (chunk.length === 1 && chunk[0] === 0) {
  71. this.push(null);
  72. } else {
  73. this.push(chunk);
  74. }
  75. callback();
  76. }
  77. });
  78. const wsStream = ws.constructor.createWebSocketStream(ws);
  79. fileStream = wsStream.pipe(eof).pipe(limiter); // limiter needs to be the last in the chain
  80. await storage.set(newId, fileStream, meta, timeLimit);
  81. if (ws.readyState === 1) {
  82. // if the socket is closed by a cancelled upload the stream
  83. // ends without an error so we need to check the state
  84. // before sending a reply.
  85. // TODO: we should handle cancelled uploads differently
  86. // in order to avoid having to check socket state and clean
  87. // up storage, possibly with an exception that we can catch.
  88. ws.send(JSON.stringify({ ok: true }));
  89. }
  90. } catch (e) {
  91. log.error('upload', e);
  92. if (ws.readyState === 1) {
  93. ws.send(
  94. JSON.stringify({
  95. error: e === 'limit' ? 413 : 500
  96. })
  97. );
  98. }
  99. }
  100. ws.close();
  101. });
  102. };