util.py (8824B)
1 import os, sys, json, json5, re 2 import collections 3 4 script_directory = os.path.dirname(os.path.abspath(__file__)) 5 template_directory = os.path.abspath( 6 os.path.join(script_directory, 'template')) 7 test_root_directory = os.path.abspath( 8 os.path.join(script_directory, '..', '..', '..')) 9 10 11 def get_template(basename): 12 with open(os.path.join(template_directory, basename), "r") as f: 13 return f.read() 14 15 16 def write_file(filename, contents): 17 with open(filename, "w") as f: 18 f.write(contents) 19 20 21 def read_nth_line(fp, line_number): 22 fp.seek(0) 23 for i, line in enumerate(fp): 24 if (i + 1) == line_number: 25 return line 26 27 28 def load_spec_json(path_to_spec): 29 re_error_location = re.compile('line ([0-9]+) column ([0-9]+)') 30 with open(path_to_spec, "r") as f: 31 try: 32 return json5.load(f, object_pairs_hook=collections.OrderedDict) 33 except ValueError as ex: 34 print(ex.message) 35 match = re_error_location.search(ex.message) 36 if match: 37 line_number, column = int(match.group(1)), int(match.group(2)) 38 print(read_nth_line(f, line_number).rstrip()) 39 print(" " * (column - 1) + "^") 40 sys.exit(1) 41 42 43 class ShouldSkip(Exception): 44 ''' 45 Raised when the given combination of subresource type, source context type, 46 delivery type etc. are not supported and we should skip that configuration. 47 ShouldSkip is expected in normal generator execution (and thus subsequent 48 generation continues), as we first enumerate a broad range of configurations 49 first, and later raise ShouldSkip to filter out unsupported combinations. 50 51 ShouldSkip is distinguished from other general errors that cause immediate 52 termination of the generator and require fix. 53 ''' 54 def __init__(self): 55 pass 56 57 58 class PolicyDelivery(object): 59 ''' 60 See `@typedef PolicyDelivery` comments in 61 `common/security-features/resources/common.sub.js`. 62 ''' 63 64 def __init__(self, delivery_type, key, value): 65 self.delivery_type = delivery_type 66 self.key = key 67 self.value = value 68 69 def __eq__(self, other): 70 return type(self) is type(other) and self.__dict__ == other.__dict__ 71 72 @classmethod 73 def list_from_json(cls, list, target_policy_delivery, 74 supported_delivery_types): 75 # type: (dict, PolicyDelivery, typing.List[str]) -> typing.List[PolicyDelivery] 76 ''' 77 Parses a JSON object `list` that represents a list of `PolicyDelivery` 78 and returns a list of `PolicyDelivery`, plus supporting placeholders 79 (see `from_json()` comments below or 80 `common/security-features/README.md`). 81 82 Can raise `ShouldSkip`. 83 ''' 84 if list is None: 85 return [] 86 87 out = [] 88 for obj in list: 89 policy_delivery = PolicyDelivery.from_json( 90 obj, target_policy_delivery, supported_delivery_types) 91 # Drop entries with null values. 92 if policy_delivery.value is None: 93 continue 94 out.append(policy_delivery) 95 return out 96 97 @classmethod 98 def from_json(cls, obj, target_policy_delivery, supported_delivery_types): 99 # type: (dict, PolicyDelivery, typing.List[str]) -> PolicyDelivery 100 ''' 101 Parses a JSON object `obj` and returns a `PolicyDelivery` object. 102 In addition to dicts (in the same format as to_json() outputs), 103 this method accepts the following placeholders: 104 "policy": 105 `target_policy_delivery` 106 "policyIfNonNull": 107 `target_policy_delivery` if its value is not None. 108 "anotherPolicy": 109 A PolicyDelivery that has the same key as 110 `target_policy_delivery` but a different value. 111 The delivery type is selected from `supported_delivery_types`. 112 113 Can raise `ShouldSkip`. 114 ''' 115 116 if obj == "policy": 117 policy_delivery = target_policy_delivery 118 elif obj == "nonNullPolicy": 119 if target_policy_delivery.value is None: 120 raise ShouldSkip() 121 policy_delivery = target_policy_delivery 122 elif obj == "anotherPolicy": 123 if len(supported_delivery_types) == 0: 124 raise ShouldSkip() 125 policy_delivery = target_policy_delivery.get_another_policy( 126 supported_delivery_types[0]) 127 elif isinstance(obj, dict): 128 policy_delivery = PolicyDelivery(obj['deliveryType'], obj['key'], 129 obj['value']) 130 else: 131 raise Exception('policy delivery is invalid: ' + obj) 132 133 # Omit unsupported combinations of source contexts and delivery type. 134 if policy_delivery.delivery_type not in supported_delivery_types: 135 raise ShouldSkip() 136 137 return policy_delivery 138 139 def to_json(self): 140 # type: () -> dict 141 return { 142 "deliveryType": self.delivery_type, 143 "key": self.key, 144 "value": self.value 145 } 146 147 def get_another_policy(self, delivery_type): 148 # type: (str) -> PolicyDelivery 149 if self.key == 'referrerPolicy': 150 # Return 'unsafe-url' (i.e. more unsafe policy than `self.value`) 151 # as long as possible, to make sure the tests to fail if the 152 # returned policy is used unexpectedly instead of `self.value`. 153 # Using safer policy wouldn't be distinguishable from acceptable 154 # arbitrary policy enforcement by user agents, as specified at 155 # Step 7 of 156 # https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer: 157 # "The user agent MAY alter referrerURL or referrerOrigin at this 158 # point to enforce arbitrary policy considerations in the 159 # interests of minimizing data leakage." 160 # See also the comments at `referrerUrlResolver` in 161 # `wpt/referrer-policy/generic/test-case.sub.js`. 162 if self.value != 'unsafe-url': 163 return PolicyDelivery(delivery_type, self.key, 'unsafe-url') 164 else: 165 return PolicyDelivery(delivery_type, self.key, 'no-referrer') 166 elif self.key == 'mixedContent': 167 if self.value == 'opt-in': 168 return PolicyDelivery(delivery_type, self.key, None) 169 else: 170 return PolicyDelivery(delivery_type, self.key, 'opt-in') 171 elif self.key == 'contentSecurityPolicy': 172 if self.value is not None: 173 return PolicyDelivery(delivery_type, self.key, None) 174 else: 175 return PolicyDelivery(delivery_type, self.key, 'worker-src-none') 176 elif self.key == 'upgradeInsecureRequests': 177 if self.value == 'upgrade': 178 return PolicyDelivery(delivery_type, self.key, None) 179 else: 180 return PolicyDelivery(delivery_type, self.key, 'upgrade') 181 else: 182 raise Exception('delivery key is invalid: ' + self.key) 183 184 185 class SourceContext(object): 186 def __init__(self, source_context_type, policy_deliveries): 187 # type: (unicode, typing.List[PolicyDelivery]) -> None 188 self.source_context_type = source_context_type 189 self.policy_deliveries = policy_deliveries 190 191 def __eq__(self, other): 192 return type(self) is type(other) and self.__dict__ == other.__dict__ 193 194 @classmethod 195 def from_json(cls, obj, target_policy_delivery, source_context_schema): 196 ''' 197 Parses a JSON object `obj` and returns a `SourceContext` object. 198 199 `target_policy_delivery` and `source_context_schema` are used for 200 policy delivery placeholders and filtering out unsupported 201 delivery types. 202 203 Can raise `ShouldSkip`. 204 ''' 205 source_context_type = obj.get('sourceContextType') 206 policy_deliveries = PolicyDelivery.list_from_json( 207 obj.get('policyDeliveries'), target_policy_delivery, 208 source_context_schema['supported_delivery_type'] 209 [source_context_type]) 210 return SourceContext(source_context_type, policy_deliveries) 211 212 def to_json(self): 213 return { 214 "sourceContextType": self.source_context_type, 215 "policyDeliveries": [x.to_json() for x in self.policy_deliveries] 216 } 217 218 219 class CustomEncoder(json.JSONEncoder): 220 ''' 221 Used to dump dicts containing `SourceContext`/`PolicyDelivery` into JSON. 222 ''' 223 def default(self, obj): 224 if isinstance(obj, SourceContext): 225 return obj.to_json() 226 if isinstance(obj, PolicyDelivery): 227 return obj.to_json() 228 return json.JSONEncoder.default(self, obj)