From 4b3d58fd36ccd3f0364d812e7682a43086010e3d Mon Sep 17 00:00:00 2001 From: Daniel Martinez Date: Thu, 15 Apr 2021 11:33:33 -0300 Subject: [PATCH] [IMPROVE] Add proxy for data export (#20998) Co-authored-by: Diego Sampaio --- app/user-data-download/server/DataExport.js | 54 +++++++++++++++++++ .../server/cronProcessDownloads.js | 4 +- .../server/exportDownload.js | 34 ++++++++++++ app/user-data-download/server/index.js | 1 + packages/rocketchat-i18n/i18n/en.i18n.json | 3 ++ packages/rocketchat-i18n/i18n/pt-BR.i18n.json | 3 ++ private/errors/error_template.html | 31 +++++++++++ server/lib/channelExport.ts | 4 +- server/methods/requestDataDownload.js | 3 +- 9 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 app/user-data-download/server/DataExport.js create mode 100644 app/user-data-download/server/exportDownload.js create mode 100644 private/errors/error_template.html diff --git a/app/user-data-download/server/DataExport.js b/app/user-data-download/server/DataExport.js new file mode 100644 index 00000000000..f1d588be4cd --- /dev/null +++ b/app/user-data-download/server/DataExport.js @@ -0,0 +1,54 @@ +import { Cookies } from 'meteor/ostrio:cookies'; + +import Users from '../../models/server/models/Users'; +import { FileUpload } from '../../file-upload/server'; +import { getURL } from '../../utils/lib/getURL'; + +const cookie = new Cookies(); +const userDataStore = FileUpload.getStore('UserDataFiles'); + +export const DataExport = { + handlers: {}, + + getPath(path = '') { + return `/data-export/${ path }`; + }, + + requestCanAccessFiles({ headers = {}, query = {} }, userId) { + let { rc_uid, rc_token } = query; + + if (!rc_uid && headers.cookie) { + rc_uid = cookie.get('rc_uid', headers.cookie); + rc_token = cookie.get('rc_token', headers.cookie); + } + + const options = { fields: { _id: 1 } }; + + if (rc_uid && rc_token && rc_uid === userId) { + return !!Users.findOneByIdAndLoginToken(rc_uid, rc_token, options); + } + + if (headers['x-user-id'] && headers['x-auth-token'] && headers['x-user-id'] === userId) { + return !!Users.findOneByIdAndLoginToken(headers['x-user-id'], headers['x-auth-token'], options); + } + + return false; + }, + + get(file, req, res, next) { + if (userDataStore && userDataStore.get) { + return userDataStore.get(file, req, res, next); + } + res.writeHead(404); + res.end(); + }, + + getErrorPage(errorType, errorDescription) { + let errorHtml = Assets.getText('errors/error_template.html'); + errorHtml = errorHtml.replace('$ERROR_TYPE$', errorType); + errorHtml = errorHtml.replace('$ERROR_DESCRIPTION$', errorDescription); + errorHtml = errorHtml.replace('$SERVER_URL$', getURL('/', { full: true, cdn: false })); + return errorHtml; + }, + +}; diff --git a/app/user-data-download/server/cronProcessDownloads.js b/app/user-data-download/server/cronProcessDownloads.js index 2c35e4e06ed..95e8f6ed360 100644 --- a/app/user-data-download/server/cronProcessDownloads.js +++ b/app/user-data-download/server/cronProcessDownloads.js @@ -11,9 +11,11 @@ import moment from 'moment'; import { settings } from '../../settings/server'; import { Subscriptions, Rooms, Users, Uploads, Messages, UserDataFiles, ExportOperations, Avatars } from '../../models/server'; import { FileUpload } from '../../file-upload/server'; +import { DataExport } from './DataExport'; import * as Mailer from '../../mailer'; import { readSecondaryPreferred } from '../../../server/database/readSecondaryPreferred'; import { joinPath } from '../../../server/lib/fileUtils'; +import { getURL } from '../../utils/lib/getURL'; const fsStat = util.promisify(fs.stat); const fsOpen = util.promisify(fs.open); @@ -569,7 +571,7 @@ async function processDataDownloads() { } const subject = TAPi18n.__('UserDataDownload_EmailSubject'); - const body = TAPi18n.__('UserDataDownload_EmailBody', { download_link: file.url }); + const body = TAPi18n.__('UserDataDownload_EmailBody', { download_link: getURL(DataExport.getPath(file._id), { cdn: false, full: true }) }); sendEmail(operation.userData, subject, body); } diff --git a/app/user-data-download/server/exportDownload.js b/app/user-data-download/server/exportDownload.js new file mode 100644 index 00000000000..d99eda5c88e --- /dev/null +++ b/app/user-data-download/server/exportDownload.js @@ -0,0 +1,34 @@ +import { WebApp } from 'meteor/webapp'; +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; + +import { UserDataFiles } from '../../models'; +import { DataExport } from './DataExport'; +import { settings } from '../../settings/server'; + + +WebApp.connectHandlers.use(DataExport.getPath(), function(req, res, next) { + const match = /^\/([^\/]+)/.exec(req.url); + + if (!settings.get('UserData_EnableDownload')) { + res.writeHead(403); + res.setHeader('Content-Type', 'text/html; charset=UTF-8'); + return res.end(DataExport.getErrorPage(TAPi18n.__('Feature_Disabled'), TAPi18n.__('UserDataDownload_FeatureDisabled'))); + } + + if (match && match[1]) { + const file = UserDataFiles.findOneById(match[1]); + if (file) { + if (!DataExport.requestCanAccessFiles(req, file.userId)) { + res.setHeader('Content-Type', 'text/html; charset=UTF-8'); + res.writeHead(403); + return res.end(DataExport.getErrorPage(TAPi18n.__('403'), TAPi18n.__('UserDataDownload_LoginNeeded'))); + } + + res.setHeader('Content-Security-Policy', 'default-src \'none\''); + res.setHeader('Cache-Control', 'max-age=31536000'); + return DataExport.get(file, req, res, next); + } + } + res.writeHead(404); + res.end(); +}); diff --git a/app/user-data-download/server/index.js b/app/user-data-download/server/index.js index 47f47e70c4a..4330e25554f 100644 --- a/app/user-data-download/server/index.js +++ b/app/user-data-download/server/index.js @@ -1,2 +1,3 @@ import './startup/settings'; import './cronProcessDownloads'; +import './exportDownload'; diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index e9b13928429..45a70d57f48 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -1760,6 +1760,7 @@ "Feature_Depends_on_Livechat_Visitor_navigation_as_a_message_to_be_enabled": "This feature depends on \"Send Visitor Navigation History as a Message\" to be enabled.", "Features": "Features", "Features_Enabled": "Features Enabled", + "Feature_Disabled": "Feature Disabled", "Federation_Dashboard": "Federation Dashboard", "FEDERATION_Discovery_Method": "Discovery Method", "FEDERATION_Discovery_Method_Description": "You can use the hub or a SRV and a TXT entry on your DNS records.", @@ -4198,6 +4199,8 @@ "UserDataDownload_CompletedRequestExistedWithLink_Text": "Your data file was already generated. Click here to download it.", "UserDataDownload_EmailBody": "Your data file is now ready to download. Click here to download it.", "UserDataDownload_EmailSubject": "Your Data File is Ready to Download", + "UserDataDownload_FeatureDisabled": "Sorry, user data exports are not enabled on this server!", + "UserDataDownload_LoginNeeded": "You need to log into your Rocket.Chat account to download this data export. Click the link below to do that, then try again.", "UserDataDownload_Requested": "Download File Requested", "UserDataDownload_Requested_Text": "Your data file will be generated. A link to download it will be sent to your email address when ready. There are __pending_operations__ queued operations to run before yours.", "UserDataDownload_RequestExisted_Text": "Your data file is already being generated. A link to download it will be sent to your email address when ready. There are __pending_operations__ queued operations to run before yours.", diff --git a/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/packages/rocketchat-i18n/i18n/pt-BR.i18n.json index 02cb1df2379..763a007da4d 100644 --- a/packages/rocketchat-i18n/i18n/pt-BR.i18n.json +++ b/packages/rocketchat-i18n/i18n/pt-BR.i18n.json @@ -1500,6 +1500,7 @@ "Favorites": "Favoritos", "Feature_Depends_on_Livechat_Visitor_navigation_as_a_message_to_be_enabled": "Esse recurso depende de \"Enviar histórico de navegação do visitante como uma mensagem\" para ser ativado.", "Features_Enabled": "Funcionalidades habilitadas", + "Feature_Disabled": "Funcionalidade desabilitada", "FEDERATION_Discovery_Method": "Método de Descoberta", "FEDERATION_Discovery_Method_Description": "Você pode usar o hub ou uma entrada SRV e TXT em seus registros DNS.", "FEDERATION_Domain": "Domínio", @@ -3448,6 +3449,8 @@ "UserDataDownload_CompletedRequestExisted_Text": "Seu arquivo de dados já foi gerado. Verifique sua conta de e-mail para o link de download.", "UserDataDownload_EmailBody": "Seu arquivo de dados está pronto para download. Clique em aquipara fazer o download.", "UserDataDownload_EmailSubject": "Seu arquivo de dados está pronto para download", + "UserDataDownload_FeatureDisabled": "Desculpe, a exportação de arquivos de dados está desativada neste servidor!", + "UserDataDownload_LoginNeeded": "Você precisa fazer login na sua conta do Rocket.Chat para baixar esse arquivo de dados. Faça isso através do link abaixo e tente novamente.", "UserDataDownload_Requested": "Download do arquivo solicitado", "UserDataDownload_Requested_Text": "Seu arquivo de dados será gerado. Um link para baixá-lo será enviado para o seu endereço de e-mail quando estiver pronto. Há __pending_operations__ operações na fila antes da sua.", "UserDataDownload_RequestExisted_Text": "Seu arquivo de dados já está sendo gerado. Um link para baixá-lo será enviado para o seu endereço de e-mail quando estiver pronto. Há __pending_operations__ operações na fila antes da sua.", diff --git a/private/errors/error_template.html b/private/errors/error_template.html new file mode 100644 index 00000000000..d023fdf801d --- /dev/null +++ b/private/errors/error_template.html @@ -0,0 +1,31 @@ + + + +
+

$ERROR_TYPE$

+

$ERROR_DESCRIPTION$

+

Go home

+
+ + diff --git a/server/lib/channelExport.ts b/server/lib/channelExport.ts index d6a04811270..720304cc924 100644 --- a/server/lib/channelExport.ts +++ b/server/lib/channelExport.ts @@ -19,6 +19,8 @@ import { } from '../../app/user-data-download/server/cronProcessDownloads'; import { IUser } from '../../definition/IUser'; import { getMomentLocale } from './getMomentLocale'; +import { getURL } from '../../app/utils/lib/getURL'; +import { DataExport } from '../../app/user-data-download/server/DataExport'; type ExportEmail = { rid: string; @@ -166,7 +168,7 @@ export const sendFile = async (data: ExportFile, user: IUser): Promise => const subject = TAPi18n.__('Channel_Export'); // eslint-disable-next-line @typescript-eslint/camelcase - const body = TAPi18n.__('UserDataDownload_EmailBody', { download_link: file.url }); + const body = TAPi18n.__('UserDataDownload_EmailBody', { download_link: getURL(DataExport.getPath(file._id), { cdn: false, full: true }) }); sendEmail(user, subject, body); }; diff --git a/server/methods/requestDataDownload.js b/server/methods/requestDataDownload.js index 578213c2627..f388b4831bd 100644 --- a/server/methods/requestDataDownload.js +++ b/server/methods/requestDataDownload.js @@ -6,6 +6,7 @@ import { Meteor } from 'meteor/meteor'; import { ExportOperations, UserDataFiles } from '../../app/models'; import { settings } from '../../app/settings'; +import { DataExport } from '../../app/user-data-download/server/DataExport'; let tempFolder = '/tmp/userData'; if (settings.get('UserData_FileSystemPath') != null) { @@ -34,7 +35,7 @@ Meteor.methods({ return { requested: false, exportOperation: lastOperation, - url: file.url, + url: DataExport.getPath(file._id), pendingOperationsBeforeMyRequest: pendingOperationsBeforeMyRequestCount, }; }