From c0a7f9cd923ed76209810b19ad2eb1ea36f6a83b Mon Sep 17 00:00:00 2001 From: Pedro Beschorner Marin Date: Tue, 9 Feb 2021 12:59:59 -0300 Subject: [PATCH] Replace FNV32a pad's id generator with salted SHA1 When managing Etherpad's pads, Meteor makes API calls to initiate the closed captions and shared notes modules. The pad id was being mapped to a shorter id than the meeting id because of a Etherpad lenght limitation. Changed to something less guessable. --- .../imports/api/captions/server/helpers.js | 15 +++++------ .../api/captions/server/methods/appendText.js | 5 ++-- .../captions/server/methods/createCaptions.js | 1 + .../captions/server/methods/editCaptions.js | 1 - .../server/methods/fetchReadOnlyPadId.js | 6 +++++ .../captions/server/methods/takeOwnership.js | 3 ++- .../imports/api/common/server/helpers.js | 27 ++----------------- .../imports/api/note/server/helpers.js | 7 ++--- .../api/note/server/methods/createNote.js | 2 ++ .../ui/components/captions/pad/service.js | 10 +++---- .../imports/ui/components/captions/service.js | 5 ++-- .../imports/ui/components/note/service.js | 18 ++++++------- bigbluebutton-html5/package-lock.json | 5 ++++ bigbluebutton-html5/package.json | 1 + .../private/config/settings.yml | 6 ----- record-and-playback/core/Gemfile | 1 - record-and-playback/core/Gemfile.lock | 2 -- .../core/lib/recordandplayback.rb | 9 ++++--- .../core/scripts/archive/archive.rb | 7 ++--- .../core/scripts/bigbluebutton.yml | 1 + .../core/scripts/events/events.rb | 7 ++--- 21 files changed, 60 insertions(+), 79 deletions(-) diff --git a/bigbluebutton-html5/imports/api/captions/server/helpers.js b/bigbluebutton-html5/imports/api/captions/server/helpers.js index 549ea8b5d7..e144c6b1eb 100644 --- a/bigbluebutton-html5/imports/api/captions/server/helpers.js +++ b/bigbluebutton-html5/imports/api/captions/server/helpers.js @@ -1,22 +1,20 @@ import { Meteor } from 'meteor/meteor'; -import { hashFNV32a } from '/imports/api/common/server/helpers'; +import { hashSHA1 } from '/imports/api/common/server/helpers'; import { check } from 'meteor/check'; +const ETHERPAD = Meteor.settings.private.etherpad; const CAPTIONS_CONFIG = Meteor.settings.public.captions; const BASENAME = Meteor.settings.public.app.basename; const APP = Meteor.settings.private.app; const LOCALES_URL = `http://${APP.host}:${APP.port}${BASENAME}${APP.localesUrl}`; -const CAPTIONS = '_captions_'; +const CAPTIONS_TOKEN = '_cc_'; const TOKEN = '$'; -// Captions padId should look like: {padId}_captions_{locale} -const generatePadId = (meetingId, locale) => { - const padId = `${hashFNV32a(meetingId, true)}${CAPTIONS}${locale}`; - return padId; -}; +// Captions padId should look like: {prefix}_cc_{locale} +const generatePadId = (meetingId, locale) => `${hashSHA1(meetingId+locale+ETHERPAD.apikey)}${CAPTIONS_TOKEN}${locale}`; const isCaptionsPad = (padId) => { - const splitPadId = padId.split(CAPTIONS); + const splitPadId = padId.split(CAPTIONS_TOKEN); return splitPadId.length === 2; }; @@ -45,6 +43,7 @@ const processForCaptionsPadOnly = fn => (message, ...args) => { }; export { + CAPTIONS_TOKEN, generatePadId, processForCaptionsPadOnly, isEnabled, diff --git a/bigbluebutton-html5/imports/api/captions/server/methods/appendText.js b/bigbluebutton-html5/imports/api/captions/server/methods/appendText.js index 672c47787b..f15099c072 100644 --- a/bigbluebutton-html5/imports/api/captions/server/methods/appendText.js +++ b/bigbluebutton-html5/imports/api/captions/server/methods/appendText.js @@ -23,8 +23,9 @@ export default function appendText(text, locale) { responseType: 'json', }).then((response) => { const { status } = response; - if (status === 200) { - Logger.verbose('Captions: appended text', { padId }); + if (status !== 200) { + Logger.error(`Could not append captions for padId=${padId}`); + return; } }).catch(error => Logger.error(`Could not append captions for padId=${padId}: ${error}`)); } diff --git a/bigbluebutton-html5/imports/api/captions/server/methods/createCaptions.js b/bigbluebutton-html5/imports/api/captions/server/methods/createCaptions.js index 888f340bca..24215b91c5 100644 --- a/bigbluebutton-html5/imports/api/captions/server/methods/createCaptions.js +++ b/bigbluebutton-html5/imports/api/captions/server/methods/createCaptions.js @@ -25,6 +25,7 @@ export default function createCaptions(meetingId) { const { status } = response; if (status !== 200) { Logger.error(`Could not get locales info for ${meetingId} ${status}`); + return; } const locales = response.data; locales.forEach((locale) => { diff --git a/bigbluebutton-html5/imports/api/captions/server/methods/editCaptions.js b/bigbluebutton-html5/imports/api/captions/server/methods/editCaptions.js index 9cd5698102..f613b08270 100644 --- a/bigbluebutton-html5/imports/api/captions/server/methods/editCaptions.js +++ b/bigbluebutton-html5/imports/api/captions/server/methods/editCaptions.js @@ -21,7 +21,6 @@ export default function editCaptions(padId, data) { return; } - const { meetingId, ownerId, diff --git a/bigbluebutton-html5/imports/api/captions/server/methods/fetchReadOnlyPadId.js b/bigbluebutton-html5/imports/api/captions/server/methods/fetchReadOnlyPadId.js index fc34f216f5..fb487fa0a8 100644 --- a/bigbluebutton-html5/imports/api/captions/server/methods/fetchReadOnlyPadId.js +++ b/bigbluebutton-html5/imports/api/captions/server/methods/fetchReadOnlyPadId.js @@ -11,11 +11,17 @@ export default function fetchReadOnlyPadId(padId) { check(padId, String); const readOnlyURL = getReadOnlyIdURL(padId); + axios({ method: 'get', url: readOnlyURL, responseType: 'json', }).then((response) => { + const { status } = response; + if (status !== 200) { + Logger.error(`Could not get closed captions readOnlyID for ${padId} ${status}`); + return; + } const readOnlyPadId = getDataFromResponse(response.data, 'readOnlyID'); if (readOnlyPadId) { updateReadOnlyPadId(padId, readOnlyPadId); diff --git a/bigbluebutton-html5/imports/api/captions/server/methods/takeOwnership.js b/bigbluebutton-html5/imports/api/captions/server/methods/takeOwnership.js index 51b176a2a6..af649a8a89 100644 --- a/bigbluebutton-html5/imports/api/captions/server/methods/takeOwnership.js +++ b/bigbluebutton-html5/imports/api/captions/server/methods/takeOwnership.js @@ -2,13 +2,14 @@ import { check } from 'meteor/check'; import Captions from '/imports/api/captions'; import updateOwnerId from '/imports/api/captions/server/modifiers/updateOwnerId'; import { extractCredentials } from '/imports/api/common/server/helpers'; +import { CAPTIONS_TOKEN } from '/imports/api/captions/server/helpers'; export default function takeOwnership(locale) { const { meetingId, requesterUserId } = extractCredentials(this.userId); check(locale, String); - const pad = Captions.findOne({ meetingId, padId: { $regex: `_captions_${locale}$` } }); + const pad = Captions.findOne({ meetingId, padId: { $regex: `${CAPTIONS_TOKEN}${locale}$` } }); if (pad) { updateOwnerId(meetingId, requesterUserId, pad.padId); diff --git a/bigbluebutton-html5/imports/api/common/server/helpers.js b/bigbluebutton-html5/imports/api/common/server/helpers.js index 99720cbdb8..c0fb424ce9 100755 --- a/bigbluebutton-html5/imports/api/common/server/helpers.js +++ b/bigbluebutton-html5/imports/api/common/server/helpers.js @@ -1,3 +1,4 @@ +import sha1 from 'crypto-js/sha1'; import Users from '/imports/api/users'; const MSG_DIRECT_TYPE = 'DIRECT'; @@ -38,31 +39,7 @@ export const processForHTML5ServerOnly = fn => (message, ...args) => { return fn(message, ...args); }; -/** - * Calculate a 32 bit FNV-1a hash - * Found here: https://gist.github.com/vaiorabbit/5657561 - * Ref.: http://isthe.com/chongo/tech/comp/fnv/ - * - * @param {string} str the input value - * @param {boolean} [asString=false] set to true to return the hash value as - * 8-digit hex string instead of an integer - * @param {integer} [seed] optionally pass the hash of the previous chunk - * @returns {integer | string} - */ -/* eslint-disable */ -export const hashFNV32a = (str, asString, seed) => { - let hval = (seed === undefined) ? 0x811c9dc5 : seed; - - for (let i = 0, l = str.length; i < l; i++) { - hval ^= str.charCodeAt(i); - hval += (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24); - } - if (asString) { - return (`0000000${(hval >>> 0).toString(16)}`).substr(-8); - } - return hval >>> 0; -}; -/* eslint-enable */ +export const hashSHA1 = (str) => sha1(str).toString(); export const extractCredentials = (credentials) => { if (!credentials) return {}; diff --git a/bigbluebutton-html5/imports/api/note/server/helpers.js b/bigbluebutton-html5/imports/api/note/server/helpers.js index b4c5214fc4..cfac36af96 100644 --- a/bigbluebutton-html5/imports/api/note/server/helpers.js +++ b/bigbluebutton-html5/imports/api/note/server/helpers.js @@ -1,5 +1,5 @@ import { Meteor } from 'meteor/meteor'; -import { hashFNV32a } from '/imports/api/common/server/helpers'; +import { hashSHA1 } from '/imports/api/common/server/helpers'; const ETHERPAD = Meteor.settings.private.etherpad; const NOTE_CONFIG = Meteor.settings.public.note; @@ -12,10 +12,7 @@ const getReadOnlyIdURL = padId => `${BASE_URL}/getReadOnlyID?apikey=${ETHERPAD.a const appendTextURL = (padId, text) => `${BASE_URL}/appendText?apikey=${ETHERPAD.apikey}&padID=${padId}&text=${encodeURIComponent(text)}`; -const generateNoteId = (meetingId) => { - const noteId = hashFNV32a(meetingId, true); - return noteId; -}; +const generateNoteId = (meetingId) => hashSHA1(meetingId+ETHERPAD.apikey); const isEnabled = () => NOTE_CONFIG.enabled; diff --git a/bigbluebutton-html5/imports/api/note/server/methods/createNote.js b/bigbluebutton-html5/imports/api/note/server/methods/createNote.js index 8ebdbac20a..656d461c06 100644 --- a/bigbluebutton-html5/imports/api/note/server/methods/createNote.js +++ b/bigbluebutton-html5/imports/api/note/server/methods/createNote.js @@ -22,6 +22,7 @@ export default function createNote(meetingId) { const noteId = generateNoteId(meetingId); const createURL = createPadURL(noteId); + axios({ method: 'get', url: createURL, @@ -30,6 +31,7 @@ export default function createNote(meetingId) { const { status } = responseOuter; if (status !== 200) { Logger.error(`Could not get note info for ${meetingId} ${status}`); + return; } const readOnlyURL = getReadOnlyIdURL(noteId); axios({ diff --git a/bigbluebutton-html5/imports/ui/components/captions/pad/service.js b/bigbluebutton-html5/imports/ui/components/captions/pad/service.js index 47af07b506..592362c672 100644 --- a/bigbluebutton-html5/imports/ui/components/captions/pad/service.js +++ b/bigbluebutton-html5/imports/ui/components/captions/pad/service.js @@ -10,11 +10,9 @@ const getLang = () => { }; const getPadParams = () => { - const { config } = NOTE_CONFIG; - const User = Users.findOne({ userId: Auth.userID }, { fields: { name: 1, color: 1 } }); - config.userName = User.name; - config.userColor = User.color; + let config = {}; config.lang = getLang(); + config.rtl = document.documentElement.getAttribute('dir') === 'rtl'; const params = []; Object.keys(config).forEach((k) => { @@ -26,12 +24,12 @@ const getPadParams = () => { const getPadURL = (padId, readOnlyPadId, ownerId) => { const userId = Auth.userID; + const params = getPadParams(); let url; if (!ownerId || (ownerId && userId === ownerId)) { - const params = getPadParams(); url = Auth.authenticateURL(`${NOTE_CONFIG.url}/p/${padId}?${params}`); } else { - url = Auth.authenticateURL(`${NOTE_CONFIG.url}/p/${readOnlyPadId}`); + url = Auth.authenticateURL(`${NOTE_CONFIG.url}/p/${readOnlyPadId}?${params}`); } return url; }; diff --git a/bigbluebutton-html5/imports/ui/components/captions/service.js b/bigbluebutton-html5/imports/ui/components/captions/service.js index a7fe093c35..c757d0e41b 100644 --- a/bigbluebutton-html5/imports/ui/components/captions/service.js +++ b/bigbluebutton-html5/imports/ui/components/captions/service.js @@ -7,7 +7,7 @@ import { Meteor } from 'meteor/meteor'; import { Session } from 'meteor/session'; const CAPTIONS_CONFIG = Meteor.settings.public.captions; -const CAPTIONS = '_captions_'; +const CAPTIONS_TOKEN = '_cc_'; const LINE_BREAK = '\n'; const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator; @@ -19,7 +19,7 @@ const getActiveCaptions = () => { const getCaptions = locale => Captions.findOne({ meetingId: Auth.meetingID, - padId: { $regex: `${CAPTIONS}${locale}$` }, + padId: { $regex: `${CAPTIONS_TOKEN}${locale}$` }, }); const getCaptionsData = () => { @@ -170,6 +170,7 @@ const initSpeechRecognition = (locale) => { }; export default { + CAPTIONS_TOKEN, getCaptionsData, getAvailableLocales, getOwnedLocales, diff --git a/bigbluebutton-html5/imports/ui/components/note/service.js b/bigbluebutton-html5/imports/ui/components/note/service.js index 303bece5e1..1a6ee84ea1 100644 --- a/bigbluebutton-html5/imports/ui/components/note/service.js +++ b/bigbluebutton-html5/imports/ui/components/note/service.js @@ -24,18 +24,15 @@ const getLang = () => { }; const getNoteParams = () => { - const { config } = NOTE_CONFIG; - const User = Users.findOne({ userId: Auth.userID }, { fields: { name: 1, color: 1 } }); - config.userName = User.name; - config.userColor = User.color; + let config = {}; config.lang = getLang(); + config.rtl = document.documentElement.getAttribute('dir') === 'rtl'; const params = []; - for (const key in config) { - if (config.hasOwnProperty(key)) { - params.push(`${key}=${encodeURIComponent(config[key])}`); - } - } + Object.keys(config).forEach((k) => { + params.push(`${k}=${encodeURIComponent(config[k])}`); + }); + return params.join('&'); }; @@ -51,7 +48,8 @@ const isLocked = () => { const getReadOnlyURL = () => { const readOnlyNoteId = getReadOnlyNoteId(); - const url = Auth.authenticateURL(`${NOTE_CONFIG.url}/p/${readOnlyNoteId}`); + const params = getNoteParams(); + const url = Auth.authenticateURL(`${NOTE_CONFIG.url}/p/${readOnlyNoteId}?${params}`); return url; }; diff --git a/bigbluebutton-html5/package-lock.json b/bigbluebutton-html5/package-lock.json index 221ca9289f..693d59c2fc 100644 --- a/bigbluebutton-html5/package-lock.json +++ b/bigbluebutton-html5/package-lock.json @@ -1127,6 +1127,11 @@ "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=", "dev": true }, + "crypto-js": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.0.0.tgz", + "integrity": "sha512-bzHZN8Pn+gS7DQA6n+iUmBfl0hO5DJq++QP3U6uTucDtk/0iGpXd/Gg7CGR0p8tJhofJyaKoWBuJI4eAO00BBg==" + }, "css-selector-tokenizer": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz", diff --git a/bigbluebutton-html5/package.json b/bigbluebutton-html5/package.json index 0fba447a48..04f35967e9 100755 --- a/bigbluebutton-html5/package.json +++ b/bigbluebutton-html5/package.json @@ -41,6 +41,7 @@ "browser-detect": "^0.2.28", "classnames": "^2.2.6", "clipboard": "^2.0.4", + "crypto-js": "^4.0.0", "eventemitter2": "~5.0.1", "fastdom": "^1.0.9", "fibers": "^3.1.1", diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml index 3149cd1087..d3ee7bd7bc 100755 --- a/bigbluebutton-html5/private/config/settings.yml +++ b/bigbluebutton-html5/private/config/settings.yml @@ -288,12 +288,6 @@ public: note: enabled: false url: ETHERPAD_HOST - config: - showLineNumbers: false - showChat: false - noColors: true - showControls: true - rtl: false layout: autoSwapLayout: false hidePresentation: false diff --git a/record-and-playback/core/Gemfile b/record-and-playback/core/Gemfile index 375fc60260..f59575913f 100644 --- a/record-and-playback/core/Gemfile +++ b/record-and-playback/core/Gemfile @@ -22,7 +22,6 @@ source "https://rubygems.org" gem "absolute_time" gem "builder" gem "fastimage" -gem "fnv" gem "java_properties" gem "journald-logger" gem "jwt" diff --git a/record-and-playback/core/Gemfile.lock b/record-and-playback/core/Gemfile.lock index e4ca02b41a..4bba32d81d 100644 --- a/record-and-playback/core/Gemfile.lock +++ b/record-and-playback/core/Gemfile.lock @@ -7,7 +7,6 @@ GEM crass (1.0.5) fastimage (2.1.5) ffi (1.11.1) - fnv (0.2.0) jaro_winkler (1.5.2) java_properties (0.0.4) journald-logger (2.0.4) @@ -48,7 +47,6 @@ DEPENDENCIES absolute_time builder fastimage - fnv java_properties journald-logger jwt diff --git a/record-and-playback/core/lib/recordandplayback.rb b/record-and-playback/core/lib/recordandplayback.rb index 59eb0b6346..db4661644f 100755 --- a/record-and-playback/core/lib/recordandplayback.rb +++ b/record-and-playback/core/lib/recordandplayback.rb @@ -36,7 +36,7 @@ require 'logger' require 'find' require 'rubygems' require 'net/http' -require 'fnv' +require 'digest' require 'shellwords' require 'English' @@ -226,9 +226,10 @@ module BigBlueButton r.split("-")[1].to_i / 1000 end - # Notes id will be an 8-sized hash string based on the meeting id - def self.get_notes_id(meeting_id) - FNV.new.fnv1a_32(meeting_id).to_s(16).rjust(8, '0') + # Notes id will be a SHA1 hash string based on the meeting id and etherpad's apikey + def self.get_notes_id(meeting_id, notes_apikey) + value = meeting_id + notes_apikey + Digest::SHA1.hexdigest value end def self.done_to_timestamp(r) diff --git a/record-and-playback/core/scripts/archive/archive.rb b/record-and-playback/core/scripts/archive/archive.rb index 824e883788..3a4167ecfb 100755 --- a/record-and-playback/core/scripts/archive/archive.rb +++ b/record-and-playback/core/scripts/archive/archive.rb @@ -45,11 +45,11 @@ def archive_events(meeting_id, redis_host, redis_port, redis_password, raw_archi end end -def archive_notes(meeting_id, notes_endpoint, notes_formats, raw_archive_dir) +def archive_notes(meeting_id, notes_endpoint, notes_formats, notes_apikey, raw_archive_dir) BigBlueButton.logger.info("Archiving notes for #{meeting_id}") notes_dir = "#{raw_archive_dir}/#{meeting_id}/notes" FileUtils.mkdir_p(notes_dir) - notes_id = BigBlueButton.get_notes_id(meeting_id) + notes_id = BigBlueButton.get_notes_id(meeting_id, notes_apikey) tmp_note = "#{notes_dir}/tmp_note.txt" BigBlueButton.try_download("#{notes_endpoint}/#{notes_id}/export/txt", tmp_note) @@ -180,6 +180,7 @@ kurento_screenshare_dir = props['kurento_screenshare_src'] log_dir = props['log_dir'] notes_endpoint = props['notes_endpoint'] notes_formats = props['notes_formats'] +notes_apikey = props['notes_apikey'] # Determine the filenames for the done and fail files if !break_timestamp.nil? @@ -198,7 +199,7 @@ archive_events(meeting_id, redis_host, redis_port, redis_password, raw_archive_d # FreeSWITCH Audio files archive_audio(meeting_id, audio_dir, raw_archive_dir) # Etherpad notes -archive_notes(meeting_id, notes_endpoint, notes_formats, raw_archive_dir) +archive_notes(meeting_id, notes_endpoint, notes_formats, notes_apikey, raw_archive_dir) # Presentation files archive_directory("#{presentation_dir}/#{meeting_id}/#{meeting_id}", "#{target_dir}/presentation") # Red5 media diff --git a/record-and-playback/core/scripts/bigbluebutton.yml b/record-and-playback/core/scripts/bigbluebutton.yml index c5dd4006a3..d491cf8aa9 100755 --- a/record-and-playback/core/scripts/bigbluebutton.yml +++ b/record-and-playback/core/scripts/bigbluebutton.yml @@ -8,6 +8,7 @@ raw_webrtc_deskshare_src: /usr/share/red5/webapps/video-broadcast/streams raw_deskshare_src: /var/bigbluebutton/deskshare raw_presentation_src: /var/bigbluebutton notes_endpoint: http://localhost:9001/p +notes_apikey: ETHERPAD_APIKEY # Specify the notes formats we archive # txt, doc and odt are also supported notes_formats: diff --git a/record-and-playback/core/scripts/events/events.rb b/record-and-playback/core/scripts/events/events.rb index 22d9035dc5..85200edcac 100755 --- a/record-and-playback/core/scripts/events/events.rb +++ b/record-and-playback/core/scripts/events/events.rb @@ -24,9 +24,9 @@ require 'logger' require 'trollop' require 'yaml' -def keep_etherpad_events(meeting_id, events_etherpad, notes_endpoint) +def keep_etherpad_events(meeting_id, events_etherpad, notes_endpoint, notes_apikey) BigBlueButton.logger.info("Keeping etherpad events for #{meeting_id}") - notes_id = BigBlueButton.get_notes_id(meeting_id) + notes_id = BigBlueButton.get_notes_id(meeting_id, notes_apikey) # Always fetch for the audit format BigBlueButton.try_download("#{notes_endpoint}/#{notes_id}/export/etherpad", events_etherpad) @@ -61,6 +61,7 @@ redis_port = props['redis_port'] redis_password = props['redis_password'] log_dir = props['log_dir'] notes_endpoint = props['notes_endpoint'] +notes_apikey = props['notes_apikey'] raw_events_xml = "#{raw_archive_dir}/#{meeting_id}/events.xml" ended_done_file = "#{recording_dir}/status/ended/#{meeting_id}.done" @@ -83,7 +84,7 @@ if not FileTest.directory?(target_dir) FileUtils.mkdir_p target_dir events_etherpad = "#{target_dir}/events.etherpad" - keep_etherpad_events(meeting_id, events_etherpad, notes_endpoint) + keep_etherpad_events(meeting_id, events_etherpad, notes_endpoint, notes_apikey) events_xml = "#{target_dir}/events.xml" if File.exist? raw_events_xml