1287 lines
65 KiB
Handlebars
1287 lines
65 KiB
Handlebars
<!DOCTYPE html> <html dir="ltr" xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta content="text/html; charset=utf-8" http-equiv="Content-Type"> <meta name="viewport" content="user-scalable=1.0,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0"> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="format-detection" content="telephone=no"> <style> body{margin:0;padding:0;border:0;color:black;font-size:13px;font-family:"Trebuchet MS", Arial, Helvetica, sans-serif;background-color:#d3d9d6;}#container{background-color:#fff;margin:0 auto;border-top:0;border-right:1px solid #b7b7b7;border-bottom:0;border-left:1px solid #b7b7b7;padding:0;}#masthead{width:auto;margin:0;padding:0;overflow:auto;text-align:right;background-color:#036;}#column_l{position:relative;float:left;margin:0;padding:0 15px;background-color:#fff;}#footer{clear:both;overflow:auto;width:100%;text-align:center;background-color:#113962;padding-top:5px;padding-bottom:5px;}.style3{text-align:center;color:white;background-color:#808080;font-weight:bold;}#footer{clear:both;overflow:auto;width:100%;text-align:center;background-color:#113962;padding-top:5px;padding-bottom:5px;}#footer a{color:#fff;text-decoration:underline;}#footer a:hover{color:#fff;text-decoration:none;}a{color:#036;text-decoration:underline;}</style> <title>MeshCentral - Login</title> </head> <body onload="if (typeof(startup) !== 'undefined') startup();"> <div id="container" style="max-height:100vh"> <div id="mastheadx"></div> <div id="masthead" style="background:url(logo.png) 0px 0px;background-color:#036;background-repeat:no-repeat;height:66px;width:100%;overflow:hidden"> <div style="float:left;height:66px;color:#c8c8c8;padding-left:20px;padding-top:8px"> <strong><font style="font-size:46px;font-family:Arial,Helvetica,sans-serif">{{{title}}}</font></strong> </div> <div style="float:left;height:66px;color:#c8c8c8;padding-left:5px;padding-top:14px"> <strong><font style="font-size:14px;font-family:Arial,Helvetica,sans-serif">{{{title2}}}</font></strong> </div> </div> <div id="page_content" style="max-height:calc(100vh-108px)"> <div id="topbar" class="noselect style3" style="height:24px;position:relative"> <div title="Toggle full width" style="cursor:pointer;color:white;position:absolute;top:3px;right:6px" onclick="toggleFullScreen(1)">↔</div> </div> <div id="column_l"> <h1>Welcome</h1> <div id="welcomeText" style="display:none">Connect to your home or office devices from anywhere in the world using <a href="http://www.meshcommander.com/meshcentral2">MeshCentral</a>, the real time, open source remote monitoring and management web site. You will need to download and install a management agent on your computers. Once installed, computers will show up in the "My Devices" section of this web site and you will be able to monitor them and take control of them.</div> <table id="centralTable" style="width:100%"> <tr> <td id="welcomeimage" align="right"> <picture> <img alt="" width="359" height="310" src="welcome.jpg"> </picture> </td> <td id="logincell" align="left"> <div id="loginpanel" style="background-color:#979797;border-radius:16px;width:300px;padding:16px;text-align:center;display:none"> <form action="login" method="post"> <div id="message1"> {{{message}}} </div> <div> <b>Log In</b> </div> <table> <tr> <td align="right" width="100">Username:</td> <td><input id="username" type="text" maxlength="64" name="username" onchange="validateLogin(1)" onkeyup="validateLogin(1,event)"></td> </tr> <tr> <td align="right">Password:</td> <td><input id="password" type="password" maxlength="256" name="password" autocomplete="off" onchange="validateLogin(2)" onkeyup="validateLogin(2,event)"></td> </tr> <tr> <td><div id="showPassHintLink" style="display:none"><a onclick="showPassHint()" style="cursor:pointer">Show Hint</a></div></td> <td align="right"><input id="loginButton" type="submit" value="Log In" disabled="disabled"></td> </tr> </table> <div id="hrAccountDiv" style="display:none"><hr></div> <div id="resetAccountDiv" style="display:none;padding:2px"> Forgot username/password? <a onclick="xgo(3)" style="cursor:pointer">Reset account</a>. </div> <div id="newAccountDiv" style="display:none;padding:2px"> Don't have an account? <a onclick="xgo(2)" style="cursor:pointer">Create one</a>. </div> </form> </div> <div id="createpanel" style="position:relative;background-color:#979797;border-radius:16px;width:300px;padding:16px;text-align:center;display:none"> <form action="createaccount" method="post"> <div id="message2"> {{{message}}} </div> <div> <b>Account Creation</b> </div> <div id="passwordPolicyCallout" style="left:-10px;width:100px;display:none;position:absolute;background-color:#FFC;border-radius:5px;padding:5px;box-shadow:0px 0px 15px #666;font-size:10px"></div> <table> <tr> <td id="nuUser" align="right" width="100">Username:</td> <td><input id="ausername" type="text" name="username" onchange="validateCreate(1)" maxlength="64" onkeydown="haltReturn(event)" onkeyup="validateCreate(1,event)"></td> </tr> <tr> <td id="nuEmail" align="right" width="100">Email:</td> <td><input id="aemail" type="text" name="email" onchange="validateCreate(2)" maxlength="256" onkeydown="haltReturn(event)" onkeyup="validateCreate(2,event)"></td> </tr> <tr> <td id="nuPass1" align="right">Password:</td> <td><input id="apassword1" type="password" name="password1" autocomplete="off" maxlength="256" onkeydown="haltReturn(event)" onchange="validateCreate(3,event)" onkeyup="validateCreate(3,event)"></td> </tr> <tr> <td id="nuPass2" align="right">Password:</td> <td><input id="apassword2" type="password" name="password2" autocomplete="off" maxlength="256" onkeydown="haltReturn(event)" onchange="validateCreate(4,event)" onkeyup="validateCreate(4,event)"></td> </tr> <tr id="createPanelHint" style="display:none"> <td id="nuHint" align="right">Password Hint:</td> <td><input id="apasswordhint" type="text" name="apasswordhint" autocomplete="off" maxlength="256" onkeydown="haltReturn(event)" onchange="validateCreate(5,event)" onkeyup="validateCreate(5,event)"></td> </tr> <tr id="newAccountPass" title="Enter the account creation token"> <td id="nuToken" align="right">Creation Token:</td> <td><input id="anewaccountpass" type="password" name="anewaccountpass" autocomplete="off" maxlength="256" onkeydown="haltReturn(event)" onchange="validateCreate(6,event)" onkeyup="validateCreate(6,event)"></td> </tr> <tr> <td colspan="2"> <div style="float:right"><input id="createButton" type="submit" value="Create Account" disabled="disabled"></div> <div id="passWarning" style="padding-top:6px"></div> </td> </tr> </table> <hr><a onclick="xgo(1)" style="cursor:pointer">Back to login</a> </form> </div> <div id="resetpanel" style="background-color:#979797;border-radius:16px;width:300px;padding:16px;text-align:center;display:none"> <form action="resetaccount" method="post"> <div id="message3"> {{{message}}} </div> <div> <b>Account Reset</b> </div> <table> <tr> <td align="right" width="100">Email:</td> <td><input id="remail" type="text" name="email" maxlength="256" onchange="validateReset()" onkeyup="validateReset(event)"></td> </tr> <tr> <td colspan="2"> <div style="float:right"><input id="eresetButton" type="submit" value="Reset Account" disabled="disabled"></div> <div id="passWarning" style="padding-top:6px"></div> </td> </tr> </table> <hr><a onclick="xgo(1)" style="cursor:pointer">Back to login</a> </form> </div> <div id="tokenpanel" style="background-color:#979797;border-radius:16px;width:300px;padding:16px;text-align:center;display:none"> <form action="tokenlogin" method="post" autocomplete="off"> <div id="message4"> {{{message}}} </div> <table> <tr> <td align="right" width="100">Login token:</td> <td> <input id="tokenInput" type="text" name="token" maxlength="50" onchange="checkToken(event)" onpaste="resetCheckToken(event)" onkeyup="checkToken(event)" onkeydown="checkToken(event)"> <input id="hwtokenInput" type="text" name="hwtoken" style="display:none"> </td> </tr> <tr> <td colspan="2"> <div style="float:right"><input id="tokenOkButton" type="submit" value="Login" disabled="disabled"></div> </td> </tr> </table> <hr><a onclick="xgo(1)" style="cursor:pointer">Back to login</a> </form> </div> <div id="resettokenpanel" style="background-color:#979797;border-radius:16px;width:300px;padding:16px;text-align:center;display:none"> <form action="resetaccount" method="post"> <div id="message5"> {{{message}}} </div> <table> <tr> <td align="right" width="100">Login token:</td> <td> <input id="resetTokenInput" type="text" name="token" maxlength="50" onchange="resetCheckToken(event)" onkeyup="resetCheckToken(event)" onkeydown="resetCheckToken(event)"> <input id="resetHwtokenInput" type="text" name="hwtoken" style="display:none"> </td> </tr> <tr> <td colspan="2"> <div style="float:right"><input id="resetTokenOkButton" type="submit" value="Login" disabled="disabled"></div> </td> </tr> </table> <hr><a onclick="xgo(1)" style="cursor:pointer">Back to login</a> </form> </div> <div id="resetpasswordpanel" style="position:relative;background-color:#979797;border-radius:16px;width:300px;padding:16px;text-align:center;display:none"> <form action="resetpassword" method="post"> <div id="message6"> {{{message}}} </div> <div id="rpasswordPolicyCallout" style="left:-10px;width:100px;display:none;position:absolute;background-color:#FFC;border-radius:5px;padding:5px;box-shadow:0px 0px 15px #666;font-size:10px"></div> <table> <tr> <td id="rnuPass1" width="100" align="right">Password:</td> <td><input id="rapassword1" type="password" name="rpassword1" autocomplete="off" maxlength="256" onkeydown="haltReturn(event)" onchange="validatePassReset(3,event)" onkeyup="validatePassReset(3,event)"></td> </tr> <tr> <td id="rnuPass2" align="right">Password:</td> <td><input id="rapassword2" type="password" name="rpassword2" autocomplete="off" maxlength="256" onkeydown="haltReturn(event)" onchange="validatePassReset(4,event)" onkeyup="validatePassReset(4,event)"></td> </tr> <tr id="resetpasswordpanelHint" style="display:none"> <td id="rnuHint" align="right">Password Hint:</td> <td><input id="rapasswordhint" type="text" name="rpasswordhint" autocomplete="off" maxlength="256" onkeydown="haltReturn(event)" onchange="validatePassReset(5,event)" onkeyup="validatePassReset(5,event)"></td> </tr> <tr> <td colspan="2"> <div style="float:right"><input id="resetPassButton" type="submit" value="Reset Password" disabled="disabled"></div> <div id="rpassWarning" style="padding-top:6px"></div> </td> </tr> </table> <hr><a onclick="xgo(1)" style="cursor:pointer">Back to login</a> </form> </div> </td> </tr> </table> <br> </div> <div id="footer"> <table cellpadding="0" cellspacing="10" style="width:100%"> <tr> <td style="text-align:left;color:white"> {{{footer}}} </td> <td style="text-align:right"> {{{rootCertLink}}} <a href="terms">Terms & Privacy</a> </td> </tr> </table> </div> </div> </div> <div id="dialog" style="z-index:1000;background-color:#EEE;box-shadow:0px 0px 15px #666;font-family:Arial,Helvetica,sans-serif;border-radius:5px;position:fixed;top:180px;width:400px;display:none"> <div style="width:100%;background-color:#003366;color:#FFF;border-radius:5px 5px 0 0"> <div id="id_dialogclose" style="float:right;padding:5px;cursor:pointer" onclick="setDialogMode()"><b>X</b></div> <div id="id_dialogtitle" style="padding:5px"></div> <div style="width:100%;margin:6px"></div> </div> <div style="margin-right:16px;margin-left:8px"> <div id="dialog1" style="margin:auto;text-align:center;margin:3px"> <div id="id_dialogMessage" style="padding:10px"></div> </div> <div id="dialog2" style="margin:auto;margin:3px"> <div id="id_dialogOptions"></div> </div> </div> <div id="idx_dlgButtonBar" style="padding:10px;margin-bottom:20px"> <input id="idx_dlgCancelButton" type="button" value="Cancel" style="float:right;width:80px;margin-left:5px" onclick="dialogclose(0)"> <input id="idx_dlgOkButton" type="button" value="OK" style="float:right;width:80px" onclick="dialogclose(1)"> </div> </div> <script>/**
|
|
* @description Set of short commonly used methods for handling HTML elements
|
|
* @author Ylian Saint-Hilaire
|
|
* @version v0.0.1b
|
|
*/
|
|
|
|
// Add startsWith for IE browser
|
|
if (!String.prototype.startsWith) { String.prototype.startsWith = function (str) { return this.lastIndexOf(str, 0) === 0; }; }
|
|
if (!String.prototype.endsWith) { String.prototype.endsWith = function (str) { return this.indexOf(str, this.length - str.length) !== -1; }; }
|
|
|
|
// Quick UI functions, a bit of a replacement for jQuery
|
|
//function Q(x) { if (document.getElementById(x) == null) { console.log('Invalid element: ' + x); } return document.getElementById(x); } // "Q"
|
|
function Q(x) { return document.getElementById(x); } // "Q"
|
|
function QS(x) { try { return Q(x).style; } catch (x) { } } // "Q" style
|
|
function QE(x, y) { try { Q(x).disabled = !y; } catch (x) { } } // "Q" enable
|
|
function QV(x, y) { try { QS(x).display = (y ? '' : 'none'); } catch (x) { } } // "Q" visible
|
|
function QA(x, y) { Q(x).innerHTML += y; } // "Q" append
|
|
function QH(x, y) { Q(x).innerHTML = y; } // "Q" html
|
|
|
|
// Move cursor to end of input box
|
|
function inputBoxFocus(x) { Q(x).focus(); var v = Q(x).value; Q(x).value = ''; Q(x).value = v; }
|
|
|
|
// Binary encoding and decoding functions
|
|
function ReadShort(v, p) { return (v.charCodeAt(p) << 8) + v.charCodeAt(p + 1); }
|
|
function ReadShortX(v, p) { return (v.charCodeAt(p + 1) << 8) + v.charCodeAt(p); }
|
|
function ReadInt(v, p) { return (v.charCodeAt(p) * 0x1000000) + (v.charCodeAt(p + 1) << 16) + (v.charCodeAt(p + 2) << 8) + v.charCodeAt(p + 3); } // We use "*0x1000000" instead of "<<24" because the shift converts the number to signed int32.
|
|
function ReadSInt(v, p) { return (v.charCodeAt(p) << 24) + (v.charCodeAt(p + 1) << 16) + (v.charCodeAt(p + 2) << 8) + v.charCodeAt(p + 3); }
|
|
function ReadIntX(v, p) { return (v.charCodeAt(p + 3) * 0x1000000) + (v.charCodeAt(p + 2) << 16) + (v.charCodeAt(p + 1) << 8) + v.charCodeAt(p); }
|
|
function ShortToStr(v) { return String.fromCharCode((v >> 8) & 0xFF, v & 0xFF); }
|
|
function ShortToStrX(v) { return String.fromCharCode(v & 0xFF, (v >> 8) & 0xFF); }
|
|
function IntToStr(v) { return String.fromCharCode((v >> 24) & 0xFF, (v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF); }
|
|
function IntToStrX(v) { return String.fromCharCode(v & 0xFF, (v >> 8) & 0xFF, (v >> 16) & 0xFF, (v >> 24) & 0xFF); }
|
|
function MakeToArray(v) { if (!v || v == null || typeof v == 'object') return v; return [v]; }
|
|
function SplitArray(v) { return v.split(','); }
|
|
function Clone(v) { return JSON.parse(JSON.stringify(v)); }
|
|
function EscapeHtml(x) { if (typeof x == "string") return x.replace(/&/g, '&').replace(/>/g, '>').replace(/</g, '<').replace(/"/g, '"').replace(/'/g, '''); if (typeof x == "boolean") return x; if (typeof x == "number") return x; }
|
|
function EscapeHtmlBreaks(x) { if (typeof x == "string") return x.replace(/&/g, '&').replace(/>/g, '>').replace(/</g, '<').replace(/"/g, '"').replace(/'/g, ''').replace(/\r/g, '<br />').replace(/\n/g, '').replace(/\t/g, ' '); if (typeof x == "boolean") return x; if (typeof x == "number") return x; }
|
|
|
|
// Move an element from one position in an array to a new position
|
|
function ArrayElementMove(arr, from, to) { arr.splice(to, 0, arr.splice(from, 1)[0]); };
|
|
|
|
// Print object for HTML
|
|
function ObjectToStringEx(x, c) {
|
|
var r = "";
|
|
if (x != 0 && (!x || x == null)) return "(Null)";
|
|
if (x instanceof Array) { for (var i in x) { r += '<br />' + gap(c) + "Item #" + i + ": " + ObjectToStringEx(x[i], c + 1); } }
|
|
else if (x instanceof Object) { for (var i in x) { r += '<br />' + gap(c) + i + " = " + ObjectToStringEx(x[i], c + 1); } }
|
|
else { r += EscapeHtml(x); }
|
|
return r;
|
|
}
|
|
|
|
// Print object for console
|
|
function ObjectToStringEx2(x, c) {
|
|
var r = "";
|
|
if (x != 0 && (!x || x == null)) return "(Null)";
|
|
if (x instanceof Array) { for (var i in x) { r += '\r\n' + gap2(c) + "Item #" + i + ": " + ObjectToStringEx2(x[i], c + 1); } }
|
|
else if (x instanceof Object) { for (var i in x) { r += '\r\n' + gap2(c) + i + " = " + ObjectToStringEx2(x[i], c + 1); } }
|
|
else { r += EscapeHtml(x); }
|
|
return r;
|
|
}
|
|
|
|
// Create an ident gap
|
|
function gap(c) { var x = ''; for (var i = 0; i < (c * 4) ; i++) { x += ' '; } return x; }
|
|
function gap2(c) { var x = ''; for (var i = 0; i < (c * 4) ; i++) { x += ' '; } return x; }
|
|
|
|
// Print an object in html
|
|
function ObjectToString(x) { return ObjectToStringEx(x, 0); }
|
|
function ObjectToString2(x) { return ObjectToStringEx2(x, 0); }
|
|
|
|
// Convert a hex string to a raw string
|
|
function hex2rstr(d) {
|
|
if (typeof d != "string" || d.length == 0) return '';
|
|
var r = '', m = ('' + d).match(/../g), t;
|
|
while (t = m.shift()) r += String.fromCharCode('0x' + t);
|
|
return r
|
|
}
|
|
|
|
// Convert decimal to hex
|
|
function char2hex(i) { return (i + 0x100).toString(16).substr(-2).toUpperCase(); }
|
|
|
|
// Convert a raw string to a hex string
|
|
function rstr2hex(input) { var r = '', i; for (i = 0; i < input.length; i++) { r += char2hex(input.charCodeAt(i)); } return r; }
|
|
|
|
// UTF-8 encoding & decoding functions
|
|
function encode_utf8(s) { return unescape(encodeURIComponent(s)); }
|
|
function decode_utf8(s) { return decodeURIComponent(escape(s)); }
|
|
|
|
// Convert a string into a blob
|
|
function data2blob(data) {
|
|
var bytes = new Array(data.length);
|
|
for (var i = 0; i < data.length; i++) bytes[i] = data.charCodeAt(i);
|
|
var blob = new Blob([new Uint8Array(bytes)]);
|
|
return blob;
|
|
}
|
|
|
|
// Generate random numbers
|
|
function random(max) { return Math.floor(Math.random() * max); }
|
|
|
|
// Trademarks
|
|
function trademarks(x) { return x.replace(/\(R\)/g, '®').replace(/\(TM\)/g, '™'); }
|
|
//Copyright 2014-2015 Google Inc. All rights reserved.
|
|
|
|
//Use of this source code is governed by a BSD-style
|
|
//license that can be found in the LICENSE file or at
|
|
//https://developers.google.com/open-source/licenses/bsd
|
|
|
|
/**
|
|
* @fileoverview The U2F api.
|
|
*/
|
|
'use strict';
|
|
if (!window.u2f) {
|
|
|
|
/**
|
|
* Namespace for the U2F api.
|
|
* @type {Object}
|
|
*/
|
|
var u2f = u2f || {};
|
|
|
|
/**
|
|
* FIDO U2F Javascript API Version
|
|
* @number
|
|
*/
|
|
var js_api_version;
|
|
|
|
/**
|
|
* The U2F extension id
|
|
* @const {string}
|
|
*/
|
|
// The Chrome packaged app extension ID.
|
|
// Uncomment this if you want to deploy a server instance that uses
|
|
// the package Chrome app and does not require installing the U2F Chrome extension.
|
|
u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd';
|
|
// The U2F Chrome extension ID.
|
|
// Uncomment this if you want to deploy a server instance that uses
|
|
// the U2F Chrome extension to authenticate.
|
|
// u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne';
|
|
|
|
|
|
/**
|
|
* Message types for messsages to/from the extension
|
|
* @const
|
|
* @enum {string}
|
|
*/
|
|
u2f.MessageTypes = {
|
|
'U2F_REGISTER_REQUEST': 'u2f_register_request',
|
|
'U2F_REGISTER_RESPONSE': 'u2f_register_response',
|
|
'U2F_SIGN_REQUEST': 'u2f_sign_request',
|
|
'U2F_SIGN_RESPONSE': 'u2f_sign_response',
|
|
'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request',
|
|
'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response'
|
|
};
|
|
|
|
|
|
/**
|
|
* Response status codes
|
|
* @const
|
|
* @enum {number}
|
|
*/
|
|
u2f.ErrorCodes = {
|
|
'OK': 0,
|
|
'OTHER_ERROR': 1,
|
|
'BAD_REQUEST': 2,
|
|
'CONFIGURATION_UNSUPPORTED': 3,
|
|
'DEVICE_INELIGIBLE': 4,
|
|
'TIMEOUT': 5
|
|
};
|
|
|
|
|
|
/**
|
|
* A message for registration requests
|
|
* @typedef {{
|
|
* type: u2f.MessageTypes,
|
|
* appId: ?string,
|
|
* timeoutSeconds: ?number,
|
|
* requestId: ?number
|
|
* }}
|
|
*/
|
|
u2f.U2fRequest;
|
|
|
|
|
|
/**
|
|
* A message for registration responses
|
|
* @typedef {{
|
|
* type: u2f.MessageTypes,
|
|
* responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse),
|
|
* requestId: ?number
|
|
* }}
|
|
*/
|
|
u2f.U2fResponse;
|
|
|
|
|
|
/**
|
|
* An error object for responses
|
|
* @typedef {{
|
|
* errorCode: u2f.ErrorCodes,
|
|
* errorMessage: ?string
|
|
* }}
|
|
*/
|
|
u2f.Error;
|
|
|
|
/**
|
|
* Data object for a single sign request.
|
|
* @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC}}
|
|
*/
|
|
u2f.Transport;
|
|
|
|
|
|
/**
|
|
* Data object for a single sign request.
|
|
* @typedef {Array<u2f.Transport>}
|
|
*/
|
|
u2f.Transports;
|
|
|
|
/**
|
|
* Data object for a single sign request.
|
|
* @typedef {{
|
|
* version: string,
|
|
* challenge: string,
|
|
* keyHandle: string,
|
|
* appId: string
|
|
* }}
|
|
*/
|
|
u2f.SignRequest;
|
|
|
|
|
|
/**
|
|
* Data object for a sign response.
|
|
* @typedef {{
|
|
* keyHandle: string,
|
|
* signatureData: string,
|
|
* clientData: string
|
|
* }}
|
|
*/
|
|
u2f.SignResponse;
|
|
|
|
|
|
/**
|
|
* Data object for a registration request.
|
|
* @typedef {{
|
|
* version: string,
|
|
* challenge: string
|
|
* }}
|
|
*/
|
|
u2f.RegisterRequest;
|
|
|
|
|
|
/**
|
|
* Data object for a registration response.
|
|
* @typedef {{
|
|
* version: string,
|
|
* keyHandle: string,
|
|
* transports: Transports,
|
|
* appId: string
|
|
* }}
|
|
*/
|
|
u2f.RegisterResponse;
|
|
|
|
|
|
/**
|
|
* Data object for a registered key.
|
|
* @typedef {{
|
|
* version: string,
|
|
* keyHandle: string,
|
|
* transports: ?Transports,
|
|
* appId: ?string
|
|
* }}
|
|
*/
|
|
u2f.RegisteredKey;
|
|
|
|
|
|
/**
|
|
* Data object for a get API register response.
|
|
* @typedef {{
|
|
* js_api_version: number
|
|
* }}
|
|
*/
|
|
u2f.GetJsApiVersionResponse;
|
|
|
|
|
|
//Low level MessagePort API support
|
|
|
|
/**
|
|
* Sets up a MessagePort to the U2F extension using the
|
|
* available mechanisms.
|
|
* @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
|
|
*/
|
|
u2f.getMessagePort = function (callback) {
|
|
if (typeof chrome != 'undefined' && chrome.runtime) {
|
|
// The actual message here does not matter, but we need to get a reply
|
|
// for the callback to run. Thus, send an empty signature request
|
|
// in order to get a failure response.
|
|
var msg = {
|
|
type: u2f.MessageTypes.U2F_SIGN_REQUEST,
|
|
signRequests: []
|
|
};
|
|
chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function () {
|
|
if (!chrome.runtime.lastError) {
|
|
// We are on a whitelisted origin and can talk directly
|
|
// with the extension.
|
|
u2f.getChromeRuntimePort_(callback);
|
|
} else {
|
|
// chrome.runtime was available, but we couldn't message
|
|
// the extension directly, use iframe
|
|
u2f.getIframePort_(callback);
|
|
}
|
|
});
|
|
} else if (u2f.isAndroidChrome_()) {
|
|
u2f.getAuthenticatorPort_(callback);
|
|
} else if (u2f.isIosChrome_()) {
|
|
u2f.getIosPort_(callback);
|
|
} else {
|
|
// chrome.runtime was not available at all, which is normal
|
|
// when this origin doesn't have access to any extensions.
|
|
u2f.getIframePort_(callback);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Detect chrome running on android based on the browser's useragent.
|
|
* @private
|
|
*/
|
|
u2f.isAndroidChrome_ = function () {
|
|
var userAgent = navigator.userAgent;
|
|
return userAgent.indexOf('Chrome') != -1 &&
|
|
userAgent.indexOf('Android') != -1;
|
|
};
|
|
|
|
/**
|
|
* Detect chrome running on iOS based on the browser's platform.
|
|
* @private
|
|
*/
|
|
u2f.isIosChrome_ = function () {
|
|
var r = ["iPhone", "iPad", "iPod"];
|
|
for (var i in r) { if (navigator.platform == r[i]) { return true; } }
|
|
return false;
|
|
//return $.inArray(navigator.platform, ["iPhone", "iPad", "iPod"]) > -1;
|
|
};
|
|
|
|
/**
|
|
* Connects directly to the extension via chrome.runtime.connect.
|
|
* @param {function(u2f.WrappedChromeRuntimePort_)} callback
|
|
* @private
|
|
*/
|
|
u2f.getChromeRuntimePort_ = function (callback) {
|
|
var port = chrome.runtime.connect(u2f.EXTENSION_ID,
|
|
{ 'includeTlsChannelId': true });
|
|
setTimeout(function () {
|
|
callback(new u2f.WrappedChromeRuntimePort_(port));
|
|
}, 0);
|
|
};
|
|
|
|
/**
|
|
* Return a 'port' abstraction to the Authenticator app.
|
|
* @param {function(u2f.WrappedAuthenticatorPort_)} callback
|
|
* @private
|
|
*/
|
|
u2f.getAuthenticatorPort_ = function (callback) {
|
|
setTimeout(function () {
|
|
callback(new u2f.WrappedAuthenticatorPort_());
|
|
}, 0);
|
|
};
|
|
|
|
/**
|
|
* Return a 'port' abstraction to the iOS client app.
|
|
* @param {function(u2f.WrappedIosPort_)} callback
|
|
* @private
|
|
*/
|
|
u2f.getIosPort_ = function (callback) {
|
|
setTimeout(function () {
|
|
callback(new u2f.WrappedIosPort_());
|
|
}, 0);
|
|
};
|
|
|
|
/**
|
|
* A wrapper for chrome.runtime.Port that is compatible with MessagePort.
|
|
* @param {Port} port
|
|
* @constructor
|
|
* @private
|
|
*/
|
|
u2f.WrappedChromeRuntimePort_ = function (port) {
|
|
this.port_ = port;
|
|
};
|
|
|
|
/**
|
|
* Format and return a sign request compliant with the JS API version supported by the extension.
|
|
* @param {Array<u2f.SignRequest>} signRequests
|
|
* @param {number} timeoutSeconds
|
|
* @param {number} reqId
|
|
* @return {Object}
|
|
*/
|
|
u2f.formatSignRequest_ =
|
|
function (appId, challenge, registeredKeys, timeoutSeconds, reqId) {
|
|
if (js_api_version === undefined || js_api_version < 1.1) {
|
|
// Adapt request to the 1.0 JS API
|
|
var signRequests = [];
|
|
for (var i = 0; i < registeredKeys.length; i++) {
|
|
signRequests[i] = {
|
|
version: registeredKeys[i].version,
|
|
challenge: challenge,
|
|
keyHandle: registeredKeys[i].keyHandle,
|
|
appId: appId
|
|
};
|
|
}
|
|
return {
|
|
type: u2f.MessageTypes.U2F_SIGN_REQUEST,
|
|
signRequests: signRequests,
|
|
timeoutSeconds: timeoutSeconds,
|
|
requestId: reqId
|
|
};
|
|
}
|
|
// JS 1.1 API
|
|
return {
|
|
type: u2f.MessageTypes.U2F_SIGN_REQUEST,
|
|
appId: appId,
|
|
challenge: challenge,
|
|
registeredKeys: registeredKeys,
|
|
timeoutSeconds: timeoutSeconds,
|
|
requestId: reqId
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Format and return a register request compliant with the JS API version supported by the extension..
|
|
* @param {Array<u2f.SignRequest>} signRequests
|
|
* @param {Array<u2f.RegisterRequest>} signRequests
|
|
* @param {number} timeoutSeconds
|
|
* @param {number} reqId
|
|
* @return {Object}
|
|
*/
|
|
u2f.formatRegisterRequest_ =
|
|
function (appId, registeredKeys, registerRequests, timeoutSeconds, reqId) {
|
|
if (js_api_version === undefined || js_api_version < 1.1) {
|
|
// Adapt request to the 1.0 JS API
|
|
for (var i = 0; i < registerRequests.length; i++) {
|
|
registerRequests[i].appId = appId;
|
|
}
|
|
var signRequests = [];
|
|
for (var i = 0; i < registeredKeys.length; i++) {
|
|
signRequests[i] = {
|
|
version: registeredKeys[i].version,
|
|
challenge: registerRequests[0],
|
|
keyHandle: registeredKeys[i].keyHandle,
|
|
appId: appId
|
|
};
|
|
}
|
|
return {
|
|
type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
|
|
signRequests: signRequests,
|
|
registerRequests: registerRequests,
|
|
timeoutSeconds: timeoutSeconds,
|
|
requestId: reqId
|
|
};
|
|
}
|
|
// JS 1.1 API
|
|
return {
|
|
type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
|
|
appId: appId,
|
|
registerRequests: registerRequests,
|
|
registeredKeys: registeredKeys,
|
|
timeoutSeconds: timeoutSeconds,
|
|
requestId: reqId
|
|
};
|
|
};
|
|
|
|
|
|
/**
|
|
* Posts a message on the underlying channel.
|
|
* @param {Object} message
|
|
*/
|
|
u2f.WrappedChromeRuntimePort_.prototype.postMessage = function (message) {
|
|
this.port_.postMessage(message);
|
|
};
|
|
|
|
|
|
/**
|
|
* Emulates the HTML 5 addEventListener interface. Works only for the
|
|
* onmessage event, which is hooked up to the chrome.runtime.Port.onMessage.
|
|
* @param {string} eventName
|
|
* @param {function({data: Object})} handler
|
|
*/
|
|
u2f.WrappedChromeRuntimePort_.prototype.addEventListener =
|
|
function (eventName, handler) {
|
|
var name = eventName.toLowerCase();
|
|
if (name == 'message' || name == 'onmessage') {
|
|
this.port_.onMessage.addListener(function (message) {
|
|
// Emulate a minimal MessageEvent object
|
|
handler({ 'data': message });
|
|
});
|
|
} else {
|
|
console.error('WrappedChromeRuntimePort only supports onMessage');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Wrap the Authenticator app with a MessagePort interface.
|
|
* @constructor
|
|
* @private
|
|
*/
|
|
u2f.WrappedAuthenticatorPort_ = function () {
|
|
this.requestId_ = -1;
|
|
this.requestObject_ = null;
|
|
}
|
|
|
|
/**
|
|
* Launch the Authenticator intent.
|
|
* @param {Object} message
|
|
*/
|
|
u2f.WrappedAuthenticatorPort_.prototype.postMessage = function (message) {
|
|
var intentUrl =
|
|
u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ +
|
|
';S.request=' + encodeURIComponent(JSON.stringify(message)) +
|
|
';end';
|
|
document.location = intentUrl;
|
|
};
|
|
|
|
/**
|
|
* Tells what type of port this is.
|
|
* @return {String} port type
|
|
*/
|
|
u2f.WrappedAuthenticatorPort_.prototype.getPortType = function () {
|
|
return "WrappedAuthenticatorPort_";
|
|
};
|
|
|
|
|
|
/**
|
|
* Emulates the HTML 5 addEventListener interface.
|
|
* @param {string} eventName
|
|
* @param {function({data: Object})} handler
|
|
*/
|
|
u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function (eventName, handler) {
|
|
var name = eventName.toLowerCase();
|
|
if (name == 'message') {
|
|
var self = this;
|
|
/* Register a callback to that executes when
|
|
* chrome injects the response. */
|
|
window.addEventListener(
|
|
'message', self.onRequestUpdate_.bind(self, handler), false);
|
|
} else {
|
|
console.error('WrappedAuthenticatorPort only supports message');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Callback invoked when a response is received from the Authenticator.
|
|
* @param function({data: Object}) callback
|
|
* @param {Object} message message Object
|
|
*/
|
|
u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ =
|
|
function (callback, message) {
|
|
var messageObject = JSON.parse(message.data);
|
|
var intentUrl = messageObject['intentURL'];
|
|
|
|
var errorCode = messageObject['errorCode'];
|
|
var responseObject = null;
|
|
if (messageObject.hasOwnProperty('data')) {
|
|
responseObject = /** @type {Object} */ (
|
|
JSON.parse(messageObject['data']));
|
|
}
|
|
|
|
callback({ 'data': responseObject });
|
|
};
|
|
|
|
/**
|
|
* Base URL for intents to Authenticator.
|
|
* @const
|
|
* @private
|
|
*/
|
|
u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ =
|
|
'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE';
|
|
|
|
/**
|
|
* Wrap the iOS client app with a MessagePort interface.
|
|
* @constructor
|
|
* @private
|
|
*/
|
|
u2f.WrappedIosPort_ = function () { };
|
|
|
|
/**
|
|
* Launch the iOS client app request
|
|
* @param {Object} message
|
|
*/
|
|
u2f.WrappedIosPort_.prototype.postMessage = function (message) {
|
|
var str = JSON.stringify(message);
|
|
var url = "u2f://auth?" + encodeURI(str);
|
|
location.replace(url);
|
|
};
|
|
|
|
/**
|
|
* Tells what type of port this is.
|
|
* @return {String} port type
|
|
*/
|
|
u2f.WrappedIosPort_.prototype.getPortType = function () {
|
|
return "WrappedIosPort_";
|
|
};
|
|
|
|
/**
|
|
* Emulates the HTML 5 addEventListener interface.
|
|
* @param {string} eventName
|
|
* @param {function({data: Object})} handler
|
|
*/
|
|
u2f.WrappedIosPort_.prototype.addEventListener = function (eventName, handler) {
|
|
var name = eventName.toLowerCase();
|
|
if (name !== 'message') {
|
|
console.error('WrappedIosPort only supports message');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Sets up an embedded trampoline iframe, sourced from the extension.
|
|
* @param {function(MessagePort)} callback
|
|
* @private
|
|
*/
|
|
u2f.getIframePort_ = function (callback) {
|
|
// Create the iframe
|
|
var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID;
|
|
var iframe = document.createElement('iframe');
|
|
iframe.src = iframeOrigin + '/u2f-comms.html';
|
|
iframe.setAttribute('style', 'display:none');
|
|
document.body.appendChild(iframe);
|
|
|
|
var channel = new MessageChannel();
|
|
var ready = function (message) {
|
|
if (message.data == 'ready') {
|
|
channel.port1.removeEventListener('message', ready);
|
|
callback(channel.port1);
|
|
} else {
|
|
console.error('First event on iframe port was not "ready"');
|
|
}
|
|
};
|
|
channel.port1.addEventListener('message', ready);
|
|
channel.port1.start();
|
|
|
|
iframe.addEventListener('load', function () {
|
|
// Deliver the port to the iframe and initialize
|
|
iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]);
|
|
});
|
|
};
|
|
|
|
|
|
//High-level JS API
|
|
|
|
/**
|
|
* Default extension response timeout in seconds.
|
|
* @const
|
|
*/
|
|
u2f.EXTENSION_TIMEOUT_SEC = 30;
|
|
|
|
/**
|
|
* A singleton instance for a MessagePort to the extension.
|
|
* @type {MessagePort|u2f.WrappedChromeRuntimePort_}
|
|
* @private
|
|
*/
|
|
u2f.port_ = null;
|
|
|
|
/**
|
|
* Callbacks waiting for a port
|
|
* @type {Array<function((MessagePort|u2f.WrappedChromeRuntimePort_))>}
|
|
* @private
|
|
*/
|
|
u2f.waitingForPort_ = [];
|
|
|
|
/**
|
|
* A counter for requestIds.
|
|
* @type {number}
|
|
* @private
|
|
*/
|
|
u2f.reqCounter_ = 0;
|
|
|
|
/**
|
|
* A map from requestIds to client callbacks
|
|
* @type {Object.<number,(function((u2f.Error|u2f.RegisterResponse))
|
|
* |function((u2f.Error|u2f.SignResponse)))>}
|
|
* @private
|
|
*/
|
|
u2f.callbackMap_ = {};
|
|
|
|
/**
|
|
* Creates or retrieves the MessagePort singleton to use.
|
|
* @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
|
|
* @private
|
|
*/
|
|
u2f.getPortSingleton_ = function (callback) {
|
|
if (u2f.port_) {
|
|
callback(u2f.port_);
|
|
} else {
|
|
if (u2f.waitingForPort_.length == 0) {
|
|
u2f.getMessagePort(function (port) {
|
|
u2f.port_ = port;
|
|
u2f.port_.addEventListener('message',
|
|
/** @type {function(Event)} */ (u2f.responseHandler_));
|
|
|
|
// Careful, here be async callbacks. Maybe.
|
|
while (u2f.waitingForPort_.length)
|
|
u2f.waitingForPort_.shift()(u2f.port_);
|
|
});
|
|
}
|
|
u2f.waitingForPort_.push(callback);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Handles response messages from the extension.
|
|
* @param {MessageEvent.<u2f.Response>} message
|
|
* @private
|
|
*/
|
|
u2f.responseHandler_ = function (message) {
|
|
var response = message.data;
|
|
var reqId = response['requestId'];
|
|
if (!reqId || !u2f.callbackMap_[reqId]) {
|
|
console.error('Unknown or missing requestId in response.');
|
|
return;
|
|
}
|
|
var cb = u2f.callbackMap_[reqId];
|
|
delete u2f.callbackMap_[reqId];
|
|
cb(response['responseData']);
|
|
};
|
|
|
|
/**
|
|
* Dispatches an array of sign requests to available U2F tokens.
|
|
* If the JS API version supported by the extension is unknown, it first sends a
|
|
* message to the extension to find out the supported API version and then it sends
|
|
* the sign request.
|
|
* @param {string=} appId
|
|
* @param {string=} challenge
|
|
* @param {Array<u2f.RegisteredKey>} registeredKeys
|
|
* @param {function((u2f.Error|u2f.SignResponse))} callback
|
|
* @param {number=} opt_timeoutSeconds
|
|
*/
|
|
u2f.sign = function (appId, challenge, registeredKeys, callback, opt_timeoutSeconds) {
|
|
if (js_api_version === undefined) {
|
|
// Send a message to get the extension to JS API version, then send the actual sign request.
|
|
u2f.getApiVersion(
|
|
function (response) {
|
|
js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version'];
|
|
//console.log("Extension JS API Version: ", js_api_version);
|
|
u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds);
|
|
});
|
|
} else {
|
|
// We know the JS API version. Send the actual sign request in the supported API version.
|
|
u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Dispatches an array of sign requests to available U2F tokens.
|
|
* @param {string=} appId
|
|
* @param {string=} challenge
|
|
* @param {Array<u2f.RegisteredKey>} registeredKeys
|
|
* @param {function((u2f.Error|u2f.SignResponse))} callback
|
|
* @param {number=} opt_timeoutSeconds
|
|
*/
|
|
u2f.sendSignRequest = function (appId, challenge, registeredKeys, callback, opt_timeoutSeconds) {
|
|
u2f.getPortSingleton_(function (port) {
|
|
var reqId = ++u2f.reqCounter_;
|
|
u2f.callbackMap_[reqId] = callback;
|
|
var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
|
|
opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
|
|
var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId);
|
|
port.postMessage(req);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Dispatches register requests to available U2F tokens. An array of sign
|
|
* requests identifies already registered tokens.
|
|
* If the JS API version supported by the extension is unknown, it first sends a
|
|
* message to the extension to find out the supported API version and then it sends
|
|
* the register request.
|
|
* @param {string=} appId
|
|
* @param {Array<u2f.RegisterRequest>} registerRequests
|
|
* @param {Array<u2f.RegisteredKey>} registeredKeys
|
|
* @param {function((u2f.Error|u2f.RegisterResponse))} callback
|
|
* @param {number=} opt_timeoutSeconds
|
|
*/
|
|
u2f.register = function (appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) {
|
|
if (js_api_version === undefined) {
|
|
// Send a message to get the extension to JS API version, then send the actual register request.
|
|
u2f.getApiVersion(
|
|
function (response) {
|
|
js_api_version = response['js_api_version'] === undefined ? 0: response['js_api_version'];
|
|
//console.log("Extension JS API Version: ", js_api_version);
|
|
u2f.sendRegisterRequest(appId, registerRequests, registeredKeys,
|
|
callback, opt_timeoutSeconds);
|
|
});
|
|
} else {
|
|
// We know the JS API version. Send the actual register request in the supported API version.
|
|
u2f.sendRegisterRequest(appId, registerRequests, registeredKeys,
|
|
callback, opt_timeoutSeconds);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Dispatches register requests to available U2F tokens. An array of sign
|
|
* requests identifies already registered tokens.
|
|
* @param {string=} appId
|
|
* @param {Array<u2f.RegisterRequest>} registerRequests
|
|
* @param {Array<u2f.RegisteredKey>} registeredKeys
|
|
* @param {function((u2f.Error|u2f.RegisterResponse))} callback
|
|
* @param {number=} opt_timeoutSeconds
|
|
*/
|
|
u2f.sendRegisterRequest = function (appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) {
|
|
u2f.getPortSingleton_(function (port) {
|
|
var reqId = ++u2f.reqCounter_;
|
|
u2f.callbackMap_[reqId] = callback;
|
|
var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
|
|
opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
|
|
var req = u2f.formatRegisterRequest_(
|
|
appId, registeredKeys, registerRequests, timeoutSeconds, reqId);
|
|
port.postMessage(req);
|
|
});
|
|
};
|
|
|
|
|
|
/**
|
|
* Dispatches a message to the extension to find out the supported
|
|
* JS API version.
|
|
* If the user is on a mobile phone and is thus using Google Authenticator instead
|
|
* of the Chrome extension, don't send the request and simply return 0.
|
|
* @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback
|
|
* @param {number=} opt_timeoutSeconds
|
|
*/
|
|
u2f.getApiVersion = function (callback, opt_timeoutSeconds) {
|
|
u2f.getPortSingleton_(function (port) {
|
|
// If we are using Android Google Authenticator or iOS client app,
|
|
// do not fire an intent to ask which JS API version to use.
|
|
if (port.getPortType) {
|
|
var apiVersion;
|
|
switch (port.getPortType()) {
|
|
case 'WrappedIosPort_':
|
|
case 'WrappedAuthenticatorPort_':
|
|
apiVersion = 1.1;
|
|
break;
|
|
|
|
default:
|
|
apiVersion = 0;
|
|
break;
|
|
}
|
|
callback({ 'js_api_version': apiVersion });
|
|
return;
|
|
}
|
|
var reqId = ++u2f.reqCounter_;
|
|
u2f.callbackMap_[reqId] = callback;
|
|
var req = {
|
|
type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST,
|
|
timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ?
|
|
opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC),
|
|
requestId: reqId
|
|
};
|
|
port.postMessage(req);
|
|
});
|
|
};
|
|
|
|
}
|
|
'use strict';
|
|
var passhint = "{{{passhint}}}";
|
|
var newAccountPass = parseInt('{{{newAccountPass}}}');
|
|
var emailCheck = ('{{{emailcheck}}}' == 'true');
|
|
var passRequirements = "{{{passRequirements}}}";
|
|
var hardwareKeyChallenge = '{{{hkey}}}';
|
|
if (passRequirements != "") { passRequirements = JSON.parse(decodeURIComponent(passRequirements)); } else { passRequirements = {}; }
|
|
var passRequirementsEx = ((passRequirements.min != null) || (passRequirements.max != null) || (passRequirements.upper != null) || (passRequirements.lower != null) || (passRequirements.numeric != null) || (passRequirements.nonalpha != null));
|
|
var features = parseInt('{{{features}}}');
|
|
var webPageFullScreen = getstore('webPageFullScreen', true);
|
|
if (webPageFullScreen == 'false') { webPageFullScreen = false; }
|
|
if (webPageFullScreen == 'true') { webPageFullScreen = true; }
|
|
var welcomeText = decodeURIComponent("{{{welcometext}}}");
|
|
var currentpanel = 0;
|
|
toggleFullScreen();
|
|
|
|
function startup() {
|
|
if ((features & 32) == 0) {
|
|
// Guard against other site's top frames (web bugs).
|
|
var loc = null;
|
|
try { loc = top.location.toString().toLowerCase(); } catch (e) { }
|
|
if (top != self && (loc == null || top.active == false)) { top.location = self.location; return; }
|
|
}
|
|
|
|
QV('createPanelHint', passRequirements.hint === true);
|
|
QV('resetpasswordpanelHint', passRequirements.hint === true);
|
|
|
|
// Display the welcome text
|
|
if (welcomeText) { QH('welcomeText', welcomeText); }
|
|
QV('welcomeText', true);
|
|
|
|
window.onresize = center;
|
|
center();
|
|
|
|
validateLogin();
|
|
validateCreate();
|
|
if ('{{loginmode}}' != '') { go(parseInt('{{loginmode}}')); } else { go(1); }
|
|
QV('newAccountDiv', ('{{{newAccount}}}' != '0') && ('{{{newAccount}}}' != 'false')); // If new accounts are not allowed, don't display the new account link.
|
|
if ((passhint != null) && (passhint.length > 0)) { QV("showPassHintLink", true); }
|
|
QV("newAccountPass", (newAccountPass == 1));
|
|
QV("resetAccountDiv", (emailCheck == true));
|
|
QV("hrAccountDiv", (emailCheck == true) || (newAccountPass == 1));
|
|
|
|
if ('{{loginmode}}' == '4') {
|
|
try { if (hardwareKeyChallenge.length > 0) { hardwareKeyChallenge = JSON.parse(hardwareKeyChallenge); } else { hardwareKeyChallenge = null; } } catch (ex) { hardwareKeyChallenge = null }
|
|
if ((hardwareKeyChallenge != null) && (hardwareKeyChallenge.type == 'webAuthn')) {
|
|
hardwareKeyChallenge.challenge = Uint8Array.from(atob(hardwareKeyChallenge.challenge), c => c.charCodeAt(0)).buffer;
|
|
|
|
const publicKeyCredentialRequestOptions = { challenge: hardwareKeyChallenge.challenge, allowCredentials: [], timeout: hardwareKeyChallenge.timeout }
|
|
for (var i = 0; i < hardwareKeyChallenge.keyIds.length; i++) {
|
|
publicKeyCredentialRequestOptions.allowCredentials.push(
|
|
{ id: Uint8Array.from(atob(hardwareKeyChallenge.keyIds[i]), c => c.charCodeAt(0)), type: 'public-key', transports: ['usb', 'ble', 'nfc'], }
|
|
);
|
|
}
|
|
|
|
// New WebAuthn hardware keys
|
|
navigator.credentials.get({ publicKey: publicKeyCredentialRequestOptions }).then(
|
|
function (rawAssertion) {
|
|
var assertion = {
|
|
id: btoa(String.fromCharCode.apply(null, new Uint8Array(rawAssertion.rawId))),
|
|
clientDataJSON: btoa(String.fromCharCode.apply(null, new Uint8Array(rawAssertion.response.clientDataJSON))),
|
|
userHandle: btoa(String.fromCharCode.apply(null, new Uint8Array(rawAssertion.response.userHandle))),
|
|
signature: btoa(String.fromCharCode.apply(null, new Uint8Array(rawAssertion.response.signature))),
|
|
authenticatorData: btoa(String.fromCharCode.apply(null, new Uint8Array(rawAssertion.response.authenticatorData))),
|
|
};
|
|
Q('hwtokenInput').value = JSON.stringify(assertion);
|
|
QE('tokenOkButton', true);
|
|
Q('tokenOkButton').click();
|
|
},
|
|
function (error) { console.log('credentials-get error', error); }
|
|
);
|
|
} else if ((hardwareKeyChallenge != null) && u2fSupported()) {
|
|
// Old U2F hardware keys
|
|
window.u2f.sign(hardwareKeyChallenge.appId, hardwareKeyChallenge.challenge, hardwareKeyChallenge.registeredKeys, function (authResponse) {
|
|
if ((currentpanel == 4) && authResponse.signatureData) {
|
|
Q('hwtokenInput').value = JSON.stringify(authResponse);
|
|
QE('tokenOkButton', true);
|
|
Q('tokenOkButton').click();
|
|
}
|
|
}, hardwareKeyChallenge.timeoutSeconds);
|
|
}
|
|
}
|
|
|
|
if ('{{loginmode}}' == '5') {
|
|
try { if (hardwareKeyChallenge.length > 0) { hardwareKeyChallenge = JSON.parse(hardwareKeyChallenge); } else { hardwareKeyChallenge = null; } } catch (ex) { hardwareKeyChallenge = null }
|
|
if ((hardwareKeyChallenge != null) && (hardwareKeyChallenge.type == 'webAuthn')) {
|
|
hardwareKeyChallenge.challenge = Uint8Array.from(atob(hardwareKeyChallenge.challenge), c => c.charCodeAt(0)).buffer;
|
|
|
|
const publicKeyCredentialRequestOptions = { challenge: hardwareKeyChallenge.challenge, allowCredentials: [], timeout: hardwareKeyChallenge.timeout }
|
|
for (var i = 0; i < hardwareKeyChallenge.keyIds.length; i++) {
|
|
publicKeyCredentialRequestOptions.allowCredentials.push(
|
|
{ id: Uint8Array.from(atob(hardwareKeyChallenge.keyIds[i]), c => c.charCodeAt(0)), type: 'public-key', transports: ['usb', 'ble', 'nfc'], }
|
|
);
|
|
}
|
|
|
|
// New WebAuthn hardware keys
|
|
navigator.credentials.get({ publicKey: publicKeyCredentialRequestOptions }).then(
|
|
function (rawAssertion) {
|
|
var assertion = {
|
|
id: btoa(String.fromCharCode.apply(null, new Uint8Array(rawAssertion.rawId))),
|
|
clientDataJSON: btoa(String.fromCharCode.apply(null, new Uint8Array(rawAssertion.response.clientDataJSON))),
|
|
userHandle: btoa(String.fromCharCode.apply(null, new Uint8Array(rawAssertion.response.userHandle))),
|
|
signature: btoa(String.fromCharCode.apply(null, new Uint8Array(rawAssertion.response.signature))),
|
|
authenticatorData: btoa(String.fromCharCode.apply(null, new Uint8Array(rawAssertion.response.authenticatorData))),
|
|
};
|
|
Q('resetHwtokenInput').value = JSON.stringify(assertion);
|
|
QE('resetTokenOkButton', true);
|
|
Q('resetTokenOkButton').click();
|
|
},
|
|
function (error) { console.log('credentials-get error', error); }
|
|
);
|
|
} else if ((hardwareKeyChallenge != null) && u2fSupported()) {
|
|
// Old U2F hardware keys
|
|
window.u2f.sign(hardwareKeyChallenge.appId, hardwareKeyChallenge.challenge, hardwareKeyChallenge.registeredKeys, function (authResponse) {
|
|
if ((currentpanel == 5) && authResponse.signatureData) {
|
|
Q('resetHwtokenInput').value = JSON.stringify(authResponse);
|
|
QE('resetTokenOkButton', true);
|
|
Q('resetTokenOkButton').click();
|
|
}
|
|
}, hardwareKeyChallenge.timeoutSeconds);
|
|
}
|
|
}
|
|
}
|
|
|
|
function showPassHint() {
|
|
messagebox("Password Hint", passhint);
|
|
}
|
|
|
|
function xgo(x) {
|
|
QV('message1', false);
|
|
QV('message2', false);
|
|
QV('message3', false);
|
|
QV('message4', false);
|
|
QV('message5', false);
|
|
QV('message6', false);
|
|
go(x);
|
|
}
|
|
|
|
function go(x) {
|
|
currentpanel = x;
|
|
setDialogMode(0);
|
|
QV("showPassHintLink", false);
|
|
QV('loginpanel', x == 1);
|
|
QV('createpanel', x == 2);
|
|
QV('resetpanel', x == 3);
|
|
QV('tokenpanel', x == 4);
|
|
QV('resettokenpanel', x == 5);
|
|
QV('resetpasswordpanel', x == 6);
|
|
if (x == 1) { Q('username').focus(); }
|
|
if (x == 2) { Q('ausername').focus(); }
|
|
if (x == 3) { Q('remail').focus(); }
|
|
if (x == 4) { Q('tokenInput').focus(); }
|
|
if (x == 5) { Q('resetTokenInput').focus(); }
|
|
if (x == 6) { Q('rapassword1').focus(); }
|
|
}
|
|
|
|
function validateLogin(box, e) {
|
|
var ok = ((Q('username').value.length > 0) && (Q('username').value.indexOf(' ') == -1) && (Q('password').value.length > 0));
|
|
QE('loginButton', ok);
|
|
setDialogMode(0);
|
|
if ((e != null) && (e.keyCode == 13)) { if (box == 1) { Q('password').focus(); } else if (box == 2) { Q('loginButton').click(); } }
|
|
if (e != null) { haltEvent(e); }
|
|
}
|
|
|
|
function validateCreate(box, e) {
|
|
setDialogMode(0);
|
|
var userok = (Q('ausername').value.length > 0) && (Q('ausername').value.indexOf(' ') == -1);
|
|
var emailok = (validateEmail(Q('aemail').value) == true);
|
|
var pass1ok = (Q('apassword1').value.length > 0);
|
|
var pass2ok = (Q('apassword2').value.length > 0) && (Q('apassword2').value == Q('apassword1').value);
|
|
var newAccOk = (newAccountPass == 0) || (Q('anewaccountpass').value.length > 0);
|
|
var ok = (userok && emailok && pass1ok && pass2ok && newAccOk);
|
|
|
|
// Color the fields
|
|
QS('nuUser').color = userok?'black':'#7b241c';
|
|
QS('nuEmail').color = emailok?'black':'#7b241c';
|
|
QS('nuPass1').color = pass1ok?'black':'#7b241c';
|
|
QS('nuPass2').color = pass2ok?'black':'#7b241c';
|
|
QS('nuToken').color = newAccOk?'black':'#7b241c';
|
|
|
|
if (Q('apassword1').value == '') {
|
|
QH('passWarning', '');
|
|
QV('passwordPolicyCallout', false);
|
|
} else {
|
|
if (!passRequirementsEx) {
|
|
// No password requirements, display password strength
|
|
var passStrength = checkPasswordStrength(Q('apassword1').value);
|
|
if (passStrength >= 80) { QH('passWarning', '<span style=color:green><b>Strong Password</b><span>'); }
|
|
else if (passStrength >= 60) { QH('passWarning', '<span style=color:blue><b>Good Password</b><span>'); }
|
|
else { QH('passWarning', '<span style=color:red><b>Weak Password</b><span>'); }
|
|
} else {
|
|
// Password requirements provided, use that
|
|
var passReq = checkPasswordRequirements(Q('apassword1').value, passRequirements);
|
|
if (passReq == false) {
|
|
ok = false;
|
|
QS('nuPass1').color = '#7b241c';
|
|
QS('nuPass2').color = '#7b241c';
|
|
QH('passWarning', '<div style=color:red;cursor:pointer onclick=showPasswordPolicy()><b>Password Policy</b><div>'); // This is also a link to the password policy
|
|
QV('passwordPolicyCallout', true);
|
|
QH('passwordPolicyCallout', passwordPolicyText(Q('apassword1').value));
|
|
} else {
|
|
QH('passWarning', '');
|
|
QV('passwordPolicyCallout', false);
|
|
}
|
|
}
|
|
}
|
|
if ((e != null) && (e.keyCode == 13)) {
|
|
if (box == 1) { Q('aemail').focus(); }
|
|
if (box == 2) { Q('apassword1').focus(); }
|
|
if (box == 3) { Q('apassword2').focus(); }
|
|
if (box == 4) { Q('apasswordhint').focus(); }
|
|
if (box == 5) { if (newAccountPass == 1) { Q('anewaccountpass').focus(); } else { Q('createButton').click(); } }
|
|
if (box == 6) { Q('createButton').click(); }
|
|
}
|
|
if (e != null) { haltEvent(e); }
|
|
QE('createButton', ok);
|
|
}
|
|
|
|
function validatePassReset(box, e) {
|
|
setDialogMode(0);
|
|
var pass1ok = (Q('rapassword1').value.length > 0);
|
|
var pass2ok = (Q('rapassword2').value.length > 0) && (Q('rapassword2').value == Q('rapassword1').value);
|
|
var ok = (pass1ok && pass2ok);
|
|
|
|
// Color the fields
|
|
QS('rnuPass1').color = pass1ok ? 'black' : '#7b241c';
|
|
QS('rnuPass2').color = pass2ok ? 'black' : '#7b241c';
|
|
|
|
if (Q('rapassword1').value == '') {
|
|
QH('rpassWarning', '');
|
|
QV('rpasswordPolicyCallout', false);
|
|
} else {
|
|
if (!passRequirementsEx) {
|
|
// No password requirements, display password strength
|
|
var passStrength = checkPasswordStrength(Q('rapassword1').value);
|
|
if (passStrength >= 80) { QH('rpassWarning', '<span style=color:green><b>Strong Password</b><span>'); }
|
|
else if (passStrength >= 60) { QH('rpassWarning', '<span style=color:blue><b>Good Password</b><span>'); }
|
|
else { QH('rpassWarning', '<span style=color:red><b>Weak Password</b><span>'); }
|
|
} else {
|
|
// Password requirements provided, use that
|
|
var passReq = checkPasswordRequirements(Q('rapassword1').value, passRequirements);
|
|
if (passReq == false) {
|
|
ok = false;
|
|
QS('rnuPass1').color = '#7b241c';
|
|
QS('rnuPass2').color = '#7b241c';
|
|
QH('rpassWarning', '<div style=color:red;cursor:pointer onclick=showPasswordPolicy()><b>Password Policy</b><div>'); // This is also a link to the password policy
|
|
QV('rpasswordPolicyCallout', true);
|
|
QH('rpasswordPolicyCallout', passwordPolicyText(Q('rapassword1').value));
|
|
} else {
|
|
QH('rpassWarning', '');
|
|
QV('rpasswordPolicyCallout', false);
|
|
}
|
|
}
|
|
}
|
|
if ((e != null) && (e.keyCode == 13)) {
|
|
if (box == 2) { Q('rapassword1').focus(); }
|
|
if (box == 3) { Q('rapassword2').focus(); }
|
|
if (box == 4) { Q('rapasswordhint').focus(); }
|
|
if (box == 6) { Q('resetPassButton').click(); }
|
|
}
|
|
if (e != null) { haltEvent(e); }
|
|
QE('resetPassButton', ok);
|
|
}
|
|
|
|
function passwordPolicyText(pass) {
|
|
var policy = '<div style=text-align:left>';
|
|
var counts = strCount(pass);
|
|
if (passRequirements.min && ((pass == null) || (pass.length < passRequirements.min))) { policy += 'Minimum length of ' + passRequirements.min + '<br />'; }
|
|
if (passRequirements.max && ((pass == null) || (pass.length > passRequirements.max))) { policy += 'Maximum length of ' + passRequirements.max + '<br />'; }
|
|
if (passRequirements.upper && ((pass == null) || (counts.upper < passRequirements.upper))) { policy += '' + passRequirements.upper + ' upper case<br />'; }
|
|
if (passRequirements.lower && ((pass == null) || (counts.lower < passRequirements.lower))) { policy += '' + passRequirements.lower + ' lower case<br />'; }
|
|
if (passRequirements.numeric && ((pass == null) || (counts.numeric < passRequirements.numeric))) { policy += '' + passRequirements.numeric + ' numeric<br />'; }
|
|
if (passRequirements.nonalpha && ((pass == null) || (counts.nonalpha < passRequirements.nonalpha))) { policy += passRequirements.nonalpha + ' non-alphanumeric<br />'; }
|
|
policy += '</div>';
|
|
return policy;
|
|
}
|
|
|
|
function showPasswordPolicy() {
|
|
messagebox("Password Policy", passwordPolicyText());
|
|
}
|
|
|
|
function validateReset(e) {
|
|
setDialogMode(0);
|
|
var x = validateEmail(Q('remail').value);
|
|
QE('eresetButton', x);
|
|
if ((e != null) && (e.keyCode == 13) && (x == true)) {
|
|
Q('eresetButton').click();
|
|
}
|
|
if (e != null) { haltEvent(e); }
|
|
}
|
|
|
|
// Return a password strength score
|
|
function checkPasswordStrength(password) {
|
|
var r = 0, letters = {}, varCount = 0, variations = { digits: /\d/.test(password), lower: /[a-z]/.test(password), upper: /[A-Z]/.test(password), nonWords: /\W/.test(password) }
|
|
if (!password) return 0;
|
|
for (var i = 0; i< password.length; i++) { letters[password[i]] = (letters[password[i]] || 0) + 1; r += 5.0 / letters[password[i]]; }
|
|
for (var c in variations) { varCount += (variations[c] == true) ? 1 : 0; }
|
|
return parseInt(r + (varCount - 1) * 10);
|
|
}
|
|
|
|
// Check password requirements
|
|
function checkPasswordRequirements(password, requirements) {
|
|
if ((requirements == null) || (requirements == '') || (typeof requirements != 'object')) return true;
|
|
if (requirements.min) { if (password.length < requirements.min) return false; }
|
|
if (requirements.max) { if (password.length > requirements.max) return false; }
|
|
var counts = strCount(password);
|
|
if (requirements.numeric && (counts.numeric < requirements.numeric)) return false;
|
|
if (requirements.lower && (counts.lower < requirements.lower)) return false;
|
|
if (requirements.upper && (counts.upper < requirements.upper)) return false;
|
|
if (requirements.nonalpha && (counts.nonalpha < requirements.nonalpha)) return false;
|
|
return true;
|
|
}
|
|
|
|
function strCount(password) {
|
|
var counts = { numeric: 0, lower: 0, upper: 0, nonalpha: 0 };
|
|
if (typeof password != 'string') return counts;
|
|
for (var i = 0; i < password.length; i++) {
|
|
if (/\d/.test(password[i])) { counts.numeric++; }
|
|
if (/[a-z]/.test(password[i])) { counts.lower++; }
|
|
if (/[A-Z]/.test(password[i])) { counts.upper++; }
|
|
if (/\W/.test(password[i])) { counts.nonalpha++; }
|
|
}
|
|
return counts;
|
|
}
|
|
|
|
function checkToken() {
|
|
var t1 = Q('tokenInput').value;
|
|
var t2 = t1.split(' ').join('');
|
|
if (t1 != t2) { Q('tokenInput').value = t2; }
|
|
QE('tokenOkButton', (Q('tokenInput').value.length == 6) || (Q('tokenInput').value.length == 8) || (Q('tokenInput').value.length == 44));
|
|
}
|
|
|
|
function resetCheckToken() {
|
|
var t1 = Q('resetTokenInput').value;
|
|
var t2 = t1.split(' ').join('');
|
|
if (t1 != t2) { Q('resetTokenInput').value = t2; }
|
|
QE('resetTokenOkButton', (Q('resetTokenInput').value.length == 6) || (Q('resetTokenInput').value.length == 8) || (Q('resetTokenInput').value.length == 44));
|
|
}
|
|
|
|
//
|
|
// POPUP DIALOG
|
|
//
|
|
|
|
// undefined = Hidden, 1 = Generic Message
|
|
var xxdialogMode;
|
|
var xxdialogFunc;
|
|
var xxdialogButtons;
|
|
var xxdialogTag;
|
|
var xxcurrentView = 0;
|
|
|
|
// Display a dialog box
|
|
// Parameters: Dialog Mode (0 = none), Dialog Title, Buttons (1 = OK, 2 = Cancel, 3 = OK & Cancel), Call back function(0 = Cancel, 1 = OK), Dialog Content (Mode 2 only)
|
|
function setDialogMode(x, y, b, f, c, tag) {
|
|
xxdialogMode = x;
|
|
xxdialogFunc = f;
|
|
xxdialogButtons = b;
|
|
xxdialogTag = tag;
|
|
QE('idx_dlgOkButton', true);
|
|
QV('idx_dlgOkButton', b & 1);
|
|
QV('idx_dlgCancelButton', b & 2);
|
|
QV('id_dialogclose', (b & 2) || (b & 8));
|
|
QV('idx_dlgButtonBar', b & 7);
|
|
if (y) QH('id_dialogtitle', y);
|
|
for (var i = 1; i < 24; i++) { QV('dialog' + i, i == x); } // Edit this line when more dialogs are added
|
|
QV('dialog', x);
|
|
if (c) { if (x == 2) { QH('id_dialogOptions', c); } else { QH('id_dialogMessage', c); } }
|
|
}
|
|
|
|
function dialogclose(x) {
|
|
var f = xxdialogFunc;
|
|
var b = xxdialogButtons;
|
|
var t = xxdialogTag;
|
|
setDialogMode();
|
|
if (((b & 8) || x) && f) f(x, t);
|
|
}
|
|
|
|
// Toggle the web page to full screen
|
|
function toggleFullScreen(toggle) {
|
|
if (toggle === 1) { webPageFullScreen = !webPageFullScreen; putstore('webPageFullScreen', webPageFullScreen); }
|
|
if (webPageFullScreen == false) {
|
|
QS('container').width = '960px';
|
|
QS('container')["min-width"] = '960px';
|
|
QS('container')['border-right'] = '1px solid #b7b7b7';
|
|
QS('container')['border-left'] = '1px solid #b7b7b7';
|
|
QS('container')['overflow'] = 'hidden';
|
|
QS('column_l').height = '';
|
|
QS('column_l').width = '930px';
|
|
QS('column_l')["overflow-y"] = '';
|
|
QS('column_l')["max-height"] = 'calc(100vh - 111px)';
|
|
QS('column_l')["min-width"] = '';
|
|
QS('masthead')["width"] = '960px';
|
|
} else {
|
|
QS('container').width = '100%';
|
|
QS('container')['min-width'] = '';
|
|
QS('container')['border-right'] = '0';
|
|
QS('container')['border-left'] = '0';
|
|
QS('container')['overflow'] = 'hidden';
|
|
QS('column_l').height = 'calc(100vh - 135px)';
|
|
QS('column_l').width = '';
|
|
QS('column_l')["overflow-y"] = 'auto';
|
|
QS('column_l')["max-height"] = 'calc(100vh - 111px)';
|
|
QS('column_l')["min-width"] = '';
|
|
QS('masthead')["width"] = '100%';
|
|
}
|
|
QV('body', true);
|
|
center();
|
|
}
|
|
|
|
function center() {
|
|
var w = getDocWidth();
|
|
QS('dialog').left = ((((w - 400) / 2)) + "px");
|
|
var showimage = (webPageFullScreen == false) || (w > 800);
|
|
QV('welcomeimage', showimage);
|
|
Q('logincell').setAttribute('align', showimage?'left':'center');
|
|
|
|
if (webPageFullScreen == false) {
|
|
QS('centralTable')['margin-top'] = '';
|
|
} else {
|
|
var h = (Q('column_l').clientHeight / 2) - 250;
|
|
if (h < 0) h = 0;
|
|
QS('centralTable')['margin-top'] = h + 'px';
|
|
}
|
|
}
|
|
function messagebox(t, m) { QH('id_dialogMessage', m); setDialogMode(1, t, 1); }
|
|
function statusbox(t, m) { QH('id_dialogMessage', m); setDialogMode(1, t); }
|
|
function getDocWidth() { if (window.innerWidth) return window.innerWidth; if (document.documentElement && document.documentElement.clientWidth && document.documentElement.clientWidth != 0) return document.documentElement.clientWidth; return document.getElementsByTagName('body')[0].clientWidth; }
|
|
function haltEvent(e) { if (e.preventDefault) e.preventDefault(); if (e.stopPropagation) e.stopPropagation(); return false; }
|
|
function haltReturn(e) { if (e.keyCode == 13) { haltEvent(e); } }
|
|
function validateEmail(v) { var emailReg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; return emailReg.test(v); } // New version
|
|
function putstore(name, val) { try { if (typeof (localStorage) === 'undefined') return; localStorage.setItem(name, val); } catch (e) { } }
|
|
function getstore(name, val) { try { if (typeof (localStorage) === 'undefined') return val; var v = localStorage.getItem(name); if ((v == null) || (v == null)) return val; return v; } catch (e) { return val; } }
|
|
function u2fSupported() { return (window.u2f && ((navigator.userAgent.indexOf('Chrome/') > 0) || (navigator.userAgent.indexOf('Firefox/') > 0) || (navigator.userAgent.indexOf('Opera/') > 0) || (navigator.userAgent.indexOf('Safari/') > 0))); }
|
|
|
|
</script></body></html> |