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""" 7add permissions to the profile 8""" 9 10from __future__ import absolute_import 11 12import codecs 13import os 14import sqlite3 15 16from six import string_types 17from six.moves.urllib import parse 18 19__all__ = ['MissingPrimaryLocationError', 'MultiplePrimaryLocationsError', 20 'DEFAULT_PORTS', 'DuplicateLocationError', 'BadPortLocationError', 21 'LocationsSyntaxError', 'Location', 'ServerLocations', 22 'Permissions'] 23 24# http://hg.mozilla.org/mozilla-central/file/b871dfb2186f/build/automation.py.in#l28 25DEFAULT_PORTS = {'http': '8888', 26 'https': '4443', 27 'ws': '4443', 28 'wss': '4443'} 29 30 31class LocationError(Exception): 32 """Signifies an improperly formed location.""" 33 34 def __str__(self): 35 s = "Bad location" 36 if self.message: 37 s += ": %s" % self.message 38 return s 39 40 41class MissingPrimaryLocationError(LocationError): 42 """No primary location defined in locations file.""" 43 44 def __init__(self): 45 LocationError.__init__(self, "missing primary location") 46 47 48class MultiplePrimaryLocationsError(LocationError): 49 """More than one primary location defined.""" 50 51 def __init__(self): 52 LocationError.__init__(self, "multiple primary locations") 53 54 55class DuplicateLocationError(LocationError): 56 """Same location defined twice.""" 57 58 def __init__(self, url): 59 LocationError.__init__(self, "duplicate location: %s" % url) 60 61 62class BadPortLocationError(LocationError): 63 """Location has invalid port value.""" 64 65 def __init__(self, given_port): 66 LocationError.__init__(self, "bad value for port: %s" % given_port) 67 68 69class LocationsSyntaxError(Exception): 70 """Signifies a syntax error on a particular line in server-locations.txt.""" 71 72 def __init__(self, lineno, err=None): 73 self.err = err 74 self.lineno = lineno 75 76 def __str__(self): 77 s = "Syntax error on line %s" % self.lineno 78 if self.err: 79 s += ": %s." % self.err 80 else: 81 s += "." 82 return s 83 84 85class Location(object): 86 """Represents a location line in server-locations.txt.""" 87 88 attrs = ('scheme', 'host', 'port') 89 90 def __init__(self, scheme, host, port, options): 91 for attr in self.attrs: 92 setattr(self, attr, locals()[attr]) 93 self.options = options 94 try: 95 int(self.port) 96 except ValueError: 97 raise BadPortLocationError(self.port) 98 99 def isEqual(self, location): 100 """compare scheme://host:port, but ignore options""" 101 return len([i for i in self.attrs 102 if getattr(self, i) == getattr(location, i)]) == len(self.attrs) 103 104 __eq__ = isEqual 105 106 def url(self): 107 return '%s://%s:%s' % (self.scheme, self.host, self.port) 108 109 def __str__(self): 110 return '%s %s' % (self.url(), ','.join(self.options)) 111 112 113class ServerLocations(object): 114 """Iterable collection of locations. 115 Use provided functions to add new locations, rather that manipulating 116 _locations directly, in order to check for errors and to ensure the 117 callback is called, if given. 118 """ 119 120 def __init__(self, filename=None, add_callback=None): 121 self.add_callback = add_callback 122 self._locations = [] 123 self.hasPrimary = False 124 if filename: 125 self.read(filename) 126 127 def __iter__(self): 128 return self._locations.__iter__() 129 130 def __len__(self): 131 return len(self._locations) 132 133 def add(self, location, suppress_callback=False): 134 if "primary" in location.options: 135 if self.hasPrimary: 136 raise MultiplePrimaryLocationsError() 137 self.hasPrimary = True 138 139 self._locations.append(location) 140 if self.add_callback and not suppress_callback: 141 self.add_callback([location]) 142 143 def add_host(self, host, port='80', scheme='http', options='privileged'): 144 if isinstance(options, string_types): 145 options = options.split(',') 146 self.add(Location(scheme, host, port, options)) 147 148 def read(self, filename, check_for_primary=True): 149 """ 150 Reads the file and adds all valid locations to the ``self._locations`` array. 151 152 :param filename: in the format of server-locations.txt_ 153 :param check_for_primary: if True, a ``MissingPrimaryLocationError`` exception is raised 154 if no primary is found 155 156 .. _server-locations.txt: http://dxr.mozilla.org/mozilla-central/source/build/pgo/server-locations.txt # noqa 157 158 The only exception is that the port, if not defined, defaults to 80 or 443. 159 160 FIXME: Shouldn't this default to the protocol-appropriate port? Is 161 there any reason to have defaults at all? 162 """ 163 164 locationFile = codecs.open(filename, "r", "UTF-8") 165 lineno = 0 166 new_locations = [] 167 168 for line in locationFile: 169 line = line.strip() 170 lineno += 1 171 172 # check for comments and blank lines 173 if line.startswith("#") or not line: 174 continue 175 176 # split the server from the options 177 try: 178 server, options = line.rsplit(None, 1) 179 options = options.split(',') 180 except ValueError: 181 server = line 182 options = [] 183 184 # parse the server url 185 if '://' not in server: 186 server = 'http://' + server 187 scheme, netloc, path, query, fragment = parse.urlsplit(server) 188 # get the host and port 189 try: 190 host, port = netloc.rsplit(':', 1) 191 except ValueError: 192 host = netloc 193 port = DEFAULT_PORTS.get(scheme, '80') 194 195 try: 196 location = Location(scheme, host, port, options) 197 self.add(location, suppress_callback=True) 198 except LocationError as e: 199 raise LocationsSyntaxError(lineno, e) 200 201 new_locations.append(location) 202 203 # ensure that a primary is found 204 if check_for_primary and not self.hasPrimary: 205 raise LocationsSyntaxError(lineno + 1, 206 MissingPrimaryLocationError()) 207 208 if self.add_callback: 209 self.add_callback(new_locations) 210 211 212class Permissions(object): 213 """Allows handling of permissions for ``mozprofile``""" 214 215 def __init__(self, profileDir, locations=None): 216 self._profileDir = profileDir 217 self._locations = ServerLocations(add_callback=self.write_db) 218 if locations: 219 if isinstance(locations, ServerLocations): 220 self._locations = locations 221 self._locations.add_callback = self.write_db 222 self.write_db(self._locations._locations) 223 elif isinstance(locations, list): 224 for l in locations: 225 self._locations.add_host(**l) 226 elif isinstance(locations, dict): 227 self._locations.add_host(**locations) 228 elif os.path.exists(locations): 229 self._locations.read(locations) 230 231 def write_db(self, locations): 232 """write permissions to the sqlite database""" 233 234 # Open database and create table 235 permDB = sqlite3.connect(os.path.join(self._profileDir, "permissions.sqlite")) 236 cursor = permDB.cursor() 237 238 # SQL copied from 239 # http://dxr.mozilla.org/mozilla-central/source/extensions/cookie/nsPermissionManager.cpp 240 cursor.execute("""CREATE TABLE IF NOT EXISTS moz_hosts ( 241 id INTEGER PRIMARY KEY 242 ,origin TEXT 243 ,type TEXT 244 ,permission INTEGER 245 ,expireType INTEGER 246 ,expireTime INTEGER 247 ,modificationTime INTEGER 248 )""") 249 250 rows = cursor.execute("PRAGMA table_info(moz_hosts)") 251 count = len(rows.fetchall()) 252 253 using_origin = False 254 # if the db contains 7 columns, we're using user_version 5 255 if count == 7: 256 statement = "INSERT INTO moz_hosts values(NULL, ?, ?, ?, 0, 0, 0)" 257 cursor.execute("PRAGMA user_version=5;") 258 using_origin = True 259 # if the db contains 9 columns, we're using user_version 4 260 elif count == 9: 261 statement = "INSERT INTO moz_hosts values(NULL, ?, ?, ?, 0, 0, 0, 0, 0)" 262 cursor.execute("PRAGMA user_version=4;") 263 # if the db contains 8 columns, we're using user_version 3 264 elif count == 8: 265 statement = "INSERT INTO moz_hosts values(NULL, ?, ?, ?, 0, 0, 0, 0)" 266 cursor.execute("PRAGMA user_version=3;") 267 else: 268 statement = "INSERT INTO moz_hosts values(NULL, ?, ?, ?, 0, 0)" 269 cursor.execute("PRAGMA user_version=2;") 270 271 for location in locations: 272 # set the permissions 273 permissions = {'allowXULXBL': 'noxul' not in location.options} 274 for perm, allow in permissions.iteritems(): 275 if allow: 276 permission_type = 1 277 else: 278 permission_type = 2 279 280 if using_origin: 281 # This is a crude approximation of the origin generation 282 # logic from ContentPrincipal and nsStandardURL. It should 283 # suffice for the permissions which the test runners will 284 # want to insert into the system. 285 origin = location.scheme + "://" + location.host 286 if (location.scheme != 'http' or location.port != '80') and \ 287 (location.scheme != 'https' or location.port != '443'): 288 origin += ':' + str(location.port) 289 290 cursor.execute(statement, 291 (origin, perm, permission_type)) 292 else: 293 # The database is still using a legacy system based on hosts 294 # We can insert the permission as a host 295 # 296 # XXX This codepath should not be hit, as tests are run with 297 # fresh profiles. However, if it was hit, permissions would 298 # not be added to the database correctly (bug 1183185). 299 cursor.execute(statement, 300 (location.host, perm, permission_type)) 301 302 # Commit and close 303 permDB.commit() 304 cursor.close() 305 306 def network_prefs(self, proxy=None): 307 """ 308 take known locations and generate preferences to handle permissions and proxy 309 returns a tuple of prefs, user_prefs 310 """ 311 312 prefs = [] 313 314 if proxy: 315 user_prefs = self.pac_prefs(proxy) 316 else: 317 user_prefs = [] 318 319 return prefs, user_prefs 320 321 def pac_prefs(self, user_proxy=None): 322 """ 323 return preferences for Proxy Auto Config. originally taken from 324 http://dxr.mozilla.org/mozilla-central/source/build/automation.py.in 325 """ 326 proxy = DEFAULT_PORTS.copy() 327 328 # We need to proxy every server but the primary one. 329 origins = ["'%s'" % l.url() 330 for l in self._locations] 331 origins = ", ".join(origins) 332 proxy["origins"] = origins 333 334 for l in self._locations: 335 if "primary" in l.options: 336 proxy["remote"] = l.host 337 proxy[l.scheme] = l.port 338 339 # overwrite defaults with user specified proxy 340 if isinstance(user_proxy, dict): 341 proxy.update(user_proxy) 342 343 # TODO: this should live in a template! 344 # If you must escape things in this string with backslashes, be aware 345 # of the multiple layers of escaping at work: 346 # 347 # - Python will unescape backslashes; 348 # - Writing out the prefs will escape things via JSON serialization; 349 # - The prefs file reader will unescape backslashes; 350 # - The JS engine parser will unescape backslashes. 351 pacURL = """data:text/plain, 352var knownOrigins = (function () { 353 return [%(origins)s].reduce(function(t, h) { t[h] = true; return t; }, {}) 354})(); 355var uriRegex = new RegExp('^([a-z][-a-z0-9+.]*)' + 356 '://' + 357 '(?:[^/@]*@)?' + 358 '(.*?)' + 359 '(?::(\\\\d+))?/'); 360var defaultPortsForScheme = { 361 'http': 80, 362 'ws': 80, 363 'https': 443, 364 'wss': 443 365}; 366var originSchemesRemap = { 367 'ws': 'http', 368 'wss': 'https' 369}; 370var proxyForScheme = { 371 'http': 'PROXY %(remote)s:%(http)s', 372 'https': 'PROXY %(remote)s:%(https)s', 373 'ws': 'PROXY %(remote)s:%(ws)s', 374 'wss': 'PROXY %(remote)s:%(wss)s' 375}; 376 377function FindProxyForURL(url, host) 378{ 379 var matches = uriRegex.exec(url); 380 if (!matches) 381 return 'DIRECT'; 382 var originalScheme = matches[1]; 383 var host = matches[2]; 384 var port = matches[3]; 385 if (!port && originalScheme in defaultPortsForScheme) { 386 port = defaultPortsForScheme[originalScheme]; 387 } 388 var schemeForOriginChecking = originSchemesRemap[originalScheme] || originalScheme; 389 390 var origin = schemeForOriginChecking + '://' + host + ':' + port; 391 if (!(origin in knownOrigins)) 392 return 'DIRECT'; 393 return proxyForScheme[originalScheme] || 'DIRECT'; 394}""" % proxy 395 pacURL = "".join(pacURL.splitlines()) 396 397 prefs = [] 398 prefs.append(("network.proxy.type", 2)) 399 prefs.append(("network.proxy.autoconfig_url", pacURL)) 400 401 return prefs 402 403 def clean_db(self): 404 """Removed permissions added by mozprofile.""" 405 406 sqlite_file = os.path.join(self._profileDir, "permissions.sqlite") 407 if not os.path.exists(sqlite_file): 408 return 409 410 # Open database and create table 411 permDB = sqlite3.connect(sqlite_file) 412 cursor = permDB.cursor() 413 414 # TODO: only delete values that we add, this would require sending 415 # in the full permissions object 416 cursor.execute("DROP TABLE IF EXISTS moz_hosts") 417 418 # Commit and close 419 permDB.commit() 420 cursor.close() 421