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