OLD | NEW |
(Empty) | |
| 1 # This Source Code Form is subject to the terms of the Mozilla Public |
| 2 # License, v. 2.0. If a copy of the MPL was not distributed with this file, |
| 3 # You can obtain one at http://mozilla.org/MPL/2.0/. |
| 4 |
| 5 |
| 6 """ |
| 7 add permissions to the profile |
| 8 """ |
| 9 |
| 10 __all__ = ['MissingPrimaryLocationError', 'MultiplePrimaryLocationsError', |
| 11 'DuplicateLocationError', 'BadPortLocationError', |
| 12 'LocationsSyntaxError', 'Location', 'ServerLocations', |
| 13 'Permissions'] |
| 14 |
| 15 import codecs |
| 16 import itertools |
| 17 import os |
| 18 try: |
| 19 import sqlite3 |
| 20 except ImportError: |
| 21 from pysqlite2 import dbapi2 as sqlite3 |
| 22 import urlparse |
| 23 |
| 24 |
| 25 class LocationError(Exception): |
| 26 "Signifies an improperly formed location." |
| 27 |
| 28 def __str__(self): |
| 29 s = "Bad location" |
| 30 if self.message: |
| 31 s += ": %s" % self.message |
| 32 return s |
| 33 |
| 34 |
| 35 class MissingPrimaryLocationError(LocationError): |
| 36 "No primary location defined in locations file." |
| 37 |
| 38 def __init__(self): |
| 39 LocationError.__init__(self, "missing primary location") |
| 40 |
| 41 |
| 42 class MultiplePrimaryLocationsError(LocationError): |
| 43 "More than one primary location defined." |
| 44 |
| 45 def __init__(self): |
| 46 LocationError.__init__(self, "multiple primary locations") |
| 47 |
| 48 |
| 49 class DuplicateLocationError(LocationError): |
| 50 "Same location defined twice." |
| 51 |
| 52 def __init__(self, url): |
| 53 LocationError.__init__(self, "duplicate location: %s" % url) |
| 54 |
| 55 |
| 56 class BadPortLocationError(LocationError): |
| 57 "Location has invalid port value." |
| 58 |
| 59 def __init__(self, given_port): |
| 60 LocationError.__init__(self, "bad value for port: %s" % given_port) |
| 61 |
| 62 |
| 63 class LocationsSyntaxError(Exception): |
| 64 "Signifies a syntax error on a particular line in server-locations.txt." |
| 65 |
| 66 def __init__(self, lineno, err=None): |
| 67 self.err = err |
| 68 self.lineno = lineno |
| 69 |
| 70 def __str__(self): |
| 71 s = "Syntax error on line %s" % self.lineno |
| 72 if self.err: |
| 73 s += ": %s." % self.err |
| 74 else: |
| 75 s += "." |
| 76 return s |
| 77 |
| 78 |
| 79 class Location(object): |
| 80 "Represents a location line in server-locations.txt." |
| 81 |
| 82 attrs = ('scheme', 'host', 'port') |
| 83 |
| 84 def __init__(self, scheme, host, port, options): |
| 85 for attr in self.attrs: |
| 86 setattr(self, attr, locals()[attr]) |
| 87 self.options = options |
| 88 try: |
| 89 int(self.port) |
| 90 except ValueError: |
| 91 raise BadPortLocationError(self.port) |
| 92 |
| 93 def isEqual(self, location): |
| 94 "compare scheme://host:port, but ignore options" |
| 95 return len([i for i in self.attrs if getattr(self, i) == getattr(locatio
n, i)]) == len(self.attrs) |
| 96 |
| 97 __eq__ = isEqual |
| 98 |
| 99 def url(self): |
| 100 return '%s://%s:%s' % (self.scheme, self.host, self.port) |
| 101 |
| 102 def __str__(self): |
| 103 return '%s %s' % (self.url(), ','.join(self.options)) |
| 104 |
| 105 |
| 106 class ServerLocations(object): |
| 107 """Iterable collection of locations. |
| 108 Use provided functions to add new locations, rather that manipulating |
| 109 _locations directly, in order to check for errors and to ensure the |
| 110 callback is called, if given. |
| 111 """ |
| 112 |
| 113 def __init__(self, filename=None, add_callback=None): |
| 114 self.add_callback = add_callback |
| 115 self._locations = [] |
| 116 self.hasPrimary = False |
| 117 if filename: |
| 118 self.read(filename) |
| 119 |
| 120 def __iter__(self): |
| 121 return self._locations.__iter__() |
| 122 |
| 123 def __len__(self): |
| 124 return len(self._locations) |
| 125 |
| 126 def add(self, location, suppress_callback=False): |
| 127 if "primary" in location.options: |
| 128 if self.hasPrimary: |
| 129 raise MultiplePrimaryLocationsError() |
| 130 self.hasPrimary = True |
| 131 |
| 132 self._locations.append(location) |
| 133 if self.add_callback and not suppress_callback: |
| 134 self.add_callback([location]) |
| 135 |
| 136 def add_host(self, host, port='80', scheme='http', options='privileged'): |
| 137 if isinstance(options, basestring): |
| 138 options = options.split(',') |
| 139 self.add(Location(scheme, host, port, options)) |
| 140 |
| 141 def read(self, filename, check_for_primary=True): |
| 142 """ |
| 143 Reads the file (in the format of server-locations.txt) and add all |
| 144 valid locations to the self._locations array. |
| 145 |
| 146 If check_for_primary is True, a MissingPrimaryLocationError |
| 147 exception is raised if no primary is found. |
| 148 |
| 149 This format: |
| 150 http://mxr.mozilla.org/mozilla-central/source/build/pgo/server-locations
.txt |
| 151 The only exception is that the port, if not defined, defaults to 80 or 4
43. |
| 152 |
| 153 FIXME: Shouldn't this default to the protocol-appropriate port? Is |
| 154 there any reason to have defaults at all? |
| 155 """ |
| 156 |
| 157 locationFile = codecs.open(filename, "r", "UTF-8") |
| 158 lineno = 0 |
| 159 new_locations = [] |
| 160 |
| 161 for line in locationFile: |
| 162 line = line.strip() |
| 163 lineno += 1 |
| 164 |
| 165 # check for comments and blank lines |
| 166 if line.startswith("#") or not line: |
| 167 continue |
| 168 |
| 169 # split the server from the options |
| 170 try: |
| 171 server, options = line.rsplit(None, 1) |
| 172 options = options.split(',') |
| 173 except ValueError: |
| 174 server = line |
| 175 options = [] |
| 176 |
| 177 # parse the server url |
| 178 if '://' not in server: |
| 179 server = 'http://' + server |
| 180 scheme, netloc, path, query, fragment = urlparse.urlsplit(server) |
| 181 # get the host and port |
| 182 try: |
| 183 host, port = netloc.rsplit(':', 1) |
| 184 except ValueError: |
| 185 host = netloc |
| 186 default_ports = {'http': '80', |
| 187 'https': '443', |
| 188 'ws': '443', |
| 189 'wss': '443'} |
| 190 port = default_ports.get(scheme, '80') |
| 191 |
| 192 try: |
| 193 location = Location(scheme, host, port, options) |
| 194 self.add(location, suppress_callback=True) |
| 195 except LocationError, e: |
| 196 raise LocationsSyntaxError(lineno, e) |
| 197 |
| 198 new_locations.append(location) |
| 199 |
| 200 # ensure that a primary is found |
| 201 if check_for_primary and not self.hasPrimary: |
| 202 raise LocationsSyntaxError(lineno + 1, |
| 203 MissingPrimaryLocationError()) |
| 204 |
| 205 if self.add_callback: |
| 206 self.add_callback(new_locations) |
| 207 |
| 208 |
| 209 class Permissions(object): |
| 210 _num_permissions = 0 |
| 211 |
| 212 def __init__(self, profileDir, locations=None): |
| 213 self._profileDir = profileDir |
| 214 self._locations = ServerLocations(add_callback=self.write_db) |
| 215 if locations: |
| 216 if isinstance(locations, ServerLocations): |
| 217 self._locations = locations |
| 218 self._locations.add_callback = self.write_db |
| 219 self.write_db(self._locations._locations) |
| 220 elif isinstance(locations, list): |
| 221 for l in locations: |
| 222 self._locations.add_host(**l) |
| 223 elif isinstance(locations, dict): |
| 224 self._locations.add_host(**locations) |
| 225 elif os.path.exists(locations): |
| 226 self._locations.read(locations) |
| 227 |
| 228 def write_db(self, locations): |
| 229 """write permissions to the sqlite database""" |
| 230 |
| 231 # Open database and create table |
| 232 permDB = sqlite3.connect(os.path.join(self._profileDir, "permissions.sql
ite")) |
| 233 cursor = permDB.cursor(); |
| 234 # SQL copied from |
| 235 # http://mxr.mozilla.org/mozilla-central/source/extensions/cookie/nsPerm
issionManager.cpp |
| 236 cursor.execute("""CREATE TABLE IF NOT EXISTS moz_hosts ( |
| 237 id INTEGER PRIMARY KEY, |
| 238 host TEXT, |
| 239 type TEXT, |
| 240 permission INTEGER, |
| 241 expireType INTEGER, |
| 242 expireTime INTEGER)""") |
| 243 |
| 244 for location in locations: |
| 245 # set the permissions |
| 246 permissions = { 'allowXULXBL': 'noxul' not in location.options } |
| 247 for perm, allow in permissions.iteritems(): |
| 248 self._num_permissions += 1 |
| 249 if allow: |
| 250 permission_type = 1 |
| 251 else: |
| 252 permission_type = 2 |
| 253 cursor.execute("INSERT INTO moz_hosts values(?, ?, ?, ?, 0, 0)", |
| 254 (self._num_permissions, location.host, perm, |
| 255 permission_type)) |
| 256 |
| 257 # Commit and close |
| 258 permDB.commit() |
| 259 cursor.close() |
| 260 |
| 261 def network_prefs(self, proxy=False): |
| 262 """ |
| 263 take known locations and generate preferences to handle permissions and
proxy |
| 264 returns a tuple of prefs, user_prefs |
| 265 """ |
| 266 |
| 267 # Grant God-power to all the privileged servers on which tests run. |
| 268 prefs = [] |
| 269 privileged = [i for i in self._locations if "privileged" in i.options] |
| 270 for (i, l) in itertools.izip(itertools.count(1), privileged): |
| 271 prefs.append(("capability.principal.codebase.p%s.granted" % i, "Univ
ersalXPConnect")) |
| 272 |
| 273 prefs.append(("capability.principal.codebase.p%s.id" % i, "%s://%s:%
s" % |
| 274 (l.scheme, l.host, l.port))) |
| 275 prefs.append(("capability.principal.codebase.p%s.subjectName" % i, "
")) |
| 276 |
| 277 if proxy: |
| 278 user_prefs = self.pac_prefs() |
| 279 else: |
| 280 user_prefs = [] |
| 281 |
| 282 return prefs, user_prefs |
| 283 |
| 284 def pac_prefs(self): |
| 285 """ |
| 286 return preferences for Proxy Auto Config. originally taken from |
| 287 http://mxr.mozilla.org/mozilla-central/source/build/automation.py.in |
| 288 """ |
| 289 |
| 290 prefs = [] |
| 291 |
| 292 # We need to proxy every server but the primary one. |
| 293 origins = ["'%s'" % l.url() |
| 294 for l in self._locations] |
| 295 |
| 296 origins = ", ".join(origins) |
| 297 |
| 298 for l in self._locations: |
| 299 if "primary" in l.options: |
| 300 webServer = l.host |
| 301 port = l.port |
| 302 |
| 303 # TODO: this should live in a template! |
| 304 # TODO: So changing the 5th line of the regex below from (\\\\\\\\d+) |
| 305 # to (\\\\d+) makes this code work. Not sure why there would be this |
| 306 # difference between automation.py.in and this file. |
| 307 pacURL = """data:text/plain, |
| 308 function FindProxyForURL(url, host) |
| 309 { |
| 310 var origins = [%(origins)s]; |
| 311 var regex = new RegExp('^([a-z][-a-z0-9+.]*)' + |
| 312 '://' + |
| 313 '(?:[^/@]*@)?' + |
| 314 '(.*?)' + |
| 315 '(?::(\\\\d+))?/'); |
| 316 var matches = regex.exec(url); |
| 317 if (!matches) |
| 318 return 'DIRECT'; |
| 319 var isHttp = matches[1] == 'http'; |
| 320 var isHttps = matches[1] == 'https'; |
| 321 var isWebSocket = matches[1] == 'ws'; |
| 322 var isWebSocketSSL = matches[1] == 'wss'; |
| 323 if (!matches[3]) |
| 324 { |
| 325 if (isHttp | isWebSocket) matches[3] = '80'; |
| 326 if (isHttps | isWebSocketSSL) matches[3] = '443'; |
| 327 } |
| 328 if (isWebSocket) |
| 329 matches[1] = 'http'; |
| 330 if (isWebSocketSSL) |
| 331 matches[1] = 'https'; |
| 332 |
| 333 var origin = matches[1] + '://' + matches[2] + ':' + matches[3]; |
| 334 if (origins.indexOf(origin) < 0) |
| 335 return 'DIRECT'; |
| 336 if (isHttp || isHttps || isWebSocket || isWebSocketSSL) |
| 337 return 'PROXY %(remote)s:%(port)s'; |
| 338 return 'DIRECT'; |
| 339 }""" % { "origins": origins, |
| 340 "remote": webServer, |
| 341 "port": port } |
| 342 pacURL = "".join(pacURL.splitlines()) |
| 343 |
| 344 prefs.append(("network.proxy.type", 2)) |
| 345 prefs.append(("network.proxy.autoconfig_url", pacURL)) |
| 346 |
| 347 return prefs |
| 348 |
| 349 def clean_db(self): |
| 350 """Removed permissions added by mozprofile.""" |
| 351 |
| 352 sqlite_file = os.path.join(self._profileDir, "permissions.sqlite") |
| 353 if not os.path.exists(sqlite_file): |
| 354 return |
| 355 |
| 356 # Open database and create table |
| 357 permDB = sqlite3.connect(sqlite_file) |
| 358 cursor = permDB.cursor(); |
| 359 |
| 360 # TODO: only delete values that we add, this would require sending in th
e full permissions object |
| 361 cursor.execute("DROP TABLE IF EXISTS moz_hosts"); |
| 362 |
| 363 # Commit and close |
| 364 permDB.commit() |
| 365 cursor.close() |
OLD | NEW |