diff --git a/app/src/HtmlInjector.js b/app/src/HtmlInjector.js
new file mode 100644
index 00000000..56f5d426
--- /dev/null
+++ b/app/src/HtmlInjector.js
@@ -0,0 +1,95 @@
+const fs = require('fs');
+
+const Logger = require('./Logger');
+
+const log = new Logger('HtmlInjection');
+
+class HtmlInjector {
+ constructor(filesPath, config) {
+ this.filesPath = filesPath; // Array of file paths to cache
+ this.cache = {}; // Object to store cached files
+ this.config = config; // Configuration containing metadata (OG, title, etc.)
+ this.injectData = this.getInjectData(); // Initialize dynamic injection data
+ this.preloadPages(filesPath); // Preload pages at startup
+ this.watchFiles(filesPath); // Watch files for changes
+ log.info('filesPath cached', this.filesPath);
+ }
+
+ // Function to get dynamic data for injection (e.g., OG data, title, etc.)
+ getInjectData() {
+ return {
+ OG_TYPE: this.config.og?.type || 'app-webrtc',
+ OG_SITE_NAME: this.config.og?.siteName || 'MiroTalk SFU',
+ OG_TITLE: this.config.og?.title || 'Click the link to make a call.',
+ OG_DESCRIPTION:
+ this.config.og?.description ||
+ 'MiroTalk SFU calling provides real-time video calls, messaging and screen sharing.',
+ OG_IMAGE: this.config.og?.image || 'https://sfu.mirotalk.com/images/mirotalksfu.png',
+ OG_URL: this.config.og?.url || 'https://sfu.mirotalk.com',
+ // Add more data here as needed with fallbacks
+ };
+ }
+
+ // Function to load a file into the cache
+ loadFileToCache(filePath) {
+ try {
+ const content = fs.readFileSync(filePath, 'utf-8');
+ this.cache[filePath] = content; // Store the content in cache
+ } catch (err) {
+ log.error(`Error reading file: ${filePath}`, err);
+ }
+ }
+
+ // Function to preload pages into the cache
+ preloadPages(filePaths) {
+ filePaths.forEach((filePath) => this.loadFileToCache(filePath));
+ }
+
+ // Function to watch a file for changes and reload the cache
+ watchFileForChanges(filePath) {
+ fs.watch(filePath, (eventType) => {
+ if (eventType === 'change') {
+ log.debug(`File changed: ${filePath}`);
+ this.loadFileToCache(filePath);
+ log.debug(`Reload the file ${filePath} into cache`);
+ }
+ });
+ }
+
+ // Function to watch all files for changes
+ watchFiles(filePaths) {
+ filePaths.forEach((filePath) => this.watchFileForChanges(filePath));
+ }
+
+ // Function to inject dynamic data (e.g., OG, TITLE, etc.) into a given file
+ injectHtml(filePath, res) {
+ // return res.send(this.cache[filePath]);
+
+ if (!this.cache[filePath]) {
+ log.error(`File not cached: ${filePath}`);
+ if (!res.headersSent) {
+ return res.status(500).send('Server Error');
+ }
+ return;
+ }
+
+ try {
+ // Replace placeholders with dynamic data (OG, TITLE, etc.)
+ const modifiedHTML = this.cache[filePath].replace(
+ /{{(OG_[A-Z_]+)}}/g,
+ (_, key) => this.injectData[key] || '',
+ );
+
+ if (!res.headersSent) {
+ res.send(modifiedHTML);
+ }
+ } catch (error) {
+ log.error('Error injecting HTML data:', error);
+ if (!res.headersSent) {
+ res.status(500).send('Server Error');
+ }
+ }
+ }
+}
+
+module.exports = HtmlInjector;
diff --git a/app/src/Server.js b/app/src/Server.js
index 0d6f60c5..1b63cc22 100644
--- a/app/src/Server.js
+++ b/app/src/Server.js
@@ -55,7 +55,7 @@ dev dependencies: {
* @license For commercial or closed source, contact us at license.mirotalk@gmail.com or purchase directly via CodeCanyon
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
- * @version 1.7.17
+ * @version 1.7.18
*
*/
@@ -81,6 +81,7 @@ const Peer = require('./Peer');
const ServerApi = require('./ServerApi');
const Logger = require('./Logger');
const Validator = require('./Validator');
+const HtmlInjector = require('./HtmlInjector');
const log = new Logger('Server');
const yaml = require('js-yaml');
const swaggerUi = require('swagger-ui-express');
@@ -237,6 +238,7 @@ if (serverRecordingEnabled) {
// html views
const views = {
+ html: path.join(__dirname, '../../public/views'),
about: path.join(__dirname, '../../', 'public/views/about.html'),
landing: path.join(__dirname, '../../', 'public/views/landing.html'),
login: path.join(__dirname, '../../', 'public/views/login.html'),
@@ -249,6 +251,10 @@ const views = {
whoAreYou: path.join(__dirname, '../../', 'public/views/whoAreYou.html'),
};
+const filesPath = [views.landing, views.newRoom, views.room, views.login];
+
+const htmlInjector = new HtmlInjector(filesPath, config.ui.brand);
+
const authHost = new Host(); // Authenticated IP by Login
const roomList = new Map(); // All Rooms
@@ -457,20 +463,19 @@ function startServer() {
});
// main page
- app.get(['/'], OIDCAuth, (req, res) => {
+ app.get('/', OIDCAuth, (req, res) => {
//log.debug('/ - hostCfg ----->', hostCfg);
-
if (!OIDC.enabled && hostCfg.protected) {
const ip = getIP(req);
if (allowedIP(ip)) {
- res.sendFile(views.landing);
+ htmlInjector.injectHtml(views.landing, res);
hostCfg.authenticated = true;
} else {
hostCfg.authenticated = false;
res.redirect('/login');
}
} else {
- res.sendFile(views.landing);
+ return htmlInjector.injectHtml(views.landing, res);
}
});
@@ -483,7 +488,7 @@ function startServer() {
});
// set new room name and join
- app.get(['/newroom'], OIDCAuth, (req, res) => {
+ app.get('/newroom', OIDCAuth, (req, res) => {
//log.info('/newroom - hostCfg ----->', hostCfg);
if (!OIDC.enabled && hostCfg.protected) {
@@ -496,12 +501,12 @@ function startServer() {
res.redirect('/login');
}
} else {
- res.sendFile(views.newRoom);
+ htmlInjector.injectHtml(views.newRoom, res);
}
});
// Check if room active (exists)
- app.post(['/isRoomActive'], (req, res) => {
+ app.post('/isRoomActive', (req, res) => {
const { roomId } = checkXSS(req.body);
if (roomId && (hostCfg.protected || hostCfg.user_auth)) {
@@ -571,8 +576,8 @@ function startServer() {
} catch (err) {
log.error('Direct Join JWT error', { error: err.message, token: token });
return hostCfg.protected || hostCfg.user_auth
- ? res.sendFile(views.login)
- : res.sendFile(views.landing);
+ ? htmlInjector.injectHtml(views.login, res)
+ : htmlInjector.injectHtml(views.landing, res);
}
} else {
const allowRoomAccess = isAllowedRoomAccess('/join/params', req, hostCfg, roomList, room);
@@ -601,9 +606,9 @@ function startServer() {
}
if (room && (hostCfg.authenticated || isPeerValid)) {
- return res.sendFile(views.room);
+ return htmlInjector.injectHtml(views.room, res);
} else {
- return res.sendFile(views.login);
+ return htmlInjector.injectHtml(views.login, res);
}
}
@@ -632,7 +637,7 @@ function startServer() {
if (!OIDC.enabled && hostCfg.protected && hostCfg.users_from_db) {
const roomExists = await roomExistsForUser(roomId);
log.debug('/join/:roomId exists from API endpoint', roomExists);
- return roomExists ? res.sendFile(views.room) : res.redirect('/login');
+ return roomExists ? htmlInjector.injectHtml(views.room, res) : res.redirect('/login');
}
// 2. Protect room access with configuration check
if (!OIDC.enabled && hostCfg.protected && !hostCfg.users_from_db) {
@@ -640,9 +645,9 @@ function startServer() {
(user) => user.allowed_rooms && (user.allowed_rooms.includes(roomId) || roomList.has(roomId)),
);
log.debug('/join/:roomId exists from config allowed rooms', roomExists);
- return roomExists ? res.sendFile(views.room) : res.redirect('/whoAreYou/' + roomId);
+ return roomExists ? htmlInjector.injectHtml(views.room, res) : res.redirect('/whoAreYou/' + roomId);
}
- res.sendFile(views.room);
+ htmlInjector.injectHtml(views.room, res);
} else {
// Who are you?
!OIDC.enabled && hostCfg.protected ? res.redirect('/whoAreYou/' + roomId) : res.redirect('/');
@@ -655,42 +660,42 @@ function startServer() {
});
// if not allow video/audio
- app.get(['/permission'], (req, res) => {
+ app.get('/permission', (req, res) => {
res.sendFile(views.permission);
});
// privacy policy
- app.get(['/privacy'], (req, res) => {
+ app.get('/privacy', (req, res) => {
res.sendFile(views.privacy);
});
// mirotalk about
- app.get(['/about'], (req, res) => {
+ app.get('/about', (req, res) => {
res.sendFile(views.about);
});
// Get stats endpoint
- app.get(['/stats'], (req, res) => {
+ app.get('/stats', (req, res) => {
const stats = config.stats ? config.stats : defaultStats;
// log.debug('Send stats', stats);
res.send(stats);
});
// handle who are you: Presenter or Guest
- app.get(['/whoAreYou/:roomId'], (req, res) => {
+ app.get('/whoAreYou/:roomId', (req, res) => {
res.sendFile(views.whoAreYou);
});
// handle login if user_auth enabled
- app.get(['/login'], (req, res) => {
+ app.get('/login', (req, res) => {
if (hostCfg.protected || hostCfg.user_auth) {
- return res.sendFile(views.login);
+ return htmlInjector.injectHtml(views.login, res);
}
res.redirect('/');
});
// handle logged on host protected
- app.get(['/logged'], (req, res) => {
+ app.get('/logged', (req, res) => {
const ip = getIP(req);
if (allowedIP(ip)) {
res.redirect('/');
@@ -706,7 +711,7 @@ function startServer() {
// ####################################################
// handle login on host protected
- app.post(['/login'], async (req, res) => {
+ app.post('/login', async (req, res) => {
const ip = getIP(req);
log.debug(`Request login to host from: ${ip}`, req.body);
@@ -753,7 +758,7 @@ function startServer() {
// KEEP RECORDING ON SERVER DIR
// ####################################################
- app.post(['/recSync'], (req, res) => {
+ app.post('/recSync', (req, res) => {
// Store recording...
if (serverRecordingEnabled) {
//
@@ -940,7 +945,7 @@ function startServer() {
// REST API
// ####################################################
- app.get([restApi.basePath + '/stats'], (req, res) => {
+ app.get(restApi.basePath + '/stats', (req, res) => {
try {
// Check if endpoint allowed
if (restApi.allowed && !restApi.allowed.stats) {
@@ -985,7 +990,7 @@ function startServer() {
});
// request meetings list
- app.get([restApi.basePath + '/meetings'], (req, res) => {
+ app.get(restApi.basePath + '/meetings', (req, res) => {
// Check if endpoint allowed
if (restApi.allowed && !restApi.allowed.meetings) {
return res.status(403).json({
@@ -1014,7 +1019,7 @@ function startServer() {
});
// request meeting room endpoint
- app.post([restApi.basePath + '/meeting'], (req, res) => {
+ app.post(restApi.basePath + '/meeting', (req, res) => {
// Check if endpoint allowed
if (restApi.allowed && !restApi.allowed.meeting) {
return res.status(403).json({
@@ -1043,7 +1048,7 @@ function startServer() {
});
// request join room endpoint
- app.post([restApi.basePath + '/join'], (req, res) => {
+ app.post(restApi.basePath + '/join', (req, res) => {
// Check if endpoint allowed
if (restApi.allowed && !restApi.allowed.join) {
return res.status(403).json({
@@ -1072,7 +1077,7 @@ function startServer() {
});
// request token endpoint
- app.post([restApi.basePath + '/token'], (req, res) => {
+ app.post(restApi.basePath + '/token', (req, res) => {
// Check if endpoint allowed
if (restApi.allowed && !restApi.allowed.token) {
return res.status(403).json({
diff --git a/package.json b/package.json
index 1c94ee6e..79098b1a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "mirotalksfu",
- "version": "1.7.17",
+ "version": "1.7.18",
"description": "WebRTC SFU browser-based video calls",
"main": "Server.js",
"scripts": {
diff --git a/public/js/Room.js b/public/js/Room.js
index fad8150a..6593d429 100644
--- a/public/js/Room.js
+++ b/public/js/Room.js
@@ -11,7 +11,7 @@ if (location.href.substr(0, 5) !== 'https') location.href = 'https' + location.h
* @license For commercial or closed source, contact us at license.mirotalk@gmail.com or purchase directly via CodeCanyon
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
- * @version 1.7.17
+ * @version 1.7.18
*
*/
@@ -4905,7 +4905,7 @@ function showAbout() {
imageUrl: image.about,
customClass: { image: 'img-about' },
position: 'center',
- title: 'WebRTC SFU v1.7.17',
+ title: 'WebRTC SFU v1.7.18',
html: `