Sat, 03 Jan 2015 20:18:00 +0100
Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.
michael@0 | 1 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 4 | 'use strict'; |
michael@0 | 5 | |
michael@0 | 6 | module.metadata = { |
michael@0 | 7 | 'stability': 'unstable' |
michael@0 | 8 | }; |
michael@0 | 9 | |
michael@0 | 10 | const { Class } = require('../core/heritage'); |
michael@0 | 11 | const { EventTarget } = require('../event/target'); |
michael@0 | 12 | const { on, off, emit } = require('../event/core'); |
michael@0 | 13 | const { requiresAddonGlobal } = require('./utils'); |
michael@0 | 14 | const { delay: async } = require('../lang/functional'); |
michael@0 | 15 | const { Ci, Cu, Cc } = require('chrome'); |
michael@0 | 16 | const timer = require('../timers'); |
michael@0 | 17 | const { URL } = require('../url'); |
michael@0 | 18 | const { sandbox, evaluate, load } = require('../loader/sandbox'); |
michael@0 | 19 | const { merge } = require('../util/object'); |
michael@0 | 20 | const { getTabForContentWindow } = require('../tabs/utils'); |
michael@0 | 21 | const { getInnerId } = require('../window/utils'); |
michael@0 | 22 | const { PlainTextConsole } = require('../console/plain-text'); |
michael@0 | 23 | |
michael@0 | 24 | // WeakMap of sandboxes so we can access private values |
michael@0 | 25 | const sandboxes = new WeakMap(); |
michael@0 | 26 | |
michael@0 | 27 | /* Trick the linker in order to ensure shipping these files in the XPI. |
michael@0 | 28 | require('./content-worker.js'); |
michael@0 | 29 | Then, retrieve URL of these files in the XPI: |
michael@0 | 30 | */ |
michael@0 | 31 | let prefix = module.uri.split('sandbox.js')[0]; |
michael@0 | 32 | const CONTENT_WORKER_URL = prefix + 'content-worker.js'; |
michael@0 | 33 | const metadata = require('@loader/options').metadata; |
michael@0 | 34 | |
michael@0 | 35 | // Fetch additional list of domains to authorize access to for each content |
michael@0 | 36 | // script. It is stored in manifest `metadata` field which contains |
michael@0 | 37 | // package.json data. This list is originaly defined by authors in |
michael@0 | 38 | // `permissions` attribute of their package.json addon file. |
michael@0 | 39 | const permissions = (metadata && metadata['permissions']) || {}; |
michael@0 | 40 | const EXPANDED_PRINCIPALS = permissions['cross-domain-content'] || []; |
michael@0 | 41 | |
michael@0 | 42 | const waiveSecurityMembrane = !!permissions['unsafe-content-script']; |
michael@0 | 43 | |
michael@0 | 44 | const nsIScriptSecurityManager = Ci.nsIScriptSecurityManager; |
michael@0 | 45 | const secMan = Cc["@mozilla.org/scriptsecuritymanager;1"]. |
michael@0 | 46 | getService(Ci.nsIScriptSecurityManager); |
michael@0 | 47 | |
michael@0 | 48 | const JS_VERSION = '1.8'; |
michael@0 | 49 | |
michael@0 | 50 | const WorkerSandbox = Class({ |
michael@0 | 51 | implements: [ EventTarget ], |
michael@0 | 52 | |
michael@0 | 53 | /** |
michael@0 | 54 | * Emit a message to the worker content sandbox |
michael@0 | 55 | */ |
michael@0 | 56 | emit: function emit(type, ...args) { |
michael@0 | 57 | // JSON.stringify is buggy with cross-sandbox values, |
michael@0 | 58 | // it may return "{}" on functions. Use a replacer to match them correctly. |
michael@0 | 59 | let replacer = (k, v) => |
michael@0 | 60 | typeof(v) === "function" |
michael@0 | 61 | ? (type === "console" ? Function.toString.call(v) : void(0)) |
michael@0 | 62 | : v; |
michael@0 | 63 | |
michael@0 | 64 | // Ensure having an asynchronous behavior |
michael@0 | 65 | async(() => |
michael@0 | 66 | emitToContent(this, JSON.stringify([type, ...args], replacer)) |
michael@0 | 67 | ); |
michael@0 | 68 | }, |
michael@0 | 69 | |
michael@0 | 70 | /** |
michael@0 | 71 | * Synchronous version of `emit`. |
michael@0 | 72 | * /!\ Should only be used when it is strictly mandatory /!\ |
michael@0 | 73 | * Doesn't ensure passing only JSON values. |
michael@0 | 74 | * Mainly used by context-menu in order to avoid breaking it. |
michael@0 | 75 | */ |
michael@0 | 76 | emitSync: function emitSync(...args) { |
michael@0 | 77 | return emitToContent(this, args); |
michael@0 | 78 | }, |
michael@0 | 79 | |
michael@0 | 80 | /** |
michael@0 | 81 | * Tells if content script has at least one listener registered for one event, |
michael@0 | 82 | * through `self.on('xxx', ...)`. |
michael@0 | 83 | * /!\ Shouldn't be used. Implemented to avoid breaking context-menu API. |
michael@0 | 84 | */ |
michael@0 | 85 | hasListenerFor: function hasListenerFor(name) { |
michael@0 | 86 | return modelFor(this).hasListenerFor(name); |
michael@0 | 87 | }, |
michael@0 | 88 | |
michael@0 | 89 | /** |
michael@0 | 90 | * Configures sandbox and loads content scripts into it. |
michael@0 | 91 | * @param {Worker} worker |
michael@0 | 92 | * content worker |
michael@0 | 93 | */ |
michael@0 | 94 | initialize: function WorkerSandbox(worker, window) { |
michael@0 | 95 | let model = {}; |
michael@0 | 96 | sandboxes.set(this, model); |
michael@0 | 97 | model.worker = worker; |
michael@0 | 98 | // We receive a wrapped window, that may be an xraywrapper if it's content |
michael@0 | 99 | let proto = window; |
michael@0 | 100 | |
michael@0 | 101 | // TODO necessary? |
michael@0 | 102 | // Ensure that `emit` has always the right `this` |
michael@0 | 103 | this.emit = this.emit.bind(this); |
michael@0 | 104 | this.emitSync = this.emitSync.bind(this); |
michael@0 | 105 | |
michael@0 | 106 | // Use expanded principal for content-script if the content is a |
michael@0 | 107 | // regular web content for better isolation. |
michael@0 | 108 | // (This behavior can be turned off for now with the unsafe-content-script |
michael@0 | 109 | // flag to give addon developers time for making the necessary changes) |
michael@0 | 110 | // But prevent it when the Worker isn't used for a content script but for |
michael@0 | 111 | // injecting `addon` object into a Panel, Widget, ... scope. |
michael@0 | 112 | // That's because: |
michael@0 | 113 | // 1/ It is useless to use multiple domains as the worker is only used |
michael@0 | 114 | // to communicate with the addon, |
michael@0 | 115 | // 2/ By using it it would prevent the document to have access to any JS |
michael@0 | 116 | // value of the worker. As JS values coming from multiple domain principals |
michael@0 | 117 | // can't be accessed by 'mono-principals' (principal with only one domain). |
michael@0 | 118 | // Even if this principal is for a domain that is specified in the multiple |
michael@0 | 119 | // domain principal. |
michael@0 | 120 | let principals = window; |
michael@0 | 121 | let wantGlobalProperties = []; |
michael@0 | 122 | let isSystemPrincipal = secMan.isSystemPrincipal( |
michael@0 | 123 | window.document.nodePrincipal); |
michael@0 | 124 | if (!isSystemPrincipal && !requiresAddonGlobal(worker)) { |
michael@0 | 125 | if (EXPANDED_PRINCIPALS.length > 0) { |
michael@0 | 126 | // We have to replace XHR constructor of the content document |
michael@0 | 127 | // with a custom cross origin one, automagically added by platform code: |
michael@0 | 128 | delete proto.XMLHttpRequest; |
michael@0 | 129 | wantGlobalProperties.push('XMLHttpRequest'); |
michael@0 | 130 | } |
michael@0 | 131 | if (!waiveSecurityMembrane) |
michael@0 | 132 | principals = EXPANDED_PRINCIPALS.concat(window); |
michael@0 | 133 | } |
michael@0 | 134 | |
michael@0 | 135 | // Instantiate trusted code in another Sandbox in order to prevent content |
michael@0 | 136 | // script from messing with standard classes used by proxy and API code. |
michael@0 | 137 | let apiSandbox = sandbox(principals, { wantXrays: true, sameZoneAs: window }); |
michael@0 | 138 | apiSandbox.console = console; |
michael@0 | 139 | |
michael@0 | 140 | // Create the sandbox and bind it to window in order for content scripts to |
michael@0 | 141 | // have access to all standard globals (window, document, ...) |
michael@0 | 142 | let content = sandbox(principals, { |
michael@0 | 143 | sandboxPrototype: proto, |
michael@0 | 144 | wantXrays: true, |
michael@0 | 145 | wantGlobalProperties: wantGlobalProperties, |
michael@0 | 146 | wantExportHelpers: true, |
michael@0 | 147 | sameZoneAs: window, |
michael@0 | 148 | metadata: { |
michael@0 | 149 | SDKContentScript: true, |
michael@0 | 150 | 'inner-window-id': getInnerId(window) |
michael@0 | 151 | } |
michael@0 | 152 | }); |
michael@0 | 153 | model.sandbox = content; |
michael@0 | 154 | |
michael@0 | 155 | // We have to ensure that window.top and window.parent are the exact same |
michael@0 | 156 | // object than window object, i.e. the sandbox global object. But not |
michael@0 | 157 | // always, in case of iframes, top and parent are another window object. |
michael@0 | 158 | let top = window.top === window ? content : content.top; |
michael@0 | 159 | let parent = window.parent === window ? content : content.parent; |
michael@0 | 160 | merge(content, { |
michael@0 | 161 | // We need 'this === window === top' to be true in toplevel scope: |
michael@0 | 162 | get window() content, |
michael@0 | 163 | get top() top, |
michael@0 | 164 | get parent() parent, |
michael@0 | 165 | // Use the Greasemonkey naming convention to provide access to the |
michael@0 | 166 | // unwrapped window object so the content script can access document |
michael@0 | 167 | // JavaScript values. |
michael@0 | 168 | // NOTE: this functionality is experimental and may change or go away |
michael@0 | 169 | // at any time! |
michael@0 | 170 | get unsafeWindow() window.wrappedJSObject |
michael@0 | 171 | }); |
michael@0 | 172 | |
michael@0 | 173 | // Load trusted code that will inject content script API. |
michael@0 | 174 | // We need to expose JS objects defined in same principal in order to |
michael@0 | 175 | // avoid having any kind of wrapper. |
michael@0 | 176 | load(apiSandbox, CONTENT_WORKER_URL); |
michael@0 | 177 | |
michael@0 | 178 | // prepare a clean `self.options` |
michael@0 | 179 | let options = 'contentScriptOptions' in worker ? |
michael@0 | 180 | JSON.stringify(worker.contentScriptOptions) : |
michael@0 | 181 | undefined; |
michael@0 | 182 | |
michael@0 | 183 | // Then call `inject` method and communicate with this script |
michael@0 | 184 | // by trading two methods that allow to send events to the other side: |
michael@0 | 185 | // - `onEvent` called by content script |
michael@0 | 186 | // - `result.emitToContent` called by addon script |
michael@0 | 187 | // Bug 758203: We have to explicitely define `__exposedProps__` in order |
michael@0 | 188 | // to allow access to these chrome object attributes from this sandbox with |
michael@0 | 189 | // content priviledges |
michael@0 | 190 | // https://developer.mozilla.org/en/XPConnect_wrappers#Other_security_wrappers |
michael@0 | 191 | let onEvent = onContentEvent.bind(null, this); |
michael@0 | 192 | // `ContentWorker` is defined in CONTENT_WORKER_URL file |
michael@0 | 193 | let chromeAPI = createChromeAPI(); |
michael@0 | 194 | let result = apiSandbox.ContentWorker.inject(content, chromeAPI, onEvent, options); |
michael@0 | 195 | |
michael@0 | 196 | // Merge `emitToContent` and `hasListenerFor` into our private |
michael@0 | 197 | // model of the WorkerSandbox so we can communicate with content |
michael@0 | 198 | // script |
michael@0 | 199 | merge(model, result); |
michael@0 | 200 | |
michael@0 | 201 | let console = new PlainTextConsole(null, getInnerId(window)); |
michael@0 | 202 | |
michael@0 | 203 | // Handle messages send by this script: |
michael@0 | 204 | setListeners(this, console); |
michael@0 | 205 | |
michael@0 | 206 | // Inject `addon` global into target document if document is trusted, |
michael@0 | 207 | // `addon` in document is equivalent to `self` in content script. |
michael@0 | 208 | if (requiresAddonGlobal(worker)) { |
michael@0 | 209 | Object.defineProperty(getUnsafeWindow(window), 'addon', { |
michael@0 | 210 | value: content.self |
michael@0 | 211 | } |
michael@0 | 212 | ); |
michael@0 | 213 | } |
michael@0 | 214 | |
michael@0 | 215 | // Inject our `console` into target document if worker doesn't have a tab |
michael@0 | 216 | // (e.g Panel, PageWorker, Widget). |
michael@0 | 217 | // `worker.tab` can't be used because bug 804935. |
michael@0 | 218 | if (!getTabForContentWindow(window)) { |
michael@0 | 219 | let win = getUnsafeWindow(window); |
michael@0 | 220 | |
michael@0 | 221 | // export our chrome console to content window, as described here: |
michael@0 | 222 | // https://developer.mozilla.org/en-US/docs/Components.utils.createObjectIn |
michael@0 | 223 | let con = Cu.createObjectIn(win); |
michael@0 | 224 | |
michael@0 | 225 | let genPropDesc = function genPropDesc(fun) { |
michael@0 | 226 | return { enumerable: true, configurable: true, writable: true, |
michael@0 | 227 | value: console[fun] }; |
michael@0 | 228 | } |
michael@0 | 229 | |
michael@0 | 230 | const properties = { |
michael@0 | 231 | log: genPropDesc('log'), |
michael@0 | 232 | info: genPropDesc('info'), |
michael@0 | 233 | warn: genPropDesc('warn'), |
michael@0 | 234 | error: genPropDesc('error'), |
michael@0 | 235 | debug: genPropDesc('debug'), |
michael@0 | 236 | trace: genPropDesc('trace'), |
michael@0 | 237 | dir: genPropDesc('dir'), |
michael@0 | 238 | group: genPropDesc('group'), |
michael@0 | 239 | groupCollapsed: genPropDesc('groupCollapsed'), |
michael@0 | 240 | groupEnd: genPropDesc('groupEnd'), |
michael@0 | 241 | time: genPropDesc('time'), |
michael@0 | 242 | timeEnd: genPropDesc('timeEnd'), |
michael@0 | 243 | profile: genPropDesc('profile'), |
michael@0 | 244 | profileEnd: genPropDesc('profileEnd'), |
michael@0 | 245 | __noSuchMethod__: { enumerable: true, configurable: true, writable: true, |
michael@0 | 246 | value: function() {} } |
michael@0 | 247 | }; |
michael@0 | 248 | |
michael@0 | 249 | Object.defineProperties(con, properties); |
michael@0 | 250 | Cu.makeObjectPropsNormal(con); |
michael@0 | 251 | |
michael@0 | 252 | win.console = con; |
michael@0 | 253 | }; |
michael@0 | 254 | |
michael@0 | 255 | // The order of `contentScriptFile` and `contentScript` evaluation is |
michael@0 | 256 | // intentional, so programs can load libraries like jQuery from script URLs |
michael@0 | 257 | // and use them in scripts. |
michael@0 | 258 | let contentScriptFile = ('contentScriptFile' in worker) ? worker.contentScriptFile |
michael@0 | 259 | : null, |
michael@0 | 260 | contentScript = ('contentScript' in worker) ? worker.contentScript : null; |
michael@0 | 261 | |
michael@0 | 262 | if (contentScriptFile) |
michael@0 | 263 | importScripts.apply(null, [this].concat(contentScriptFile)); |
michael@0 | 264 | if (contentScript) { |
michael@0 | 265 | evaluateIn( |
michael@0 | 266 | this, |
michael@0 | 267 | Array.isArray(contentScript) ? contentScript.join(';\n') : contentScript |
michael@0 | 268 | ); |
michael@0 | 269 | } |
michael@0 | 270 | }, |
michael@0 | 271 | destroy: function destroy(reason) { |
michael@0 | 272 | if (typeof reason != 'string') |
michael@0 | 273 | reason = ''; |
michael@0 | 274 | this.emitSync('event', 'detach', reason); |
michael@0 | 275 | let model = modelFor(this); |
michael@0 | 276 | model.sandbox = null |
michael@0 | 277 | model.worker = null; |
michael@0 | 278 | }, |
michael@0 | 279 | |
michael@0 | 280 | }); |
michael@0 | 281 | |
michael@0 | 282 | exports.WorkerSandbox = WorkerSandbox; |
michael@0 | 283 | |
michael@0 | 284 | /** |
michael@0 | 285 | * Imports scripts to the sandbox by reading files under urls and |
michael@0 | 286 | * evaluating its source. If exception occurs during evaluation |
michael@0 | 287 | * `'error'` event is emitted on the worker. |
michael@0 | 288 | * This is actually an analog to the `importScript` method in web |
michael@0 | 289 | * workers but in our case it's not exposed even though content |
michael@0 | 290 | * scripts may be able to do it synchronously since IO operation |
michael@0 | 291 | * takes place in the UI process. |
michael@0 | 292 | */ |
michael@0 | 293 | function importScripts (workerSandbox, ...urls) { |
michael@0 | 294 | let { worker, sandbox } = modelFor(workerSandbox); |
michael@0 | 295 | for (let i in urls) { |
michael@0 | 296 | let contentScriptFile = urls[i]; |
michael@0 | 297 | try { |
michael@0 | 298 | let uri = URL(contentScriptFile); |
michael@0 | 299 | if (uri.scheme === 'resource') |
michael@0 | 300 | load(sandbox, String(uri)); |
michael@0 | 301 | else |
michael@0 | 302 | throw Error('Unsupported `contentScriptFile` url: ' + String(uri)); |
michael@0 | 303 | } |
michael@0 | 304 | catch(e) { |
michael@0 | 305 | emit(worker, 'error', e); |
michael@0 | 306 | } |
michael@0 | 307 | } |
michael@0 | 308 | } |
michael@0 | 309 | |
michael@0 | 310 | function setListeners (workerSandbox, console) { |
michael@0 | 311 | let { worker } = modelFor(workerSandbox); |
michael@0 | 312 | // console.xxx calls |
michael@0 | 313 | workerSandbox.on('console', function consoleListener (kind, ...args) { |
michael@0 | 314 | console[kind].apply(console, args); |
michael@0 | 315 | }); |
michael@0 | 316 | |
michael@0 | 317 | // self.postMessage calls |
michael@0 | 318 | workerSandbox.on('message', function postMessage(data) { |
michael@0 | 319 | // destroyed? |
michael@0 | 320 | if (worker) |
michael@0 | 321 | emit(worker, 'message', data); |
michael@0 | 322 | }); |
michael@0 | 323 | |
michael@0 | 324 | // self.port.emit calls |
michael@0 | 325 | workerSandbox.on('event', function portEmit (...eventArgs) { |
michael@0 | 326 | // If not destroyed, emit event information to worker |
michael@0 | 327 | // `eventArgs` has the event name as first element, |
michael@0 | 328 | // and remaining elements are additional arguments to pass |
michael@0 | 329 | if (worker) |
michael@0 | 330 | emit.apply(null, [worker.port].concat(eventArgs)); |
michael@0 | 331 | }); |
michael@0 | 332 | |
michael@0 | 333 | // unwrap, recreate and propagate async Errors thrown from content-script |
michael@0 | 334 | workerSandbox.on('error', function onError({instanceOfError, value}) { |
michael@0 | 335 | if (worker) { |
michael@0 | 336 | let error = value; |
michael@0 | 337 | if (instanceOfError) { |
michael@0 | 338 | error = new Error(value.message, value.fileName, value.lineNumber); |
michael@0 | 339 | error.stack = value.stack; |
michael@0 | 340 | error.name = value.name; |
michael@0 | 341 | } |
michael@0 | 342 | emit(worker, 'error', error); |
michael@0 | 343 | } |
michael@0 | 344 | }); |
michael@0 | 345 | } |
michael@0 | 346 | |
michael@0 | 347 | /** |
michael@0 | 348 | * Evaluates code in the sandbox. |
michael@0 | 349 | * @param {String} code |
michael@0 | 350 | * JavaScript source to evaluate. |
michael@0 | 351 | * @param {String} [filename='javascript:' + code] |
michael@0 | 352 | * Name of the file |
michael@0 | 353 | */ |
michael@0 | 354 | function evaluateIn (workerSandbox, code, filename) { |
michael@0 | 355 | let { worker, sandbox } = modelFor(workerSandbox); |
michael@0 | 356 | try { |
michael@0 | 357 | evaluate(sandbox, code, filename || 'javascript:' + code); |
michael@0 | 358 | } |
michael@0 | 359 | catch(e) { |
michael@0 | 360 | emit(worker, 'error', e); |
michael@0 | 361 | } |
michael@0 | 362 | } |
michael@0 | 363 | |
michael@0 | 364 | /** |
michael@0 | 365 | * Method called by the worker sandbox when it needs to send a message |
michael@0 | 366 | */ |
michael@0 | 367 | function onContentEvent (workerSandbox, args) { |
michael@0 | 368 | // As `emit`, we ensure having an asynchronous behavior |
michael@0 | 369 | async(function () { |
michael@0 | 370 | // We emit event to chrome/addon listeners |
michael@0 | 371 | emit.apply(null, [workerSandbox].concat(JSON.parse(args))); |
michael@0 | 372 | }); |
michael@0 | 373 | } |
michael@0 | 374 | |
michael@0 | 375 | |
michael@0 | 376 | function modelFor (workerSandbox) { |
michael@0 | 377 | return sandboxes.get(workerSandbox); |
michael@0 | 378 | } |
michael@0 | 379 | |
michael@0 | 380 | function getUnsafeWindow (win) { |
michael@0 | 381 | return win.wrappedJSObject || win; |
michael@0 | 382 | } |
michael@0 | 383 | |
michael@0 | 384 | function emitToContent (workerSandbox, args) { |
michael@0 | 385 | return modelFor(workerSandbox).emitToContent(args); |
michael@0 | 386 | } |
michael@0 | 387 | |
michael@0 | 388 | function createChromeAPI () { |
michael@0 | 389 | return { |
michael@0 | 390 | timers: { |
michael@0 | 391 | setTimeout: timer.setTimeout, |
michael@0 | 392 | setInterval: timer.setInterval, |
michael@0 | 393 | clearTimeout: timer.clearTimeout, |
michael@0 | 394 | clearInterval: timer.clearInterval, |
michael@0 | 395 | __exposedProps__: { |
michael@0 | 396 | setTimeout: 'r', |
michael@0 | 397 | setInterval: 'r', |
michael@0 | 398 | clearTimeout: 'r', |
michael@0 | 399 | clearInterval: 'r' |
michael@0 | 400 | }, |
michael@0 | 401 | }, |
michael@0 | 402 | sandbox: { |
michael@0 | 403 | evaluate: evaluate, |
michael@0 | 404 | __exposedProps__: { |
michael@0 | 405 | evaluate: 'r' |
michael@0 | 406 | } |
michael@0 | 407 | }, |
michael@0 | 408 | __exposedProps__: { |
michael@0 | 409 | timers: 'r', |
michael@0 | 410 | sandbox: 'r' |
michael@0 | 411 | } |
michael@0 | 412 | }; |
michael@0 | 413 | } |