1/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ 2/* vim: set sts=2 sw=2 et tw=80: */ 3/* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6"use strict"; 7 8var EXPORTED_SYMBOLS = ["ProxyScriptContext", "ProxyChannelFilter"]; 9 10/* exported ProxyScriptContext, ProxyChannelFilter */ 11 12ChromeUtils.import("resource://gre/modules/Services.jsm"); 13ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); 14ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm"); 15ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm"); 16 17ChromeUtils.defineModuleGetter(this, "ExtensionChild", 18 "resource://gre/modules/ExtensionChild.jsm"); 19ChromeUtils.defineModuleGetter(this, "ExtensionParent", 20 "resource://gre/modules/ExtensionParent.jsm"); 21ChromeUtils.defineModuleGetter(this, "Schemas", 22 "resource://gre/modules/Schemas.jsm"); 23XPCOMUtils.defineLazyServiceGetter(this, "ProxyService", 24 "@mozilla.org/network/protocol-proxy-service;1", 25 "nsIProtocolProxyService"); 26 27XPCOMUtils.defineLazyGetter(this, "tabTracker", () => { 28 return ExtensionParent.apiManager.global.tabTracker; 29}); 30 31const CATEGORY_EXTENSION_SCRIPTS_CONTENT = "webextension-scripts-content"; 32 33// DNS is resolved on the SOCKS proxy server. 34const {TRANSPARENT_PROXY_RESOLVES_HOST} = Ci.nsIProxyInfo; 35 36// The length of time (seconds) to wait for a proxy to resolve before ignoring it. 37const PROXY_TIMEOUT_SEC = 10; 38 39const { 40 ExtensionError, 41 defineLazyGetter, 42} = ExtensionUtils; 43 44const { 45 BaseContext, 46 CanOfAPIs, 47 LocalAPIImplementation, 48 SchemaAPIManager, 49} = ExtensionCommon; 50 51const PROXY_TYPES = Object.freeze({ 52 DIRECT: "direct", 53 HTTPS: "https", 54 PROXY: "http", // Synonym for PROXY_TYPES.HTTP 55 HTTP: "http", 56 SOCKS: "socks", // SOCKS5 57 SOCKS4: "socks4", 58}); 59 60const ProxyInfoData = { 61 validate(proxyData) { 62 if (proxyData.type && proxyData.type.toLowerCase() === "direct") { 63 return {type: proxyData.type}; 64 } 65 for (let prop of ["type", "host", "port", "username", "password", "proxyDNS", "failoverTimeout"]) { 66 this[prop](proxyData); 67 } 68 return proxyData; 69 }, 70 71 type(proxyData) { 72 let {type} = proxyData; 73 if (typeof type !== "string" || !PROXY_TYPES.hasOwnProperty(type.toUpperCase())) { 74 throw new ExtensionError(`ProxyInfoData: Invalid proxy server type: "${type}"`); 75 } 76 proxyData.type = PROXY_TYPES[type.toUpperCase()]; 77 }, 78 79 host(proxyData) { 80 let {host} = proxyData; 81 if (typeof host !== "string" || host.includes(" ")) { 82 throw new ExtensionError(`ProxyInfoData: Invalid proxy server host: "${host}"`); 83 } 84 if (!host.length) { 85 throw new ExtensionError("ProxyInfoData: Proxy server host cannot be empty"); 86 } 87 proxyData.host = host; 88 }, 89 90 port(proxyData) { 91 let port = Number.parseInt(proxyData.port, 10); 92 if (!Number.isInteger(port)) { 93 throw new ExtensionError(`ProxyInfoData: Invalid proxy server port: "${port}"`); 94 } 95 96 if (port < 1 || port > 0xffff) { 97 throw new ExtensionError(`ProxyInfoData: Proxy server port ${port} outside range 1 to 65535`); 98 } 99 proxyData.port = port; 100 }, 101 102 username(proxyData) { 103 let {username} = proxyData; 104 if (username !== undefined && typeof username !== "string") { 105 throw new ExtensionError(`ProxyInfoData: Invalid proxy server username: "${username}"`); 106 } 107 }, 108 109 password(proxyData) { 110 let {password} = proxyData; 111 if (password !== undefined && typeof password !== "string") { 112 throw new ExtensionError(`ProxyInfoData: Invalid proxy server password: "${password}"`); 113 } 114 }, 115 116 proxyDNS(proxyData) { 117 let {proxyDNS, type} = proxyData; 118 if (proxyDNS !== undefined) { 119 if (typeof proxyDNS !== "boolean") { 120 throw new ExtensionError(`ProxyInfoData: Invalid proxyDNS value: "${proxyDNS}"`); 121 } 122 if (proxyDNS && type !== PROXY_TYPES.SOCKS && type !== PROXY_TYPES.SOCKS4) { 123 throw new ExtensionError(`ProxyInfoData: proxyDNS can only be true for SOCKS proxy servers`); 124 } 125 } 126 }, 127 128 failoverTimeout(proxyData) { 129 let {failoverTimeout} = proxyData; 130 if (failoverTimeout !== undefined && (!Number.isInteger(failoverTimeout) || failoverTimeout < 1)) { 131 throw new ExtensionError(`ProxyInfoData: Invalid failover timeout: "${failoverTimeout}"`); 132 } 133 }, 134 135 createProxyInfoFromData(proxyDataList, defaultProxyInfo, proxyDataListIndex = 0) { 136 if (proxyDataListIndex >= proxyDataList.length) { 137 return defaultProxyInfo; 138 } 139 let proxyData = proxyDataList[proxyDataListIndex]; 140 if (proxyData == null) { 141 return null; 142 } 143 let {type, host, port, username, password, proxyDNS, failoverTimeout} = 144 ProxyInfoData.validate(proxyData); 145 if (type === PROXY_TYPES.DIRECT) { 146 return defaultProxyInfo; 147 } 148 let failoverProxy = this.createProxyInfoFromData(proxyDataList, defaultProxyInfo, proxyDataListIndex + 1); 149 150 if (type === PROXY_TYPES.SOCKS || type === PROXY_TYPES.SOCKS4) { 151 return ProxyService.newProxyInfoWithAuth( 152 type, host, port, username, password, 153 proxyDNS ? TRANSPARENT_PROXY_RESOLVES_HOST : 0, 154 failoverTimeout ? failoverTimeout : PROXY_TIMEOUT_SEC, 155 failoverProxy); 156 } 157 return ProxyService.newProxyInfo( 158 type, host, port, 159 proxyDNS ? TRANSPARENT_PROXY_RESOLVES_HOST : 0, 160 failoverTimeout ? failoverTimeout : PROXY_TIMEOUT_SEC, 161 failoverProxy); 162 }, 163 164 /** 165 * Creates a new proxy info data object using the return value of FindProxyForURL. 166 * 167 * @param {Array<string>} rule A single proxy rule returned by FindProxyForURL. 168 * (e.g. "PROXY 1.2.3.4:8080", "SOCKS 1.1.1.1:9090" or "DIRECT") 169 * @returns {nsIProxyInfo} The proxy info to apply for the given URI. 170 */ 171 parseProxyInfoDataFromPAC(rule) { 172 if (!rule) { 173 throw new ExtensionError("ProxyInfoData: Missing Proxy Rule"); 174 } 175 176 let parts = rule.toLowerCase().split(/\s+/); 177 if (!parts[0] || parts.length > 2) { 178 throw new ExtensionError(`ProxyInfoData: Invalid arguments passed for proxy rule: "${rule}"`); 179 } 180 let type = parts[0]; 181 let [host, port] = parts.length > 1 ? parts[1].split(":") : []; 182 183 switch (PROXY_TYPES[type.toUpperCase()]) { 184 case PROXY_TYPES.HTTP: 185 case PROXY_TYPES.HTTPS: 186 case PROXY_TYPES.SOCKS: 187 case PROXY_TYPES.SOCKS4: 188 if (!host || !port) { 189 throw new ExtensionError(`ProxyInfoData: Invalid host or port from proxy rule: "${rule}"`); 190 } 191 return {type, host, port}; 192 case PROXY_TYPES.DIRECT: 193 if (host || port) { 194 throw new ExtensionError(`ProxyInfoData: Invalid argument for proxy type: "${type}"`); 195 } 196 return {type}; 197 default: 198 throw new ExtensionError(`ProxyInfoData: Unrecognized proxy type: "${type}"`); 199 } 200 }, 201 202 proxyInfoFromProxyData(context, proxyData, defaultProxyInfo) { 203 switch (typeof proxyData) { 204 case "string": 205 let proxyRules = []; 206 try { 207 for (let result of proxyData.split(";")) { 208 proxyRules.push(ProxyInfoData.parseProxyInfoDataFromPAC(result.trim())); 209 } 210 } catch (e) { 211 // If we have valid proxies already, lets use them and just emit 212 // errors for the failovers. 213 if (proxyRules.length === 0) { 214 throw e; 215 } 216 let error = context.normalizeError(e); 217 context.extension.emit("proxy-error", { 218 message: error.message, 219 fileName: error.fileName, 220 lineNumber: error.lineNumber, 221 stack: error.stack, 222 }); 223 } 224 proxyData = proxyRules; 225 // fall through 226 case "object": 227 if (Array.isArray(proxyData) && proxyData.length > 0) { 228 return ProxyInfoData.createProxyInfoFromData(proxyData, defaultProxyInfo); 229 } 230 // Not an array, fall through to error. 231 default: 232 throw new ExtensionError("ProxyInfoData: proxyData must be a string or array of objects"); 233 } 234 }, 235}; 236 237function normalizeFilter(filter) { 238 if (!filter) { 239 filter = {}; 240 } 241 242 return {urls: filter.urls || null, types: filter.types || null}; 243} 244 245class ProxyChannelFilter { 246 constructor(context, listener, filter, extraInfoSpec) { 247 this.context = context; 248 this.filter = normalizeFilter(filter); 249 this.listener = listener; 250 this.extraInfoSpec = extraInfoSpec || []; 251 252 ProxyService.registerChannelFilter( 253 this /* nsIProtocolProxyChannelFilter aFilter */, 254 0 /* unsigned long aPosition */ 255 ); 256 } 257 258 // Copy from WebRequest.jsm with small changes. 259 getRequestData(channel, extraData) { 260 let data = { 261 requestId: String(channel.id), 262 url: channel.finalURL, 263 method: channel.method, 264 type: channel.type, 265 fromCache: !!channel.fromCache, 266 267 originUrl: channel.originURL || undefined, 268 documentUrl: channel.documentURL || undefined, 269 270 frameId: channel.windowId, 271 parentFrameId: channel.parentWindowId, 272 273 frameAncestors: channel.frameAncestors || undefined, 274 275 timeStamp: Date.now(), 276 277 ...extraData, 278 }; 279 if (this.extraInfoSpec.includes("requestHeaders")) { 280 data.requestHeaders = channel.getRequestHeaders(); 281 } 282 return data; 283 } 284 285 /** 286 * This method (which is required by the nsIProtocolProxyService interface) 287 * is called to apply proxy filter rules for the given URI and proxy object 288 * (or list of proxy objects). 289 * 290 * @param {nsIProtocolProxyService} service A reference to the Protocol Proxy Service. 291 * @param {nsIChannel} channel The channel for which these proxy settings apply. 292 * @param {nsIProxyInfo} defaultProxyInfo The proxy (or list of proxies) that 293 * would be used by default for the given URI. This may be null. 294 * @param {nsIProtocolProxyChannelFilter} proxyFilter 295 */ 296 async applyFilter(service, channel, defaultProxyInfo, proxyFilter) { 297 let proxyInfo; 298 try { 299 let wrapper = ChannelWrapper.get(channel); 300 301 let browserData = {tabId: -1, windowId: -1}; 302 if (wrapper.browserElement) { 303 browserData = tabTracker.getBrowserData(wrapper.browserElement); 304 } 305 let {filter} = this; 306 if (filter.tabId != null && browserData.tabId !== filter.tabId) { 307 return; 308 } 309 if (filter.windowId != null && browserData.windowId !== filter.windowId) { 310 return; 311 } 312 313 if (wrapper.matches(filter, this.context.extension.policy, {isProxy: true})) { 314 let data = this.getRequestData(wrapper, {tabId: browserData.tabId}); 315 316 let ret = await this.listener(data); 317 if (ret == null) { 318 // If ret undefined or null, fall through to the `finally` block to apply the proxy result. 319 proxyInfo = ret; 320 return; 321 } 322 // We only accept proxyInfo objects, not the PAC strings. ProxyInfoData will 323 // accept either, so we want to enforce the limit here. 324 if (typeof ret !== "object") { 325 throw new ExtensionError("ProxyInfoData: proxyData must be an object or array of objects"); 326 } 327 // We allow the call to return either a single proxyInfo or an array of proxyInfo. 328 if (!Array.isArray(ret)) { 329 ret = [ret]; 330 } 331 proxyInfo = ProxyInfoData.createProxyInfoFromData(ret, defaultProxyInfo); 332 } 333 } catch (e) { 334 let error = this.context.normalizeError(e); 335 this.context.extension.emit("proxy-error", { 336 message: error.message, 337 fileName: error.fileName, 338 lineNumber: error.lineNumber, 339 stack: error.stack, 340 }); 341 } finally { 342 // We must call onProxyFilterResult. proxyInfo may be null or nsIProxyInfo. 343 // defaultProxyInfo will be null unless a prior proxy handler has set something. 344 // If proxyInfo is null, that removes any prior proxy config. This allows a 345 // proxy extension to override higher level (e.g. prefs) config under certain 346 // circumstances. 347 proxyFilter.onProxyFilterResult(proxyInfo !== undefined ? proxyInfo : defaultProxyInfo); 348 } 349 } 350 351 destroy() { 352 ProxyService.unregisterFilter(this); 353 } 354} 355 356class ProxyScriptContext extends BaseContext { 357 constructor(extension, url, contextInfo = {}) { 358 super("proxy_script", extension); 359 this.contextInfo = contextInfo; 360 this.extension = extension; 361 this.messageManager = Services.cpmm; 362 this.sandbox = Cu.Sandbox(this.extension.principal, { 363 sandboxName: `Extension Proxy Script (${extension.policy.debugName}): ${url}`, 364 metadata: {addonID: extension.id}, 365 }); 366 this.url = url; 367 this.FindProxyForURL = null; 368 } 369 370 /** 371 * Loads and validates a proxy script into the sandbox, and then 372 * registers a new proxy filter for the context. 373 * 374 * @returns {boolean} true if load succeeded; false otherwise. 375 */ 376 load() { 377 Schemas.exportLazyGetter(this.sandbox, "browser", () => this.browserObj); 378 379 try { 380 Services.scriptloader.loadSubScript(this.url, this.sandbox, "UTF-8"); 381 } catch (error) { 382 this.extension.emit("proxy-error", { 383 message: this.normalizeError(error).message, 384 }); 385 return false; 386 } 387 388 this.FindProxyForURL = Cu.unwaiveXrays(this.sandbox.FindProxyForURL); 389 if (typeof this.FindProxyForURL !== "function") { 390 this.extension.emit("proxy-error", { 391 message: "The proxy script must define FindProxyForURL as a function", 392 }); 393 return false; 394 } 395 396 ProxyService.registerFilter( 397 this /* nsIProtocolProxyFilter aFilter */, 398 0 /* unsigned long aPosition */ 399 ); 400 401 return true; 402 } 403 404 get principal() { 405 return this.extension.principal; 406 } 407 408 get cloneScope() { 409 return this.sandbox; 410 } 411 412 /** 413 * This method (which is required by the nsIProtocolProxyService interface) 414 * is called to apply proxy filter rules for the given URI and proxy object 415 * (or list of proxy objects). 416 * 417 * @param {Object} service A reference to the Protocol Proxy Service. 418 * @param {Object} uri The URI for which these proxy settings apply. 419 * @param {Object} defaultProxyInfo The proxy (or list of proxies) that 420 * would be used by default for the given URI. This may be null. 421 * @param {Object} callback nsIProxyProtocolFilterResult to call onProxyFilterResult 422 on with the proxy info to apply for the given URI. 423 */ 424 applyFilter(service, uri, defaultProxyInfo, callback) { 425 try { 426 // TODO Bug 1337001 - provide path and query components to non-https URLs. 427 let ret = this.FindProxyForURL(uri.prePath, uri.host, this.contextInfo); 428 ret = ProxyInfoData.proxyInfoFromProxyData(this, ret, defaultProxyInfo); 429 callback.onProxyFilterResult(ret); 430 } catch (e) { 431 let error = this.normalizeError(e); 432 this.extension.emit("proxy-error", { 433 message: error.message, 434 fileName: error.fileName, 435 lineNumber: error.lineNumber, 436 stack: error.stack, 437 }); 438 callback.onProxyFilterResult(defaultProxyInfo); 439 } 440 } 441 442 /** 443 * Unloads the proxy filter and shuts down the sandbox. 444 */ 445 unload() { 446 super.unload(); 447 ProxyService.unregisterFilter(this); 448 Cu.nukeSandbox(this.sandbox); 449 this.sandbox = null; 450 } 451} 452 453class ProxyScriptAPIManager extends SchemaAPIManager { 454 constructor() { 455 super("proxy", Schemas); 456 this.initialized = false; 457 } 458 459 lazyInit() { 460 if (!this.initialized) { 461 this.initGlobal(); 462 let entries = XPCOMUtils.enumerateCategoryEntries(CATEGORY_EXTENSION_SCRIPTS_CONTENT); 463 for (let [/* name */, value] of entries) { 464 this.loadScript(value); 465 } 466 this.initialized = true; 467 } 468 } 469} 470 471class ProxyScriptInjectionContext { 472 constructor(context, apiCan) { 473 this.context = context; 474 this.localAPIs = apiCan.root; 475 this.apiCan = apiCan; 476 } 477 478 shouldInject(namespace, name, allowedContexts) { 479 if (this.context.envType !== "proxy_script") { 480 throw new Error(`Unexpected context type "${this.context.envType}"`); 481 } 482 483 // Do not generate proxy script APIs unless explicitly allowed. 484 return allowedContexts.includes("proxy"); 485 } 486 487 getImplementation(namespace, name) { 488 this.apiCan.findAPIPath(`${namespace}.${name}`); 489 let obj = this.apiCan.findAPIPath(namespace); 490 491 if (obj && name in obj) { 492 return new LocalAPIImplementation(obj, name, this.context); 493 } 494 } 495 496 get cloneScope() { 497 return this.context.cloneScope; 498 } 499 500 get principal() { 501 return this.context.principal; 502 } 503} 504 505defineLazyGetter(ProxyScriptContext.prototype, "messenger", function() { 506 let sender = {id: this.extension.id, frameId: this.frameId, url: this.url}; 507 let filter = {extensionId: this.extension.id, toProxyScript: true}; 508 return new ExtensionChild.Messenger(this, [this.messageManager], sender, filter); 509}); 510 511let proxyScriptAPIManager = new ProxyScriptAPIManager(); 512 513defineLazyGetter(ProxyScriptContext.prototype, "browserObj", function() { 514 let localAPIs = {}; 515 let can = new CanOfAPIs(this, proxyScriptAPIManager, localAPIs); 516 proxyScriptAPIManager.lazyInit(); 517 518 let browserObj = Cu.createObjectIn(this.sandbox); 519 let injectionContext = new ProxyScriptInjectionContext(this, can); 520 proxyScriptAPIManager.schema.inject(browserObj, injectionContext); 521 return browserObj; 522}); 523