2223 lines
77 KiB
JavaScript
Executable File
2223 lines
77 KiB
JavaScript
Executable File
/*
|
||
http://patorjk.com/software/taag/#p=display&f=ANSI%20Regular&t=Server
|
||
|
||
███████ ███████ ██████ ██ ██ ███████ ██████
|
||
██ ██ ██ ██ ██ ██ ██ ██ ██
|
||
███████ █████ ██████ ██ ██ █████ ██████
|
||
██ ██ ██ ██ ██ ██ ██ ██ ██
|
||
███████ ███████ ██ ██ ████ ███████ ██ ██
|
||
|
||
dependencies: {
|
||
@mattermost/client : https://www.npmjs.com/package/@mattermost/client
|
||
@ngrok/ngrok : https://www.npmjs.com/package/@ngrok/ngrok
|
||
@sentry/node : https://www.npmjs.com/package/@sentry/node
|
||
axios : https://www.npmjs.com/package/axios
|
||
chokidar : https://www.npmjs.com/package/chokidar
|
||
colors : https://www.npmjs.com/package/colors
|
||
compression : https://www.npmjs.com/package/compression
|
||
cors : https://www.npmjs.com/package/cors
|
||
crypto-js : https://www.npmjs.com/package/crypto-js
|
||
dompurify : https://www.npmjs.com/package/dompurify
|
||
dotenv : https://www.npmjs.com/package/dotenv
|
||
express : https://www.npmjs.com/package/express
|
||
express-openid-connect : https://www.npmjs.com/package/express-openid-connect
|
||
he : https://www.npmjs.com/package/he
|
||
helmet : https://www.npmjs.com/package/helmet
|
||
httpolyglot : https://www.npmjs.com/package/httpolyglot
|
||
jsdom : https://www.npmjs.com/package/jsdom
|
||
jsonwebtoken : https://www.npmjs.com/package/jsonwebtoken
|
||
js-yaml : https://www.npmjs.com/package/js-yaml
|
||
nodemailer : https://www.npmjs.com/package/nodemailer
|
||
openai : https://www.npmjs.com/package/openai
|
||
qs : https://www.npmjs.com/package/qs
|
||
socket.io : https://www.npmjs.com/package/socket.io
|
||
swagger-ui-express : https://www.npmjs.com/package/swagger-ui-express
|
||
uuid : https://www.npmjs.com/package/uuid
|
||
}
|
||
*/
|
||
|
||
/**
|
||
* MiroTalk P2P - Server component
|
||
*
|
||
* @link GitHub: https://github.com/miroslavpejic85/mirotalk
|
||
* @link Official Live demo: https://p2p.mirotalk.com
|
||
* @license For open source use: AGPLv3
|
||
* @license For commercial use or closed source, contact us at license.mirotalk@gmail.com or purchase directly from CodeCanyon
|
||
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-p2p-webrtc-realtime-video-conferences/38376661
|
||
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
|
||
* @version 1.5.45
|
||
*
|
||
*/
|
||
|
||
'use strict'; // https://www.w3schools.com/js/js_strict.asp
|
||
|
||
require('dotenv').config();
|
||
|
||
const { auth, requiresAuth } = require('express-openid-connect');
|
||
const { Server } = require('socket.io');
|
||
const httpolyglot = require('httpolyglot');
|
||
const compression = require('compression');
|
||
const express = require('express');
|
||
const cors = require('cors');
|
||
const helmet = require('helmet');
|
||
const path = require('path');
|
||
const axios = require('axios');
|
||
const jwt = require('jsonwebtoken');
|
||
const app = express();
|
||
const fs = require('fs');
|
||
const checkXSS = require('./xss.js');
|
||
const ServerApi = require('./api');
|
||
const MattermostController = require('./mattermost');
|
||
const Validate = require('./validate');
|
||
const HtmlInjector = require('./htmlInjector');
|
||
const Host = require('./host');
|
||
const Logs = require('./logs');
|
||
const log = new Logs('server');
|
||
|
||
// Custom Brand and buttons
|
||
const config = safeRequire('./config');
|
||
|
||
// Email alerts and notifications
|
||
const nodemailer = require('./lib/nodemailer');
|
||
|
||
const packageJson = require('../../package.json');
|
||
|
||
const port = process.env.PORT || 3000; // must be the same to client.js signalingServerPort
|
||
const host = process.env.HOST || `http://localhost:${port}`;
|
||
|
||
const authHost = new Host(); // Authenticated IP by Login
|
||
|
||
// Define paths to the SSL key and certificate files
|
||
const keyPath = path.join(__dirname, '../ssl/key.pem');
|
||
const certPath = path.join(__dirname, '../ssl/cert.pem');
|
||
|
||
// Read SSL key and certificate files securely
|
||
const options = {
|
||
key: fs.readFileSync(keyPath, 'utf-8'),
|
||
cert: fs.readFileSync(certPath, 'utf-8'),
|
||
};
|
||
|
||
// Server both http and https
|
||
const server = httpolyglot.createServer(options, app);
|
||
|
||
// Trust Proxy
|
||
const trustProxy = !!getEnvBoolean(process.env.TRUST_PROXY);
|
||
|
||
// Cors
|
||
const cors_origin = process.env.CORS_ORIGIN;
|
||
const cors_methods = process.env.CORS_METHODS;
|
||
|
||
let corsOrigin = '*';
|
||
let corsMethods = ['GET', 'POST'];
|
||
|
||
if (cors_origin && cors_origin !== '*') {
|
||
try {
|
||
corsOrigin = JSON.parse(cors_origin);
|
||
} catch (error) {
|
||
log.error('Error parsing CORS_ORIGIN', error.message);
|
||
}
|
||
}
|
||
|
||
if (cors_methods && cors_methods !== '') {
|
||
try {
|
||
corsMethods = JSON.parse(cors_methods);
|
||
} catch (error) {
|
||
log.error('Error parsing CORS_METHODS', error.message);
|
||
}
|
||
}
|
||
|
||
const corsOptions = {
|
||
origin: corsOrigin,
|
||
methods: corsMethods,
|
||
};
|
||
|
||
/*
|
||
Set maxHttpBufferSize from 1e6 (1MB) to 1e7 (10MB)
|
||
*/
|
||
const io = new Server({
|
||
maxHttpBufferSize: 1e7,
|
||
transports: ['websocket'],
|
||
cors: corsOptions,
|
||
}).listen(server);
|
||
|
||
// console.log(io);
|
||
|
||
// Host protection (disabled by default)
|
||
const hostProtected = getEnvBoolean(process.env.HOST_PROTECTED);
|
||
const userAuth = getEnvBoolean(process.env.HOST_USER_AUTH);
|
||
const hostUsersString = process.env.HOST_USERS || '[{"username": "MiroTalk", "password": "P2P"}]';
|
||
const hostUsers = JSON.parse(hostUsersString);
|
||
const hostCfg = {
|
||
protected: hostProtected,
|
||
user_auth: userAuth,
|
||
users: hostUsers,
|
||
authenticated: !hostProtected,
|
||
maxRoomParticipants: parseInt(process.env.ROOM_MAX_PARTICIPANTS) || 8,
|
||
};
|
||
|
||
// JWT config
|
||
const jwtCfg = {
|
||
JWT_KEY: process.env.JWT_KEY || 'mirotalk_jwt_secret',
|
||
JWT_EXP: process.env.JWT_EXP || '1h',
|
||
};
|
||
|
||
// Room presenters
|
||
const roomPresentersString = process.env.PRESENTERS || '["MiroTalk P2P"]';
|
||
const roomPresenters = JSON.parse(roomPresentersString);
|
||
|
||
// Swagger config
|
||
const yaml = require('js-yaml');
|
||
const swaggerUi = require('swagger-ui-express');
|
||
const swaggerDocument = yaml.load(fs.readFileSync(path.join(__dirname, '/../api/swagger.yaml'), 'utf8'));
|
||
|
||
// Api config
|
||
const { v4: uuidV4 } = require('uuid');
|
||
const apiBasePath = '/api/v1'; // api endpoint path
|
||
const api_docs = host + apiBasePath + '/docs'; // api docs
|
||
const api_key_secret = process.env.API_KEY_SECRET || 'mirotalkp2p_default_secret';
|
||
const apiDisabledString = process.env.API_DISABLED || '["token", "meetings"]';
|
||
const api_disabled = JSON.parse(apiDisabledString);
|
||
|
||
// Ngrok config
|
||
const ngrok = require('@ngrok/ngrok');
|
||
const ngrokEnabled = getEnvBoolean(process.env.NGROK_ENABLED);
|
||
const ngrokAuthToken = process.env.NGROK_AUTH_TOKEN;
|
||
|
||
// Stun (https://bloggeek.me/webrtcglossary/stun/)
|
||
// Turn (https://bloggeek.me/webrtcglossary/turn/)
|
||
const iceServers = [];
|
||
const stunServerUrl = process.env.STUN_SERVER_URL;
|
||
const turnServerUrl = process.env.TURN_SERVER_URL;
|
||
const turnServerUsername = process.env.TURN_SERVER_USERNAME;
|
||
const turnServerCredential = process.env.TURN_SERVER_CREDENTIAL;
|
||
const stunServerEnabled = getEnvBoolean(process.env.STUN_SERVER_ENABLED);
|
||
const turnServerEnabled = getEnvBoolean(process.env.TURN_SERVER_ENABLED);
|
||
// Stun is mandatory for not internal network
|
||
if (stunServerEnabled && stunServerUrl) iceServers.push({ urls: stunServerUrl });
|
||
// Turn is recommended if direct peer to peer connection is not possible
|
||
if (turnServerEnabled && turnServerUrl && turnServerUsername && turnServerCredential) {
|
||
iceServers.push({ urls: turnServerUrl, username: turnServerUsername, credential: turnServerCredential });
|
||
}
|
||
|
||
// Test Stun and Turn connection with query params
|
||
// const testStunTurn = host + '/icetest?iceServers=' + JSON.stringify(iceServers);
|
||
const testStunTurn = host + '/icetest';
|
||
|
||
// IP Lookup
|
||
const IPLookupEnabled = getEnvBoolean(process.env.IP_LOOKUP_ENABLED);
|
||
|
||
// Survey URL
|
||
const surveyEnabled = getEnvBoolean(process.env.SURVEY_ENABLED);
|
||
const surveyURL = process.env.SURVEY_URL || 'https://www.questionpro.com/t/AUs7VZq00L';
|
||
|
||
// Redirect URL
|
||
const redirectEnabled = getEnvBoolean(process.env.REDIRECT_ENABLED);
|
||
const redirectURL = process.env.REDIRECT_URL || '/newcall';
|
||
|
||
// Sentry config
|
||
const Sentry = require('@sentry/node');
|
||
const sentryEnabled = getEnvBoolean(process.env.SENTRY_ENABLED);
|
||
const sentryDSN = process.env.SENTRY_DSN;
|
||
const sentryTracesSampleRate = parseFloat(process.env.SENTRY_TRACES_SAMPLE_RATE || '0.0');
|
||
|
||
// Slack API
|
||
const CryptoJS = require('crypto-js');
|
||
const qS = require('qs');
|
||
const slackEnabled = getEnvBoolean(process.env.SLACK_ENABLED);
|
||
const slackSigningSecret = process.env.SLACK_SIGNING_SECRET;
|
||
|
||
// Setup sentry client
|
||
if (sentryEnabled && typeof sentryDSN === 'string' && sentryDSN.trim()) {
|
||
log.info('Sentry monitoring started...');
|
||
|
||
Sentry.init({
|
||
dsn: sentryDSN,
|
||
tracesSampleRate: sentryTracesSampleRate,
|
||
});
|
||
|
||
const logLevels = process.env.SENTRY_LOG_LEVELS
|
||
? process.env.SENTRY_LOG_LEVELS.split(',').map((level) => level.trim())
|
||
: ['error'];
|
||
|
||
const originalConsole = {};
|
||
logLevels.forEach((level) => {
|
||
originalConsole[level] = console[level];
|
||
console[level] = function (...args) {
|
||
switch (level) {
|
||
case 'warn':
|
||
Sentry.captureMessage(args.join(' '), 'warning');
|
||
break;
|
||
case 'error':
|
||
args[0] instanceof Error
|
||
? Sentry.captureException(args[0])
|
||
: Sentry.captureException(new Error(args.join(' ')));
|
||
break;
|
||
}
|
||
originalConsole[level].apply(console, args);
|
||
};
|
||
});
|
||
|
||
// log.error('Sentry error', { foo: 'bar' });
|
||
// log.warn('Sentry warning');
|
||
}
|
||
|
||
// OpenAI/ChatGPT
|
||
let chatGPT;
|
||
const configChatGPT = {
|
||
enabled: getEnvBoolean(process.env.CHATGPT_ENABLED),
|
||
basePath: process.env.CHATGPT_BASE_PATH,
|
||
apiKey: process.env.CHATGPT_APIKEY,
|
||
model: process.env.CHATGPT_MODEL,
|
||
max_tokens: parseInt(process.env.CHATGPT_MAX_TOKENS),
|
||
temperature: parseInt(process.env.CHATGPT_TEMPERATURE),
|
||
};
|
||
if (configChatGPT.enabled) {
|
||
if (configChatGPT.apiKey) {
|
||
const { OpenAI } = require('openai');
|
||
const configuration = {
|
||
basePath: configChatGPT.basePath,
|
||
apiKey: configChatGPT.apiKey,
|
||
};
|
||
chatGPT = new OpenAI(configuration);
|
||
} else {
|
||
log.warning('ChatGPT seems enabled, but you missing the apiKey!');
|
||
}
|
||
}
|
||
|
||
// IP Whitelist
|
||
const ipWhitelist = {
|
||
enabled: getEnvBoolean(process.env.IP_WHITELIST_ENABLED),
|
||
allowed: process.env.IP_WHITELIST_ALLOWED ? JSON.parse(process.env.IP_WHITELIST_ALLOWED) : [],
|
||
};
|
||
|
||
// OIDC - Open ID Connect
|
||
const OIDC = {
|
||
enabled: process.env.OIDC_ENABLED ? getEnvBoolean(process.env.OIDC_ENABLED) : false,
|
||
allowRoomCreationForAuthUsers: process.env.OIDC_ALLOW_ROOMS_CREATION_FOR_AUTH_USERS
|
||
? getEnvBoolean(process.env.OIDC_ALLOW_ROOMS_CREATION_FOR_AUTH_USERS)
|
||
: false,
|
||
baseUrlDynamic: process.env.OIDC_BASE_URL_DYNAMIC ? getEnvBoolean(process.env.OIDC_BASE_URL_DYNAMIC) : false,
|
||
config: {
|
||
issuerBaseURL: process.env.OIDC_ISSUER_BASE_URL,
|
||
clientID: process.env.OIDC_CLIENT_ID,
|
||
clientSecret: process.env.OIDC_CLIENT_SECRET,
|
||
baseURL: process.env.OIDC_BASE_URL,
|
||
secret: process.env.SESSION_SECRET,
|
||
authorizationParams: {
|
||
response_type: 'code',
|
||
scope: 'openid profile email',
|
||
},
|
||
authRequired: process.env.OIDC_AUTH_REQUIRED ? getEnvBoolean(process.env.OIDC_AUTH_REQUIRED) : false, // Set to true if authentication is required for all routes
|
||
auth0Logout: process.env.OIDC_AUTH_LOGOUT ? getEnvBoolean(process.env.OIDC_AUTH_LOGOUT) : true, // Set to true to enable logout with Auth0
|
||
routes: {
|
||
callback: '/auth/callback', // Indicating the endpoint where your application will handle the callback from the authentication provider after a user has been authenticated.
|
||
login: false, // Dedicated route in your application for user login.
|
||
logout: '/logout', // Indicating the endpoint where your application will handle user logout requests.
|
||
},
|
||
},
|
||
};
|
||
|
||
// Custom middleware function for OIDC authentication
|
||
function OIDCAuth(req, res, next) {
|
||
if (OIDC.enabled) {
|
||
function handleHostProtected(req) {
|
||
if (!hostCfg.protected) return;
|
||
|
||
const ip = authHost.getIP(req);
|
||
hostCfg.authenticated = true;
|
||
authHost.setAuthorizedIP(ip, true);
|
||
// Check...
|
||
log.debug('OIDC ------> Host protected', {
|
||
authenticated: hostCfg.authenticated,
|
||
authorizedIPs: authHost.getAuthorizedIPs(),
|
||
});
|
||
}
|
||
|
||
if (req.oidc.isAuthenticated()) {
|
||
log.debug('OIDC ------> User already Authenticated');
|
||
handleHostProtected(req);
|
||
return next();
|
||
}
|
||
|
||
// Apply requiresAuth() middleware conditionally
|
||
requiresAuth()(req, res, function () {
|
||
log.debug('OIDC ------> requiresAuth');
|
||
// Check if user is authenticated
|
||
if (req.oidc.isAuthenticated()) {
|
||
log.debug('[OIDC] ------> User isAuthenticated');
|
||
handleHostProtected(req);
|
||
next();
|
||
} else {
|
||
// User is not authenticated
|
||
res.status(401).send('Unauthorized');
|
||
}
|
||
});
|
||
} else {
|
||
next();
|
||
}
|
||
}
|
||
|
||
// Mattermost config
|
||
const mattermostCfg = {
|
||
enabled: getEnvBoolean(process.env.MATTERMOST_ENABLED),
|
||
server_url: process.env.MATTERMOST_SERVER_URL,
|
||
username: process.env.MATTERMOST_USERNAME,
|
||
password: process.env.MATTERMOST_PASSWORD,
|
||
token: process.env.MATTERMOST_TOKEN,
|
||
roomTokenExpire: process.env.MATTERMOST_ROOM_TOKEN_EXPIRE,
|
||
encryptionKey: process.env.JWT_KEY,
|
||
security: hostCfg.protected || OIDC.enabled,
|
||
api_disabled: api_disabled,
|
||
};
|
||
|
||
// stats configuration
|
||
const statsData = {
|
||
enabled: process.env.STATS_ENABLED ? getEnvBoolean(process.env.STATS_ENABLED) : true,
|
||
src: process.env.STATS_SCR || 'https://stats.mirotalk.com/script.js',
|
||
id: process.env.STATS_ID || 'c7615aa7-ceec-464a-baba-54cb605d7261',
|
||
};
|
||
|
||
// directory
|
||
const dir = {
|
||
public: path.join(__dirname, '../../', 'public'),
|
||
};
|
||
// html views
|
||
const views = {
|
||
about: path.join(__dirname, '../../', 'public/views/about.html'),
|
||
client: path.join(__dirname, '../../', 'public/views/client.html'),
|
||
landing: path.join(__dirname, '../../', 'public/views/landing.html'),
|
||
login: path.join(__dirname, '../../', 'public/views/login.html'),
|
||
newCall: path.join(__dirname, '../../', 'public/views/newcall.html'),
|
||
notFound: path.join(__dirname, '../../', 'public/views/404.html'),
|
||
privacy: path.join(__dirname, '../../', 'public/views/privacy.html'),
|
||
stunTurn: path.join(__dirname, '../../', 'public/views/testStunTurn.html'),
|
||
};
|
||
|
||
// File to cache and inject custom HTML data like OG tags and any other elements.
|
||
const filesPath = [views.landing, views.newCall, views.client, views.login];
|
||
const htmlInjector = new HtmlInjector(filesPath, config?.brand || null);
|
||
|
||
const channels = {}; // collect channels
|
||
const sockets = {}; // collect sockets
|
||
const peers = {}; // collect peers info grp by channels
|
||
const presenters = {}; // collect presenters grp by channels
|
||
|
||
app.set('trust proxy', trustProxy); // Enables trust for proxy headers (e.g., X-Forwarded-For) based on the trustProxy setting
|
||
app.use(helmet.noSniff()); // Enable content type sniffing prevention
|
||
|
||
// Use all static files from the public folder
|
||
const staticOptions = {
|
||
setHeaders: (res, filePath) => {
|
||
if (filePath.endsWith('.js')) {
|
||
res.setHeader('Content-Type', 'application/javascript');
|
||
}
|
||
// Add other headers if needed...
|
||
},
|
||
};
|
||
|
||
// Serve static files from root (/)
|
||
app.use(express.static(dir.public, staticOptions));
|
||
|
||
// Also serve the same files under /mattermost
|
||
app.use('/mattermost', express.static(dir.public, staticOptions));
|
||
|
||
app.use(cors(corsOptions)); // Enable CORS with options
|
||
app.use(compression()); // Compress all HTTP responses using GZip
|
||
app.use(express.json()); // Parse JSON bodies
|
||
app.use(express.urlencoded({ extended: false })); // Parse URL-encoded bodies
|
||
app.use(apiBasePath + '/docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); // api docs
|
||
|
||
// Restrict access to specified IP
|
||
app.use((req, res, next) => {
|
||
if (!ipWhitelist.enabled) return next();
|
||
const clientIP = getIP(req);
|
||
log.debug('Check IP', clientIP);
|
||
if (ipWhitelist.allowed.includes(clientIP)) {
|
||
next();
|
||
} else {
|
||
log.info('Forbidden: Access denied from this IP address', { clientIP: clientIP });
|
||
res.status(403).json({ error: 'Forbidden', message: 'Access denied from this IP address.' });
|
||
}
|
||
});
|
||
|
||
app.use((req, res, next) => {
|
||
const ipAddress = getIP(req);
|
||
log.debug('New request:', {
|
||
ip: ipAddress,
|
||
method: req.method,
|
||
path: req.originalUrl,
|
||
body: req.body,
|
||
//headers: req.headers,
|
||
});
|
||
next();
|
||
});
|
||
|
||
// Mattermost
|
||
const mattermost = new MattermostController(app, mattermostCfg, htmlInjector, views.client);
|
||
|
||
// Remove trailing slashes in url handle bad requests
|
||
app.use((err, req, res, next) => {
|
||
if (err instanceof SyntaxError || err.status === 400 || 'body' in err) {
|
||
log.error('Request Error', {
|
||
header: req.headers,
|
||
body: req.body,
|
||
error: err.message,
|
||
});
|
||
return res.status(400).send({ status: 404, message: err.message }); // Bad request
|
||
}
|
||
if (req.path.substr(-1) === '/' && req.path.length > 1) {
|
||
let query = req.url.slice(req.path.length);
|
||
res.redirect(301, req.path.slice(0, -1) + query);
|
||
} else {
|
||
next();
|
||
}
|
||
});
|
||
|
||
// OpenID Connect - Dynamically set baseURL based on incoming host and protocol
|
||
if (OIDC.enabled) {
|
||
const getDynamicConfig = (host, protocol) => {
|
||
const baseURL = `${protocol}://${host}`;
|
||
const config = OIDC.baseUrlDynamic
|
||
? {
|
||
...OIDC.config,
|
||
baseURL,
|
||
}
|
||
: OIDC.config;
|
||
return config;
|
||
};
|
||
|
||
// Apply the authentication middleware using dynamic baseURL configuration
|
||
app.use((req, res, next) => {
|
||
const host = req.headers.host;
|
||
const protocol = req.protocol === 'https' ? 'https' : 'http';
|
||
const dynamicOIDCConfig = getDynamicConfig(host, protocol);
|
||
try {
|
||
auth(dynamicOIDCConfig)(req, res, next);
|
||
} catch (err) {
|
||
log.error('OIDC Auth Middleware Error', err);
|
||
process.exit(1);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Route to display user information
|
||
app.get('/profile', OIDCAuth, (req, res) => {
|
||
if (OIDC.enabled) {
|
||
log.debug('OIDC User profile requested', req.oidc.user);
|
||
return res.json(req.oidc.user); // Send user information as JSON
|
||
}
|
||
return res.json({ profile: false });
|
||
});
|
||
|
||
// Authentication Callback Route
|
||
app.get('/auth/callback', (req, res, next) => {
|
||
next(); // Let express-openid-connect handle this route
|
||
});
|
||
|
||
// Logout Route
|
||
app.get('/logout', (req, res) => {
|
||
if (OIDC.enabled) {
|
||
//
|
||
if (hostCfg.protected) {
|
||
const ip = authHost.getIP(req);
|
||
if (authHost.isAuthorizedIP(ip)) {
|
||
authHost.deleteIP(ip);
|
||
}
|
||
hostCfg.authenticated = false;
|
||
//
|
||
log.debug('[OIDC] ------> Logout', {
|
||
authenticated: hostCfg.authenticated,
|
||
authorizedIPs: authHost.getAuthorizedIPs(),
|
||
});
|
||
}
|
||
req.logout(); // Logout user
|
||
}
|
||
res.redirect('/'); // Redirect to the home page after logout
|
||
});
|
||
|
||
// main page
|
||
app.get('/', OIDCAuth, (req, res) => {
|
||
if (!OIDC.enabled && hostCfg.protected) {
|
||
const ip = getIP(req);
|
||
if (allowedIP(ip)) {
|
||
htmlInjector.injectHtml(views.landing, res);
|
||
hostCfg.authenticated = true;
|
||
} else {
|
||
hostCfg.authenticated = false;
|
||
res.redirect('/login');
|
||
}
|
||
} else {
|
||
return htmlInjector.injectHtml(views.landing, res);
|
||
}
|
||
});
|
||
|
||
// set new room name and join
|
||
app.get('/newcall', OIDCAuth, (req, res) => {
|
||
if (!OIDC.enabled && hostCfg.protected) {
|
||
const ip = getIP(req);
|
||
if (allowedIP(ip)) {
|
||
res.redirect('/');
|
||
hostCfg.authenticated = true;
|
||
} else {
|
||
hostCfg.authenticated = false;
|
||
res.redirect('/login');
|
||
}
|
||
} else {
|
||
htmlInjector.injectHtml(views.newCall, res);
|
||
}
|
||
});
|
||
|
||
// Get stats endpoint
|
||
app.get('/stats', (req, res) => {
|
||
//log.debug('Send stats', statsData);
|
||
res.send(statsData);
|
||
});
|
||
|
||
// mirotalk about
|
||
app.get(['/about'], (req, res) => {
|
||
res.sendFile(views.about);
|
||
});
|
||
|
||
// privacy policy
|
||
app.get(['/privacy'], (req, res) => {
|
||
res.sendFile(views.privacy);
|
||
});
|
||
|
||
// test Stun and Turn connections
|
||
app.get(['/icetest'], (req, res) => {
|
||
if (Object.keys(req.query).length > 0) {
|
||
log.debug('Request Query', req.query);
|
||
}
|
||
res.sendFile(views.stunTurn);
|
||
});
|
||
|
||
// Handle Direct join room with params
|
||
app.get('/join/', async (req, res) => {
|
||
if (Object.keys(req.query).length > 0) {
|
||
log.debug('Request Query', req.query);
|
||
/*
|
||
http://localhost:3000/join?room=test&name=mirotalk&audio=1&video=1&screen=0¬ify=0&hide=0
|
||
https://p2p.mirotalk.com/join?room=test&name=mirotalk&audio=1&video=1&screen=0¬ify=0&hide=0
|
||
https://mirotalk.up.railway.app/join?room=test&name=mirotalk&audio=1&video=1&screen=0¬ify=0&hide=0
|
||
*/
|
||
const { room, name, audio, video, screen, notify, hide, token } = checkXSS(req.query);
|
||
|
||
if (!room) {
|
||
log.warn('/join/params room empty', room);
|
||
return res.status(401).json({ message: 'Direct Room Join: Missing mandatory room parameter!' });
|
||
}
|
||
|
||
if (!Validate.isValidRoomName(room)) {
|
||
return res.status(400).json({
|
||
message: 'Invalid Room name!\nPath traversal pattern detected!',
|
||
});
|
||
}
|
||
|
||
const allowRoomAccess = isAllowedRoomAccess('/join/params', req, hostCfg, peers, room);
|
||
|
||
if (!allowRoomAccess && !token) {
|
||
return res.status(401).json({ message: 'Direct Room Join Unauthorized' });
|
||
}
|
||
|
||
let peerUsername,
|
||
peerPassword = '';
|
||
let isPeerValid = false;
|
||
let isPeerPresenter = false;
|
||
|
||
if (token) {
|
||
try {
|
||
// Check if valid JWT token
|
||
const validToken = await isValidToken(token);
|
||
|
||
// Not valid token
|
||
if (!validToken) {
|
||
return res.status(401).json({ message: 'Invalid Token' });
|
||
}
|
||
|
||
const { username, password, presenter } = checkXSS(decodeToken(token));
|
||
// Peer credentials
|
||
peerUsername = username;
|
||
peerPassword = password;
|
||
// Check if valid peer
|
||
isPeerValid = isAuthPeer(username, password);
|
||
// Check if presenter
|
||
isPeerPresenter = presenter === '1' || presenter === 'true';
|
||
} catch (err) {
|
||
// Invalid token
|
||
log.error('Direct Join JWT error', err.message);
|
||
return hostCfg.protected || hostCfg.user_auth
|
||
? htmlInjector.injectHtml(views.login, res)
|
||
: htmlInjector.injectHtml(views.landing, res);
|
||
}
|
||
}
|
||
|
||
const OIDCUserAuthenticated = OIDC.enabled && req.oidc.isAuthenticated();
|
||
|
||
// Peer valid going to auth as host
|
||
if ((hostCfg.protected && isPeerValid && isPeerPresenter && !hostCfg.authenticated) || OIDCUserAuthenticated) {
|
||
const ip = getIP(req);
|
||
hostCfg.authenticated = true;
|
||
authHost.setAuthorizedIP(ip, true);
|
||
log.debug('Direct Join user auth as host done', {
|
||
ip: ip,
|
||
username: peerUsername,
|
||
password: peerPassword,
|
||
});
|
||
}
|
||
|
||
// Check if peer authenticated or valid
|
||
if (room && (hostCfg.authenticated || isPeerValid)) {
|
||
// only room mandatory
|
||
return htmlInjector.injectHtml(views.client, res);
|
||
} else {
|
||
return htmlInjector.injectHtml(views.login, res);
|
||
}
|
||
}
|
||
});
|
||
|
||
// Join Room by id
|
||
app.get('/join/:roomId', function (req, res) {
|
||
//
|
||
const { roomId } = req.params;
|
||
|
||
if (!roomId) {
|
||
log.warn('/join/:roomId empty', roomId);
|
||
return res.redirect('/');
|
||
}
|
||
|
||
if (!Validate.isValidRoomName(roomId)) {
|
||
log.warn('/join/:roomId invalid', roomId);
|
||
return res.redirect('/');
|
||
}
|
||
|
||
const allowRoomAccess = isAllowedRoomAccess('/join/:roomId', req, hostCfg, peers, roomId);
|
||
|
||
if (allowRoomAccess) {
|
||
htmlInjector.injectHtml(views.client, res);
|
||
} else {
|
||
!OIDC.enabled && hostCfg.protected ? res.redirect('/login') : res.redirect('/');
|
||
}
|
||
});
|
||
|
||
// Not specified correctly the room id
|
||
app.get('/join/\\*', function (req, res) {
|
||
res.redirect('/');
|
||
});
|
||
|
||
// Login
|
||
app.get(['/login'], (req, res) => {
|
||
if (hostCfg.protected || hostCfg.user_auth) {
|
||
return htmlInjector.injectHtml(views.login, res);
|
||
}
|
||
res.redirect('/');
|
||
});
|
||
|
||
// Logged
|
||
app.get('/logged', (req, res) => {
|
||
if (!OIDC.enabled && hostCfg.protected) {
|
||
const ip = getIP(req);
|
||
if (allowedIP(ip)) {
|
||
res.redirect('/');
|
||
} else {
|
||
hostCfg.authenticated = false;
|
||
res.redirect('/login');
|
||
}
|
||
} else {
|
||
res.redirect('/');
|
||
}
|
||
});
|
||
|
||
/* AXIOS */
|
||
|
||
// handle login on host protected
|
||
app.post('/login', (req, res) => {
|
||
//
|
||
const ip = getIP(req);
|
||
log.debug(`Request login to host from: ${ip}`, req.body);
|
||
|
||
const { username, password } = checkXSS(req.body);
|
||
|
||
const isPeerValid = isAuthPeer(username, password);
|
||
|
||
// Peer valid going to auth as host
|
||
if (hostCfg.protected && isPeerValid && !hostCfg.authenticated) {
|
||
hostCfg.authenticated = true;
|
||
authHost.setAuthorizedIP(ip, true);
|
||
log.debug('HOST LOGIN OK', {
|
||
ip: ip,
|
||
authorized: authHost.isAuthorizedIP(ip),
|
||
authorizedIps: authHost.getAuthorizedIPs(),
|
||
});
|
||
const token = encodeToken({ username: username, password: password, presenter: true });
|
||
return res.status(200).json({ message: token });
|
||
}
|
||
|
||
// Peer auth valid
|
||
if (isPeerValid) {
|
||
log.debug('PEER LOGIN OK', { ip: ip, authorized: true });
|
||
const isPresenter = roomPresenters && roomPresenters.includes(username).toString();
|
||
const token = encodeToken({ username: username, password: password, presenter: isPresenter });
|
||
return res.status(200).json({ message: token });
|
||
} else {
|
||
return res.status(401).json({ message: 'unauthorized' });
|
||
}
|
||
});
|
||
|
||
// UI buttons configuration
|
||
app.get('/buttons', (req, res) => {
|
||
res.status(200).json({ message: config && config.buttons ? config.buttons : false });
|
||
});
|
||
|
||
// UI brand configuration
|
||
app.get('/brand', (req, res) => {
|
||
res.status(200).json({ message: config && config.brand ? config.brand : false });
|
||
});
|
||
|
||
// Join roomId redirect to /join?room=roomId
|
||
app.get('/:roomId', (req, res) => {
|
||
const { roomId } = checkXSS(req.params);
|
||
|
||
if (!roomId) {
|
||
log.warn('/:roomId empty', roomId);
|
||
return res.redirect('/');
|
||
}
|
||
|
||
log.debug('Detected roomId --> redirect to /join?room=roomId');
|
||
res.redirect(`/join/${roomId}`);
|
||
});
|
||
|
||
/**
|
||
MiroTalk API v1
|
||
For api docs we use: https://swagger.io/
|
||
*/
|
||
|
||
// request stats list
|
||
app.get(`${apiBasePath}/stats`, (req, res) => {
|
||
// Check if endpoint allowed
|
||
if (api_disabled.includes('stats')) {
|
||
return res.status(403).json({
|
||
error: 'This endpoint has been disabled. Please contact the administrator for further information.',
|
||
});
|
||
}
|
||
// check if user was authorized for the api call
|
||
const { host, authorization } = req.headers;
|
||
const api = new ServerApi(host, authorization, api_key_secret);
|
||
if (!api.isAuthorized()) {
|
||
log.debug('MiroTalk get stats - Unauthorized', {
|
||
header: req.headers,
|
||
body: req.body,
|
||
});
|
||
return res.status(403).json({ error: 'Unauthorized!' });
|
||
}
|
||
// Get stats
|
||
const { timestamp, totalRooms, totalPeers } = api.getStats(peers);
|
||
res.json({
|
||
success: true,
|
||
timestamp,
|
||
totalRooms,
|
||
totalPeers,
|
||
});
|
||
// log.debug the output if all done
|
||
log.debug('MiroTalk get stats - Authorized', {
|
||
header: req.headers,
|
||
body: req.body,
|
||
timestamp,
|
||
totalRooms,
|
||
totalPeers,
|
||
});
|
||
});
|
||
|
||
// request token endpoint
|
||
app.post(`${apiBasePath}/token`, (req, res) => {
|
||
// Check if endpoint allowed
|
||
if (api_disabled.includes('token')) {
|
||
return res.status(403).json({
|
||
error: 'This endpoint has been disabled. Please contact the administrator for further information.',
|
||
});
|
||
}
|
||
// check if user was authorized for the api call
|
||
const { host, authorization } = req.headers;
|
||
const api = new ServerApi(host, authorization, api_key_secret);
|
||
if (!api.isAuthorized()) {
|
||
log.debug('MiroTalk get token - Unauthorized', {
|
||
header: req.headers,
|
||
body: req.body,
|
||
});
|
||
return res.status(403).json({ error: 'Unauthorized!' });
|
||
}
|
||
// Get Token
|
||
const token = api.getToken(req.body);
|
||
res.json({ token: token });
|
||
// log.debug the output if all done
|
||
log.debug('MiroTalk get token - Authorized', {
|
||
header: req.headers,
|
||
body: req.body,
|
||
token: token,
|
||
});
|
||
});
|
||
|
||
// request meetings list
|
||
app.get(`${apiBasePath}/meetings`, (req, res) => {
|
||
// Check if endpoint allowed
|
||
if (api_disabled.includes('meetings')) {
|
||
return res.status(403).json({
|
||
error: 'This endpoint has been disabled. Please contact the administrator for further information.',
|
||
});
|
||
}
|
||
// check if user was authorized for the api call
|
||
const { host, authorization } = req.headers;
|
||
const api = new ServerApi(host, authorization, api_key_secret);
|
||
if (!api.isAuthorized()) {
|
||
log.debug('MiroTalk get meetings - Unauthorized', {
|
||
header: req.headers,
|
||
body: req.body,
|
||
});
|
||
return res.status(403).json({ error: 'Unauthorized!' });
|
||
}
|
||
// Get meetings
|
||
const meetings = api.getMeetings(peers);
|
||
res.json({ meetings: meetings });
|
||
// log.debug the output if all done
|
||
log.debug('MiroTalk get meetings - Authorized', {
|
||
header: req.headers,
|
||
body: req.body,
|
||
meetings: meetings,
|
||
});
|
||
});
|
||
|
||
// API request meeting room endpoint
|
||
app.post(`${apiBasePath}/meeting`, (req, res) => {
|
||
// Check if endpoint allowed
|
||
if (api_disabled.includes('meeting')) {
|
||
return res.status(403).json({
|
||
error: 'This endpoint has been disabled. Please contact the administrator for further information.',
|
||
});
|
||
}
|
||
const { host, authorization } = req.headers;
|
||
const api = new ServerApi(host, authorization, api_key_secret);
|
||
if (!api.isAuthorized()) {
|
||
log.debug('MiroTalk get meeting - Unauthorized', {
|
||
header: req.headers,
|
||
body: req.body,
|
||
});
|
||
return res.status(403).json({ error: 'Unauthorized!' });
|
||
}
|
||
const meetingURL = api.getMeetingURL();
|
||
res.json({ meeting: meetingURL });
|
||
log.debug('MiroTalk get meeting - Authorized', {
|
||
header: req.headers,
|
||
body: req.body,
|
||
meeting: meetingURL,
|
||
});
|
||
});
|
||
|
||
// API request join room endpoint
|
||
app.post(`${apiBasePath}/join`, (req, res) => {
|
||
// Check if endpoint allowed
|
||
if (api_disabled.includes('join')) {
|
||
return res.status(403).json({
|
||
error: 'This endpoint has been disabled. Please contact the administrator for further information.',
|
||
});
|
||
}
|
||
const { host, authorization } = req.headers;
|
||
const api = new ServerApi(host, authorization, api_key_secret);
|
||
if (!api.isAuthorized()) {
|
||
log.debug('MiroTalk get join - Unauthorized', {
|
||
header: req.headers,
|
||
body: req.body,
|
||
});
|
||
return res.status(403).json({ error: 'Unauthorized!' });
|
||
}
|
||
const joinURL = api.getJoinURL(req.body);
|
||
res.json({ join: joinURL });
|
||
log.debug('MiroTalk get join - Authorized', {
|
||
header: req.headers,
|
||
body: req.body,
|
||
join: joinURL,
|
||
});
|
||
});
|
||
|
||
/*
|
||
MiroTalk Slack app v1
|
||
https://api.slack.com/authentication/verifying-requests-from-slack
|
||
*/
|
||
|
||
// Slack request meeting room endpoint
|
||
app.post('/slack', (req, res) => {
|
||
if (!slackEnabled) return res.end('`Under maintenance` - Please check back soon.');
|
||
|
||
// Check if endpoint allowed
|
||
if (api_disabled.includes('slack')) {
|
||
return res.end('`This endpoint has been disabled`. Please contact the administrator for further information.');
|
||
}
|
||
|
||
log.debug('Slack', req.headers);
|
||
|
||
if (!slackSigningSecret) return res.end('`Slack Signing Secret is empty!`');
|
||
|
||
const slackSignature = req.headers['x-slack-signature'];
|
||
const requestBody = qS.stringify(req.body, { format: 'RFC1738' });
|
||
const timeStamp = req.headers['x-slack-request-timestamp'];
|
||
const time = Math.floor(new Date().getTime() / 1000);
|
||
|
||
// The request timestamp is more than five minutes from local time. It could be a replay attack, so let's ignore it.
|
||
if (Math.abs(time - timeStamp) > 300) return res.end('`Wrong timestamp` - Ignore this request.');
|
||
|
||
// Get Signature to compare it later
|
||
const sigBaseString = 'v0:' + timeStamp + ':' + requestBody;
|
||
const mySignature = 'v0=' + CryptoJS.HmacSHA256(sigBaseString, slackSigningSecret);
|
||
|
||
// Valid Signature return a meetingURL
|
||
if (mySignature == slackSignature) {
|
||
const host = req.headers.host;
|
||
const meetingURL = getMeetingURL(host);
|
||
log.debug('Slack', { meeting: meetingURL });
|
||
return res.end(meetingURL);
|
||
}
|
||
// Something wrong
|
||
return res.end('`Wrong signature` - Verification failed!');
|
||
});
|
||
|
||
/**
|
||
* Request meeting room endpoint
|
||
* @returns entrypoint / Room URL for your meeting.
|
||
*/
|
||
function getMeetingURL(host) {
|
||
return 'http' + (host.includes('localhost') ? '' : 's') + '://' + host + '/join/' + uuidV4();
|
||
}
|
||
|
||
// end of MiroTalk API v1
|
||
|
||
// not match any of page before, so 404 not found
|
||
app.use((req, res) => {
|
||
res.sendFile(views.notFound);
|
||
});
|
||
|
||
/**
|
||
* Get Server config
|
||
* @param {string} tunnel
|
||
* @returns server config
|
||
*/
|
||
function getServerConfig(tunnel = false) {
|
||
return {
|
||
// General Server Information
|
||
server: host,
|
||
server_tunnel: tunnel,
|
||
trust_proxy: trustProxy,
|
||
api_docs: api_docs,
|
||
|
||
// Core Configurations
|
||
jwtCfg: jwtCfg,
|
||
cors: corsOptions,
|
||
iceServers: iceServers,
|
||
test_ice_servers: testStunTurn,
|
||
email: nodemailer.emailCfg.alert ? nodemailer.emailCfg : false,
|
||
|
||
// Security, Authorization, and User Management
|
||
oidc: OIDC.enabled ? OIDC : false,
|
||
host_protected: hostCfg.protected || hostCfg.user_auth ? hostCfg : false,
|
||
presenters: roomPresenters,
|
||
ip_whitelist: ipWhitelist.enabled ? ipWhitelist : false,
|
||
api_key_secret: api_key_secret,
|
||
|
||
// Media and Connection Settings
|
||
turn_enabled: turnServerEnabled,
|
||
ip_lookup_enabled: IPLookupEnabled,
|
||
|
||
// Integrations
|
||
chatGPT_enabled: configChatGPT.enabled ? configChatGPT : false,
|
||
slack_enabled: slackEnabled,
|
||
mattermost_enabled: mattermostCfg.enabled ? mattermostCfg : false,
|
||
|
||
// Monitoring and Logging
|
||
sentry_enabled: sentryEnabled,
|
||
stats: statsData.enabled ? statsData : false,
|
||
|
||
// Ngrok Configuration
|
||
ngrok: ngrokEnabled
|
||
? {
|
||
enabled: ngrokEnabled,
|
||
token: ngrokAuthToken,
|
||
}
|
||
: false,
|
||
|
||
// URLs for Redirection and Survey
|
||
survey: surveyEnabled ? surveyURL : false,
|
||
redirect: redirectEnabled ? redirectURL : false,
|
||
|
||
// Versions information
|
||
app_version: packageJson.version,
|
||
node_version: process.versions.node,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Expose server to external with https tunnel using ngrok
|
||
* https://ngrok.com
|
||
*/
|
||
async function ngrokStart() {
|
||
try {
|
||
await ngrok.authtoken(ngrokAuthToken);
|
||
const listener = await ngrok.forward({ addr: port });
|
||
const tunnelUrl = listener.url();
|
||
log.info('Server config', getServerConfig(tunnelUrl));
|
||
} catch (err) {
|
||
log.warn('[Error] ngrokStart', err);
|
||
await ngrok.kill();
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Start Local Server with ngrok https tunnel (optional)
|
||
*/
|
||
server.listen(port, null, () => {
|
||
log.debug(
|
||
`%c
|
||
|
||
███████╗██╗ ██████╗ ███╗ ██╗ ███████╗███████╗██████╗ ██╗ ██╗███████╗██████╗
|
||
██╔════╝██║██╔════╝ ████╗ ██║ ██╔════╝██╔════╝██╔══██╗██║ ██║██╔════╝██╔══██╗
|
||
███████╗██║██║ ███╗██╔██╗ ██║█████╗███████╗█████╗ ██████╔╝██║ ██║█████╗ ██████╔╝
|
||
╚════██║██║██║ ██║██║╚██╗██║╚════╝╚════██║██╔══╝ ██╔══██╗╚██╗ ██╔╝██╔══╝ ██╔══██╗
|
||
███████║██║╚██████╔╝██║ ╚████║ ███████║███████╗██║ ██║ ╚████╔╝ ███████╗██║ ██║
|
||
╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝ started...
|
||
|
||
`,
|
||
'font-family:monospace'
|
||
);
|
||
|
||
// https tunnel
|
||
if (ngrokEnabled) {
|
||
ngrokStart();
|
||
} else {
|
||
log.info('Server config', getServerConfig());
|
||
}
|
||
});
|
||
|
||
/**
|
||
* On peer connected
|
||
* Users will connect to the signaling server, after which they'll issue a "join"
|
||
* to join a particular channel. The signaling server keeps track of all sockets
|
||
* who are in a channel, and on join will send out 'addPeer' events to each pair
|
||
* of users in a channel. When clients receive the 'addPeer' even they'll begin
|
||
* setting up an RTCPeerConnection with one another. During this process they'll
|
||
* need to relay ICECandidate information to one another, as well as SessionDescription
|
||
* information. After all of that happens, they'll finally be able to complete
|
||
* the peer connection and will be in streaming audio/video between eachother.
|
||
*/
|
||
io.sockets.on('connect', async (socket) => {
|
||
log.debug('[' + socket.id + '] connection accepted', {
|
||
host: socket.handshake.headers.host.split(':')[0],
|
||
time: socket.handshake.time,
|
||
});
|
||
|
||
socket.channels = {};
|
||
sockets[socket.id] = socket;
|
||
|
||
const transport = socket.conn.transport.name; // in most cases, "polling"
|
||
log.debug('[' + socket.id + '] Connection transport', transport);
|
||
|
||
/**
|
||
* Check upgrade transport
|
||
*/
|
||
socket.conn.on('upgrade', () => {
|
||
const upgradedTransport = socket.conn.transport.name; // in most cases, "websocket"
|
||
log.debug('[' + socket.id + '] Connection upgraded transport', upgradedTransport);
|
||
});
|
||
|
||
/**
|
||
* On peer disconnected
|
||
*/
|
||
socket.on('disconnect', async (reason) => {
|
||
removeIP(socket);
|
||
for (let channel in socket.channels) {
|
||
await removePeerFrom(channel);
|
||
}
|
||
log.debug('[' + socket.id + '] disconnected', { reason: reason });
|
||
delete sockets[socket.id];
|
||
});
|
||
|
||
/**
|
||
* Handle incoming data, res with a callback
|
||
*/
|
||
socket.on('data', async (dataObj, cb) => {
|
||
const data = checkXSS(dataObj);
|
||
|
||
log.debug('Socket Promise', data);
|
||
//...
|
||
const { room_id, peer_id, peer_name, method, params } = data;
|
||
|
||
switch (method) {
|
||
case 'checkPeerName':
|
||
log.debug('Check if peer name exists', { peer_name: peer_name, room_id: room_id });
|
||
for (let id in peers[room_id]) {
|
||
if (peer_id != id && peers[room_id][id]['peer_name'] == peer_name) {
|
||
log.debug('Peer name found', { peer_name: peer_name, room_id: room_id });
|
||
cb(true);
|
||
break;
|
||
}
|
||
}
|
||
break;
|
||
case 'getChatGPT':
|
||
// https://platform.openai.com/docs/introduction
|
||
if (!configChatGPT.enabled) return cb({ message: 'ChatGPT seems disabled, try later!' });
|
||
// https://platform.openai.com/docs/api-reference/completions/create
|
||
try {
|
||
const { time, prompt, context } = params;
|
||
// Add the prompt to the context
|
||
context.push({ role: 'user', content: prompt });
|
||
// Call OpenAI's API to generate response
|
||
const completion = await chatGPT.chat.completions.create({
|
||
model: configChatGPT.model || 'gpt-3.5-turbo',
|
||
messages: context,
|
||
max_tokens: configChatGPT.max_tokens || 1000,
|
||
temperature: configChatGPT.temperature || 0,
|
||
});
|
||
// Extract message from completion
|
||
const message = completion.choices[0].message.content.trim();
|
||
// Add response to context
|
||
context.push({ role: 'assistant', content: message });
|
||
// Log conversation details
|
||
log.info('ChatGPT', {
|
||
time: time,
|
||
room: room_id,
|
||
name: peer_name,
|
||
context: context,
|
||
});
|
||
// Callback response to client
|
||
cb({ message: message, context: context });
|
||
} catch (error) {
|
||
if (error.name === 'APIError') {
|
||
log.error('ChatGPT', {
|
||
name: error.name,
|
||
status: error.status,
|
||
message: error.message,
|
||
code: error.code,
|
||
type: error.type,
|
||
});
|
||
cb({ message: error.message });
|
||
} else {
|
||
// Non-API error
|
||
log.error('ChatGPT', error);
|
||
cb({ message: error.message });
|
||
}
|
||
}
|
||
break;
|
||
//....
|
||
default:
|
||
cb(false);
|
||
break;
|
||
}
|
||
cb(false);
|
||
});
|
||
|
||
/**
|
||
* On peer join
|
||
*/
|
||
socket.on('join', async (cfg) => {
|
||
// Get peer IPv4 (::1 Its the loopback address in ipv6, equal to 127.0.0.1 in ipv4)
|
||
const peer_ip = getSocketIP(socket);
|
||
|
||
// Get peer Geo Location
|
||
if (IPLookupEnabled && peer_ip != '::1') {
|
||
cfg.peer_geo = await getPeerGeoLocation(peer_ip);
|
||
}
|
||
|
||
// Prevent XSS injection
|
||
const config = checkXSS(cfg);
|
||
|
||
// log.debug('Join room', config);
|
||
log.debug('[' + socket.id + '] join ', config);
|
||
|
||
const {
|
||
channel,
|
||
channel_password,
|
||
peer_uuid,
|
||
peer_name,
|
||
peer_avatar,
|
||
peer_token,
|
||
peer_video,
|
||
peer_audio,
|
||
peer_video_status,
|
||
peer_audio_status,
|
||
peer_screen_status,
|
||
peer_hand_status,
|
||
peer_rec_status,
|
||
peer_privacy_status,
|
||
peer_info,
|
||
} = config;
|
||
|
||
if (!Validate.isValidRoomName(channel)) {
|
||
log.warn('[' + socket.id + '] - Invalid room name', channel);
|
||
return socket.emit('unauthorized');
|
||
}
|
||
|
||
if (channel in socket.channels) {
|
||
return log.debug('[' + socket.id + '] [Warning] already joined', channel);
|
||
}
|
||
// no channel aka room in channels init
|
||
if (!(channel in channels)) channels[channel] = {};
|
||
|
||
// no channel aka room in peers init
|
||
if (!(channel in peers)) peers[channel] = {};
|
||
|
||
// no presenter aka host in presenters init
|
||
if (!(channel in presenters)) presenters[channel] = {};
|
||
|
||
let is_presenter = true;
|
||
|
||
// User Auth required, we check if peer valid
|
||
if (hostCfg.user_auth || peer_token) {
|
||
// Check JWT
|
||
if (peer_token) {
|
||
try {
|
||
const validToken = await isValidToken(peer_token);
|
||
|
||
if (!validToken) {
|
||
// redirect peer to login page
|
||
return socket.emit('unauthorized');
|
||
}
|
||
|
||
const { username, password, presenter } = checkXSS(decodeToken(peer_token));
|
||
|
||
const isPeerValid = isAuthPeer(username, password);
|
||
|
||
if (!isPeerValid) {
|
||
// redirect peer to login page
|
||
return socket.emit('unauthorized');
|
||
}
|
||
|
||
// Presenter if token 'presenter' is '1'/'true' or first to join room
|
||
is_presenter =
|
||
presenter === '1' || presenter === 'true' || Object.keys(presenters[channel]).length === 0;
|
||
|
||
log.debug('[' + socket.id + '] JOIN ROOM - USER AUTH check peer', {
|
||
ip: peer_ip,
|
||
peer_username: username,
|
||
peer_password: password,
|
||
peer_valid: isPeerValid,
|
||
peer_presenter: is_presenter,
|
||
});
|
||
} catch (err) {
|
||
// redirect peer to login page
|
||
log.error('[' + socket.id + '] [Warning] Join Room JWT error', err.message);
|
||
return socket.emit('unauthorized');
|
||
}
|
||
} else {
|
||
// redirect peer to login page
|
||
return socket.emit('unauthorized');
|
||
}
|
||
}
|
||
|
||
// room locked by the participants can't join
|
||
if (peers[channel]['lock'] === true && peers[channel]['password'] != channel_password) {
|
||
log.debug('[' + socket.id + '] [Warning] Room Is Locked', channel);
|
||
return socket.emit('roomIsLocked');
|
||
}
|
||
|
||
// Set the presenters
|
||
const presenter = {
|
||
peer_ip: peer_ip,
|
||
peer_name: peer_name,
|
||
peer_uuid: peer_uuid,
|
||
is_presenter: is_presenter,
|
||
};
|
||
// first we check if the username match the presenters username
|
||
if (roomPresenters && roomPresenters.includes(peer_name)) {
|
||
presenters[channel][socket.id] = presenter;
|
||
} else {
|
||
// if not match the presenters username, the first one join room is the presenter
|
||
if (Object.keys(presenters[channel]).length === 0) {
|
||
presenters[channel][socket.id] = presenter;
|
||
}
|
||
}
|
||
|
||
// Check if peer is presenter, if token check the presenter key
|
||
const isPresenter = peer_token ? is_presenter : isPeerPresenter(channel, socket.id, peer_name, peer_uuid);
|
||
|
||
// Some peer info data
|
||
const { osName, osVersion, browserName, browserVersion } = peer_info;
|
||
|
||
// collect peers info grp by channels
|
||
peers[channel][socket.id] = {
|
||
peer_name: peer_name,
|
||
peer_avatar: peer_avatar,
|
||
peer_presenter: isPresenter,
|
||
peer_video: peer_video,
|
||
peer_audio: peer_audio,
|
||
peer_video_status: peer_video_status,
|
||
peer_audio_status: peer_audio_status,
|
||
peer_screen_status: peer_screen_status,
|
||
peer_hand_status: peer_hand_status,
|
||
peer_rec_status: peer_rec_status,
|
||
peer_privacy_status: peer_privacy_status,
|
||
os: osName ? `${osName} ${osVersion}` : '',
|
||
browser: browserName ? `${browserName} ${browserVersion}` : '',
|
||
};
|
||
|
||
const activeRooms = getActiveRooms();
|
||
|
||
log.info('[Join] - active rooms and peers count', activeRooms);
|
||
|
||
log.info('[Join] - connected presenters grp by roomId', presenters);
|
||
|
||
log.info('[Join] - connected peers grp by roomId', peers);
|
||
|
||
await addPeerTo(channel);
|
||
|
||
channels[channel][socket.id] = socket;
|
||
socket.channels[channel] = channel;
|
||
|
||
const peerCounts = Object.keys(peers[channel]).length;
|
||
|
||
// Send some server info to joined peer
|
||
await sendToPeer(socket.id, sockets, 'serverInfo', {
|
||
peers_count: peerCounts,
|
||
host_protected: hostCfg.protected,
|
||
user_auth: hostCfg.user_auth,
|
||
is_presenter: isPresenter,
|
||
survey: {
|
||
active: surveyEnabled,
|
||
url: surveyURL,
|
||
},
|
||
redirect: {
|
||
active: redirectEnabled,
|
||
url: redirectURL,
|
||
},
|
||
maxRoomParticipants: hostCfg.maxRoomParticipants,
|
||
//...
|
||
});
|
||
|
||
// SCENARIO: Notify when the first user join room and is awaiting assistance...
|
||
if (peerCounts === 1) {
|
||
nodemailer.sendEmailAlert('join', {
|
||
room_id: channel,
|
||
peer_name: peer_name,
|
||
domain: socket.handshake.headers.host.split(':')[0],
|
||
os: osName ? `${osName} ${osVersion}` : '',
|
||
browser: browserName ? `${browserName} ${browserVersion}` : '',
|
||
}); // .env EMAIL_ALERT=true
|
||
}
|
||
});
|
||
|
||
/**
|
||
* Relay ICE to peers
|
||
*/
|
||
socket.on('relayICE', async (config) => {
|
||
const { peer_id, ice_candidate } = config;
|
||
|
||
// log.debug('[' + socket.id + '] relay ICE-candidate to [' + peer_id + '] ', {
|
||
// address: config.ice_candidate,
|
||
// });
|
||
|
||
await sendToPeer(peer_id, sockets, 'iceCandidate', {
|
||
peer_id: socket.id,
|
||
ice_candidate: ice_candidate,
|
||
});
|
||
});
|
||
|
||
/**
|
||
* Relay SDP to peers
|
||
*/
|
||
socket.on('relaySDP', async (config) => {
|
||
const { peer_id, session_description } = config;
|
||
|
||
log.debug('[' + socket.id + '] relay SessionDescription to [' + peer_id + '] ', {
|
||
type: session_description.type,
|
||
});
|
||
|
||
await sendToPeer(peer_id, sockets, 'sessionDescription', {
|
||
peer_id: socket.id,
|
||
session_description: session_description,
|
||
});
|
||
});
|
||
|
||
/**
|
||
* Handle Room action
|
||
*/
|
||
socket.on('roomAction', async (cfg) => {
|
||
// Prevent XSS injection
|
||
const config = checkXSS(cfg);
|
||
//log.debug('[' + socket.id + '] Room action:', config);
|
||
const { room_id, peer_id, peer_name, peer_uuid, password, action } = config;
|
||
|
||
// Check if peer is presenter
|
||
const isPresenter = isPeerPresenter(room_id, peer_id, peer_name, peer_uuid);
|
||
|
||
let room_is_locked = false;
|
||
//
|
||
try {
|
||
switch (action) {
|
||
case 'lock':
|
||
if (!isPresenter) return;
|
||
peers[room_id]['lock'] = true;
|
||
peers[room_id]['password'] = password;
|
||
await sendToRoom(room_id, socket.id, 'roomAction', {
|
||
peer_name: peer_name,
|
||
action: action,
|
||
});
|
||
room_is_locked = true;
|
||
break;
|
||
case 'unlock':
|
||
if (!isPresenter) return;
|
||
delete peers[room_id]['lock'];
|
||
delete peers[room_id]['password'];
|
||
await sendToRoom(room_id, socket.id, 'roomAction', {
|
||
peer_name: peer_name,
|
||
action: action,
|
||
});
|
||
break;
|
||
case 'checkPassword':
|
||
const data = {
|
||
peer_name: peer_name,
|
||
action: action,
|
||
password: password == peers[room_id]['password'] ? 'OK' : 'KO',
|
||
};
|
||
await sendToPeer(socket.id, sockets, 'roomAction', data);
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
} catch (err) {
|
||
log.error('Room action', toJson(err));
|
||
}
|
||
log.debug('[' + socket.id + '] Room ' + room_id, { locked: room_is_locked, password: password });
|
||
});
|
||
|
||
/**
|
||
* Relay NAME to peers
|
||
*/
|
||
socket.on('peerName', async (cfg) => {
|
||
// Prevent XSS injection
|
||
const config = checkXSS(cfg);
|
||
// log.debug('Peer name', config);
|
||
const { room_id, peer_name_old, peer_name_new, peer_avatar } = config;
|
||
|
||
let peer_id_to_update = null;
|
||
|
||
for (let peer_id in peers[room_id]) {
|
||
if (peers[room_id][peer_id]['peer_name'] == peer_name_old && peer_id == socket.id) {
|
||
peers[room_id][peer_id]['peer_name'] = peer_name_new;
|
||
// presenter
|
||
if (presenters && presenters[room_id] && presenters[room_id][peer_id]) {
|
||
presenters[room_id][peer_id]['peer_name'] = peer_name_new;
|
||
}
|
||
peer_id_to_update = peer_id;
|
||
log.debug('[' + socket.id + '] Peer name changed', {
|
||
peer_name_old: peer_name_old,
|
||
peer_name_new: peer_name_new,
|
||
});
|
||
}
|
||
}
|
||
|
||
if (peer_id_to_update) {
|
||
const data = {
|
||
peer_id: peer_id_to_update,
|
||
peer_name: peer_name_new,
|
||
peer_avatar: peer_avatar,
|
||
};
|
||
log.debug('[' + socket.id + '] emit peerName to [room_id: ' + room_id + ']', data);
|
||
|
||
await sendToRoom(room_id, socket.id, 'peerName', data);
|
||
}
|
||
});
|
||
|
||
/**
|
||
* Handle messages
|
||
*/
|
||
socket.on('message', async (message) => {
|
||
const data = checkXSS(message);
|
||
log.debug('Got message', data);
|
||
await sendToRoom(data.room_id, socket.id, 'message', data);
|
||
});
|
||
|
||
/**
|
||
* Relay commands to peers or specific peer in the same room
|
||
* @param {Object} cfg - The configuration object containing command details.
|
||
* @param {string} cfg.action - The action to be performed (e.g., 'geoLocation').
|
||
* @param {boolean} cfg.send_to_all - Whether to send the command to all peers in the room.
|
||
* @param {Object} cfg.data - The data associated with the command.
|
||
*/
|
||
socket.on('cmd', async (cfg) => {
|
||
const config = checkXSS(cfg);
|
||
|
||
const { action, send_to_all, data } = config;
|
||
|
||
const { room_id, peer_id, peer_name, peer_uuid, to_peer_id } = data;
|
||
|
||
log.info('cmd', config);
|
||
|
||
// Only the presenter can do this actions
|
||
const presenterActions = ['geoLocation'];
|
||
if (presenterActions.some((v) => action === v)) {
|
||
// Check if peer is presenter
|
||
const isPresenter = isPeerPresenter(room_id, peer_id, peer_name, peer_uuid);
|
||
// if not presenter do nothing
|
||
if (!isPresenter) return;
|
||
}
|
||
|
||
if (send_to_all) {
|
||
log.debug('[' + socket.id + '] emit cmd to [room_id: ' + room_id + ']', config);
|
||
|
||
await sendToRoom(room_id, socket.id, 'cmd', config);
|
||
} else {
|
||
log.debug('[' + socket.id + '] emit cmd to [' + to_peer_id + '] from room_id [' + room_id + ']');
|
||
|
||
await sendToPeer(to_peer_id, sockets, 'cmd', config);
|
||
}
|
||
});
|
||
|
||
/**
|
||
* Relay Audio Video Hand ... Status to peers
|
||
*/
|
||
socket.on('peerStatus', async (cfg) => {
|
||
// Prevent XSS injection
|
||
const config = checkXSS(cfg);
|
||
// log.debug('Peer status', config);
|
||
const { room_id, peer_name, peer_id, element, status } = config;
|
||
|
||
const data = {
|
||
peer_id: peer_id,
|
||
peer_name: peer_name,
|
||
element: element,
|
||
status: status,
|
||
};
|
||
|
||
try {
|
||
for (let peer_id in peers[room_id]) {
|
||
if (peers[room_id][peer_id]['peer_name'] == peer_name && peer_id == socket.id) {
|
||
switch (element) {
|
||
case 'video':
|
||
peers[room_id][peer_id]['peer_video_status'] = status;
|
||
break;
|
||
case 'audio':
|
||
peers[room_id][peer_id]['peer_audio_status'] = status;
|
||
break;
|
||
case 'screen':
|
||
peers[room_id][peer_id]['peer_screen_status'] = status;
|
||
break;
|
||
case 'hand':
|
||
peers[room_id][peer_id]['peer_hand_status'] = status;
|
||
break;
|
||
case 'rec':
|
||
peers[room_id][peer_id]['peer_rec_status'] = status;
|
||
break;
|
||
case 'privacy':
|
||
peers[room_id][peer_id]['peer_privacy_status'] = status;
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
log.debug('[' + socket.id + '] emit peerStatus to [room_id: ' + room_id + ']', data);
|
||
|
||
await sendToRoom(room_id, socket.id, 'peerStatus', data);
|
||
} catch (err) {
|
||
log.error('Peer Status', toJson(err));
|
||
}
|
||
});
|
||
|
||
/**
|
||
* Relay actions to peers or specific peer in the same room
|
||
*/
|
||
socket.on('peerAction', async (cfg) => {
|
||
// Prevent XSS injection
|
||
const config = checkXSS(cfg);
|
||
// log.debug('Peer action', config);
|
||
const { room_id, peer_id, peer_uuid, peer_name, peer_avatar, peer_use_video, peer_action, send_to_all } =
|
||
config;
|
||
|
||
// Only the presenter can do this actions
|
||
const presenterActions = ['muteAudio', 'hideVideo', 'ejectAll'];
|
||
if (presenterActions.some((v) => peer_action === v)) {
|
||
// Check if peer is presenter
|
||
const isPresenter = isPeerPresenter(room_id, peer_id, peer_name, peer_uuid);
|
||
// if not presenter do nothing
|
||
if (!isPresenter) return;
|
||
}
|
||
|
||
const data = {
|
||
peer_id: peer_id,
|
||
peer_name: peer_name,
|
||
peer_avatar: peer_avatar,
|
||
peer_action: peer_action,
|
||
peer_use_video: peer_use_video,
|
||
};
|
||
|
||
if (send_to_all) {
|
||
log.debug('[' + socket.id + '] emit peerAction to [room_id: ' + room_id + ']', data);
|
||
|
||
await sendToRoom(room_id, socket.id, 'peerAction', data);
|
||
} else {
|
||
log.debug('[' + socket.id + '] emit peerAction to [' + peer_id + '] from room_id [' + room_id + ']');
|
||
|
||
await sendToPeer(peer_id, sockets, 'peerAction', data);
|
||
}
|
||
});
|
||
|
||
/**
|
||
* Start caption
|
||
*/
|
||
socket.on('caption', async (cfg) => {
|
||
// Prevent XSS injection
|
||
const config = checkXSS(cfg);
|
||
await sendToRoom(cfg.room_id, sockets, 'caption', config);
|
||
});
|
||
|
||
/**
|
||
* Relay Kick out peer from room
|
||
*/
|
||
socket.on('kickOut', async (cfg) => {
|
||
// Prevent XSS injection
|
||
const config = checkXSS(cfg);
|
||
const { room_id, peer_id, peer_uuid, peer_name } = config;
|
||
|
||
// Check if peer is presenter
|
||
const isPresenter = await isPeerPresenter(room_id, peer_id, peer_name, peer_uuid);
|
||
|
||
// Only the presenter can kickOut others
|
||
if (isPresenter) {
|
||
log.debug('[' + socket.id + '] kick out peer [' + peer_id + '] from room_id [' + room_id + ']');
|
||
|
||
await sendToPeer(peer_id, sockets, 'kickOut', {
|
||
peer_name: peer_name,
|
||
});
|
||
}
|
||
});
|
||
|
||
/**
|
||
* Relay File info
|
||
*/
|
||
socket.on('fileInfo', async (cfg) => {
|
||
// Prevent XSS injection
|
||
const config = checkXSS(cfg);
|
||
// log.debug('File info', config);
|
||
const { room_id, peer_id, peer_name, peer_avatar, broadcast, file } = config;
|
||
|
||
// check if valid fileName
|
||
if (!isValidFileName(file.fileName)) {
|
||
log.debug('[' + socket.id + '] File name not valid', config);
|
||
return;
|
||
}
|
||
|
||
function bytesToSize(bytes) {
|
||
let sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||
if (bytes == 0) return '0 Byte';
|
||
let i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
|
||
return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i];
|
||
}
|
||
|
||
log.debug('[' + socket.id + '] Peer [' + peer_name + '] send file to room_id [' + room_id + ']', {
|
||
peerName: peer_name,
|
||
peerAvatar: peer_avatar,
|
||
fileName: file.fileName,
|
||
fileSize: bytesToSize(file.fileSize),
|
||
fileType: file.fileType,
|
||
broadcast: broadcast,
|
||
});
|
||
|
||
if (broadcast) {
|
||
await sendToRoom(room_id, socket.id, 'fileInfo', config);
|
||
} else {
|
||
await sendToPeer(peer_id, sockets, 'fileInfo', config);
|
||
}
|
||
});
|
||
|
||
/**
|
||
* Abort file sharing
|
||
*/
|
||
socket.on('fileAbort', async (cfg) => {
|
||
// Prevent XSS injection
|
||
const config = checkXSS(cfg);
|
||
const { room_id, peer_name } = config;
|
||
|
||
log.debug('[' + socket.id + '] Peer [' + peer_name + '] send fileAbort to room_id [' + room_id + ']');
|
||
await sendToRoom(room_id, socket.id, 'fileAbort');
|
||
});
|
||
|
||
socket.on('fileReceiveAbort', async (cfg) => {
|
||
const config = checkXSS(cfg);
|
||
const { room_id, peer_name } = config;
|
||
log.debug('[' + socket.id + '] Peer [' + peer_name + '] send fileReceiveAbort to room_id [' + room_id + ']');
|
||
await sendToRoom(room_id, socket.id, 'fileReceiveAbort', config);
|
||
});
|
||
|
||
/**
|
||
* Relay video player action
|
||
*/
|
||
socket.on('videoPlayer', async (cfg) => {
|
||
// Prevent XSS injection
|
||
const config = checkXSS(cfg);
|
||
// log.debug('Video player', config);
|
||
const { room_id, peer_id, peer_name, video_action, video_src } = config;
|
||
|
||
// Check if valid video src url
|
||
if (video_action == 'open' && !isValidHttpURL(video_src)) {
|
||
log.debug('[' + socket.id + '] Video src not valid', config);
|
||
return;
|
||
}
|
||
|
||
const data = {
|
||
peer_id: socket.id,
|
||
peer_name: peer_name,
|
||
video_action: video_action,
|
||
video_src: video_src,
|
||
};
|
||
|
||
if (peer_id) {
|
||
log.debug('[' + socket.id + '] emit videoPlayer to [' + peer_id + '] from room_id [' + room_id + ']', data);
|
||
|
||
await sendToPeer(peer_id, sockets, 'videoPlayer', data);
|
||
} else {
|
||
log.debug('[' + socket.id + '] emit videoPlayer to [room_id: ' + room_id + ']', data);
|
||
|
||
await sendToRoom(room_id, socket.id, 'videoPlayer', data);
|
||
}
|
||
});
|
||
|
||
/**
|
||
* Whiteboard actions for all user in the same room
|
||
*/
|
||
socket.on('wbCanvasToJson', async (cfg) => {
|
||
// Prevent XSS injection
|
||
const config = checkXSS(cfg);
|
||
// log.debug('Whiteboard send canvas', config);
|
||
const { room_id } = config;
|
||
await sendToRoom(room_id, socket.id, 'wbCanvasToJson', config);
|
||
});
|
||
|
||
socket.on('whiteboardAction', async (cfg) => {
|
||
// Prevent XSS injection
|
||
const config = checkXSS(cfg);
|
||
log.debug('Whiteboard', config);
|
||
const { room_id } = config;
|
||
await sendToRoom(room_id, socket.id, 'whiteboardAction', config);
|
||
});
|
||
|
||
/**
|
||
* Add peers to channel
|
||
* @param {string} channel room id
|
||
*/
|
||
async function addPeerTo(channel) {
|
||
for (let id in channels[channel]) {
|
||
// offer false
|
||
await channels[channel][id].emit('addPeer', {
|
||
peer_id: socket.id,
|
||
peers: peers[channel],
|
||
should_create_offer: false,
|
||
iceServers: iceServers,
|
||
});
|
||
// offer true
|
||
socket.emit('addPeer', {
|
||
peer_id: id,
|
||
peers: peers[channel],
|
||
should_create_offer: true,
|
||
iceServers: iceServers,
|
||
});
|
||
log.debug('[' + socket.id + '] emit addPeer [' + id + ']');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Remove peers from channel
|
||
* @param {string} channel room id
|
||
*/
|
||
async function removePeerFrom(channel) {
|
||
if (!(channel in socket.channels)) {
|
||
return log.debug('[' + socket.id + '] [Warning] not in ', channel);
|
||
}
|
||
try {
|
||
delete socket.channels[channel];
|
||
delete channels[channel][socket.id];
|
||
delete peers[channel][socket.id]; // delete peer data from the room
|
||
|
||
switch (Object.keys(peers[channel]).length) {
|
||
case 0: // last peer disconnected from the room without room lock & password set
|
||
delete peers[channel];
|
||
delete presenters[channel];
|
||
break;
|
||
case 2: // last peer disconnected from the room having room lock & password set
|
||
if (peers[channel]['lock'] && peers[channel]['password']) {
|
||
delete peers[channel]; // clean lock and password value from the room
|
||
delete presenters[channel]; // clean the presenter from the channel
|
||
}
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
} catch (err) {
|
||
log.error('Remove Peer', toJson(err));
|
||
}
|
||
|
||
const activeRooms = getActiveRooms();
|
||
|
||
log.info('[removePeerFrom] - active rooms and peers count', activeRooms);
|
||
|
||
log.info('[removePeerFrom] - connected presenters grp by roomId', presenters);
|
||
|
||
log.info('[removePeerFrom] - connected peers grp by roomId', peers);
|
||
|
||
for (let id in channels[channel]) {
|
||
await channels[channel][id].emit('removePeer', { peer_id: socket.id });
|
||
socket.emit('removePeer', { peer_id: id });
|
||
log.debug('[' + socket.id + '] emit removePeer [' + id + ']');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Object to Json
|
||
* @param {object} data object
|
||
* @returns {json} indent 4 spaces
|
||
*/
|
||
function toJson(data) {
|
||
return JSON.stringify(data, null, 4); // "\t"
|
||
}
|
||
|
||
/**
|
||
* Send async data to all peers in the same room except yourself
|
||
* @param {string} room_id id of the room to send data
|
||
* @param {string} socket_id socket id of peer that send data
|
||
* @param {string} msg message to send to the peers in the same room
|
||
* @param {object} config data to send to the peers in the same room
|
||
*/
|
||
async function sendToRoom(room_id, socket_id, msg, config = {}) {
|
||
for (let peer_id in channels[room_id]) {
|
||
// not send data to myself
|
||
if (peer_id != socket_id) {
|
||
await channels[room_id][peer_id].emit(msg, config);
|
||
//console.log('Send to room', { msg: msg, config: config });
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Send async data to specified peer
|
||
* @param {string} peer_id id of the peer to send data
|
||
* @param {object} sockets all peers connections
|
||
* @param {string} msg message to send to the peer in the same room
|
||
* @param {object} config data to send to the peer in the same room
|
||
*/
|
||
async function sendToPeer(peer_id, sockets, msg, config = {}) {
|
||
if (peer_id in sockets) {
|
||
await sockets[peer_id].emit(msg, config);
|
||
//console.log('Send to peer', { msg: msg, config: config });
|
||
}
|
||
}
|
||
}); // end [sockets.on-connect]
|
||
|
||
/**
|
||
* Get Env as boolean
|
||
* @param {string} key
|
||
* @param {boolean} force_true_if_undefined
|
||
* @returns boolean
|
||
*/
|
||
function getEnvBoolean(key, force_true_if_undefined = false) {
|
||
if (key == undefined && force_true_if_undefined) return true;
|
||
return key == 'true' ? true : false;
|
||
}
|
||
|
||
/**
|
||
* Check if valid filename
|
||
* @param {string} fileName
|
||
* @returns boolean
|
||
*/
|
||
function isValidFileName(fileName) {
|
||
const invalidChars = /[\\\/\?\*\|:"<>]/;
|
||
return !invalidChars.test(fileName);
|
||
}
|
||
|
||
/**
|
||
* Check if valid URL
|
||
* @param {string} str to check
|
||
* @returns boolean true/false
|
||
*/
|
||
function isValidHttpURL(input) {
|
||
try {
|
||
const url = new URL(input);
|
||
return url.protocol === 'http:' || url.protocol === 'https:';
|
||
} catch (_) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* get Peer geo Location using GeoJS
|
||
* https://www.geojs.io/docs/v1/endpoints/geo/
|
||
*
|
||
* @param {string} ip
|
||
* @returns json
|
||
*/
|
||
async function getPeerGeoLocation(ip) {
|
||
const endpoint = `https://get.geojs.io/v1/ip/geo/${ip}.json`;
|
||
log.debug('Get peer geo', { ip: ip, endpoint: endpoint });
|
||
return axios
|
||
.get(endpoint)
|
||
.then((response) => response.data)
|
||
.catch((error) => log.error(error));
|
||
}
|
||
|
||
/**
|
||
* Check if peer is Presenter
|
||
* @param {string} room_id
|
||
* @param {string} peer_id
|
||
* @param {string} peer_name
|
||
* @param {string} peer_uuid
|
||
* @returns boolean
|
||
*/
|
||
function isPeerPresenter(room_id, peer_id, peer_name, peer_uuid) {
|
||
try {
|
||
if (!presenters[room_id] || !presenters[room_id][peer_id]) {
|
||
// Presenter not in the presenters config list, disconnected, or peer_id changed...
|
||
for (const [existingPeerID, presenter] of Object.entries(presenters[room_id] || {})) {
|
||
if (presenter.peer_name === peer_name) {
|
||
log.debug('[' + peer_id + '] Presenter found', presenters[room_id][existingPeerID]);
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
const isPresenter =
|
||
(typeof presenters[room_id] === 'object' &&
|
||
Object.keys(presenters[room_id][peer_id]).length > 1 &&
|
||
presenters[room_id][peer_id]['peer_name'] === peer_name &&
|
||
presenters[room_id][peer_id]['peer_uuid'] === peer_uuid) ||
|
||
(roomPresenters && roomPresenters.includes(peer_name));
|
||
|
||
log.debug('[' + peer_id + '] isPeerPresenter', presenters[room_id][peer_id]);
|
||
|
||
return isPresenter;
|
||
} catch (err) {
|
||
log.error('isPeerPresenter', err);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Check if peer is present in the host users
|
||
* @param {string} username
|
||
* @param {string} password
|
||
* @returns Boolean true/false
|
||
*/
|
||
function isAuthPeer(username, password) {
|
||
return hostCfg.users && hostCfg.users.some((user) => user.username === username && user.password === password);
|
||
}
|
||
|
||
/**
|
||
* Check if valid JWT token
|
||
* @param {string} token
|
||
* @returns boolean
|
||
*/
|
||
async function isValidToken(token) {
|
||
return new Promise((resolve, reject) => {
|
||
jwt.verify(token, jwtCfg.JWT_KEY, (err, decoded) => {
|
||
if (err) {
|
||
// Token is invalid
|
||
resolve(false);
|
||
} else {
|
||
// Token is valid
|
||
resolve(true);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Encode JWT token payload
|
||
* @param {object} token
|
||
* @returns
|
||
*/
|
||
function encodeToken(token) {
|
||
if (!token) return '';
|
||
|
||
const { username = 'username', password = 'password', presenter = false, expire } = token;
|
||
|
||
const expireValue = expire || jwtCfg.JWT_EXP;
|
||
|
||
// Constructing payload
|
||
const payload = {
|
||
username: String(username),
|
||
password: String(password),
|
||
presenter: String(presenter),
|
||
};
|
||
|
||
// Encrypt payload using AES encryption
|
||
const payloadString = JSON.stringify(payload);
|
||
const encryptedPayload = CryptoJS.AES.encrypt(payloadString, jwtCfg.JWT_KEY).toString();
|
||
|
||
// Constructing JWT token
|
||
const jwtToken = jwt.sign({ data: encryptedPayload }, jwtCfg.JWT_KEY, { expiresIn: expireValue });
|
||
|
||
return jwtToken;
|
||
}
|
||
|
||
/**
|
||
* Decode JWT Payload data
|
||
* @param {object} jwtToken
|
||
* @returns mixed
|
||
*/
|
||
function decodeToken(jwtToken) {
|
||
if (!jwtToken) return null;
|
||
|
||
// Verify and decode the JWT token
|
||
const decodedToken = jwt.verify(jwtToken, jwtCfg.JWT_KEY);
|
||
if (!decodedToken || !decodedToken.data) {
|
||
throw new Error('Invalid token');
|
||
}
|
||
|
||
// Decrypt the payload using AES decryption
|
||
const decryptedPayload = CryptoJS.AES.decrypt(decodedToken.data, jwtCfg.JWT_KEY).toString(CryptoJS.enc.Utf8);
|
||
|
||
// Parse the decrypted payload as JSON
|
||
const payload = JSON.parse(decryptedPayload);
|
||
|
||
return payload;
|
||
}
|
||
|
||
/**
|
||
* Get All connected peers count grouped by roomId
|
||
* @return {object} array
|
||
*/
|
||
function getActiveRooms() {
|
||
const roomPeersArray = [];
|
||
// Iterate through each room
|
||
for (const roomId in peers) {
|
||
if (peers.hasOwnProperty(roomId)) {
|
||
// Get the count of peers in the current room
|
||
const peersCount = Object.keys(peers[roomId]).length;
|
||
roomPeersArray.push({
|
||
roomId: roomId,
|
||
peersCount: peersCount,
|
||
});
|
||
}
|
||
}
|
||
return roomPeersArray;
|
||
}
|
||
|
||
/**
|
||
* Check if Allowed Room Access
|
||
* @param {string} logMessage
|
||
* @param {object} req
|
||
* @param {object} hostCfg
|
||
* @param {object} peers
|
||
* @param {string} roomId
|
||
* @returns boolean true/false
|
||
*/
|
||
function isAllowedRoomAccess(logMessage, req, hostCfg, peers, roomId) {
|
||
const OIDCUserAuthenticated = OIDC.enabled && req.oidc.isAuthenticated();
|
||
const OIDCAllowRoomCreationForAuthUsers = OIDC.allowRoomCreationForAuthUsers;
|
||
const hostUserAuthenticated = hostCfg.protected && hostCfg.authenticated;
|
||
const roomExist = roomId in peers;
|
||
const roomCount = Object.keys(peers).length;
|
||
|
||
const allowRoomAccess =
|
||
(!hostCfg.protected && !OIDC.enabled) || // No host protection and OIDC mode enabled (default)
|
||
(OIDCUserAuthenticated && roomExist) || // User authenticated via OIDC and room Exist
|
||
(hostUserAuthenticated && roomExist) || // User authenticated via Login and room Exist
|
||
((OIDCUserAuthenticated || hostUserAuthenticated) && roomCount === 0) || // User authenticated joins the first room
|
||
(OIDCUserAuthenticated && OIDCAllowRoomCreationForAuthUsers) || // Allow room creation if authenticated via OIDC
|
||
roomExist; // User Or Guest join an existing Room
|
||
|
||
log.debug(logMessage, {
|
||
OIDCUserAuthenticated: OIDCUserAuthenticated,
|
||
hostUserAuthenticated: hostUserAuthenticated,
|
||
roomExist: roomExist,
|
||
roomCount: roomCount,
|
||
extraInfo: {
|
||
roomId: roomId,
|
||
OIDCUserEnabled: OIDC.enabled,
|
||
hostProtected: hostCfg.protected,
|
||
hostAuthenticated: hostCfg.authenticated,
|
||
OIDCAllowRoomCreationForAuthUsers,
|
||
},
|
||
allowRoomAccess: allowRoomAccess,
|
||
});
|
||
|
||
return allowRoomAccess;
|
||
}
|
||
|
||
/**
|
||
* Get ip
|
||
* @param {object} req
|
||
* @returns string ip
|
||
*/
|
||
function getIP(req) {
|
||
return req.headers['x-forwarded-for'] || req.headers['X-Forwarded-For'] || req.socket.remoteAddress || req.ip;
|
||
}
|
||
|
||
/**
|
||
* Get IP from socket
|
||
* @param {object} socket
|
||
* @returns string
|
||
*/
|
||
function getSocketIP(socket) {
|
||
return (
|
||
socket.handshake.headers['x-forwarded-for'] ||
|
||
socket.handshake.headers['X-Forwarded-For'] ||
|
||
socket.handshake.address
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Check if auth ip
|
||
* @param {string} ip
|
||
* @returns boolean
|
||
*/
|
||
function allowedIP(ip) {
|
||
const authorizedIPs = authHost.getAuthorizedIPs();
|
||
const authorizedIP = authHost.isAuthorizedIP(ip);
|
||
log.info('Allowed IPs', { ip: ip, authorizedIP: authorizedIP, authorizedIPs: authorizedIPs });
|
||
return authHost != null && authorizedIP;
|
||
}
|
||
|
||
/**
|
||
* Remove hosts auth ip on socket disconnect
|
||
* @param {object} socket
|
||
*/
|
||
function removeIP(socket) {
|
||
if (hostCfg.protected) {
|
||
const ip = getSocketIP(socket);
|
||
log.debug('[removeIP] - Host protected check ip', { ip: ip });
|
||
if (ip && allowedIP(ip)) {
|
||
authHost.deleteIP(ip);
|
||
hostCfg.authenticated = false;
|
||
log.info('[removeIP] - Remove IP from auth', {
|
||
ip: ip,
|
||
authorizedIps: authHost.getAuthorizedIPs(),
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Load modules if exists
|
||
* @param {string} filePath
|
||
* @returns
|
||
*/
|
||
function safeRequire(filePath) {
|
||
let data = null;
|
||
try {
|
||
data = require(filePath);
|
||
} catch (error) {
|
||
log.error(error);
|
||
}
|
||
return data;
|
||
}
|
||
|
||
/**
|
||
* Cleanup HTML injector when the application is shutting down
|
||
*/
|
||
process.on('SIGINT', () => {
|
||
log.debug('PROCESS', 'SIGINT');
|
||
htmlInjector.cleanup();
|
||
process.exit();
|
||
});
|
||
|
||
process.on('SIGTERM', () => {
|
||
log.debug('PROCESS', 'SIGTERM');
|
||
htmlInjector.cleanup();
|
||
process.exit();
|
||
});
|