ws-user
Version:
User authentication and authorization for WebSocket applications.
1,016 lines (756 loc) • 78.1 kB
JavaScript
/** Copyright (c) Manuel Lõhmus (MIT License) */
'use strict';
var path = require('path'),
fs = require('fs'),
crypto = require("crypto"),
DataContext = require('data-context'),
configSets = require('config-sets'),
userConfigSets = configSets('ws-user', {
isDebug: false,
pathToFrontendErrorFile: 'log/frontend_errors.log',
pathToLogFile: '',
pathToLoggedUsers: 'logged_users.json',
pathToUsersDir: 'users',
fileSecurityCodeText: "./public/www/templates/.security-code.txt",
fileSecurityCodeHtml: "./public/www/templates/.security-code.html"
}),
loggedUsers = DataContext.watchJsonFile({ filePath: userConfigSets.pathToLoggedUsers });
Object.defineProperties(CreateWsUser, {
CONNECTING: { value: 0, writable: false, enumerable: false, configurable: false },
OPEN: { value: 1, writable: false, enumerable: false, configurable: false },
CLOSING: { value: 2, writable: false, enumerable: false, configurable: false },
CLOSED: { value: 3, writable: false, enumerable: false, configurable: false },
PAUSE: { value: 4, writable: false, enumerable: false, configurable: false },
addUser: { value: addUser, writable: false, enumerable: false, configurable: false },
});
//*** nodemailer ***
var nodemailer = require("nodemailer"),
nodemailerOptions = configSets('nodemailer', {
isDebug: false,
domain_account: 'localhost',
auth: {
user: "*user*@localhost",
pass: ""
}
});
/**
* @typedef {object} MailOptions
* @property {string} from
* @property {string} to
* @property {string} cc
* @property {string} bcc
* @property {string} subject
* @property {string} text
* @property {string} html
* @property {any[]} attachments
*/
/**
* Send mail from noreply@*******.**
* @param {MailOptions} mailOptions
* @param {(err:Error|null)=>void} callback
*/
function sendMail(mailOptions, callback) {
if (nodemailerOptions.auth.user === "*user*@localhost" || nodemailerOptions.auth.pass === "") {
var errorMessage = "Please configure 'nodemailer' transporter options in 'config-sets.json' file.\n\tSee options: https://nodemailer.com/smtp/";
if (nodemailerOptions.isDebug) { console.error("[ ERROR ] 'nodemailer'", errorMessage); }
return callback(errorMessage);
}
nodemailer.createTransport(nodemailerOptions)
.sendMail(mailOptions, function (err, info) {
if (err) {
if (nodemailerOptions.isDebug) { console.error("[ ERROR ] 'nodemailer'", err); }
callback(err);
}
else {
var isOK = info.response.startsWith("250");
mailOptions.to
.split(",")
.map(function (str) { return str.trim() })
.forEach(function (str) { if (!info.accepted.includes(str)) { isOK = false; } });
if (nodemailerOptions.isDebug) { console.log("[ INFO ] 'nodemailer'", info); }
callback(isOK ? null : new Error("Sending email failed."));
}
});
}
return module.exports = CreateWsUser;
/**
* CreateWsUser
* @param {Options} options
* @property {0} CONNECTING - The connection is not yet open.
* @property {1} OPEN - The connection is open and ready to communicate.
* @property {2} CLOSING - The connection is in the process of closing.
* @property {3} CLOSED - The connection is closed or couldn't be opened.
* @property {4} PAUSE - The connection is paused.
* @returns {WsUser}
*
* @typedef {Object} Options
* @property {http.IncomingMessage|http.ClientRequest} request - Reference link: https://nodejs.org/docs/latest/api/http.html#class-httpincomingmessage or https://nodejs.org/docs/latest/api/http.html#class-httpclientrequest
* @property {Object.<string, string>} headers - Key-value pairs of header names and values. Header names are lower-cased.
* @property {net.Socket} socket - This class is an abstraction of a TCP socket or a streaming IPC endpoint.
* @property {string} protocol - The sub-protocol selected by the server. Default ''.
* @property {string} origin - String. Default empty string.
* @property {number} heartbeatInterval_ms - The interval after which ping pong takes place. Default on the client side 0ms and on the server side 30000ms.
* @property {Object} extension - The extensions selected by the server. Default 'permessage-deflate'
* @property {string} url - This is the URL of the WebSocket server to connect to. Default empty string.
*
* @typedef {Object} WsUser
* @property {0|1|2|3|4} redyState CONNECTING: 0 | OPEN: 1 | CLOSING: 2 | CLOSED: 3 | PAUSE: 4
* @property {()=>void} onopen - Event handler for the 'open' event.
* @property {(ev:{data})=>void} onmessage - Event handler for the 'message' event.
* @property {(ev:{error})=>void} onerror - Event handler for the 'error' event.
* @property {(ev:{code, reason})=>void} onclose - Event handler for the 'close' event.
* @property {(data:string|BinaryData)=>void} send - Sends data to the server over the WebSocket connection.
* @property {(code:number, reason:string)=>void} close - Closes the WebSocket connection or connection attempt, if any.
*/
function CreateWsUser({
request,
url = '',
headers,
socket,
protocol = '',
origin = '',
heartbeatInterval_ms = 0,
extension,
} = {}) {
if (CreateWsUser === this?.constructor) { throw new Error('CreateWsUser must be called without `new` keyword!'); }
/**
* CONNECTING: 0 | OPEN: 1 | CLOSING: 2 | CLOSED: 3 | PAUSE: 4
* @type {0|1|2|3|4} readyState
*/
var readyState = CreateWsUser.CONNECTING,
isWsUser = false,
connID = '',
email = '',
roles = [],
organizations = [],
sendedData = null,
ws = createWebSocket(),
location = origin || headers?.origin || request?.headers?.origin,
self = Object.create(null);
if (!ws) { return null; }
loggedUsers.on('-change', (event) => {
if (!loggedUsers[email] && readyState === 1 && roles.length) {
sendUserinfo({ isLogged: false });
}
return Boolean(self);
});
return Object.defineProperties(self, {
// Public properties
readyState: {
get: function () { return readyState },
set: function (v) { readyState = v; this.onreadystate?.(v); },
enumerable: false, configurable: false
},
protocol: { get: function () { return protocol }, enumerable: false, configurable: false },
connID: { get: function () { return connID }, enumerable: false, configurable: false },
email: { get: function () { return email }, enumerable: false, configurable: false },
roles: { get: function () { return roles }, enumerable: false, configurable: false },
organizations: { get: function () { return organizations }, enumerable: false, configurable: false },
url: { get: function () { return url }, enumerable: false, configurable: false },
isLoggedIn: { get: function () { return Boolean(loggedUsers[email]?.connIDs?.includes(connID)); }, enumerable: false, configurable: false },
extensionCommands: { value: Object.create(null), writable: false, enumerable: false, configurable: false },
// Public events (callbacks)
onreadystate: { value: null, writable: true, enumerable: false, configurable: false },
onopen: { value: null, writable: true, enumerable: false, configurable: false },
onmessage: { value: null, writable: true, enumerable: false, configurable: false },
onerror: { value: null, writable: true, enumerable: false, configurable: false },
onclose: { value: null, writable: true, enumerable: false, configurable: false },
onloggedin: { value: null, writable: true, enumerable: false, configurable: false },
// Public methods
send: { value: sendData, writable: false, enumerable: false, configurable: false },
messageHandled: { value: messageHandled, writable: false, enumerable: false, configurable: false },
close: { value: close, writable: false, enumerable: false, configurable: false }
});
function createWebSocket() {
if (!headers) { headers = request.headers; }
if (headers?.['sec-websocket-protocol']) {
var[pro, id, mail] = headers['sec-websocket-protocol']
.replace(/,+/g, ' ')
.split(' ')
.filter(p => p);
protocol = pro ? pro : protocol;
connID = id ? id : '';
email = mail ? mail.replace("*", "@") : '';
isWsUser = protocol === 'ws-user';
}
if (loggedUsers[email]?.connIDs?.includes(connID)
&& loggedUsers[email].roles) {
roles = loggedUsers[email].roles;
organizations = loggedUsers[email].organizations;
}
ws = require('ws13')({
isDebug: userConfigSets.isDebug,
request,
headers,
socket,
protocol,
origin,
heartbeatInterval_ms,
extension,
});
if (!ws) { return null; }
ws.onopen = onWsOpen;
ws.onmessage = onWsMessage;
ws.onerror = onWsError;
ws.onclose = onWsClose;
return ws;
}
// Event handlers
function onWsOpen() {
pDebug('Connection opened');
self.readyState = CreateWsUser.OPEN;
if (isWsUser && loggedUsers[email]?.connIDs.includes(connID)) {
sendUserinfo({ isLogged: true, user: loggedUsers[email] });
}
else if (isWsUser) { sendUserinfo({ isLogged: false }); }
if (sendedData?.startsWith('$userinfo')) { sendedData = null; }
if (sendedData) { sendData(sendedData); }
self.onopen && self.onopen.call(self);
}
function onWsMessage(event) {
if (typeof event.data === 'string' && !event.data) {
pDebug('Received - State => OPEN');
self.readyState = CreateWsUser.OPEN;
sendedData = null;
return;
}
if (executeCommand(event.data)) { return; }
pDebug(`'Message received'`, (event.data + '').replace(/\s+/g, '').substring(0, 64));
self.onmessage && self.onmessage.call(self, event);
}
function onWsError(event) {
pError(event);
self.onerror && self.onerror.call(self, event);
}
function onWsClose(event) {
pDebug('Connection closed', event);
self.readyState = CreateWsUser.CLOSED;
self.onclose && self.onclose.call(self, event);
// websocket - Going Away
if (isWsUser && (event.code === 1001 || event.code === 1006)) { connLogout(); }
function connLogout() {
if (loggedUsers[email]?.connIDs.includes(connID)) {
loggedUsers[email].connIDs = loggedUsers[email].connIDs.filter(c => c !== connID);
}
if (!loggedUsers[email]?.connIDs.length) { delete loggedUsers[email]; }
}
}
// Public methods
function sendData(data) {
if (!data || !(data?.length || data?.byteLength)
|| self.readyState === CreateWsUser.CLOSING
|| self.readyState === CreateWsUser.CLOSED) {
return;
}
// OPEN: 1
if (self.readyState === CreateWsUser.OPEN) {
pDebug('Send - State => PAUSE');
pDebug(`'Sending message'`, data.replace(/\s+/g, '').substring(0, 64));
self.readyState = CreateWsUser.PAUSE;
ws.send(data);
sendedData = data;
return;
}
// Wait for connection to open and try again in 100ms intervals
setTimeout(sendData, 100, data);
}
function messageHandled() { ws.send(''); }
function close(code = 1000, reason = 'Normal closure') {
if (!ws) { return; }
self.readyState = CreateWsUser.CLOSING;
ws.close(code, reason);
}
// Private methods
function executeCommand(message) {
if (ws.protocol !== 'ws-user') { return false; }
var msgObj = parseMsg(message);
if (msgObj) {
setTimeout(execute, 0, msgObj);
return true;
}
return false;
function parseMsg(message) {
if (message[0] !== '$') { return null; }
var msgObj = {
cmd: '',
rawBody: ''
};
message = (message + '').split(':');
msgObj.cmd = (message.shift() + '').trim();
if (msgObj.cmd[0] === '$') { msgObj.cmd = msgObj.cmd.substring(1); }
msgObj.rawBody = message.join(':');
return msgObj;
}
function execute(msgObj) {
var functionMap = {
log: function (msg) {
if (!msg || !userConfigSets.pathToLogFile) { return; }
var text = `${new Date().toISOString()}|${ws?.ip || 'undefined'}`;
while (text.length < 40) { text += ' '; }
text += `${msg}\n`;
var filePath = path.parse(userConfigSets.pathToLogFile);
filePath.base = location.substring(location.indexOf('//') + 2) + ('-' + filePath.base || '.log');
filePath = path.resolve(
path.parse(process.argv[1]).dir.split("node_modules").shift(),
filePath.dir,
filePath.base
);
if (!fs.existsSync(path.parse(filePath).dir)) {
fs.mkdirSync(
path.parse(filePath).dir,
{ recursive: true }
);
}
fs.appendFile(filePath, text, function (err) {
if (err) { pError(err); }
});
done();
},
log_error: function (msg) {
var text = `[${new Date().toISOString()}][ ERROR ]\t${msg}\n`;
var filePath = path.resolve(path.parse(process.argv[1]).dir.split("node_modules").shift(), userConfigSets.pathToFrontendErrors);
if (!fs.existsSync(path.parse(filePath).dir)) {
fs.mkdirSync(
path.parse(filePath).dir,
{ recursive: true }
);
}
fs.appendFile(filePath, text, function (err) {
if (err) { pError(err); }
});
pError('Frontend:', msg);
done();
},
login: function (msg) {
msg = unmasking(msg, connID);
var oldEmail = email, [userEmail, password] = msg.split(':');
var user = getUserDataFromFile(userEmail);
if (!user) { return; }
email = userEmail;
if (user.password !== encryptPassword(password)) {
sendUserinfo({
isLogged: false,
message: 'Wrong password.',
alerttype: 'alert-warning',
user: { email: userEmail, securityCode: user.securityCode }
});
done();
return;
}
// Logout
if (loggedUsers[oldEmail]?.connIDs.includes(connID)) {
loggedUsers[oldEmail].connIDs = loggedUsers[oldEmail].connIDs.filter(c => c !== connID);
organizations = [];
roles = [];
if (typeof self.onloggedin === 'function') { self.onloggedin.call(self, { isLoggedIn: false }); }
}
if (!loggedUsers[oldEmail]?.connIDs.length) { delete loggedUsers[oldEmail]; }
// Login
if (!loggedUsers[userEmail]) {
loggedUsers[userEmail] = {
connIDs: [],
email: user.email,
name: user.name,
organizations: user.organizations,
roles: user.roles
};
}
if (!loggedUsers[userEmail].connIDs.includes(connID)) { loggedUsers[userEmail].connIDs.push(connID); }
organizations = user.organizations;
roles = user.roles;
sendUserinfo({ isLogged: true, message: 'You are logged in.', user });
if (!user.ip_addresses) { user.ip_addresses = []; }
if (!user.ip_addresses.includes(ws.ip)) {
user.ip_addresses.push(ws.ip);
saveUserDataToFile(user, function () { done(); });
}
if (typeof self.onloggedin === 'function') { self.onloggedin.call(self, { isLoggedIn: true }); }
done();
},
logout: function (msg) {
var conn_id = msg;
if (conn_id && loggedUsers[email]) {
loggedUsers[email].connIDs = loggedUsers[email].connIDs.filter(c => c !== conn_id);
sendUserinfo({ message: 'You are logged out.' });
}
else {
delete loggedUsers[email];
sendUserinfo({ message: 'You are logged out everywhere.' });
}
if (typeof self.onloggedin === 'function') { self.onloggedin.call(self, { isLoggedIn: false }); }
done();
},
create_account: function (msg) {
msg = unmasking(msg, connID);
var [userEmail, password, username] = msg.split(':'),
filePath = findUserDataFilePath(userEmail);
if (filePath) {
sendUserinfo({
isLogged: false,
message: 'Email in use.',
alerttype: 'alert-warning',
});
done();
return;
}
email = userEmail;
password = encryptPassword(password);
self.create_account = {
email: userEmail,
password,
name: username,
organizations: [],
roles: ['user']
};
sendSecurityCode(self.create_account, done);
},
update_name: function update_name(msg) {
var [userEmail, username] = msg.split(':'),
user = getUserDataFromFile(userEmail);
if (!user) { return; }
user.name = username;
if (loggedUsers[userEmail]) { loggedUsers[userEmail].name = username; }
saveUserDataToFile(user, function () {
sendUserinfo({ isLogged: true, message: 'You name is updated.', user });
done();
});
},
update_password: function update_password(msg) {
msg = unmasking(msg, connID);
var [userEmail, password] = msg.split(':'),
user = getUserDataFromFile(userEmail);
if (!user) { return; }
user.password = encryptPassword(password);
saveUserDataToFile(user, function () {
sendUserinfo({ isLogged: true, message: 'You password is updated.', user });
done();
});
},
security_code: function (msg) {
msg = unmasking(msg, connID);
var [userEmail, securityCode, resetPassword] = msg.split(':');
// end create account
if (securityCode && !resetPassword && self.create_account?.email === userEmail) {
if (self.create_account.securityCode !== securityCode) {
sendUserinfo({
isLogged: false,
message: `The security code does not match, try again. (${userEmail})`,
alerttype: 'alert-warning',
user: { email: userEmail }
});
delete self.create_account;
if (typeof self.onloggedin === 'function') { self.onloggedin.call(self, { isLoggedIn: false }); }
done();
return;
}
delete self.create_account.securityCode;
saveUserDataToFile(self.create_account, function () {
sendUserinfo({ isLogged: true, message: 'You are logged in.', user: self.create_account });
delete self.create_account;
if (typeof self.onloggedin === 'function') { self.onloggedin.call(self, { isLoggedIn: true }); }
done();
});
return;
}
var user = getUserDataFromFile(userEmail, done);
if (!user) { return; }
if (securityCode) {
if (securityCode !== user.securityCode) {
sendUserinfo({
isLogged: false,
message: `The security code does not match, try again. (${userEmail})`,
alerttype: 'alert-warning',
user: { email: userEmail }
});
return;
}
delete user.securityCode;
user.resetPassword = true;
sendUserinfo({ isLogged: true, message: 'You are logged in.', user });
delete user.resetPassword;
roles = user.roles;
saveUserDataToFile(user, done);
return;
}
sendSecurityCode(user, function () {
saveUserDataToFile(user, done);
});
}
}
if (typeof functionMap[msgObj.cmd] === 'function') {
return functionMap[msgObj.cmd](msgObj.rawBody);
}
if (typeof self.extensionCommands[msgObj.cmd] === 'function') {
return self.extensionCommands[msgObj.cmd].call(self, { message: msgObj.rawBody, done });
}
pDebug('Command not found >', msgObj.cmd);
done();
return;
function done() { messageHandled(); }
function getUserDataFilePath(userEmail) {
var filePath = findUserDataFilePath(userEmail);
if (!filePath) {
sendUserinfo({
isLogged: false,
message: `User does not exist. (${userEmail})`,
alerttype: 'alert-warning',
user: { email: userEmail }
});
done();
return;
}
return filePath;
}
function getUserDataFromFile(userEmail) {
if (!validEmail(userEmail)) {
sendUserinfo({
isLogged: false,
message: 'User email not validated.',
alerttype: 'alert-warning',
});
done();
return;
}
var filePath = getUserDataFilePath(userEmail, done);
if (!filePath) { return; }
return DataContext.parse(fs.readFileSync(filePath, { encoding: 'utf8' }), DataContext);
}
function saveUserDataToFile(user, cb_ok) {
var filePath = calcUserDataFilePath(user.email);
if (!filePath) { return; }
if (!fs.existsSync(path.parse(filePath).dir)) {
fs.mkdirSync(
path.parse(filePath).dir,
{ recursive: true }
);
}
fs.writeFile(
filePath,
DataContext.stringify(user, null, 2),
{ encoding: 'utf8' },
(err) => {
if (err) throw err;
if (typeof cb_ok === 'function') { cb_ok(); }
}
);
}
function sendSecurityCode(user, cb_ok) {
user.securityCode = generateSecurityCode();
sendSecurityCodeEmail(user, function (err) {
if (err) {
if (userConfigSets.isDebug) {
pDebug(user.email, 'securityCode', user.securityCode);
}
else {
delete user.securityCode;
sendUserinfo({ isLogged: false, message: 'Sending email failed.', alerttype: 'alert-warning' });
done();
return;
}
}
sendUserinfo({
isLogged: false,
message: 'Please check your email for the security code.',
alerttype: 'alert-info',
user: { securityCode: true, email: user.email }
});
if (typeof cb_ok === 'function') { cb_ok(); }
});
function sendSecurityCodeEmail(user, callback) {
var strText = userConfigSets?.fileSecurityCodeText && fs.existsSync(userConfigSets.fileSecurityCodeText) ? fs.readFileSync(fileText).toString() : "",
strHtml = userConfigSets?.fileSecurityCodeHtml && fs.existsSync(userConfigSets.fileSecurityCodeHtml) ? fs.readFileSync(fileHtml).toString() : "",
domain = nodemailerOptions.domain_account;
if (!domain || domain.includes('localhost')) {
domain = require('node:os').hostname();
}
var data = {
domain: domain.replace(/^\w/, function (c) { return c.toUpperCase(); }),
email: maskEmail(user.email),
securitycode: user.securityCode
};
Object.keys(data).forEach(function (key) {
strText = strText.split('[' + key + ']').join(data[key]);
strHtml = strHtml.split('[' + key + ']').join(htmlEncode(data[key]));
});
sendMail({
from: nodemailerOptions.auth.user, // sender address
to: user.email,// + ", conextra.ee@outlook.com", // list of receivers
subject: domain + " account security code", // Subject line
text: strText, // plain text body
html: strHtml, // html body
},
callback
);
function maskEmail(email) {
email = (email + '').split('@');
email[0] = email[0].length > 3
? email[0].substr(0, 2) + '**********'
: email[0].substr(0, 1) + '**********';
return email.join('@');
}
}
}
}
}
/**
* @typedef {Object} Userinfo
* @property {boolan} isLogged
* @property {string} message
* @property {string} alerttype "alert-error" | "alert-success" | "alert-info" | "alert-warning"
* @property {object} user
*
* @param {Userinfo} options
* @returns {void}
*
*/
function sendUserinfo({ isLogged = false, message = '', alerttype = 'alert-success', user = {} } = {}) {
sendData('$userinfo:' + JSON.stringify({
isLogged,
message,
alerttype,
email: user.email || '',
name: user.name || '',
organizations: user.organizations || [],
roles: user.roles || [],
securityCode: Boolean(user.securityCode),
resetPassword: Boolean(user.resetPassword)
}));
}
// Debugging
function pDebug(...args) { if (userConfigSets.isDebug) { console.log(`[ DEBUG ] 'ws-user' protocol:`, protocol, ...args); } }
function pError(...args) { if (userConfigSets.isDebug) { console.error(`[ ERROR ] 'ws-user' protocol:`, protocol, ...args); } }
}
//*** Helpers ***/
function encryptPassword(password) {
return sha512('us' + password);
function sha512(data) { return crypto.createHash("sha512").update(data, "binary").digest("hex"); }
}
function unmasking(str, key) {
str = Buffer.from(str, 'base64');
key = Buffer.from(key.substring(0, 4), 'utf-8');
for (var i = 0, n = str.length; i < n; i++) {
str[i] = str[i] ^ key[i & 3];
}
return str.toString('utf8');
}
function generateSecurityCode() {
function getRandomInt(max) { return Math.floor(Math.random() * Math.floor(max)); }
var key = Array.from("0123456789");
var code = '';
while (code.length < 7) {
code += key.splice(getRandomInt(key.length - 1), 1).join('');
}
return code;
}
function calcFilePath(filePath) {
return path.resolve(path.parse(process.argv[1]).dir.split("node_modules").shift(), filePath);
}
function calcUserDataFilePath(userEmail) {
return calcFilePath(path.join(userConfigSets.pathToUsersDir, userEmail, userEmail + '.json'));
}
function findUserDataFilePath(userEmail) {
var filePath = calcUserDataFilePath(userEmail);
if (!fs.existsSync(filePath)) { return; }
return filePath;
}
function addUser(email, password, username) {
if (!validEmail(email)) { return `Error: Email not validated.`; }
if (!password) { return `Error: Password not validated.`; }
if (password.length < 6) { return `Error: Password too short.`; }
var filePath = findUserDataFilePath(email);
if (filePath) { return `Error: Email in use.`; }
if (!username) {
// If username is not provided, use the part before the '@' in the email address
username = email.split('@')[0];
// Replace all '.' with ' ' in username
username = username.replace(/\./g, ' ');
// Remove all special characters from username
username = username.trim().replace(/[^a-zA-Z0-9_\s]/g, '');
// Capitalize the first letters
username = username.replace(/^\w/, function (c) { return c.toUpperCase(); });
}
password = encryptPassword(password);
var user = {
email,
password,
name: username,
organizations: [],
roles: ['user'],
//securityCode: generateSecurityCode()
};
// Create user directory if it doesn't exist
filePath = calcUserDataFilePath(email);
if (!fs.existsSync(path.parse(filePath).dir)) {
fs.mkdirSync(
path.parse(filePath).dir,
{ recursive: true }
);
}
// Write user data to file
fs.writeFileSync(filePath, DataContext.stringify(user, null, 2), { encoding: 'utf8' });
return 'User created successfully.';
}
function validEmail(mail) { return /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()\.,;\s@\"]+\.{0,1})+([^<>()\.,;:\s@\"]{2,}|[\d\.]+))$/.test(mail); }
function htmlEncode(input) {
var strOutput;
if (input && input.constructor === String) {
strOutput = "";
for (var i = 0; i < input.length; i++) {
var charCode = input.charCodeAt(i);
switch (charCode) {
case 9: strOutput += " "; break; // \t
case 10: strOutput += input.charCodeAt(i - 1) === 13 ? "" : "<br/>"; break; // \n
case 13: strOutput += "<br/>"; break; // \r
case 32: strOutput += " "; break; // " "
case 34: strOutput += """; break; // "
case 38: strOutput += "&"; break; // &
case 39: strOutput += "'"; break; // '
case 60: strOutput += "<"; break; // <
case 62: strOutput += ">"; break; // >
case 338: strOutput += "Œ"; break; // ?
case 339: strOutput += "œ"; break; // ?
case 352: strOutput += "Š"; break; // Š
case 353: strOutput += "š"; break; // š
case 376: strOutput += "Ÿ"; break; // ?
case 402: strOutput += "ƒ"; break; // ?
case 710: strOutput += "ˆ"; break; // ?
case 732: strOutput += "˜"; break; // ?
case 8194: strOutput += " "; break; //
case 8195: strOutput += " "; break; //
case 8201: strOutput += " "; break; //
case 8204: strOutput += "‌"; break; //
case 8205: strOutput += "‍"; break; //
case 8206: strOutput += "‎"; break; //
case 8207: strOutput += "‏"; break; //
case 8211: strOutput += "–"; break; // –
case 8212: strOutput += "—"; break; // —
case 8216: strOutput += "‘"; break; // ‘
case 8217: strOutput += "’"; break; // ’
case 8218: strOutput += "‚"; break; // ‚
case 8220: strOutput += "“"; break; // “
case 8221: strOutput += "”"; break; // ”
case 8222: strOutput += "„"; break; // „
case 8224: strOutput += "†"; break; // †
case 8225: strOutput += "‡"; break; // ‡
case 8226: strOutput += "•"; break; // •
case 8249: strOutput += "‹"; break; // ‹
case 8250: strOutput += "›"; break; // ›
//case : strOutput += "&;"; break; //
default:
if (charCode < 128)
strOutput += input.charAt(i);
else
strOutput += "&#" + charCode + ";";
break;
}
}
return strOutput;
}
return input;
};
function htmlDecode(input) {
var strOutput;
if (input && input.constructor === String) {
strOutput = "";
/** @const */
var entities = { "Aacute": [193], "aacute": [225], "Abreve": [258], "abreve": [259], "ac": [8766], "acd": [8767], "acE": [8766, 819], "Acirc": [194], "acirc": [226], "acute": [180], "Acy": [1040], "acy": [1072], "AElig": [198], "aelig": [230], "af": [8289], "Afr": [120068], "afr": [120094], "Agrave": [192], "agrave": [224], "alefsym": [8501], "aleph": [8501], "Alpha": [913], "alpha": [945], "Amacr": [256], "amacr": [257], "amalg": [10815], "AMP": [38], "amp": [38], "And": [10835], "and": [8743], "andand": [10837], "andd": [10844], "andslope": [10840], "andv": [10842], "ang": [8736], "ange": [10660], "angle": [8736], "angmsd": [8737], "angmsdaa": [10664], "angmsdab": [10665], "angmsdac": [10666], "angmsdad": [10667], "angmsdae": [10668], "angmsdaf": [10669], "angmsdag": [10670], "angmsdah": [10671], "angrt": [8735], "angrtvb": [8894], "angrtvbd": [10653], "angsph": [8738], "angst": [197], "angzarr": [9084], "Aogon": [260], "aogon": [261], "Aopf": [120120], "aopf": [120146], "ap": [8776], "apacir": [10863], "apE": [10864], "ape": [8778], "apid": [8779], "apos": [39], "ApplyFunction": [8289], "approx": [8776], "approxeq": [8778], "Aring": [197], "aring": [229], "Ascr": [119964], "ascr": [119990], "Assign": [8788], "ast": [42], "asymp": [8776], "asympeq": [8781], "Atilde": [195], "atilde": [227], "Auml": [196], "auml": [228], "awconint": [8755], "awint": [10769], "backcong": [8780], "backepsilon": [1014], "backprime": [8245], "backsim": [8765], "backsimeq": [8909], "Backslash": [8726], "Barv": [10983], "barvee": [8893], "Barwed": [8966], "barwed": [8965], "barwedge": [8965], "bbrk": [9141], "bbrktbrk": [9142], "bcong": [8780], "Bcy": [1041], "bcy": [1073], "bdquo": [8222], "becaus": [8757], "Because": [8757], "because": [8757], "bemptyv": [10672], "bepsi": [1014], "bernou": [8492], "Bernoullis": [8492], "Beta": [914], "beta": [946], "beth": [8502], "between": [8812], "Bfr": [120069], "bfr": [120095], "bigcap": [8898], "bigcirc": [9711], "bigcup": [8899], "bigodot": [10752], "bigoplus": [10753], "bigotimes": [10754], "bigsqcup": [10758], "bigstar": [9733], "bigtriangledown": [9661], "bigtriangleup": [9651], "biguplus": [10756], "bigvee": [8897], "bigwedge": [8896], "bkarow": [10509], "blacklozenge": [10731], "blacksquare": [9642], "blacktriangle": [9652], "blacktriangledown": [9662], "blacktriangleleft": [9666], "blacktriangleright": [9656], "blank": [9251], "blk12": [9618], "blk14": [9617], "blk34": [9619], "block": [9608], "bne": [61, 8421], "bnequiv": [8801, 8421], "bNot": [10989], "bnot": [8976], "Bopf": [120121], "bopf": [120147], "bot": [8869], "bottom": [8869], "bowtie": [8904], "boxbox": [10697], "boxDL": [9559], "boxDl": [9558], "boxdL": [9557], "boxdl": [9488], "boxDR": [9556], "boxDr": [9555], "boxdR": [9554], "boxdr": [9484], "boxH": [9552], "boxh": [9472], "boxHD": [9574], "boxHd": [9572], "boxhD": [9573], "boxhd": [9516], "boxHU": [9577], "boxHu": [9575], "boxhU": [9576], "boxhu": [9524], "boxminus": [8863], "boxplus": [8862], "boxtimes": [8864], "boxUL": [9565], "boxUl": [9564], "boxuL": [9563], "boxul": [9496], "boxUR": [9562], "boxUr": [9561], "boxuR": [9560], "boxur": [9492], "boxV": [9553], "boxv": [9474], "boxVH": [9580], "boxVh": [9579], "boxvH": [9578], "boxvh": [9532], "boxVL": [9571], "boxVl": [9570], "boxvL": [9569], "boxvl": [9508], "boxVR": [9568], "boxVr": [9567], "boxvR": [9566], "boxvr": [9500], "bprime": [8245], "Breve": [728], "breve": [728], "brvbar": [166], "Bscr": [8492], "bscr": [119991], "bsemi": [8271], "bsim": [8765], "bsime": [8909], "bsol": [92], "bsolb": [10693], "bsolhsub": [10184], "bull": [8226], "bullet": [8226], "bump": [8782], "bumpE": [10926], "bumpe": [8783], "Bumpeq": [8782], "bumpeq": [8783], "Cacute": [262], "cacute": [263], "Cap": [8914], "cap": [8745], "capand": [10820], "capbrcup": [10825], "capcap": [10827], "capcup": [10823], "capdot": [10816], "CapitalDifferentialD": [8517], "caps": [8745, 65024], "caret": [8257], "caron": [711], "Cayleys": [8493], "ccaps": [10829], "Ccaron": [268], "ccaron": [269], "Ccedil": [199], "ccedil": [231], "Ccirc": [264], "ccirc": [265], "Cconint": [8752], "ccups": [10828], "ccupssm": [10832], "Cdot": [266], "cdot": [267], "cedil": [184], "Cedilla": [184], "cemptyv": [10674], "cent": [162], "CenterDot": [183], "centerdot": [183], "Cfr": [8493], "cfr": [120096], "CHcy": [1063], "chcy": [1095], "check": [10003], "checkmark": [10003], "Chi": [935], "chi": [967], "cir": [9675], "circ": [710], "circeq": [8791], "circlearrowleft": [8634], "circlearrowright": [8635], "circledast": [8859], "circledcirc": [8858], "circleddash": [8861], "CircleDot": [8857], "circledR": [174], "circledS": [9416], "CircleMinus": [8854], "CirclePlus": [8853], "CircleTimes": [8855], "cirE": [10691], "cire": [8791], "cirfnint": [10768], "cirmid": [10991], "cirscir": [10690], "ClockwiseContourIntegral": [8754], "CloseCurlyDoubleQuote": [8221], "CloseCurlyQuote": [8217], "clubs": [9827], "clubsuit": [9827], "Colon": [8759], "colon": [58], "Colone": [10868], "colone": [8788], "coloneq": [8788], "comma": [44], "commat": [64], "comp": [8705], "compfn": [8728], "complement": [8705], "complexes": [8450], "cong": [8773], "congdot": [10861], "Congruent": [8801], "Conint": [8751], "conint": [8750], "ContourIntegral": [8750], "Copf": [8450], "copf": [120148], "coprod": [8720], "Coproduct": [8720], "COPY": [169], "copy": [169], "copysr": [8471], "CounterClockwiseContourIntegral": [8755], "crarr": [8629], "Cross": [10799], "cross": [10007], "Cscr": [119966], "cscr": [119992], "csub": [10959], "csube": [10961], "csup": [10960], "csupe": [10962], "ctdot": [8943], "cudarrl": [10552], "cudarrr": [10549], "cuepr": [8926], "cuesc": [8927], "cularr": [8630], "cularrp": [10557], "Cup": [8915], "cup": [8746], "cupbrcap": [10824], "CupCap": [8781], "cupcap": [10822], "cupcup": [10826], "cupdot": [8845], "cupor": [10821], "cups": [8746, 65024], "curarr": [8631], "curarrm": [10556], "curlyeqprec": [8926], "curlyeqsucc": [8927], "curlyvee": [8910], "curlywedge": [8911], "curren": [164], "curvearrowleft": [8630], "curvearrowright": [8631], "cuvee": [8910], "cuwed": [8911], "cwconint": [8754], "cwint": [8753], "cylcty": [9005], "Dagger": [8225], "dagger": [8224], "daleth": [8504], "Darr": [8609], "dArr": [8659], "darr": [8595], "dash": [8208], "Dashv": [10980], "dashv": [8867], "dbkarow": [10511], "dblac": [733], "Dcaron": [270], "dcaron": [271], "Dcy": [1044], "dcy": [1076], "DD": [8517], "dd": [8518], "ddagger": [8225], "ddarr": [8650], "DDotrahd": [10513], "ddotseq": [10871], "deg": [176], "Del": [8711], "Delta": [916], "delta": [948], "demptyv": [10673], "dfisht": [10623], "Dfr": [120071], "dfr": [120097], "dHar": [10597], "dharl": [8643], "dharr": [8642], "DiacriticalAcute": [180], "DiacriticalDot": [729], "DiacriticalDoubleAcute": [733], "DiacriticalGrave": [96], "DiacriticalTilde": [732], "diam": [8900], "Diamond": [8900], "diamond": [8900], "diamondsuit": [9830], "diams": [9830], "die": [168], "DifferentialD": [8518], "digamma": [989], "disin": [8946], "div": [247], "divide": [247], "divideontimes": [8903], "divonx": [8903], "DJcy": [1026], "djcy": [1106], "dlcorn": [8990], "dlcrop": [8973], "dollar": [36], "Dopf": [120123], "dopf": [120149], "Dot": [168], "dot": [729], "DotDot": [8412], "doteq": [8784], "doteqdot": [8785], "DotEqual": [8784], "dotminus": [8760], "dotplus": [8724], "dotsquare": [8865], "doublebarwedge": [8966], "DoubleContourIntegral": [8751], "DoubleDot": [168], "DoubleDownArrow": [8659], "DoubleLeftArrow": [8656], "DoubleLeftRightArrow": [8660], "DoubleLeftTee": [10980], "DoubleLongLeftArrow": [10232], "DoubleLongLeftRightArrow": [10234], "DoubleLongRightArrow": [10233], "DoubleRightArrow": [8658], "DoubleRightTee": [8872], "DoubleUpArrow": [8657], "DoubleUpDownArrow": [8661], "DoubleVerticalBar": [8741], "DownArrow": [8595], "Downarrow": [8659], "downarrow": [8595], "DownArrowBar": [10515], "DownArrowUpArrow": [8693], "DownBreve": [785], "downdownarrows": [8650], "downharpoonleft": [8643], "downharpoonright": [8642], "DownLeftRightVector": [10576], "DownLeftTeeVector": [10590], "DownLeftVector": [8637], "DownLeftVectorBar": [10582], "DownRightTeeVector": [10591], "DownRightVector": [8641], "DownRightVectorBar": [10583], "DownTee": [8868], "DownTeeArrow": [8615], "drbkarow": [10512], "drcorn": [8991], "drcrop": [8972], "Dscr": [119967], "dscr": [119993], "DScy": [1029], "dscy": [1109], "dsol": [10742], "Dstrok": [272], "dstrok": [273], "dtdot": [8945], "dtri": [9663], "dtrif": [9662], "duarr": [8693], "duhar": [10607], "dwangle": [10662], "DZcy": [1039], "dzcy": [1119], "dzigrarr": [10239], "Eacute": [201], "eacute": [233], "easter": [10862], "Ecaron": [282], "ecaron": [283], "ecir": [8790], "Ecirc": [202], "ecirc": [234], "ecolon": [8789], "Ecy": [1069], "ecy": [1101], "eDDot": [10871], "Edot": [278], "eDot": [8785], "edot": [279], "ee": [8519], "efDot": [8786], "Efr": [120072], "efr": [120098], "eg": [10906], "Egrave": [200], "egrave": [232], "egs": [10902], "egsdot": [10904], "el": [10905], "Element": [8712], "elinters": [9191], "ell": [8467], "els": [10901], "elsdot": [10903], "Emacr": [274], "emacr": [275], "empty": [8709], "emptyset": [8709], "EmptySmallSquare": [9723], "emptyv": [8709], "EmptyVerySmallSquare": [9643], "emsp": [8195], "emsp13": [8196], "emsp14": [8197], "ENG": [330], "eng": [331], "ensp": [8194], "Eogon": [280], "eogon": [281], "Eopf": [120124], "eopf": [120150], "epar": [8917], "eparsl": [10723], "eplus": [10865], "epsi": [949], "Epsilon": [917], "epsilon": [949], "epsiv": [1013], "eqcirc": [8790], "eqcolon": [8789], "eqsim": [8770], "eqslantgtr": [10902], "eqslantless": [10901], "Equal": [10869], "equals": [61], "EqualTilde": [8770], "equest": [8799], "Equilibrium": [8652], "equiv": [8801], "equivDD": [10872], "eqvparsl": [10725], "erarr": [10609], "erDot": [8787], "Escr": [8496], "escr": [8495], "esdot": [8784], "Esim": [10867], "esim": [8770], "Eta": [919], "eta": [951], "ETH": [208], "eth": [240], "Euml": [203], "euml": [235], "euro": [8364], "excl": [33], "exist": [8707], "Exists": [8707], "expectation": [8496], "ExponentialE": [8519], "exponentiale": [8519], "fallingdotseq": [8786], "Fcy": [1060], "fcy": [1092], "female": [9792], "ffilig": [64259], "fflig": [64256], "ffllig": [64260], "Ffr": [120073], "ffr": [120099], "filig": [64257], "FilledSmallSquare": [9724], "FilledVerySmallSquare": [9642], "fjlig": [102, 106], "flat": [9837], "fllig": [64258], "fltns": [9649], "fnof": [402], "Fopf": [120125], "fopf": [120151], "ForAll": [8704], "forall": [8704], "fork": [8916], "forkv": [10969], "Fouriertrf": [8497], "fpartint": [10765], "frac12": [189], "frac13": [8531], "frac14": [188], "frac15": [8533], "frac16": [8537], "frac18": [8539], "frac23": [8532], "frac25": [8534], "frac34": [190], "frac35": [8535], "frac38": [8540], "frac45": [8536], "frac56": [8538], "frac58": [8541], "frac78": [8542], "frasl": [8260], "frown": [8994], "Fscr": [8497], "fscr": [119995], "gacute": [501], "Gamma": [915], "gamma": [947], "Gammad": [988], "gammad": [989], "gap": [10886], "Gbreve": [286], "gbreve": [287], "Gcedil": [290], "Gcirc": [284], "gcirc": [285], "Gcy": [1043], "gcy": [1075], "Gdot": [288], "gdot": [289], "gE": [8807], "ge": [8805], "gEl": [10892], "gel": [8923], "geq": [8805], "geqq": [8807], "geqslant": [10878], "ges": [10878], "gescc": [10921], "gesdot": [10880], "gesdoto": [10882], "gesdotol": [10884], "gesl": [8923, 65024], "gesles": [10900], "Gfr": [120074], "gfr": [120100], "Gg": [8921], "gg": [8811], "ggg": [8921], "gimel": [8503], "GJcy": [1027], "gjcy": [1107], "gl": [8823], "gla": [10917], "glE": [10898], "glj": [10916], "gnap": [10890], "gnapprox": [10890], "gnE": [8809], "gne": [10888], "gneq": [10888], "gneqq": [8809], "gnsim": [8935], "Gopf": [120126], "gopf": [120152], "grave": [96], "GreaterEqual": [8805], "GreaterEqualLess": [8923], "GreaterFullEqual": [8807], "GreaterGreater": [10914], "GreaterLess": [8823], "GreaterSlantEqual": [10878], "GreaterTilde": [8819], "Gscr": [119970], "gscr": [8458], "gsim": [8819], "gsime": [10894], "gsiml": [10896], "GT": [62], "Gt": [8811], "gt": [62], "gtcc": [10919], "gtcir": [10874], "gtdot": [8919], "gtlPar": [10645], "gtquest": [10876], "gtrapprox": [10886], "gtrarr": [10616], "gtrdot": [8919], "gtreqless": [8923], "gtreqqless": [10892], "gtrless": [8823], "gtrsim": [8819], "gvertneqq": [8809, 65024], "gvnE": [8809, 65024], "Hacek": [711], "hairsp": [8202], "half": [189], "hamilt": [8459], "HARDcy": [1066], "hardcy": [1098], "hArr": [8660], "harr": [8596], "harrcir": [10568], "harrw": [8621], "Hat": [94], "hbar": [8463], "Hcirc": [292], "hcirc": [293], "hearts": [9829], "heartsuit": [9829], "hellip": [8230], "hercon": [8889], "Hfr": [8460], "hfr": [120101], "HilbertSpace": [8459], "hksearow": [10533], "hkswarow": [10534], "hoarr": [8703], "homtht": [8763], "hookleftarrow": [8617], "hookrightarrow": [8618], "Hopf": [8461], "hopf": [120153], "horbar": [8213], "HorizontalLine": [9472], "Hscr": [8459], "hscr": [119997], "hslash": [8463], "Hstrok": [294], "hstrok": [295], "HumpDownHump": [8782]