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.
This commit is contained in:
parent
705ea9915a
commit
c0a7f9cd92
@ -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,
|
||||
|
@ -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}`));
|
||||
}
|
||||
|
@ -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) => {
|
||||
|
@ -21,7 +21,6 @@ export default function editCaptions(padId, data) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const {
|
||||
meetingId,
|
||||
ownerId,
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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 {};
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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({
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
5
bigbluebutton-html5/package-lock.json
generated
5
bigbluebutton-html5/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user