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