1
0
mirror of https://github.com/kkapsner/CanvasBlocker synced 2024-12-22 12:50:36 +01:00

Added navigator protection

This commit is contained in:
kkapsner 2019-02-27 23:49:00 +01:00
parent 479ee74903
commit e56df7160f
23 changed files with 711 additions and 1 deletions

View File

@ -37,6 +37,7 @@ Beschützte "Fingerprinting"-APIs:
<li>history</li>
<li>window (standardmäßig deaktiviert)</li>
<li>DOMRect</li>
<li>navigator (standardmäßig deaktiviert)</li>
</ul>
Falls Sie Fehler finden oder Verbesserungsvorschläge haben, teilen Sie mir das bitte auf https://github.com/kkapsner/CanvasBlocker/issues mit.

View File

@ -38,6 +38,7 @@ Protected "fingerprinting" APIs:
<li>history</li>
<li>window (disabled by default)</li>
<li>DOMRect</li>
<li>navigator (disabled by default)</li>
</ul>
Please report issues and feature requests at https://github.com/kkapsner/CanvasBlocker/issues

View File

@ -35,6 +35,7 @@ API para «huellas digitales» protegidas:
<li>history</li>
<li>window (deshabilitada por defecto)</li>
<li>DOMRect</li>
<li>navigator (deshabilitada por defecto)</li>
</ul>
Puede informar sobre problemas y solicitar funciones en https://github.com/kkapsner/CanvasBlocker/issues

View File

@ -21,6 +21,7 @@
<li>history</li>
<li>window (disabled by default)</li>
<li>DOMRect</li>
<li>navigator (disabled by default)</li>
</ul>
Сообщите о проблемах и пожеланиях на странице https://github.com/kkapsner/CanvasBlocker/issues.

View File

@ -5,9 +5,11 @@
"Blockiermodus",
"Captcha",
"Ignorierliste",
"KHTML",
"Maleficient",
"Nachfrageverweigerungsmodus",
"Oakenpants",
"Palemoon",
"PDFs",
"Rect",
"Rects",
@ -23,7 +25,9 @@
"fragmenter",
"ignorelist",
"micrococo",
"monero",
"onloaded",
"oscpu",
"prefs",
"promisify",
"ruleset",

View File

@ -27,6 +27,7 @@ Protected "fingerprinting" APIs:
* history
* window (disabled by default)
* DOMRect
* navigator (disabled by default)
Special thanks to:
* spodermenpls for finding all the typos

View File

@ -119,6 +119,10 @@
"message": "DOMRect-API",
"description": ""
},
"section_Navigator-api": {
"message": "Navigator-API",
"description": ""
},
"displayAdvancedSettings_title": {
"message": "Expertenmodus",
"description": ""
@ -231,6 +235,18 @@
"message": "Wollen Sie das Auslesen über die DOMRect-API erlauben?",
"description": ""
},
"askForNavigatorPermission": {
"message": "Wollen Sie die Navigator-API erlauben?",
"description": ""
},
"askForNavigatorInputPermission": {
"message": "Wollen Sie das Schreiben über die Navigator-API erlauben?",
"description": ""
},
"askForNavigatorReadoutPermission": {
"message": "Wollen Sie das Auslesen über die Navigator-API erlauben?",
"description": ""
},
"askOnlyOnce_title": {
"message": "Nur einmal nachfragen",
"description": ""
@ -567,6 +583,10 @@
"message": "DOMRect-Auslese vorgetäuscht auf {url}",
"description": ""
},
"fakedNavigatorReadout": {
"message": "Navigator-Auslese vorgetäuscht auf {url}",
"description": ""
},
"fakedInput": {
"message": "Bei Ausgabe vorgetäuscht auf {url}",
"description": ""
@ -959,6 +979,50 @@
"message": "Ein Bruchteil eines Pixels kann durch CSS kontrolliert werden. Eigenschaften eines DOMRect, die multipliziert mit diesem Faktor eine ganze Zahl ergeben, dürfen nicht verändert werden um eine Detektion zu verhindern.",
"description": ""
},
"protectNavigator_title": {
"message": "Navigator-API beschützen",
"description": ""
},
"protectNavigator_description": {
"message": "Dies ermöglicht Änderungen an der Navigator-API. Diesen Schutz zu aktivieren ändert standardmäßig noch nichts. Öffnen Sie die Navigatoreinstellungen um die gewünschten Änderungen durchzuführen.",
"description": ""
},
"openNavigatorSettings_title": {
"message": "Navigatoreinstellungen",
"description": ""
},
"openNavigatorSettings_description": {
"message": "",
"description": ""
},
"openNavigatorSettings_label": {
"message": "Öffnen",
"description": ""
},
"navigatorSettings_title": {
"message": "CanvasBlocker Navigatoreinstellungen",
"description": ""
},
"navigatorSettings_description": {
"message": "Auf dieser Seite können Sie die Navigatoreinstellungen festlegen. Wenn Sie eine Voreinstellung verwenden möchten, sollten Sie immer sowohl eine Betriebssystem- als auch eine Browservoreinstellung verwenden. Danach können Sie Ihre individuellen Änderungen vornehmen.",
"description": ""
},
"navigatorSettings_presetSection.os": {
"message": "Betriebssystemvoreinstellungen",
"description": ""
},
"navigatorSettings_presetSection.browser": {
"message": "Browservoreinstellungen",
"description": ""
},
"navigatorSettings_values": {
"message": "Navigatorwerte",
"description": ""
},
"navigatorSettings_reset": {
"message": "Zurücksetzen",
"description": ""
},
"theme_title": {
"message": "Theme",
"description": ""

View File

@ -125,6 +125,10 @@
"message": "DOMRect API",
"description": ""
},
"section_Navigator-api":{
"message": "Navigator API",
"description": ""
},
"displayAdvancedSettings_title": {
"message": "Expert mode",
@ -242,6 +246,18 @@
"message": "Do you want to allow DOMRect API readout?",
"description": ""
},
"askForNavigatorPermission": {
"message": "Do you want to allow the navigator API?",
"description": ""
},
"askForNavigatorInputPermission": {
"message": "Do you want to allow navigator API input?",
"description": ""
},
"askForNavigatorReadoutPermission": {
"message": "Do you want to allow navigator API readout?",
"description": ""
},
"askOnlyOnce_title": {
"message": "Ask only once",
"description": ""
@ -599,6 +615,10 @@
"message": "Faked DOMRect readout on {url}",
"description": ""
},
"fakedNavigatorReadout": {
"message": "Faked navigator readout on {url}",
"description": ""
},
"fakedInput": {
"message": "Faked at input on {url}",
"description": ""
@ -1003,6 +1023,53 @@
"description": ""
},
"protectNavigator_title": {
"message": "Protect navigator API",
"description": ""
},
"protectNavigator_description": {
"message": "This page allows for changes in the navigator API. Enabling this protection does not change anything by default. Open the navigator settings to specify the changes you want to have there.",
"description": ""
},
"openNavigatorSettings_title": {
"message": "Navigator settings",
"description": ""
},
"openNavigatorSettings_description": {
"message": "",
"description": ""
},
"openNavigatorSettings_label": {
"message": "Open",
"description": ""
},
"navigatorSettings_title": {
"message": "CanvasBlocker navigator settings",
"description": ""
},
"navigatorSettings_description": {
"message": "In this page you can set the navigator settings. If using a preset you should always use a operating system and browser preset. After selecting these you can still make modifications.",
"description": ""
},
"navigatorSettings_presetSection.os": {
"message": "Operating system presets",
"description": ""
},
"navigatorSettings_presetSection.browser": {
"message": "Browser presets",
"description": ""
},
"navigatorSettings_values": {
"message": "Navigator values",
"description": ""
},
"navigatorSettings_reset": {
"message": "Reset",
"description": ""
},
"theme_title": {
"message": "Theme",
"description": ""

View File

@ -140,6 +140,9 @@
message("Initialize data-URL workaround.");
require("./dataUrls").init();
message("Initialize navigator HTTP header protection.");
require("./navigator").init();
browser.runtime.onInstalled.addListener(function(details){
function openOptions(reason){
if (

View File

@ -540,4 +540,5 @@
appendModified(require("./modifiedHistoryAPI"));
appendModified(require("./modifiedWindowAPI"));
appendModified(modifiedDOMRectAPI);
appendModified(require("./modifiedNavigatorAPI"));
}());

View File

@ -0,0 +1,52 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
(function(){
"use strict";
var scope;
if ((typeof exports) !== "undefined"){
scope = exports;
}
else {
scope = require.register("./modifiedNavigatorAPI", {});
}
const {checkerWrapper} = require("./modifiedAPIFunctions");
const navigator = require("./navigator");
scope.changedGetters = navigator.allProperties.map(function(property){
return {
objectGetters: [function(window){return window.Navigator && window.Navigator.prototype;}],
name: property,
getterGenerator: function(checker){
const temp = {
get [property](){
return checkerWrapper(checker, this, arguments, function(args, check){
const {prefs, notify, window, original} = check;
const originalValue = original.apply(this, window.Array.from(args));
const returnValue = navigator.getNavigatorValue(property);
if (originalValue !== returnValue){
notify("fakedNavigatorReadout");
}
return returnValue;
});
}
};
return Object.getOwnPropertyDescriptor(temp, property).get;
}
};
});
function getStatus(obj, status, prefs){
status = Object.create(status);
status.active = prefs("protectNavigator", status.url);
return status;
}
scope.changedGetters.forEach(function(changedGetter){
changedGetter.type = "readout";
changedGetter.getStatus = getStatus;
changedGetter.api = "navigator";
});
}());

116
lib/navigator.js Normal file
View File

@ -0,0 +1,116 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
(function(){
"use strict";
var scope;
if ((typeof exports) !== "undefined"){
scope = exports;
}
else {
scope = require.register("./navigator", {});
}
const settings = require("./settings");
const logging = require("./logging");
scope.allProperties = [
"appCodeName", "appName",
"appVersion", "buildID", "oscpu", "platform",
"product",
"productSub", "userAgent", "vendor", "vendorSub"];
const original = {};
scope.allProperties.forEach(function(property){
original[property] = window.navigator[property];
});
original["real Firefox version"] = window.navigator.userAgent.replace(/^.+Firefox\//, "");
let changedValues = {};
settings.onloaded(function(){
changedValues = settings.navigatorDetails;
});
settings.on("navigatorDetails", function({newValue}){
changedValues = newValue;
});
function getValue(name, stack = []){
if (stack.indexOf(name) !== -1){
return "[ERROR: loop in property definition]";
}
stack.push(name);
switch (name){
case "original value":
return original[stack[stack.length - 2]];
case "random":
return String.fromCharCode(Math.floor(65 + 85 * Math.random()));
default:
if (changedValues.hasOwnProperty(name)){
return parseString(changedValues[name], stack.slice());
}
else {
return original[name];
}
}
}
function parseString(string, stack){
return string.replace(/{([a-z[\]_. -]*)}/ig, function(m, name){
return getValue(name, stack.slice());
});
}
scope.getNavigatorValue = function getNavigatorValue(name){
return getValue(name);
};
function changeHTTPHeader(details){
if (
settings.protectNavigator &&
(
!settings.protectedAPIFeatures.hasOwnProperty("userAgent") ||
settings.protectedAPIFeatures.userAgent
)
){
for (var header of details.requestHeaders){
if (header.name.toLowerCase() === "user-agent"){
header.value = getValue("userAgent");
}
}
}
return details;
}
scope.registerHeaderChange = function(){
logging.message("Register HTTP header modification for navigator protection.");
if (!browser.webRequest.onBeforeSendHeaders.hasListener(changeHTTPHeader)){
browser.webRequest.onBeforeSendHeaders.addListener(
changeHTTPHeader,
{
urls: ["<all_urls>"],
},
["blocking", "requestHeaders"]);
}
};
scope.unregisterHeaderChange = function(){
logging.message("Removing header modification for navigator protection.");
browser.webRequest.onBeforeSendHeaders.removeListener(changeHTTPHeader);
};
scope.init = function (){
settings.onloaded(function(){
if (!settings.protectNavigator){
scope.unregisterHeaderChange();
}
});
settings.on("protectNavigator", function({newValue}){
if (newValue){
scope.registerHeaderChange();
}
else {
scope.unregisterHeaderChange();
}
});
};
}());

View File

@ -117,6 +117,18 @@
"intersectionRect",
"boundingClientRect",
"rootBounds",
{name: "Navigator-API", level: 1},
"appCodeName",
"appName",
"appVersion",
"buildID",
"oscpu",
"platform",
"product",
"productSub",
"userAgent",
"vendor",
"vendorSub"
],
defaultKeyValue: true
},
@ -222,6 +234,7 @@
"history",
"window",
"domRect",
"navigator",
],
defaultKeyValue: false
},
@ -293,6 +306,14 @@
name: "blockDataURLs",
defaultValue: true
},
{
name: "protectNavigator",
defaultValue: false
},
{
name: "navigatorDetails",
defaultValue: {},
},
{
name: "displayAdvancedSettings",
defaultValue: false

View File

@ -19,6 +19,7 @@
"lib/persistentRndStorage.js",
"lib/dataUrls.js",
"lib/notification.js",
"lib/navigator.js",
"lib/main.js"
]
},
@ -43,6 +44,8 @@
"lib/modifiedHistoryAPI.js",
"lib/modifiedWindowAPI.js",
"lib/modifiedDOMRectAPI.js",
"lib/navigator.js",
"lib/modifiedNavigatorAPI.js",
"lib/modifiedAPI.js",
"lib/randomSupplies.js",
"lib/intercept.js",

36
options/navigator.css Normal file
View File

@ -0,0 +1,36 @@
.presetSection ul {
list-style-type: none;
padding: 0;
margin: 0;
}
.presetSection ul li {
display: inline-block;
}
.button {
display: inline-block;
border: none;
background-color: white;
border: 1px solid lightgray;
margin: 2px;
width: 7em;
box-sizing: border-box;
padding: 0.5em;
outline: none;
cursor: pointer;
}
.active .button {
border-color: red;
}
.button:focus {
outline: none;
border-style: dotted;
}
.button::-moz-focus-inner {
border: 0;
}
.values tbody + tbody tr:first-child td {
padding-top: 1em;
}

17
options/navigator.html Normal file
View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<title>CanvasBlocker navigator settings</title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<link rel="stylesheet" type="text/css" media="screen" href="navigator.css" />
</head>
<body>
<script src="../lib/require.js"></script>
<script src="../lib/logging.js"></script>
<script src="../lib/settingDefinitions.js"></script>
<script src="../lib/settingContainers.js"></script>
<script src="../lib/settings.js"></script>
<script src="../lib/navigator.js"></script>
<script src="navigator.js"></script>
</body>
</html>

237
options/navigator.js Normal file
View File

@ -0,0 +1,237 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
(function(){
"use strict";
const settings = require("./settings");
const navigator = require("./navigator");
const title = document.createElement("h1");
title.className = "title";
title.textContent = browser.i18n.getMessage("navigatorSettings_title");
document.body.appendChild(title);
document.querySelector("head title").textContent = title.textContent;
const description = document.createElement("div");
description.className = "description";
description.textContent = browser.i18n.getMessage("navigatorSettings_description");
document.body.appendChild(description);
function presetSection(title, presets){
const container = document.createElement("div");
container.className = "presetSection";
const titleNode = document.createElement("h2");
titleNode.className = "title";
titleNode.textContent = browser.i18n.getMessage("navigatorSettings_presetSection." + title);
container.appendChild(titleNode);
const presetsList = document.createElement("ul");
presetsList.className = "presets";
container.appendChild(presetsList);
Object.keys(presets).forEach(function(presetName){
const li = document.createElement("li");
li.className = "preset " + presetName;
presetsList.appendChild(li);
const button = document.createElement("button");
button.className = "button";
button.textContent = presetName;
li.appendChild(button);
const presetProperties = presets[presetName];
function checkActive(currentProperties){
if (Object.keys(presetProperties).every(function(property){
return currentProperties[property] === presetProperties[property];
})){
li.classList.add("active");
}
else {
li.classList.remove("active");
}
}
settings.on("navigatorDetails", function({newValue}){
checkActive(newValue);
});
settings.onloaded(function(){
checkActive(settings.navigatorDetails);
});
button.addEventListener("click", function(){
const data = settings.navigatorDetails;
Object.keys(presetProperties).forEach(function(property){
if (presetProperties[property] === undefined){
delete data[property];
}
else {
data[property] = presetProperties[property];
}
});
settings.navigatorDetails = data;
});
});
return container;
}
const osPresets = {
Windows: {
windowManager: "Windows",
platform: "Win32",
platformDetails: "Windows NT 10.0; Win64; x64",
oscpu: "{platformDetails}"
},
Linux: {
windowManager: "X11",
platform: "Linux x86_64",
platformDetails: "X11; Linux x86_64",
oscpu: "{platform}"
},
"Mac OS X": {
windowManager: "Macintosh",
platform: "MacIntel",
platformDetails: "Macintosh; {oscpu}",
oscpu: "Intel Mac OS X 10.14.3"
}
};
const browserPresets = {
Edge: {
chromeVersion: "71.0.3578.98",
edgeVersion: "17.17134",
firefoxVersion: undefined,
operaVersion: undefined,
safariVersion: undefined,
appVersion: "5.0 ({platformDetails}) AppleWebKit/537.36 (KHTML, like Gecko) " +
"Chrome/{chromeVersion} Safari/537.36 Edge/{edgeVersion}",
productSub: "20030107",
userAgent: "Mozilla/{appVersion}",
vendor: undefined,
},
Opera: {
chromeVersion: "71.0.3578.98",
edgeVersion: undefined,
firefoxVersion: undefined,
operaVersion: "58.0.3135.65",
safariVersion: undefined,
appVersion: "5.0 ({platformDetails}) AppleWebKit/537.36 (KHTML, like Gecko) " +
"Chrome/{chromeVersion} Safari/537.36 OPR/{operaVersion}",
productSub: "20030107",
userAgent: "Mozilla/{appVersion}",
vendor: "Google Inc.",
},
Chrome: {
chromeVersion: "71.0.3578.98",
edgeVersion: undefined,
firefoxVersion: undefined,
operaVersion: undefined,
safariVersion: undefined,
appVersion: "5.0 ({platformDetails}) AppleWebKit/537.36 (KHTML, like Gecko) " +
"Chrome/{chromeVersion} Safari/537.36",
productSub: "20030107",
userAgent: "Mozilla/{appVersion}",
vendor: "Google Inc.",
},
Safari: {
chromeVersion: undefined,
edgeVersion: undefined,
firefoxVersion: undefined,
operaVersion: undefined,
safariVersion: "12.0.3",
appVersion: "5.0 ({platformDetails}) AppleWebKit/605.1.15 (KHTML, like Gecko) " +
"Version/{safariVersion} Safari/605.1.15",
productSub: "20030107",
userAgent: "Mozilla/{appVersion}",
vendor: "Apple Computer, Inc.",
},
Firefox: {
chromeVersion: undefined,
edgeVersion: undefined,
firefoxVersion: "{real Firefox version}",
operaVersion: undefined,
safariVersion: undefined,
appVersion: "5.0 ({windowManager})",
buildID: "20181001000000",
productSub: "20100101",
userAgent: "Mozilla/5.0 ({platformDetails}; rv:{firefoxVersion}) Gecko/20100101 Firefox/{firefoxVersion}",
vendor: undefined,
}
};
document.body.appendChild(presetSection("os", osPresets));
document.body.appendChild(presetSection("browser", browserPresets));
const valueTitle = document.createElement("h2");
valueTitle.textContent = browser.i18n.getMessage("navigatorSettings_values");
document.body.appendChild(valueTitle);
const valueSection = document.createElement("table");
valueSection.className = "values";
document.body.appendChild(valueSection);
function updateValueSection(currentProperties){
function createPropertyRow(section, property){
const row = document.createElement("tr");
const name = document.createElement("td");
name.textContent = property;
row.appendChild(name);
const value = document.createElement("td");
row.appendChild(value);
const input = document.createElement("input");
value.appendChild(input);
input.value = currentProperties.hasOwnProperty(property)? currentProperties[property]: "{original value}";
input.addEventListener("change", function(){
currentProperties[property] = this.value;
settings.navigatorDetails = currentProperties;
});
const computedValue = document.createElement("td");
computedValue.textContent = navigator.getNavigatorValue(property);
row.appendChild(computedValue);
section.appendChild(row);
}
valueSection.innerHTML = "";
let section = document.createElement("tbody");
section.className = "helperValues";
valueSection.appendChild(section);
Object.keys(currentProperties).filter(function(property){
return navigator.allProperties.indexOf(property) === -1;
}).sort().forEach(createPropertyRow.bind(undefined, section));
section = document.createElement("tbody");
section.className = "realValues";
valueSection.appendChild(section);
navigator.allProperties.forEach(createPropertyRow.bind(undefined, section));
}
settings.on("navigatorDetails", function({newValue}){
updateValueSection(newValue);
});
settings.onloaded(function(){
updateValueSection(settings.navigatorDetails);
});
const resetButton = document.createElement("button");
resetButton.className = "button";
resetButton.textContent = browser.i18n.getMessage("navigatorSettings_reset");
resetButton.addEventListener("click", function(){
settings.navigatorDetails = {};
});
document.body.appendChild(resetButton);
}());

View File

@ -15,6 +15,10 @@
const searchParameters = new URLSearchParams(window.location.search);
var callbacks = {
openNavigatorSettings: function(){
logging.verbose("open navigator settings");
window.open("navigator.html", "_blank");
},
showReleaseNotes: function(){
logging.verbose("open release notes");
window.open("../releaseNotes.txt", "_blank");

View File

@ -486,6 +486,28 @@
"displayAdvancedSettings": [true]
}
},
"Navigator-API",
{
"name": "protectNavigator"
},
{
"name": "protectedAPIFeatures",
"displayedSection": "Navigator-API",
"displayDependencies": [
{
"protectNavigator": [true],
"displayAdvancedSettings": [true]
}
]
},
{
"name": "openNavigatorSettings",
"displayDependencies": [
{
"protectNavigator": [true]
}
]
},
"misc",
{
"name": "theme"

View File

@ -3,7 +3,7 @@ Version 0.5.9:
-
new features:
-
- added protection for navigator properties
fixes:
-

View File

@ -17,6 +17,7 @@
<li><a href="detectionTest.html">Detection test</a></li>
<li><a href="performanceTest.html">Performance test</a></li>
<li><a href="webGL-Test.html">Support for webGL</a></li>
<li><a href="navigatorTest.php">Navigator test</a></li>
<li><a href="settingsLoading.php">Settings loading</a></li>
</ul>
</body></html>

34
test/navigatorTest.js Normal file
View File

@ -0,0 +1,34 @@
var createLog = function(){
"use strict";
var div = document.getElementById("log");
return function createLog(){
var logDiv = document.createElement("div");
logDiv.className = "log";
div.appendChild(logDiv);
return function createLine(str){
var logLine = document.createElement("div");
logLine.className = "logLine";
logDiv.appendChild(logLine);
logLine.textContent = str;
return function updateLine(str){
logLine.textContent = str;
};
};
};
}();
var log = createLog();
log("user agent equal between server and client: " + (serverUserAgent === navigator.userAgent));
Object.keys(navigator.__proto__).sort().forEach(function(property){
"use strict";
var value = navigator[property];
if ((typeof value) === "string"){
log(property + ": " + value);
}
});

23
test/navigatorTest.php Normal file
View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<title>Navigator test</title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<link href="testIcon.svg" type="image/png" rel="icon">
<link href="testIcon.svg" type="image/png" rel="shortcut icon">
</head>
<body>
<h1>Navigator test</h1>
Tests the navigator properties.
<div id="log">
<div class="log">
<div class="logLine">
server site user agent: <?php echo htmlentities($_SERVER["HTTP_USER_AGENT"], ENT_QUOTES, "UTF-8");?>
</div>
</div>
</div>
<script>
var serverUserAgent = <?php echo json_encode($_SERVER["HTTP_USER_AGENT"]);?>;
</script>
<script src="navigatorTest.js"></script>
</body></html>