| # This Source Code Form is subject to the terms of the Mozilla Public |
| # License, v. 2.0. If a copy of the MPL was not distributed with this file, |
| # You can obtain one at http://mozilla.org/MPL/2.0/. |
| |
| |
| """ |
| add permissions to the profile |
| """ |
| |
| __all__ = ['MissingPrimaryLocationError', 'MultiplePrimaryLocationsError', |
| 'DEFAULT_PORTS', 'DuplicateLocationError', 'BadPortLocationError', |
| 'LocationsSyntaxError', 'Location', 'ServerLocations', |
| 'Permissions'] |
| |
| import codecs |
| import os |
| import sqlite3 |
| import urlparse |
| |
| # http://hg.mozilla.org/mozilla-central/file/b871dfb2186f/build/automation.py.in#l28 |
| DEFAULT_PORTS = { 'http': '8888', |
| 'https': '4443', |
| 'ws': '4443', |
| 'wss': '4443' } |
| |
| class LocationError(Exception): |
| """Signifies an improperly formed location.""" |
| |
| def __str__(self): |
| s = "Bad location" |
| if self.message: |
| s += ": %s" % self.message |
| return s |
| |
| |
| class MissingPrimaryLocationError(LocationError): |
| """No primary location defined in locations file.""" |
| |
| def __init__(self): |
| LocationError.__init__(self, "missing primary location") |
| |
| |
| class MultiplePrimaryLocationsError(LocationError): |
| """More than one primary location defined.""" |
| |
| def __init__(self): |
| LocationError.__init__(self, "multiple primary locations") |
| |
| |
| class DuplicateLocationError(LocationError): |
| """Same location defined twice.""" |
| |
| def __init__(self, url): |
| LocationError.__init__(self, "duplicate location: %s" % url) |
| |
| |
| class BadPortLocationError(LocationError): |
| """Location has invalid port value.""" |
| |
| def __init__(self, given_port): |
| LocationError.__init__(self, "bad value for port: %s" % given_port) |
| |
| |
| class LocationsSyntaxError(Exception): |
| """Signifies a syntax error on a particular line in server-locations.txt.""" |
| |
| def __init__(self, lineno, err=None): |
| self.err = err |
| self.lineno = lineno |
| |
| def __str__(self): |
| s = "Syntax error on line %s" % self.lineno |
| if self.err: |
| s += ": %s." % self.err |
| else: |
| s += "." |
| return s |
| |
| |
| class Location(object): |
| """Represents a location line in server-locations.txt.""" |
| |
| attrs = ('scheme', 'host', 'port') |
| |
| def __init__(self, scheme, host, port, options): |
| for attr in self.attrs: |
| setattr(self, attr, locals()[attr]) |
| self.options = options |
| try: |
| int(self.port) |
| except ValueError: |
| raise BadPortLocationError(self.port) |
| |
| def isEqual(self, location): |
| """compare scheme://host:port, but ignore options""" |
| return len([i for i in self.attrs if getattr(self, i) == getattr(location, i)]) == len(self.attrs) |
| |
| __eq__ = isEqual |
| |
| def url(self): |
| return '%s://%s:%s' % (self.scheme, self.host, self.port) |
| |
| def __str__(self): |
| return '%s %s' % (self.url(), ','.join(self.options)) |
| |
| |
| class ServerLocations(object): |
| """Iterable collection of locations. |
| Use provided functions to add new locations, rather that manipulating |
| _locations directly, in order to check for errors and to ensure the |
| callback is called, if given. |
| """ |
| |
| def __init__(self, filename=None, add_callback=None): |
| self.add_callback = add_callback |
| self._locations = [] |
| self.hasPrimary = False |
| if filename: |
| self.read(filename) |
| |
| def __iter__(self): |
| return self._locations.__iter__() |
| |
| def __len__(self): |
| return len(self._locations) |
| |
| def add(self, location, suppress_callback=False): |
| if "primary" in location.options: |
| if self.hasPrimary: |
| raise MultiplePrimaryLocationsError() |
| self.hasPrimary = True |
| |
| self._locations.append(location) |
| if self.add_callback and not suppress_callback: |
| self.add_callback([location]) |
| |
| def add_host(self, host, port='80', scheme='http', options='privileged'): |
| if isinstance(options, basestring): |
| options = options.split(',') |
| self.add(Location(scheme, host, port, options)) |
| |
| def read(self, filename, check_for_primary=True): |
| """ |
| Reads the file and adds all valid locations to the ``self._locations`` array. |
| |
| :param filename: in the format of server-locations.txt_ |
| :param check_for_primary: if True, a ``MissingPrimaryLocationError`` exception is raised if no primary is found |
| |
| .. _server-locations.txt: http://mxr.mozilla.org/mozilla-central/source/build/pgo/server-locations.txt |
| |
| The only exception is that the port, if not defined, defaults to 80 or 443. |
| |
| FIXME: Shouldn't this default to the protocol-appropriate port? Is |
| there any reason to have defaults at all? |
| """ |
| |
| locationFile = codecs.open(filename, "r", "UTF-8") |
| lineno = 0 |
| new_locations = [] |
| |
| for line in locationFile: |
| line = line.strip() |
| lineno += 1 |
| |
| # check for comments and blank lines |
| if line.startswith("#") or not line: |
| continue |
| |
| # split the server from the options |
| try: |
| server, options = line.rsplit(None, 1) |
| options = options.split(',') |
| except ValueError: |
| server = line |
| options = [] |
| |
| # parse the server url |
| if '://' not in server: |
| server = 'http://' + server |
| scheme, netloc, path, query, fragment = urlparse.urlsplit(server) |
| # get the host and port |
| try: |
| host, port = netloc.rsplit(':', 1) |
| except ValueError: |
| host = netloc |
| port = DEFAULT_PORTS.get(scheme, '80') |
| |
| try: |
| location = Location(scheme, host, port, options) |
| self.add(location, suppress_callback=True) |
| except LocationError, e: |
| raise LocationsSyntaxError(lineno, e) |
| |
| new_locations.append(location) |
| |
| # ensure that a primary is found |
| if check_for_primary and not self.hasPrimary: |
| raise LocationsSyntaxError(lineno + 1, |
| MissingPrimaryLocationError()) |
| |
| if self.add_callback: |
| self.add_callback(new_locations) |
| |
| |
| class Permissions(object): |
| """Allows handling of permissions for ``mozprofile``""" |
| |
| def __init__(self, profileDir, locations=None): |
| self._profileDir = profileDir |
| self._locations = ServerLocations(add_callback=self.write_db) |
| if locations: |
| if isinstance(locations, ServerLocations): |
| self._locations = locations |
| self._locations.add_callback = self.write_db |
| self.write_db(self._locations._locations) |
| elif isinstance(locations, list): |
| for l in locations: |
| self._locations.add_host(**l) |
| elif isinstance(locations, dict): |
| self._locations.add_host(**locations) |
| elif os.path.exists(locations): |
| self._locations.read(locations) |
| |
| def write_db(self, locations): |
| """write permissions to the sqlite database""" |
| |
| # Open database and create table |
| permDB = sqlite3.connect(os.path.join(self._profileDir, "permissions.sqlite")) |
| cursor = permDB.cursor(); |
| |
| # SQL copied from |
| # http://mxr.mozilla.org/mozilla-central/source/extensions/cookie/nsPermissionManager.cpp |
| cursor.execute("""CREATE TABLE IF NOT EXISTS moz_hosts ( |
| id INTEGER PRIMARY KEY |
| ,origin TEXT |
| ,type TEXT |
| ,permission INTEGER |
| ,expireType INTEGER |
| ,expireTime INTEGER |
| ,modificationTime INTEGER |
| )""") |
| |
| rows = cursor.execute("PRAGMA table_info(moz_hosts)") |
| count = len(rows.fetchall()) |
| |
| using_origin = False |
| # if the db contains 7 columns, we're using user_version 5 |
| if count == 7: |
| statement = "INSERT INTO moz_hosts values(NULL, ?, ?, ?, 0, 0, 0)" |
| cursor.execute("PRAGMA user_version=5;") |
| using_origin = True |
| # if the db contains 9 columns, we're using user_version 4 |
| elif count == 9: |
| statement = "INSERT INTO moz_hosts values(NULL, ?, ?, ?, 0, 0, 0, 0, 0)" |
| cursor.execute("PRAGMA user_version=4;") |
| # if the db contains 8 columns, we're using user_version 3 |
| elif count == 8: |
| statement = "INSERT INTO moz_hosts values(NULL, ?, ?, ?, 0, 0, 0, 0)" |
| cursor.execute("PRAGMA user_version=3;") |
| else: |
| statement = "INSERT INTO moz_hosts values(NULL, ?, ?, ?, 0, 0)" |
| cursor.execute("PRAGMA user_version=2;") |
| |
| for location in locations: |
| # set the permissions |
| permissions = { 'allowXULXBL': 'noxul' not in location.options } |
| for perm, allow in permissions.iteritems(): |
| if allow: |
| permission_type = 1 |
| else: |
| permission_type = 2 |
| |
| if using_origin: |
| # This is a crude approximation of the origin generation logic from |
| # nsPrincipal and nsStandardURL. It should suffice for the permissions |
| # which the test runners will want to insert into the system. |
| origin = location.scheme + "://" + location.host |
| if (location.scheme != 'http' or location.port != '80') and \ |
| (location.scheme != 'https' or location.port != '443'): |
| origin += ':' + str(location.port) |
| |
| cursor.execute(statement, |
| (origin, perm, permission_type)) |
| else: |
| # The database is still using a legacy system based on hosts |
| # We can insert the permission as a host |
| # |
| # XXX This codepath should not be hit, as tests are run with |
| # fresh profiles. However, if it was hit, permissions would |
| # not be added to the database correctly (bug 1183185). |
| cursor.execute(statement, |
| (location.host, perm, permission_type)) |
| |
| # Commit and close |
| permDB.commit() |
| cursor.close() |
| |
| def network_prefs(self, proxy=None): |
| """ |
| take known locations and generate preferences to handle permissions and proxy |
| returns a tuple of prefs, user_prefs |
| """ |
| |
| prefs = [] |
| |
| if proxy: |
| user_prefs = self.pac_prefs(proxy) |
| else: |
| user_prefs = [] |
| |
| return prefs, user_prefs |
| |
| def pac_prefs(self, user_proxy=None): |
| """ |
| return preferences for Proxy Auto Config. originally taken from |
| http://mxr.mozilla.org/mozilla-central/source/build/automation.py.in |
| """ |
| proxy = DEFAULT_PORTS.copy() |
| |
| # We need to proxy every server but the primary one. |
| origins = ["'%s'" % l.url() |
| for l in self._locations] |
| origins = ", ".join(origins) |
| proxy["origins"] = origins |
| |
| for l in self._locations: |
| if "primary" in l.options: |
| proxy["remote"] = l.host |
| proxy[l.scheme] = l.port |
| |
| # overwrite defaults with user specified proxy |
| if isinstance(user_proxy, dict): |
| proxy.update(user_proxy) |
| |
| # TODO: this should live in a template! |
| # If you must escape things in this string with backslashes, be aware |
| # of the multiple layers of escaping at work: |
| # |
| # - Python will unescape backslashes; |
| # - Writing out the prefs will escape things via JSON serialization; |
| # - The prefs file reader will unescape backslashes; |
| # - The JS engine parser will unescape backslashes. |
| pacURL = """data:text/plain, |
| var knownOrigins = (function () { |
| return [%(origins)s].reduce(function(t, h) { t[h] = true; return t; }, {}) |
| })(); |
| var uriRegex = new RegExp('^([a-z][-a-z0-9+.]*)' + |
| '://' + |
| '(?:[^/@]*@)?' + |
| '(.*?)' + |
| '(?::(\\\\d+))?/'); |
| var defaultPortsForScheme = { |
| 'http': 80, |
| 'ws': 80, |
| 'https': 443, |
| 'wss': 443 |
| }; |
| var originSchemesRemap = { |
| 'ws': 'http', |
| 'wss': 'https' |
| }; |
| var proxyForScheme = { |
| 'http': 'PROXY %(remote)s:%(http)s', |
| 'https': 'PROXY %(remote)s:%(https)s', |
| 'ws': 'PROXY %(remote)s:%(ws)s', |
| 'wss': 'PROXY %(remote)s:%(wss)s' |
| }; |
| |
| function FindProxyForURL(url, host) |
| { |
| var matches = uriRegex.exec(url); |
| if (!matches) |
| return 'DIRECT'; |
| var originalScheme = matches[1]; |
| var host = matches[2]; |
| var port = matches[3]; |
| if (!port && originalScheme in defaultPortsForScheme) { |
| port = defaultPortsForScheme[originalScheme]; |
| } |
| var schemeForOriginChecking = originSchemesRemap[originalScheme] || originalScheme; |
| |
| var origin = schemeForOriginChecking + '://' + host + ':' + port; |
| if (!(origin in knownOrigins)) |
| return 'DIRECT'; |
| return proxyForScheme[originalScheme] || 'DIRECT'; |
| }""" % proxy |
| pacURL = "".join(pacURL.splitlines()) |
| |
| prefs = [] |
| prefs.append(("network.proxy.type", 2)) |
| prefs.append(("network.proxy.autoconfig_url", pacURL)) |
| |
| return prefs |
| |
| def clean_db(self): |
| """Removed permissions added by mozprofile.""" |
| |
| sqlite_file = os.path.join(self._profileDir, "permissions.sqlite") |
| if not os.path.exists(sqlite_file): |
| return |
| |
| # Open database and create table |
| permDB = sqlite3.connect(sqlite_file) |
| cursor = permDB.cursor(); |
| |
| # TODO: only delete values that we add, this would require sending in the full permissions object |
| cursor.execute("DROP TABLE IF EXISTS moz_hosts"); |
| |
| # Commit and close |
| permDB.commit() |
| cursor.close() |