Publish Vulpes Preview 15 sources for desktop, Android and Tundra

Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View file

@ -0,0 +1,220 @@
/* global DUMP, SettingsHelper, SettingsListener, SettingsURL, FindMyDevice,
PasscodeHelper */
'use strict';
var Commands = {
TRACK_UPDATE_INTERVAL_MS: 10000,
_ringer: null,
_lockscreenEnabled: false,
_lockscreenPassCodeEnabled: false,
_geolocationEnabled: false,
init: function fmdc_init() {
var ringer = this._ringer = new Audio();
ringer.mozAudioChannelType = 'ringer';
ringer.loop = true;
var ringtoneURL = new SettingsURL();
SettingsListener.observe('dialer.ringtone', '', function(value) {
var ringing = !ringer.paused;
ringer.pause();
ringer.src = ringtoneURL.set(value);
if (ringing) {
ringer.play();
}
});
var self = this;
SettingsListener.observe('lockscreen.enabled', false, function(value) {
self._lockscreenEnabled = value;
});
SettingsListener.observe('lockscreen.passcode-lock.enabled', false,
function(value) {
self._lockscreenPassCodeEnabled = value;
}
);
SettingsListener.observe('geolocation.enabled', false, function(value) {
self._geolocationEnabled = value;
});
},
getEnabledCommands: function fmdc_get_enabled_commands() {
var commands = Object.keys(this._commands);
if (!this._geolocationEnabled) {
var idx = commands.indexOf('track');
if (idx >= 0) {
commands.splice(idx, 1);
}
}
return commands;
},
invokeCommand: function fmdc_get_command(name, args) {
FindMyDevice.beginHighPriority('command');
this._commands[name].apply(this, args);
},
deviceHasPasscode: function fmdc_device_has_passcode() {
return !!(this._lockscreenEnabled && this._lockscreenPassCodeEnabled);
},
_watchPositionId: null,
_trackTimeoutId: null,
_ringTimeoutId: null,
_commands: {
track: function fmdc_track(duration, reply) {
var self = this;
function stop() {
navigator.geolocation.clearWatch(self._watchPositionId);
self._watchPositionId = null;
clearTimeout(self._trackTimeoutId);
self._trackTimeoutId = null;
SettingsHelper('findmydevice.tracking').set(false);
FindMyDevice.endHighPriority('command');
}
if (this._watchPositionId !== null || this._trackTimeoutId !== null) {
// already tracking
stop();
}
if (duration === 0) {
if (reply) {
reply(true);
}
FindMyDevice.endHighPriority('command');
return;
}
var lastPositionTimestamp = 0;
// start watching the current position, but throttle updates to one
// every TRACK_UPDATE_INTERVAL_MS
SettingsHelper('findmydevice.tracking').set(true);
self._watchPositionId = navigator.geolocation.watchPosition(
function(position) {
DUMP('received location (' +
position.coords.latitude + ', ' +
position.coords.longitude + ')'
);
var timeElapsed = position.timestamp - lastPositionTimestamp;
if (timeElapsed < self.TRACK_UPDATE_INTERVAL_MS) {
DUMP('ignoring position due to throttling');
return;
}
lastPositionTimestamp = position.timestamp;
reply(true, position);
}, function(error) {
reply(false, 'failed to get location: ' + error.message);
});
duration = (isNaN(duration) || duration < 0) ? 1 : duration;
self._trackTimeoutId = setTimeout(stop, duration * 1000);
},
erase: function fmdc_erase(reply) {
navigator.mozPower.factoryReset('wipe');
// factoryReset() won't return, unless we're testing,
// in which case mozPower is a mock. The reply() below
// is thus only used for testing.
reply(true);
},
killswitch: function fmdc_killswitch(reply) {
if (navigator.mozKillSwitch) {
navigator.mozKillSwitch.enable();
}
FindMyDevice.endHighPriority('command');
reply(true);
},
lock: function fmdc_lock(message, passcode, reply) {
var pr;
var settings = {
'lockscreen.enabled': true,
'lockscreen.notifications-preview.enabled': false,
'lockscreen.passcode-lock.enabled': true,
'lockscreen.lock-immediately': true
};
if (message) {
settings['lockscreen.lock-message'] = message;
}
if (passcode) {
pr = PasscodeHelper.set(passcode);
}
var request = SettingsListener.getSettingsLock().set(settings);
Promise.all([pr, request]).then(() => {
reply(true);
}).catch(() => {
reply(false);
});
FindMyDevice.endHighPriority('command');
},
ring: function fmdc_ring(duration, reply) {
var ringer = this._ringer;
var stop = function() {
ringer.pause();
ringer.currentTime = 0;
clearTimeout(this._ringTimeoutId);
this._ringTimeoutId = null;
FindMyDevice.endHighPriority('command');
}.bind(this);
var ringing = !ringer.paused || this._ringTimeoutId !== null;
if (ringing || duration === 0) {
if (ringing && duration === 0) {
stop();
}
if (reply) {
reply(true);
}
FindMyDevice.endHighPriority('command');
return;
}
var request = SettingsListener.getSettingsLock().set({
// hard-coded max volume taken from
// https://wiki.mozilla.org/WebAPI/AudioChannels
'audio.volume.notification': 15
});
request.onsuccess = function() {
ringer.play();
reply(true);
};
request.onerror = function() {
reply(false, 'failed to set volume');
};
this._ringTimeoutId = setTimeout(stop, duration * 1000);
}
}
};
Commands.init();

View file

@ -0,0 +1,4 @@
Config = {
"api_url": "https://find.firefox.com",
"api_version": "1"
};

View file

@ -0,0 +1,665 @@
/* global SettingsHelper */
/* global asyncStorage */
/* global Requester */
/* global Commands */
/* global Config */
/* global DUMP */
/* global IAC_API_WAKEUP_REASON_ENABLED_CHANGED */
/* global IAC_API_WAKEUP_REASON_LOGIN */
/* global IAC_API_WAKEUP_REASON_LOGOUT */
/* global IAC_API_WAKEUP_REASON_STALE_REGISTRATION */
/* global IAC_API_WAKEUP_REASON_TRY_DISABLE */
/* global IAC_API_WAKEUP_REASON_LOCKSCREEN_CLOSED */
'use strict';
var FindMyDevice = {
_state: null,
_registering: false,
_refreshingClientID: false,
_reply: {},
_registered: false,
_registeredHelper: null,
_enabled: false,
_enabledHelper: null,
_loggedIn: false,
_currentClientID: '',
_currentClientIDHelper: null,
_canDisable: false,
_canDisableHelper: null,
_retryCount: -1,
_retryCountHelper: null,
_disableAttempt: false,
// There are two situations in which we want to make sure
// FMD is alive:
// - when it is running client logic that accesses the network;
// - when it is executing a command with a duration.
// Command wakelocks are managed by individual commands.
// For client logic wakelocks, we use the fact that when a network
// request succeeds, we'll either update 'findmydevice.registered',
// or 'findmydevice.can-disable', or process a command and set
// a 'ping' timer, and when it fails, we'll set a timer to retry.
// Since each network transaction finishes with either a change
// in one of these settings or with a timer, we release the locks
// in the observers for the settings and in _scheduleAlarm.
_highPriorityWakeLocks: {
clientLogic: [],
command: []
},
_fxaReady: false,
init: function fmd_init() {
var self = this;
navigator.mozId.watch({
wantIssuer: 'firefox-accounts',
audience: Config.api_url,
onready: self._onReady.bind(self),
onlogin: self._onLogin.bind(self),
onlogout: self._onLogout.bind(self),
onerror: self._onFxAError.bind(self)
});
},
_observeSettings: function fmd_observe_settings() {
var settings = navigator.mozSettings;
settings.addObserver('findmydevice.registered',
this._onRegisteredChanged.bind(this));
settings.addObserver('findmydevice.enabled',
this._onEnabledChanged.bind(this));
settings.addObserver('findmydevice.current-clientid',
this._onClientIDChanged.bind(this));
// We only allow disabling Find My Device if the same person
// who first enabled it is logged in to FxA, and in that case
// 'findmydevice.can-disable' is true.
// However, note that FMD doesn't store the user's email address,
// but rather a client ID that is derived from the FxA assertion
// and stored in this._state.clientid.
settings.addObserver('findmydevice.can-disable',
this._onCanDisableChanged.bind(this));
},
_onReady: function fmd_fxa_onready() {
var self = this;
this._loadState(function() {
self._initSettings(self._initMessageHandlers.bind(self));
});
this._fxaReady = true;
this._observeSettings();
},
_loadState: function fmd_load_state(callback) {
var self = this;
asyncStorage.getItem('findmydevice-state', function(state) {
self._state = state;
if (state) {
Requester.setHawkCredentials(state.deviceid, state.secret);
}
callback && callback();
});
},
_initSettings: function fmd_init_settings(callback) {
// for each setting we're interested in, create a SettingsHelper,
// cache its initial value in a property (e.g., _enabled), and
// store the SettingsHelper as another property (e.g., _enabledHelper).
var settingsToProperties = {
// setting name : property name for cached value
'findmydevice.enabled': '_enabled',
'findmydevice.registered': '_registered',
'findmydevice.current-clientid': '_currentClientID',
'findmydevice.can-disable': '_canDisable',
'findmydevice.retry-count': '_retryCount'
};
var settings = Object.keys(settingsToProperties);
var loaded = 0;
var totalSettings = Object.keys(settingsToProperties).length;
settings.forEach(function(s) {
var prop = settingsToProperties[s];
var helper = this[prop + 'Helper'] = SettingsHelper(s);
helper.get((function(value) {
this[prop] = value;
if (++loaded === totalSettings) {
callback && callback();
}
}).bind(this));
}, this);
},
_initMessageHandlers: function fmd_init_message_handlers() {
navigator.mozSetMessageHandler('push', (function(message) {
DUMP('findmydevice got push notification!');
this._contactServer();
}).bind(this));
navigator.mozSetMessageHandler('push-register', (function(message) {
DUMP('findmydevice lost push endpoint, re-registering');
this.beginHighPriority('clientLogic');
this._registeredHelper.set(false);
}).bind(this));
navigator.mozSetMessageHandler('alarm', (function(alarm) {
DUMP('findmydevice alarm!');
this._contactServer();
this._refreshClientIDIfRegistered(false);
}).bind(this));
navigator.mozSetMessageHandler('connection', (function(request) {
var port = request.port;
port.onmessage = (function(event) {
if (request.keyword === 'findmydevice-wakeup') {
DUMP('got wake up request');
var reason = event.data;
if (reason === IAC_API_WAKEUP_REASON_ENABLED_CHANGED) {
DUMP('enabled state changed, trying to reach the server');
// Ensure the retry counter is reset to 0 on enable
this._retryCountHelper.set(0);
this._contactServer();
} else if (reason === IAC_API_WAKEUP_REASON_STALE_REGISTRATION) {
DUMP('stale registration, re-registering');
this.beginHighPriority('clientLogic');
this._registeredHelper.set(false);
} else if (reason === IAC_API_WAKEUP_REASON_LOGIN) {
DUMP('new login, invalidating client id');
this.beginHighPriority('clientLogic');
this._loggedIn = true;
this._currentClientIDHelper.set('');
} else if (reason === IAC_API_WAKEUP_REASON_LOGOUT) {
DUMP('logout, invalidating client id');
this.beginHighPriority('clientLogic');
this._loggedIn = false;
this._currentClientIDHelper.set('');
} else if (reason === IAC_API_WAKEUP_REASON_TRY_DISABLE) {
DUMP('refreshing client id and attempting to disable');
this._disableAttempt = true;
this._refreshClientIDIfRegistered(true);
} else if (reason === IAC_API_WAKEUP_REASON_LOCKSCREEN_CLOSED) {
DUMP('unlocked');
this._onLockscreenClosed();
}
return;
}
if (request.keyword === 'findmydevice-test') {
DUMP('got request for test command!');
this._processCommands(event.data, true);
}
}).bind(this);
}).bind(this));
},
_contactServer: function fmd_contact_server() {
this.beginHighPriority('clientLogic');
if (this._registered && this._enabled) {
this._replyAndFetchCommands();
} else if (this._registered && !this._enabled) {
this._reportDisabled();
} else if (!this._registered && this._enabled) {
this._register();
} else {
// XXX(ggp) this should never happen, but let's play
// it safe and release lock we acquired
DUMP('can\'t contact the server while not registered and not enabled!!');
this.endHighPriority('clientLogic');
}
},
_reportDisabled: function fmd_report_disabled() {
DUMP('reporting disabled');
Requester.post(
this._getCommandEndpoint(),
{enabled: false},
this._handleServerResponse.bind(this),
this._handleServerError.bind(this));
},
_register: function fmd_register() {
DUMP('findmydevice attempting registration.');
DUMP('registering: ' + this._registering);
if (this._registering) {
this._registering = false;
this.endHighPriority('clientLogic');
return;
}
this._registering = true;
if (this._state === null) {
// We are not registered yet, so we need to give the server a FxA
// assertion. Request a fresh one here and continue the registration
// process from the login callback.
if (!this._loggedIn) {
// Can't request() while not logged in, as that would bring up the
// FxA dialog. Attempting a first registration while logged out is
// theoretically possible (if, for example, a first registration
// failed, and this is an alarm-driven re-attempt, and the user logged
// out while we slept before this attempt), but it's a very unlikely
// corner case. For now, just give up if this happens, but we still
// need to notify the user somehow (bug 1013423).
this._registering = false;
this.endHighPriority('clientLogic');
return;
}
navigator.mozId.request();
} else {
// We don't need to send assertions on re-registrations, just rely on
// our client ID and HAWK signature to authenticate us. This works even
// if we're logged out of FxA, so use null for the assertion.
this._continueRegistration(null);
}
},
_onLogin: function fmd_on_login(assertion) {
DUMP('logged in to FxA');
// XXX(ggp) When initializing, we'll process FxA's automatic invocation
// of onlogin/onlogout before any IAC message, so use these calls to
// initialize _loggedIn. However, when FMD is already running, there's
// no guaranteee that onlogin/onlogout will fire before the IAC handler,
// so we have dedicated IAC messages (IAC_API_WAKEUP_REASON_LOGIN and
// IAC_API_WAKEUP_REASON_LOGOUT) to cover login state changes, and we
// use the handlers for these messages to update _loggedIn.
this._loggedIn = true;
if (this._refreshingClientID) {
DUMP('resuming client id refresh');
this._fetchClientID(assertion);
}
if (!this._enabled || !this._registering) {
return;
}
// We are in the middle of a registration, so continue here
this._continueRegistration(assertion);
},
_continueRegistration: function fmd_continue_registration(assertion) {
var self = this;
if (assertion == null && this._state == null) {
// this shouldn't happen
throw new Error('Trying to register with no assertion and no state!');
}
var pushRequest = navigator.push.register();
pushRequest.onsuccess = function fmd_push_handler() {
DUMP('findmydevice received push endpoint!');
var endpoint = pushRequest.result;
if (self._enabled) {
self._requestRegistration(assertion, endpoint);
}
self._registering = false;
};
pushRequest.onerror = function fmd_push_error_handler() {
DUMP('findmydevice push request failed!');
self._registering = false;
self._countRegistrationRetry();
};
},
_requestRegistration: function fmd_request_registration(assertion, endpoint) {
var obj = {
pushurl: endpoint,
accepts: Commands.getEnabledCommands()
};
if (assertion != null) {
obj.assert = assertion;
}
if (this._state !== null) {
obj.deviceid = this._state.deviceid;
}
var self = this;
Requester.post('/register/', obj, function(response) {
DUMP('findmydevice successfully registered: ', response);
asyncStorage.setItem('findmydevice-state', response, function() {
self._registeredHelper.set(true);
});
}, this._handleServerError.bind(this));
},
_onLogout: function fmd_fxa_onlogout() {
DUMP('logged out of FxA');
this._loggedIn = false;
},
_onFxAError: function fmd_on_error(error) {
DUMP('FxA error: ' + error);
if (!this._fxaReady) {
// FIXME(ggp) workaround for bug 1040935, if FxA errors out
// while we are initializing (usually due to an unverified
// account), just give up for now.
window.close();
}
if (this._refreshingClientID) {
this._cancelClientIDRefresh();
}
this._scheduleAlarm('retry');
},
_cancelClientIDRefresh: function fmd_cancel_clientid_refresh() {
this._disableAttempt = false;
this._refreshingClientID = false;
// If we've been woken up by the Settings app because of a disable
// attempt, it expects us to change these two settings at the end
// of the operation, so set them to their current values if we're
// canceling to make sure the state in the Settings is updated
// properly.
this._enabledHelper.set(this._enabled);
this._currentClientIDHelper.set(this._currentClientID);
// XXX(ggp) no need to unlock the 'currentClient' lock here, let
// _onClientIDChanged do it for us
},
_scheduleAlarm: function fmd_schedule_alarm(mode) {
var nextAlarm = new Date();
if (mode === 'ping') {
// this is just a regular ping to the server to make
// sure we're still registered, so use a long interval
nextAlarm.setHours(nextAlarm.getHours() + 6);
} else if (mode === 'retry') {
// something went wrong when registering or talking to the
// server, we should check back shortly
var interval = 1 + Math.floor(5 * Math.random());
nextAlarm.setMinutes(nextAlarm.getMinutes() + interval);
} else {
DUMP('invalid alarm mode!');
return;
}
var self = this;
var request = navigator.mozAlarms.getAll();
request.onsuccess = function fmd_alarms_get_all() {
this.result.forEach(function(alarm) {
navigator.mozAlarms.remove(alarm.id);
});
var data = {type: 'findmydevice-alarm'};
var request = navigator.mozAlarms.add(nextAlarm, 'honorTimezone', data);
request.onsuccess = function() {
self.endHighPriority('clientLogic');
};
};
},
_replyAndFetchCommands: function fmd_reply_and_fetch() {
this._reply.has_passcode = Commands.deviceHasPasscode();
Requester.post(
this._getCommandEndpoint(),
this._reply,
this._handleServerResponse.bind(this),
this._handleServerError.bind(this));
this._reply = {};
},
_onRegisteredChanged: function fmd_registered_changed(event) {
this._registered = event.settingValue;
DUMP('registered: ' + this._registered);
if (!this._registered) {
this._contactServer();
this.endHighPriority('clientLogic');
} else {
this._loadState((function() {
this._contactServer();
this._currentClientIDHelper.set('');
// XXX(ggp) we're still holding a 'clientLogic' lock, but let
// _onClientIDChanged or _onCanDisableChanged release it (depending
// on whether we're logged in or not).
}).bind(this));
}
},
_onEnabledChanged: function fmd_enabled_changed(event) {
// No need to contact the server here if we've been enabled,
// since that means we'll get a wake up request from the System
// app
this._enabled = event.settingValue;
DUMP('enabled: ' + this._enabled);
},
_onClientIDChanged: function fmd_client_id_changed(event) {
this._currentClientID = event.settingValue;
DUMP('current id set to: ', this._currentClientID);
if (this._loggedIn && this._currentClientID === '') {
this._refreshClientIDIfRegistered(false);
this.endHighPriority('clientLogic');
} else if (this._registered) {
this._canDisableHelper.set(
this._loggedIn &&
this._currentClientID === this._state.clientid);
} else {
this.endHighPriority('clientLogic');
}
},
_onCanDisableChanged: function fmd_can_disable_changed(event) {
if (event.settingValue === true && this._disableAttempt) {
this._enabledHelper.set(false);
}
this._disableAttempt = false;
this.endHighPriority('clientLogic');
},
_refreshClientIDIfRegistered: function fmd_refresh_client_id(forceReauth) {
DUMP('refreshing client id if registered and logged in: ',
{registered: this._registered, loggedIn: this._loggedIn});
if (!this._registered || !this._loggedIn) {
return;
}
DUMP('requesting assertion to refresh client id, forceReauth: ' +
forceReauth);
this.beginHighPriority('clientLogic');
this._refreshingClientID = true;
var mozIdRequestOptions = {};
if (forceReauth) {
mozIdRequestOptions.refreshAuthentication = 0;
mozIdRequestOptions.oncancel = this._cancelClientIDRefresh.bind(this);
}
navigator.mozId.request(mozIdRequestOptions);
},
_onLockscreenClosed: function fmd_on_lockscreen_closed() {
if (Commands.deviceHasPasscode()) {
DUMP('cancelling ring and track');
Commands.invokeCommand('ring', [0]);
Commands.invokeCommand('track', [0]);
}
},
_fetchClientID: function fmd_fetch_client_id(assertion) {
Requester.post('/validate/', {assert: assertion},
this._onClientIDResponse.bind(this),
this._onClientIDServerError.bind(this));
this._refreshingClientID = false;
},
_onClientIDResponse: function fmd_on_client_id(response) {
DUMP('got clientid reponse: ', response);
if (response.valid) {
this._currentClientIDHelper.set(response.uid);
return;
}
DUMP('failed to verify assertion for client id!');
this._scheduleAlarm('retry');
},
_onClientIDServerError: function fmd_on_client_id_error(err) {
DUMP('failed to fetch client id with status: ' + err.status);
this._scheduleAlarm('retry');
this._disableAttempt = false;
},
_handleServerResponse: function(response, testing) {
if (response && (this._enabled || testing)) {
this._processCommands(response);
}
this._scheduleAlarm('ping');
},
_processCommands: function fmd_process_commands(cmdobj) {
function noop() {} // callback for testing
for (var cmd in cmdobj) {
// map server (short) commands to methods in the
// commands object, and parse the arguments.
var argsObj = cmdobj[cmd], command, args;
switch (cmd) {
case 'e':
command = 'erase';
args = [];
break;
case 'k':
command = 'killswitch';
args = [];
break;
case 'l':
command = 'lock';
args = [argsObj.m, argsObj.c];
break;
case 'r':
command = 'ring';
args = [parseInt(argsObj.d, 10)];
break;
case 't':
command = 'track';
args = [parseInt(argsObj.d, 10)];
break;
default:
this._replyCallback(cmd, false, 'command not available');
return;
}
DUMP('command ' + cmd + ', args ' + JSON.stringify(args));
// add the callback as the last argument
if (cmdobj.testing !== true) {
args.push(this._replyCallback.bind(this, cmd));
} else {
args.push(noop);
}
Commands.invokeCommand(command, args);
}
},
_countRegistrationRetry: function fmd_count_registration_retry (){
this._scheduleAlarm('retry');
if (!navigator.onLine) {
return;
}
if (!this._registered) {
this._retryCountHelper.get(count => {
this._retryCountHelper.set((count || 0) + 1);
});
}
},
_handleServerError: function fmd_handle_server_error(err) {
DUMP('findmydevice request failed with status: ' + err.status);
if (err.status === 401 && this._registered) {
this._registeredHelper.set(false);
} else {
this._countRegistrationRetry();
}
},
_replyCallback: function fmd_reply(cmd, ok, retval) {
var value = {ok: ok};
if (cmd === 't' && ok === true && retval !== undefined) {
value.la = retval.coords.latitude;
value.lo = retval.coords.longitude;
value.acc = retval.coords.accuracy;
value.ti = retval.timestamp;
} else if (ok === false) {
value.error = retval;
}
this._reply[cmd] = value;
this._contactServer();
},
_getCommandEndpoint: function fmd_get_command_endpoint() {
return '/cmd/' + this._state.deviceid;
},
beginHighPriority: function(reason) {
DUMP('begin high priority section, reason: ', reason);
if (Object.keys(this._highPriorityWakeLocks).indexOf(reason) === -1) {
DUMP('unknown reason for high priority section?!');
return;
}
DUMP('acquiring one wakelock, wakelocks are: ',
this._highPriorityWakeLocks);
this._highPriorityWakeLocks[reason].push(
navigator.requestWakeLock('high-priority'));
},
endHighPriority: function(reason) {
DUMP('end high priority section, reason: ', reason);
if (!this._highPriorityWakeLocks[reason]) {
return;
}
DUMP('releasing one wakelock, wakelocks are: ',
this._highPriorityWakeLocks);
this._highPriorityWakeLocks[reason].pop().unlock();
}
};
FindMyDevice.init();

View file

@ -0,0 +1,507 @@
/*
HTTP Hawk Authentication Scheme
Copyright (c) 2012-2013, Eran Hammer <eran@hueniverse.com>
MIT Licensed
*/
// Declare namespace
var hawk = {};
// Export if used as a module
if (typeof module !== "undefined" && module.exports) {
module.exports = hawk;
}
hawk.client = {
// Generate an Authorization header for a given request
/*
uri: 'http://example.com/resource?a=b'
method: HTTP verb (e.g. 'GET', 'POST')
options: {
// Required
credentials: {
id: 'dh37fgj492je',
key: 'aoijedoaijsdlaksjdl',
algorithm: 'sha256' // 'sha1', 'sha256'
},
// Optional
ext: 'application-specific', // Application specific data sent via the ext attribute
timestamp: Date.now() / 1000, // A pre-calculated timestamp in seconds
nonce: '2334f34f', // A pre-generated nonce
localtimeOffsetMsec: 400, // Time offset to sync with server time (ignored if timestamp provided)
payload: '{"some":"payload"}', // UTF-8 encoded string for body hash generation (ignored if hash provided)
contentType: 'application/json', // Payload content-type (ignored if hash provided)
hash: 'U4MKKSmiVxk37JCCrAVIjV=', // Pre-calculated payload hash
app: '24s23423f34dx', // Oz application id
dlg: '234sz34tww3sd' // Oz delegated-by application id
}
*/
header: function (uri, method, options) {
var result = {
field: '',
artifacts: {}
};
// Validate inputs
if (!uri || (typeof uri !== 'string' && typeof uri !== 'object') ||
!method || typeof method !== 'string' ||
!options || typeof options !== 'object') {
result.err = 'Invalid argument type';
return result;
}
// Application time
var timestamp = options.timestamp || Math.floor((hawk.utils.now() + (options.localtimeOffsetMsec || 0)) / 1000)
// Validate credentials
var credentials = options.credentials;
if (!credentials ||
!credentials.id ||
!credentials.key ||
!credentials.algorithm) {
result.err = 'Invalid credential object';
return result;
}
if (hawk.crypto.algorithms.indexOf(credentials.algorithm) === -1) {
result.err = 'Unknown algorithm';
return result;
}
// Parse URI
if (typeof uri === 'string') {
uri = hawk.utils.parseUri(uri);
}
// Calculate signature
var artifacts = {
ts: timestamp,
nonce: options.nonce || hawk.utils.randomString(6),
method: method,
resource: uri.relative,
host: uri.hostname,
port: uri.port,
hash: options.hash,
ext: options.ext,
app: options.app,
dlg: options.dlg
};
result.artifacts = artifacts;
// Calculate payload hash
if (!artifacts.hash &&
options.hasOwnProperty('payload')) {
artifacts.hash = hawk.crypto.calculatePayloadHash(options.payload, credentials.algorithm, options.contentType);
}
var mac = hawk.crypto.calculateMac('header', credentials, artifacts);
// Construct header
var hasExt = artifacts.ext !== null && artifacts.ext !== undefined && artifacts.ext !== ''; // Other falsey values allowed
var header = 'Hawk id="' + credentials.id +
'", ts="' + artifacts.ts +
'", nonce="' + artifacts.nonce +
(artifacts.hash ? '", hash="' + artifacts.hash : '') +
(hasExt ? '", ext="' + hawk.utils.escapeHeaderAttribute(artifacts.ext) : '') +
'", mac="' + mac + '"';
if (artifacts.app) {
header += ', app="' + artifacts.app +
(artifacts.dlg ? '", dlg="' + artifacts.dlg : '') + '"';
}
result.field = header;
return result;
},
// Validate server response
/*
request: object created via 'new XMLHttpRequest()' after response received
artifacts: object recieved from header().artifacts
options: {
payload: optional payload received
required: specifies if a Server-Authorization header is required. Defaults to 'false'
}
*/
authenticate: function (request, credentials, artifacts, options) {
options = options || {};
if (request.getResponseHeader('www-authenticate')) {
// Parse HTTP WWW-Authenticate header
var attributes = hawk.utils.parseAuthorizationHeader(request.getResponseHeader('www-authenticate'), ['ts', 'tsm', 'error']);
if (!attributes) {
return false;
}
if (attributes.ts) {
var tsm = hawk.crypto.calculateTsMac(attributes.ts, credentials);
if (tsm !== attributes.tsm) {
return false;
}
hawk.utils.setNtpOffset(attributes.ts - Math.floor((new Date()).getTime() / 1000)); // Keep offset at 1 second precision
}
}
// Parse HTTP Server-Authorization header
if (!request.getResponseHeader('server-authorization') &&
!options.required) {
return true;
}
var attributes = hawk.utils.parseAuthorizationHeader(request.getResponseHeader('server-authorization'), ['mac', 'ext', 'hash']);
if (!attributes) {
return false;
}
var modArtifacts = {
ts: artifacts.ts,
nonce: artifacts.nonce,
method: artifacts.method,
resource: artifacts.resource,
host: artifacts.host,
port: artifacts.port,
hash: attributes.hash,
ext: attributes.ext,
app: artifacts.app,
dlg: artifacts.dlg
};
var mac = hawk.crypto.calculateMac('response', credentials, modArtifacts);
if (mac !== attributes.mac) {
return false;
}
if (!options.hasOwnProperty('payload')) {
return true;
}
if (!attributes.hash) {
return false;
}
var calculatedHash = hawk.crypto.calculatePayloadHash(options.payload, credentials.algorithm, request.getResponseHeader('content-type'));
return (calculatedHash === attributes.hash);
},
message: function (host, port, message, options) {
// Validate inputs
if (!host || typeof host !== 'string' ||
!port || typeof port !== 'number' ||
message === null || message === undefined || typeof message !== 'string' ||
!options || typeof options !== 'object') {
return null;
}
// Application time
var timestamp = options.timestamp || Math.floor((hawk.utils.now() + (options.localtimeOffsetMsec || 0)) / 1000)
// Validate credentials
var credentials = options.credentials;
if (!credentials ||
!credentials.id ||
!credentials.key ||
!credentials.algorithm) {
// Invalid credential object
return null;
}
if (hawk.crypto.algorithms.indexOf(credentials.algorithm) === -1) {
return null;
}
// Calculate signature
var artifacts = {
ts: timestamp,
nonce: options.nonce || hawk.utils.randomString(6),
host: host,
port: port,
hash: hawk.crypto.calculatePayloadHash(message, credentials.algorithm)
};
// Construct authorization
var result = {
id: credentials.id,
ts: artifacts.ts,
nonce: artifacts.nonce,
hash: artifacts.hash,
mac: hawk.crypto.calculateMac('message', credentials, artifacts)
};
return result;
},
authenticateTimestamp: function (message, credentials, updateClock) { // updateClock defaults to true
var tsm = hawk.crypto.calculateTsMac(message.ts, credentials);
if (tsm !== message.tsm) {
return false;
}
if (updateClock !== false) {
hawk.utils.setNtpOffset(message.ts - Math.floor((new Date()).getTime() / 1000)); // Keep offset at 1 second precision
}
return true;
}
};
hawk.crypto = {
headerVersion: '1',
algorithms: ['sha1', 'sha256'],
calculateMac: function (type, credentials, options) {
var normalized = hawk.crypto.generateNormalizedString(type, options);
var hmac = CryptoJS['Hmac' + credentials.algorithm.toUpperCase()](normalized, credentials.key);
return hmac.toString(CryptoJS.enc.Base64);
},
generateNormalizedString: function (type, options) {
var normalized = 'hawk.' + hawk.crypto.headerVersion + '.' + type + '\n' +
options.ts + '\n' +
options.nonce + '\n' +
(options.method || '').toUpperCase() + '\n' +
(options.resource || '') + '\n' +
options.host.toLowerCase() + '\n' +
options.port + '\n' +
(options.hash || '') + '\n';
if (options.ext) {
normalized += options.ext.replace('\\', '\\\\').replace('\n', '\\n');
}
normalized += '\n';
if (options.app) {
normalized += options.app + '\n' +
(options.dlg || '') + '\n';
}
return normalized;
},
calculatePayloadHash: function (payload, algorithm, contentType) {
var hash = CryptoJS.algo[algorithm.toUpperCase()].create();
hash.update('hawk.' + hawk.crypto.headerVersion + '.payload\n');
hash.update(hawk.utils.parseContentType(contentType) + '\n');
hash.update(payload || '');
hash.update('\n');
return hash.finalize().toString(CryptoJS.enc.Base64);
},
calculateTsMac: function (ts, credentials) {
var hash = CryptoJS['Hmac' + credentials.algorithm.toUpperCase()]('hawk.' + hawk.crypto.headerVersion + '.ts\n' + ts + '\n', credentials.key);
return hash.toString(CryptoJS.enc.Base64);
}
};
hawk.utils = {
storage: { // localStorage compatible interface
_cache: {},
setItem: function (key, value) {
hawk.utils.storage._cache[key] = value;
},
getItem: function (key) {
return hawk.utils.storage._cache[key];
}
},
setStorage: function (storage) {
var ntpOffset = hawk.utils.getNtpOffset() || 0;
hawk.utils.storage = storage;
hawk.utils.setNtpOffset(ntpOffset);
},
setNtpOffset: function (offset) {
try {
hawk.utils.storage.setItem('hawk_ntp_offset', offset);
}
catch (err) {
console.error('[hawk] could not write to storage.');
console.error(err);
}
},
getNtpOffset: function () {
return parseInt(hawk.utils.storage.getItem('hawk_ntp_offset') || '0', 10);
},
now: function () {
return (new Date()).getTime() + hawk.utils.getNtpOffset();
},
escapeHeaderAttribute: function (attribute) {
return attribute.replace(/\\/g, '\\\\').replace(/\"/g, '\\"');
},
parseContentType: function (header) {
if (!header) {
return '';
}
return header.split(';')[0].replace(/^\s+|\s+$/g, '').toLowerCase();
},
parseAuthorizationHeader: function (header, keys) {
if (!header) {
return null;
}
var headerParts = header.match(/^(\w+)(?:\s+(.*))?$/); // Header: scheme[ something]
if (!headerParts) {
return null;
}
var scheme = headerParts[1];
if (scheme.toLowerCase() !== 'hawk') {
return null;
}
var attributesString = headerParts[2];
if (!attributesString) {
return null;
}
var attributes = {};
var verify = attributesString.replace(/(\w+)="([^"\\]*)"\s*(?:,\s*|$)/g, function ($0, $1, $2) {
// Check valid attribute names
if (keys.indexOf($1) === -1) {
return;
}
// Allowed attribute value characters: !#$%&'()*+,-./:;<=>?@[]^_`{|}~ and space, a-z, A-Z, 0-9
if ($2.match(/^[ \w\!#\$%&'\(\)\*\+,\-\.\/\:;<\=>\?@\[\]\^`\{\|\}~]+$/) === null) {
return;
}
// Check for duplicates
if (attributes.hasOwnProperty($1)) {
return;
}
attributes[$1] = $2;
return '';
});
if (verify !== '') {
return null;
}
return attributes;
},
randomString: function (size) {
var randomSource = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
var len = randomSource.length;
var result = [];
for (var i = 0; i < size; ++i) {
result[i] = randomSource[Math.floor(Math.random() * len)];
}
return result.join('');
},
parseUri: function (input) {
// Based on: parseURI 1.2.2
// http://blog.stevenlevithan.com/archives/parseuri
// (c) Steven Levithan <stevenlevithan.com>
// MIT License
var keys = ['source', 'protocol', 'authority', 'userInfo', 'user', 'password', 'hostname', 'port', 'resource', 'relative', 'pathname', 'directory', 'file', 'query', 'fragment'];
var uriRegex = /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?(((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?)(?:#(.*))?)/;
var uriByNumber = uriRegex.exec(input);
var uri = {};
var i = 15;
while (i--) {
uri[keys[i]] = uriByNumber[i] || '';
}
if (uri.port === null ||
uri.port === '') {
uri.port = (uri.protocol.toLowerCase() === 'http' ? '80' : (uri.protocol.toLowerCase() === 'https' ? '443' : ''));
}
return uri;
}
};
// Based on: Crypto-JS v3.1.2
// Copyright (c) 2009-2013, Jeff Mott. All rights reserved.
// http://code.google.com/p/crypto-js/
// http://code.google.com/p/crypto-js/wiki/License
var CryptoJS=CryptoJS||function(h,r){var k={},l=k.lib={},n=function(){},f=l.Base={extend:function(a){n.prototype=this;var b=new n;a&&b.mixIn(a);b.hasOwnProperty("init")||(b.init=function(){b.$super.init.apply(this,arguments)});b.init.prototype=b;b.$super=this;return b},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var b in a)a.hasOwnProperty(b)&&(this[b]=a[b]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}},j=l.WordArray=f.extend({init:function(a,b){a=this.words=a||[];this.sigBytes=b!=r?b:4*a.length},toString:function(a){return(a||s).stringify(this)},concat:function(a){var b=this.words,d=a.words,c=this.sigBytes;a=a.sigBytes;this.clamp();if(c%4)for(var e=0;e<a;e++)b[c+e>>>2]|=(d[e>>>2]>>>24-8*(e%4)&255)<<24-8*((c+e)%4);else if(65535<d.length)for(e=0;e<a;e+=4)b[c+e>>>2]=d[e>>>2];else b.push.apply(b,d);this.sigBytes+=a;return this},clamp:function(){var a=this.words,b=this.sigBytes;a[b>>>2]&=4294967295<<32-8*(b%4);a.length=h.ceil(b/4)},clone:function(){var a=f.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var b=[],d=0;d<a;d+=4)b.push(4294967296*h.random()|0);return new j.init(b,a)}}),m=k.enc={},s=m.Hex={stringify:function(a){var b=a.words;a=a.sigBytes;for(var d=[],c=0;c<a;c++){var e=b[c>>>2]>>>24-8*(c%4)&255;d.push((e>>>4).toString(16));d.push((e&15).toString(16))}return d.join("")},parse:function(a){for(var b=a.length,d=[],c=0;c<b;c+=2)d[c>>>3]|=parseInt(a.substr(c,2),16)<<24-4*(c%8);return new j.init(d,b/2)}},p=m.Latin1={stringify:function(a){var b=a.words;a=a.sigBytes;for(var d=[],c=0;c<a;c++)d.push(String.fromCharCode(b[c>>>2]>>>24-8*(c%4)&255));return d.join("")},parse:function(a){for(var b=a.length,d=[],c=0;c<b;c++)d[c>>>2]|=(a.charCodeAt(c)&255)<<24-8*(c%4);return new j.init(d,b)}},t=m.Utf8={stringify:function(a){try{return decodeURIComponent(escape(p.stringify(a)))}catch(b){throw Error("Malformed UTF-8 data");}},parse:function(a){return p.parse(unescape(encodeURIComponent(a)))}},q=l.BufferedBlockAlgorithm=f.extend({reset:function(){this._data=new j.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=t.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var b=this._data,d=b.words,c=b.sigBytes,e=this.blockSize,f=c/(4*e),f=a?h.ceil(f):h.max((f|0)-this._minBufferSize,0);a=f*e;c=h.min(4*a,c);if(a){for(var g=0;g<a;g+=e)this._doProcessBlock(d,g);g=d.splice(0,a);b.sigBytes-=c}return new j.init(g,c)},clone:function(){var a=f.clone.call(this);a._data=this._data.clone();return a},_minBufferSize:0});l.Hasher=q.extend({cfg:f.extend(),init:function(a){this.cfg=this.cfg.extend(a);this.reset()},reset:function(){q.reset.call(this);this._doReset()},update:function(a){this._append(a);this._process();return this},finalize:function(a){a&&this._append(a);return this._doFinalize()},blockSize:16,_createHelper:function(a){return function(b,d){return(new a.init(d)).finalize(b)}},_createHmacHelper:function(a){return function(b,d){return(new u.HMAC.init(a,d)).finalize(b)}}});var u=k.algo={};return k}(Math);
(function () { var k = CryptoJS, b = k.lib, m = b.WordArray, l = b.Hasher, d = [], b = k.algo.SHA1 = l.extend({ _doReset: function () { this._hash = new m.init([1732584193, 4023233417, 2562383102, 271733878, 3285377520]) }, _doProcessBlock: function (n, p) { for (var a = this._hash.words, e = a[0], f = a[1], h = a[2], j = a[3], b = a[4], c = 0; 80 > c; c++) { if (16 > c) d[c] = n[p + c] | 0; else { var g = d[c - 3] ^ d[c - 8] ^ d[c - 14] ^ d[c - 16]; d[c] = g << 1 | g >>> 31 } g = (e << 5 | e >>> 27) + b + d[c]; g = 20 > c ? g + ((f & h | ~f & j) + 1518500249) : 40 > c ? g + ((f ^ h ^ j) + 1859775393) : 60 > c ? g + ((f & h | f & j | h & j) - 1894007588) : g + ((f ^ h ^ j) - 899497514); b = j; j = h; h = f << 30 | f >>> 2; f = e; e = g } a[0] = a[0] + e | 0; a[1] = a[1] + f | 0; a[2] = a[2] + h | 0; a[3] = a[3] + j | 0; a[4] = a[4] + b | 0 }, _doFinalize: function () { var b = this._data, d = b.words, a = 8 * this._nDataBytes, e = 8 * b.sigBytes; d[e >>> 5] |= 128 << 24 - e % 32; d[(e + 64 >>> 9 << 4) + 14] = Math.floor(a / 4294967296); d[(e + 64 >>> 9 << 4) + 15] = a; b.sigBytes = 4 * d.length; this._process(); return this._hash }, clone: function () { var b = l.clone.call(this); b._hash = this._hash.clone(); return b } }); k.SHA1 = l._createHelper(b); k.HmacSHA1 = l._createHmacHelper(b) })();
(function (k) { for (var g = CryptoJS, h = g.lib, v = h.WordArray, j = h.Hasher, h = g.algo, s = [], t = [], u = function (q) { return 4294967296 * (q - (q | 0)) | 0 }, l = 2, b = 0; 64 > b;) { var d; a: { d = l; for (var w = k.sqrt(d), r = 2; r <= w; r++) if (!(d % r)) { d = !1; break a } d = !0 } d && (8 > b && (s[b] = u(k.pow(l, 0.5))), t[b] = u(k.pow(l, 1 / 3)), b++); l++ } var n = [], h = h.SHA256 = j.extend({ _doReset: function () { this._hash = new v.init(s.slice(0)) }, _doProcessBlock: function (q, h) { for (var a = this._hash.words, c = a[0], d = a[1], b = a[2], k = a[3], f = a[4], g = a[5], j = a[6], l = a[7], e = 0; 64 > e; e++) { if (16 > e) n[e] = q[h + e] | 0; else { var m = n[e - 15], p = n[e - 2]; n[e] = ((m << 25 | m >>> 7) ^ (m << 14 | m >>> 18) ^ m >>> 3) + n[e - 7] + ((p << 15 | p >>> 17) ^ (p << 13 | p >>> 19) ^ p >>> 10) + n[e - 16] } m = l + ((f << 26 | f >>> 6) ^ (f << 21 | f >>> 11) ^ (f << 7 | f >>> 25)) + (f & g ^ ~f & j) + t[e] + n[e]; p = ((c << 30 | c >>> 2) ^ (c << 19 | c >>> 13) ^ (c << 10 | c >>> 22)) + (c & d ^ c & b ^ d & b); l = j; j = g; g = f; f = k + m | 0; k = b; b = d; d = c; c = m + p | 0 } a[0] = a[0] + c | 0; a[1] = a[1] + d | 0; a[2] = a[2] + b | 0; a[3] = a[3] + k | 0; a[4] = a[4] + f | 0; a[5] = a[5] + g | 0; a[6] = a[6] + j | 0; a[7] = a[7] + l | 0 }, _doFinalize: function () { var d = this._data, b = d.words, a = 8 * this._nDataBytes, c = 8 * d.sigBytes; b[c >>> 5] |= 128 << 24 - c % 32; b[(c + 64 >>> 9 << 4) + 14] = k.floor(a / 4294967296); b[(c + 64 >>> 9 << 4) + 15] = a; d.sigBytes = 4 * b.length; this._process(); return this._hash }, clone: function () { var b = j.clone.call(this); b._hash = this._hash.clone(); return b } }); g.SHA256 = j._createHelper(h); g.HmacSHA256 = j._createHmacHelper(h) })(Math);
(function(){var c=CryptoJS,k=c.enc.Utf8;c.algo.HMAC=c.lib.Base.extend({init:function(a,b){a=this._hasher=new a.init;"string"==typeof b&&(b=k.parse(b));var c=a.blockSize,e=4*c;b.sigBytes>e&&(b=a.finalize(b));b.clamp();for(var f=this._oKey=b.clone(),g=this._iKey=b.clone(),h=f.words,j=g.words,d=0;d<c;d++)h[d]^=1549556828,j[d]^=909522486;f.sigBytes=g.sigBytes=e;this.reset()},reset:function(){var a=this._hasher;a.reset();a.update(this._iKey)},update:function(a){this._hasher.update(a);return this},finalize:function(a){var b=this._hasher;a=b.finalize(a);b.reset();return b.finalize(this._oKey.clone().concat(a))}})})();
(function(){var h=CryptoJS,j=h.lib.WordArray;h.enc.Base64={stringify:function(b){var e=b.words,f=b.sigBytes,c=this._map;b.clamp();b=[];for(var a=0;a<f;a+=3)for(var d=(e[a>>>2]>>>24-8*(a%4)&255)<<16|(e[a+1>>>2]>>>24-8*((a+1)%4)&255)<<8|e[a+2>>>2]>>>24-8*((a+2)%4)&255,g=0;4>g&&a+0.75*g<f;g++)b.push(c.charAt(d>>>6*(3-g)&63));if(e=c.charAt(64))for(;b.length%4;)b.push(e);return b.join("")},parse:function(b){var e=b.length,f=this._map,c=f.charAt(64);c&&(c=b.indexOf(c),-1!=c&&(e=c));for(var c=[],a=0,d=0;d<e;d++)if(d%4){var g=f.indexOf(b.charAt(d-1))<<2*(d%4),h=f.indexOf(b.charAt(d))>>>6-2*(d%4);c[a>>>2]|=(g|h)<<24-8*(a%4);a++}return j.create(c,a)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}})();

View file

@ -0,0 +1,84 @@
/* global hawk */
/* global Config */
/* global DUMP */
'use strict';
var Requester = {
XHR_TIMEOUT_MS: 60000,
_url: null,
_hawkCredentials: null,
init: function fmdr_init() {
this._url = Config.api_url + '/' + Config.api_version;
},
setHawkCredentials: function fmdr_set_hawk_credentials(id, key) {
this._hawkCredentials = {
id: id,
key: key,
algorithm: 'sha256'
};
},
post: function fmdr_post(url, data, onsuccess, onerror) {
url = this._url + url;
data = JSON.stringify(data);
DUMP('POST-ing to ' + url + ': ' + data);
var xhr = new XMLHttpRequest({mozSystem: true});
xhr.open('POST', url);
xhr.timeout = this.XHR_TIMEOUT_MS;
xhr.setRequestHeader('Content-Type', 'application/json');
var hawkHeader = null;
if (this._hawkCredentials) {
var hawkOptions = {
credentials: this._hawkCredentials,
contentType: 'application/json',
payload: data
};
hawkHeader = hawk.client.header(url, 'POST', hawkOptions);
xhr.setRequestHeader('Authorization', hawkHeader.field);
}
xhr.onload = function fmdr_xhr_onload() {
var valid = true;
if (hawkHeader !== null) {
valid = hawk.client.authenticate(
xhr, this._hawkCredentials, hawkHeader.artifacts,
{payload: xhr.responseText});
}
if (!valid) {
DUMP('ignoring invalid HAWK signature');
return;
}
if (xhr.status == 200) {
DUMP('successful request, response: ' + xhr.response);
onsuccess && onsuccess(JSON.parse(xhr.responseText));
} else if (xhr.status !== 200) {
DUMP('request failed with status ' + xhr.status);
onerror && onerror(xhr);
}
};
xhr.onerror = function fmd_xhr_onerror() {
DUMP('request failed with status ' + xhr.status);
onerror && onerror(xhr);
};
xhr.ontimeout = function fmd_xhr_ontimeout() {
DUMP('server request timed out!');
xhr.onerror();
};
xhr.send(data);
}
};
Requester.init();