Improved login page customization, improved Chinese.

This commit is contained in:
Ylian Saint-Hilaire 2020-09-03 02:19:17 -07:00
parent df4c302470
commit 64efc47129
8 changed files with 332 additions and 297 deletions

View File

@ -15,7 +15,7 @@
<p>服务器上的使用者[[[USERNAME]]] <a href="[[[SERVERURL]]]">[[[SERVERNAME]]]</a> 正在要求您安装软件以启动远程控制会话。</p> <p>服务器上的使用者[[[USERNAME]]] <a href="[[[SERVERURL]]]">[[[SERVERNAME]]]</a> 正在要求您安装软件以启动远程控制会话。</p>
<area-msg> <area-msg>
<p> <p>
息: <b notrans="1">[[[MSG]]]</b> 息: <b notrans="1">[[[MSG]]]</b>
</p> </p>
</area-msg> </area-msg>
<area-windows> <area-windows>
@ -34,7 +34,7 @@
</area-linux> </area-linux>
<area-link> <area-link>
<p> <p>
要安裝軟體 <a href="[[[SERVERURL]]][[[LINKURL]]]">点击这里</a> 并按照说明进行操作。 要安装软件 <a href="[[[SERVERURL]]][[[LINKURL]]]">点击这里</a> 并按照说明进行操作。
</p> </p>
</area-link> </area-link>
<p>如果您没有发起此请求,请不理此邮件。</p> <p>如果您没有发起此请求,请不理此邮件。</p>

View File

@ -5,7 +5,7 @@
服务器[[[SERVERNAME]]][[[SERVERURL]]]/)上的用户[[[USERNAME]]]请求您安装软件以启动远程控制。 服务器[[[SERVERNAME]]][[[SERVERURL]]]/)上的用户[[[USERNAME]]]请求您安装软件以启动远程控制。
~<area-msg> ~<area-msg>
~ ~
息:[[[MSG]]] 息:[[[MSG]]]
~ ~
~</area-msg> ~</area-msg>
~<area-windows> ~<area-windows>

View File

@ -141,10 +141,11 @@
"items": { "items": {
"type": "object", "type": "object",
"properties": { "properties": {
"siteStyle": { "type": "integer", "default": 1, "description": "Valid numbers are 1 and 2, changes the style of the login page and some secondary pages." } "siteStyle": { "type": "integer", "default": 1, "description": "Valid numbers are 1 and 2, changes the style of the login page and some secondary pages." },
"title": { "type": "string" }, "title": { "type": "string", "default": "MeshCentral", "description": "The title of this web site. All web pages will have this title." },
"title2": { "type": "string" }, "title2": { "type": "string", "default": null, "description": "Secondary title text that is placed on the upper right on the title on many web pages." },
"titlePicture": { "type": "string" }, "titlePicture": { "type": "string", "default": null, "description": "Web site .png logo file that is 450x66 in size placed in meshcentral-data that is used on the top of many pages." },
"loginPicture": { "type": "string", "default": null, "description": "Web site .png logo file placed in meshcentral-data that used on the login page when sitestyle is 2." },
"userQuota": { "type": "integer" }, "userQuota": { "type": "integer" },
"meshQuota": { "type": "integer" }, "meshQuota": { "type": "integer" },
"minify": { "type": "boolean", "default": false, "description": "When enabled, the server will send reduced sided web pages." }, "minify": { "type": "boolean", "default": false, "description": "When enabled, the server will send reduced sided web pages." },
@ -154,7 +155,7 @@
"newAccountEmailDomains": { "type": "array", "uniqueItems": true, "items": { "type": "string" } }, "newAccountEmailDomains": { "type": "array", "uniqueItems": true, "items": { "type": "string" } },
"newAccountsRights": { "type": "array", "uniqueItems": true, "items": { "type": "string" } }, "newAccountsRights": { "type": "array", "uniqueItems": true, "items": { "type": "string" } },
"welcomeText": { "type": "string", "description": "Text that will be shown on the login screen." }, "welcomeText": { "type": "string", "description": "Text that will be shown on the login screen." },
"welcomePicture": { "type": "string", "description": "Name of the JPG file that will be shown on the login screen. Put this file in the meshcentral-data folder and place the name here." }, "welcomePicture": { "type": "string", "description": "Name of the PNG or JPEG file that will be shown on the login screen. Put this file in the meshcentral-data folder and place the file name here." },
"hide": { "type": "integer" }, "hide": { "type": "integer" },
"footer": { "type": "string" }, "footer": { "type": "string" },
"certUrl": { "type": "string", "format": "uri", "description": "https url when to get the TLS certificate that MeshAgent's will see when connecting to this server. This setting is used when a reverse proxy like NGINX is used in front of MeshCentral." }, "certUrl": { "type": "string", "format": "uri", "description": "https url when to get the TLS certificate that MeshAgent's will see when connecting to this server. This setting is used when a reverse proxy like NGINX is used in front of MeshCentral." },

View File

@ -118,6 +118,7 @@
"title": "MyServer", "title": "MyServer",
"title2": "Servername", "title2": "Servername",
"_titlePicture": "title-sample.png", "_titlePicture": "title-sample.png",
"_loginPicture": "title-sample.png",
"_userQuota": 1048576, "_userQuota": 1048576,
"_meshQuota": 248576, "_meshQuota": 248576,
"minify": true, "minify": true,

View File

@ -440,7 +440,7 @@ function startEx(argv) {
function totext(source, target, lang) { function totext(source, target, lang) {
// Load the source language file // Load the source language file
var sourceLangFileData = null; var sourceLangFileData = null;
try { sourceLangFileData = JSON.parse(fs.readFileSync(source)); } catch (ex) { } try { sourceLangFileData = JSON.parse(fs.readFileSync(source)); } catch (ex) { console.log(ex); }
if ((sourceLangFileData == null) || (sourceLangFileData.strings == null)) { log("Invalid source language file."); process.exit(); return; } if ((sourceLangFileData == null) || (sourceLangFileData.strings == null)) { log("Invalid source language file."); process.exit(); return; }
log('Writing ' + lang + '...'); log('Writing ' + lang + '...');
@ -485,7 +485,7 @@ function totext(source, target, lang) {
function fromtext(source, target, lang) { function fromtext(source, target, lang) {
// Load the source language file // Load the source language file
var sourceLangFileData = null; var sourceLangFileData = null;
try { sourceLangFileData = JSON.parse(fs.readFileSync(source)); } catch (ex) { } try { sourceLangFileData = JSON.parse(fs.readFileSync(source)); } catch (ex) { console.log(ex); }
if ((sourceLangFileData == null) || (sourceLangFileData.strings == null)) { log("Invalid source language file."); process.exit(); return; } if ((sourceLangFileData == null) || (sourceLangFileData.strings == null)) { log("Invalid source language file."); process.exit(); return; }
log('Updating ' + lang + '...'); log('Updating ' + lang + '...');
@ -514,12 +514,12 @@ function fromtext(source, target, lang) {
function merge(source, target, lang) { function merge(source, target, lang) {
// Load the source language file // Load the source language file
var sourceLangFileData = null; var sourceLangFileData = null;
try { sourceLangFileData = JSON.parse(fs.readFileSync(source)); } catch (ex) { } try { sourceLangFileData = JSON.parse(fs.readFileSync(source)); } catch (ex) { console.log(ex); }
if ((sourceLangFileData == null) || (sourceLangFileData.strings == null)) { log("Invalid source language file."); process.exit(); return; } if ((sourceLangFileData == null) || (sourceLangFileData.strings == null)) { log("Invalid source language file."); process.exit(); return; }
// Load the target language file // Load the target language file
var targetLangFileData = null; var targetLangFileData = null;
try { targetLangFileData = JSON.parse(fs.readFileSync(target)); } catch (ex) { } try { targetLangFileData = JSON.parse(fs.readFileSync(target)); } catch (ex) { console.log(ex); }
if ((targetLangFileData == null) || (targetLangFileData.strings == null)) { log("Invalid target language file."); process.exit(); return; } if ((targetLangFileData == null) || (targetLangFileData.strings == null)) { log("Invalid target language file."); process.exit(); return; }
log('Merging ' + lang + '...'); log('Merging ' + lang + '...');

File diff suppressed because it is too large Load Diff

View File

@ -25,11 +25,13 @@
</style> </style>
</head> </head>
<body id="body" onload="if (typeof(startup) !== 'undefined') startup();" class="arg_hide login"> <body id="body" onload="if (typeof(startup) !== 'undefined') startup();" class="arg_hide login">
<img style="position:absolute;left:0;bottom:0;z-index:-1;height:60%;opacity:0.1" src="images/login/back.png" /> <img style="position:absolute;left:0;bottom:0;z-index:-1;height:60%;opacity:0.1" src="welcome.png" />
<table id="centralTable" class="container" style="height:100%;z-index:1"> <table id="centralTable" class="container" style="height:100%;z-index:1">
<tr> <tr>
<td id="logincell"> <td id="logincell">
<div style="font-size:46px;font-family:Arial,Helvetica,sans-serif;font-weight:bold;padding-bottom:10px;color:#c8c8c8;text-shadow: 2px 2px 2px #000;">{{{title1}}}</div> {{{titlehtml}}}
<img id="loginPicture" />
<div style="font-size:46px;font-family:Arial,Helvetica,sans-serif;font-weight:bold;padding-bottom:10px;color:#c8c8c8;text-shadow: 2px 2px 2px #000;">{{{title1}}}<sup style="font-size:20px"> {{{title2}}}</sup></div>
<div id=loginpanel style="display:none;box-shadow:1px 1px 4px #000"> <div id=loginpanel style="display:none;box-shadow:1px 1px 4px #000">
<form method=post> <form method=post>
<input type=hidden name=action value=login /> <input type=hidden name=action value=login />
@ -37,22 +39,22 @@
<table style="width:100%"> <table style="width:100%">
<tr> <tr>
<td> <td>
<input id=username title="Username" style="width:250px;border:0;border-radius:8px;padding:8px;background-color:#FFF8CC" autocomplete="username" placeholder="Username" type=text maxlength=64 name=username onchange=validateLogin(1) onkeyup=validateLogin(1,event) /> <input id=username title="Username" style="box-sizing:border-box;width:280px;border:0;border-radius:4px;padding:8px;background-color:#FFF8CC" autocomplete="username" placeholder="Username" type=text maxlength=64 name=username onchange=validateLogin(1) onkeyup=validateLogin(1,event) />
</td> </td>
</tr> </tr>
<tr> <tr>
<td> <td>
<input id=password title="Password" style="width:250px;border:0;border-radius:8px;padding:8px;background-color:#FFF8CC" autocomplete="current-password" placeholder="Password" type=password maxlength=256 name=password onchange=validateLogin(2) onkeyup=validateLogin(2,event) /> <input id=password title="Password" style="box-sizing:border-box;width:280px;border:0;border-radius:4px;padding:8px;background-color:#FFF8CC" autocomplete="current-password" placeholder="Password" type=password maxlength=256 name=password onchange=validateLogin(2) onkeyup=validateLogin(2,event) />
</td> </td>
</tr> </tr>
<tr> <tr>
<td> <td>
<div id=showPassHintLink title="Password Hint" style="width:250px;border:0;border-radius:8px;padding:8px;background-color:#FFF8CC;display:none"><a onclick="return showPassHint(event);" href="#" style="cursor:pointer">Show Hint</a></div> <div id=showPassHintLink title="Password Hint" style="box-sizing:border-box;width:280px;border:0;border-radius:4px;padding:8px;background-color:#FFF8CC;display:none"><a onclick="return showPassHint(event);" href="#" style="cursor:pointer">Show Hint</a></div>
</td> </td>
</tr> </tr>
<tr> <tr>
<td> <td>
<input id=loginButton style="width:250px;border:0;border-radius:4px;padding:6px" type=submit value="Log In" disabled="disabled" /> <input id=loginButton style="box-sizing:border-box;width:280px;border:0;border-radius:4px;padding:6px" type=submit value="Log In" disabled="disabled" />
</td> </td>
</tr> </tr>
</table> </table>
@ -132,12 +134,12 @@
<table style="width:100%;margin-top:4px;margin-bottom:4px"> <table style="width:100%;margin-top:4px;margin-bottom:4px">
<tr> <tr>
<td> <td>
<input id=remail title="Email" style="width:250px;border:0;border-radius:8px;padding:8px;background-color:#FFF8CC" autocomplete="username" placeholder="Email" type=text maxlength=256 name=email onchange=validateReset() onkeyup=validateReset(event) /> <input id=remail title="Email" style="box-sizing:border-box;width:280px;border:0;border-radius:4px;padding:8px;background-color:#FFF8CC" autocomplete="username" placeholder="Email" type=text maxlength=256 name=email onchange=validateReset() onkeyup=validateReset(event) />
</td> </td>
</tr> </tr>
<tr> <tr>
<td> <td>
<input id=eresetButton style="width:250px;border:0;border-radius:4px;padding:6px" type=submit value="Reset Account" disabled="disabled" /> <input id=eresetButton style="box-sizing:border-box;width:280px;border:0;border-radius:4px;padding:6px" type=submit value="Reset Account" disabled="disabled" />
</td> </td>
</tr> </tr>
</table> </table>
@ -153,14 +155,14 @@
<table style="width:100%"> <table style="width:100%">
<tr> <tr>
<td> <td>
<input id=tokenInput autocomplete="one-time-code" title="Token" style="width:250px;border:0;border-radius:8px;padding:8px;background-color:#FFF8CC" placeholder="Token" type=text maxlength=50 name=token onchange=checkToken(event) onpaste=resetCheckToken(event) onkeyup=checkToken(event) onkeydown=checkToken(event) /><br /> <input id=tokenInput autocomplete="one-time-code" title="Token" style="box-sizing:border-box;width:280px;border:0;border-radius:4px;padding:8px;background-color:#FFF8CC" placeholder="Token" type=text maxlength=50 name=token onchange=checkToken(event) onpaste=resetCheckToken(event) onkeyup=checkToken(event) onkeydown=checkToken(event) /><br />
<input id=hwtokenInput type=text name=hwtoken style="display:none" /> <input id=hwtokenInput type=text name=hwtoken style="display:none" />
</td> </td>
</tr> </tr>
<tr> <tr>
<td> <td>
<div> <div>
<input id=tokenOkButton style="width:250px;border:0;border-radius:4px;padding:6px" type=submit value="Log In" disabled="disabled" /> <input id=tokenOkButton style="box-sizing:border-box;width:280px;border:0;border-radius:4px;padding:6px" type=submit value="Log In" disabled="disabled" />
</div> </div>
</td> </td>
</tr> </tr>
@ -265,8 +267,8 @@
<tr style="height:20px"> <tr style="height:20px">
<td> <td>
<div> <div>
<div id="flink" style="margin-left:4px">{{{footer}}}</div>
<div id="flink" style="float:right;margin-right:4px">{{{rootCertLink}}}&nbsp;<a href=terms>Terms &amp; Privacy</a></div> <div id="flink" style="float:right;margin-right:4px">{{{rootCertLink}}}&nbsp;<a href=terms>Terms &amp; Privacy</a></div>
<div id="flink" style="margin-left:4px">{{{footer}}}</div>
</div> </div>
</td> </td>
</tr> </tr>
@ -311,6 +313,8 @@
var authStrategies = '{{{authStrategies}}}'.split(','); var authStrategies = '{{{authStrategies}}}'.split(',');
function startup() { function startup() {
if (decodeURIComponent('{{{loginpicture}}}') == 'true') { Q('loginPicture').src = "loginlogo.png"; }
// Display the right server message // Display the right server message
var i; var i;
var messageid = parseInt('{{{messageid}}}'); var messageid = parseInt('{{{messageid}}}');

View File

@ -2459,7 +2459,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
if (domain.customui != null) { customui = encodeURIComponent(JSON.stringify(domain.customui)); } if (domain.customui != null) { customui = encodeURIComponent(JSON.stringify(domain.customui)); }
// Render the login page // Render the login page
render(req, res, getRenderPage((domain.sitestyle == 2)?'login2':'login', req, domain), getRenderArgs({ loginmode: loginmode, rootCertLink: getRootCertLink(), newAccount: newAccountsAllowed, newAccountPass: (((domain.newaccountspass == null) || (domain.newaccountspass == '')) ? 0 : 1), serverDnsName: obj.getWebServerName(domain), serverPublicPort: httpsPort, emailcheck: emailcheck, features: features, sessiontime: args.sessiontime, passRequirements: passRequirements, customui: customui, footer: (domain.footer == null) ? '' : domain.footer, hkey: encodeURIComponent(hardwareKeyChallenge).replace(/'/g, '%27'), messageid: msgid, passhint: passhint, welcometext: domain.welcometext ? encodeURIComponent(domain.welcometext).split('\'').join('\\\'') : null, hwstate: hwstate, otpemail: otpemail, otpsms: otpsms, twoFactorCookieDays: twoFactorCookieDays, authStrategies: authStrategies.join(',') }, req, domain)); render(req, res, getRenderPage((domain.sitestyle == 2) ? 'login2' : 'login', req, domain), getRenderArgs({ loginmode: loginmode, rootCertLink: getRootCertLink(), newAccount: newAccountsAllowed, newAccountPass: (((domain.newaccountspass == null) || (domain.newaccountspass == '')) ? 0 : 1), serverDnsName: obj.getWebServerName(domain), serverPublicPort: httpsPort, emailcheck: emailcheck, features: features, sessiontime: args.sessiontime, passRequirements: passRequirements, customui: customui, footer: (domain.footer == null) ? '' : domain.footer, hkey: encodeURIComponent(hardwareKeyChallenge).replace(/'/g, '%27'), messageid: msgid, passhint: passhint, welcometext: domain.welcometext ? encodeURIComponent(domain.welcometext).split('\'').join('\\\'') : null, hwstate: hwstate, otpemail: otpemail, otpsms: otpsms, twoFactorCookieDays: twoFactorCookieDays, authStrategies: authStrategies.join(','), loginpicture: (typeof domain.loginpicture == 'string') }, req, domain));
} }
// Handle a post request on the root // Handle a post request on the root
@ -2822,7 +2822,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
if (domain.titlepicture) { if (domain.titlepicture) {
if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.titlepicture] != null)) { if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.titlepicture] != null)) {
// Use the logo in the database // Use the logo in the database
res.set({ 'Content-Type': 'image/jpeg' }); res.set({ 'Content-Type': domain.titlepicture.toLowerCase().endsWith('.png')?'image/png':'image/jpeg' });
res.send(parent.configurationFiles[domain.titlepicture]); res.send(parent.configurationFiles[domain.titlepicture]);
return; return;
} else { } else {
@ -2843,6 +2843,25 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
} }
} }
// Handle login logo request
function handleLoginLogoRequest(req, res) {
const domain = checkUserIpAddress(req, res);
if (domain == null) { return; }
//res.set({ 'Cache-Control': 'max-age=86400' }); // 1 day
if (domain.loginpicture) {
if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.loginpicture] != null)) {
// Use the logo in the database
res.set({ 'Content-Type': domain.loginpicture.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg' });
res.send(parent.configurationFiles[domain.loginpicture]);
return;
} else {
// Use the logo on file
try { res.sendFile(obj.path.join(obj.parent.datapath, domain.loginpicture)); return; } catch (ex) { res.sendStatus(404); }
}
}
}
// Handle translation request // Handle translation request
function handleTranslationsRequest(req, res) { function handleTranslationsRequest(req, res) {
const domain = checkUserIpAddress(req, res); const domain = checkUserIpAddress(req, res);
@ -2916,7 +2935,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
if (domain.welcomepicture) { if (domain.welcomepicture) {
if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.welcomepicture] != null)) { if ((parent.configurationFiles != null) && (parent.configurationFiles[domain.welcomepicture] != null)) {
// Use the welcome image in the database // Use the welcome image in the database
res.set({ 'Content-Type': 'image/jpeg' }); res.set({ 'Content-Type': domain.welcomepicture.toLowerCase().endsWith('.png')?'image/png':'image/jpeg' });
res.send(parent.configurationFiles[domain.welcomepicture]); res.send(parent.configurationFiles[domain.welcomepicture]);
return; return;
} }
@ -2925,29 +2944,31 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
try { res.sendFile(obj.path.join(obj.parent.datapath, domain.welcomepicture)); return; } catch (ex) { } try { res.sendFile(obj.path.join(obj.parent.datapath, domain.welcomepicture)); return; } catch (ex) { }
} }
var imagefile = 'images/mainwelcome.jpg';
if (domain.sitestyle == 2) { imagefile = 'images/login/back.png'; }
if (domain.webpublicpath != null) { if (domain.webpublicpath != null) {
obj.fs.exists(obj.path.join(domain.webpublicpath, 'images/mainwelcome.jpg'), function (exists) { obj.fs.exists(obj.path.join(domain.webpublicpath, imagefile), function (exists) {
if (exists) { if (exists) {
// Use the domain logo picture // Use the domain logo picture
try { res.sendFile(obj.path.join(domain.webpublicpath, 'images/mainwelcome.jpg')); } catch (ex) { res.sendStatus(404); } try { res.sendFile(obj.path.join(domain.webpublicpath, imagefile)); } catch (ex) { res.sendStatus(404); }
} else { } else {
// Use the default logo picture // Use the default logo picture
try { res.sendFile(obj.path.join(obj.parent.webPublicPath, 'images/mainwelcome.jpg')); } catch (ex) { res.sendStatus(404); } try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
} }
}); });
} else if (parent.webPublicOverridePath) { } else if (parent.webPublicOverridePath) {
obj.fs.exists(obj.path.join(obj.parent.webPublicOverridePath, 'images/mainwelcome.jpg'), function (exists) { obj.fs.exists(obj.path.join(obj.parent.webPublicOverridePath, imagefile), function (exists) {
if (exists) { if (exists) {
// Use the override logo picture // Use the override logo picture
try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, 'images/mainwelcome.jpg')); } catch (ex) { res.sendStatus(404); } try { res.sendFile(obj.path.join(obj.parent.webPublicOverridePath, imagefile)); } catch (ex) { res.sendStatus(404); }
} else { } else {
// Use the default logo picture // Use the default logo picture
try { res.sendFile(obj.path.join(obj.parent.webPublicPath, 'images/mainwelcome.jpg')); } catch (ex) { res.sendStatus(404); } try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
} }
}); });
} else { } else {
// Use the default logo picture // Use the default logo picture
try { res.sendFile(obj.path.join(obj.parent.webPublicPath, 'images/mainwelcome.jpg')); } catch (ex) { res.sendStatus(404); } try { res.sendFile(obj.path.join(obj.parent.webPublicPath, imagefile)); } catch (ex) { res.sendStatus(404); }
} }
} }
@ -4707,8 +4728,10 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
obj.app.ws(url + 'devicefile.ashx', function (ws, req) { obj.meshDeviceFileHandler.CreateMeshDeviceFile(obj, ws, null, req, domain); }); obj.app.ws(url + 'devicefile.ashx', function (ws, req) { obj.meshDeviceFileHandler.CreateMeshDeviceFile(obj, ws, null, req, domain); });
obj.app.get(url + 'devicefile.ashx', handleDeviceFile); obj.app.get(url + 'devicefile.ashx', handleDeviceFile);
obj.app.get(url + 'logo.png', handleLogoRequest); obj.app.get(url + 'logo.png', handleLogoRequest);
obj.app.get(url + 'loginlogo.png', handleLoginLogoRequest);
obj.app.post(url + 'translations', handleTranslationsRequest); obj.app.post(url + 'translations', handleTranslationsRequest);
obj.app.get(url + 'welcome.jpg', handleWelcomeImageRequest); obj.app.get(url + 'welcome.jpg', handleWelcomeImageRequest);
obj.app.get(url + 'welcome.png', handleWelcomeImageRequest);
obj.app.get(url + 'recordings.ashx', handleGetRecordings); obj.app.get(url + 'recordings.ashx', handleGetRecordings);
obj.app.get(url + 'player.htm', handlePlayerRequest); obj.app.get(url + 'player.htm', handlePlayerRequest);
obj.app.get(url + 'player', handlePlayerRequest); obj.app.get(url + 'player', handlePlayerRequest);