1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462
|
#!/usr/bin/env python3
import argparse
import collections
import copy
import json
import os
import sys
import spec_validator
import util
def expand_pattern(expansion_pattern, test_expansion_schema):
expansion = {}
for artifact_key in expansion_pattern:
artifact_value = expansion_pattern[artifact_key]
if artifact_value == '*':
expansion[artifact_key] = test_expansion_schema[artifact_key]
elif isinstance(artifact_value, list):
expansion[artifact_key] = artifact_value
elif isinstance(artifact_value, dict):
# Flattened expansion.
expansion[artifact_key] = []
values_dict = expand_pattern(artifact_value,
test_expansion_schema[artifact_key])
for sub_key in values_dict.keys():
expansion[artifact_key] += values_dict[sub_key]
else:
expansion[artifact_key] = [artifact_value]
return expansion
def permute_expansion(expansion,
artifact_order,
selection={},
artifact_index=0):
assert isinstance(artifact_order, list), "artifact_order should be a list"
if artifact_index >= len(artifact_order):
yield selection
return
artifact_key = artifact_order[artifact_index]
for artifact_value in expansion[artifact_key]:
selection[artifact_key] = artifact_value
for next_selection in permute_expansion(expansion, artifact_order,
selection, artifact_index + 1):
yield next_selection
# Dumps the test config `selection` into a serialized JSON string.
def dump_test_parameters(selection):
return json.dumps(
selection,
indent=2,
separators=(',', ': '),
sort_keys=True,
cls=util.CustomEncoder)
def get_test_filename(spec_directory, spec_json, selection):
'''Returns the filname for the main test HTML file'''
selection_for_filename = copy.deepcopy(selection)
# Use 'unset' rather than 'None' in test filenames.
if selection_for_filename['delivery_value'] is None:
selection_for_filename['delivery_value'] = 'unset'
return os.path.join(
spec_directory,
spec_json['test_file_path_pattern'] % selection_for_filename)
def get_csp_value(value):
'''
Returns actual CSP header values (e.g. "worker-src 'self'") for the
given string used in PolicyDelivery's value (e.g. "worker-src-self").
'''
# script-src
# Test-related scripts like testharness.js and inline scripts containing
# test bodies.
# 'unsafe-inline' is added as a workaround here. This is probably not so
# bad, as it shouldn't intefere non-inline-script requests that we want to
# test.
if value == 'script-src-wildcard':
return "script-src * 'unsafe-inline'"
if value == 'script-src-self':
return "script-src 'self' 'unsafe-inline'"
# Workaround for "script-src 'none'" would be more complicated, because
# - "script-src 'none' 'unsafe-inline'" is handled somehow differently from
# "script-src 'none'", i.e.
# https://w3c.github.io/webappsec-csp/#match-url-to-source-list Step 3
# handles the latter but not the former.
# - We need nonce- or path-based additional values to allow same-origin
# test scripts like testharness.js.
# Therefore, we disable 'script-src-none' tests for now in
# `/content-security-policy/spec.src.json`.
if value == 'script-src-none':
return "script-src 'none'"
# worker-src
if value == 'worker-src-wildcard':
return 'worker-src *'
if value == 'worker-src-self':
return "worker-src 'self'"
if value == 'worker-src-none':
return "worker-src 'none'"
raise Exception('Invalid delivery_value: %s' % value)
def handle_deliveries(policy_deliveries):
'''
Generate <meta> elements and HTTP headers for the given list of
PolicyDelivery.
TODO(hiroshige): Merge duplicated code here, scope/document.py, etc.
'''
meta = ''
headers = {}
for delivery in policy_deliveries:
if delivery.value is None:
continue
if delivery.key == 'referrerPolicy':
if delivery.delivery_type == 'meta':
meta += \
'<meta name="referrer" content="%s">' % delivery.value
elif delivery.delivery_type == 'http-rp':
headers['Referrer-Policy'] = delivery.value
# TODO(kristijanburnik): Limit to WPT origins.
headers['Access-Control-Allow-Origin'] = '*'
else:
raise Exception(
'Invalid delivery_type: %s' % delivery.delivery_type)
elif delivery.key == 'mixedContent':
assert (delivery.value == 'opt-in')
if delivery.delivery_type == 'meta':
meta += '<meta http-equiv="Content-Security-Policy" ' + \
'content="block-all-mixed-content">'
elif delivery.delivery_type == 'http-rp':
headers['Content-Security-Policy'] = 'block-all-mixed-content'
else:
raise Exception(
'Invalid delivery_type: %s' % delivery.delivery_type)
elif delivery.key == 'contentSecurityPolicy':
csp_value = get_csp_value(delivery.value)
if delivery.delivery_type == 'meta':
meta += '<meta http-equiv="Content-Security-Policy" ' + \
'content="' + csp_value + '">'
elif delivery.delivery_type == 'http-rp':
headers['Content-Security-Policy'] = csp_value
else:
raise Exception(
'Invalid delivery_type: %s' % delivery.delivery_type)
elif delivery.key == 'upgradeInsecureRequests':
# https://w3c.github.io/webappsec-upgrade-insecure-requests/#delivery
assert (delivery.value == 'upgrade')
if delivery.delivery_type == 'meta':
meta += '<meta http-equiv="Content-Security-Policy" ' + \
'content="upgrade-insecure-requests">'
elif delivery.delivery_type == 'http-rp':
headers[
'Content-Security-Policy'] = 'upgrade-insecure-requests'
else:
raise Exception(
'Invalid delivery_type: %s' % delivery.delivery_type)
else:
raise Exception('Invalid delivery_key: %s' % delivery.key)
return {"meta": meta, "headers": headers}
def generate_selection(spec_json, selection):
'''
Returns a scenario object (with a top-level source_context_list entry,
which will be removed in generate_test_file() later).
'''
target_policy_delivery = util.PolicyDelivery(selection['delivery_type'],
selection['delivery_key'],
selection['delivery_value'])
del selection['delivery_type']
del selection['delivery_key']
del selection['delivery_value']
# Parse source context list and policy deliveries of source contexts.
# `util.ShouldSkip()` exceptions are raised if e.g. unsuppported
# combinations of source contexts and policy deliveries are used.
source_context_list_scheme = spec_json['source_context_list_schema'][
selection['source_context_list']]
selection['source_context_list'] = [
util.SourceContext.from_json(source_context, target_policy_delivery,
spec_json['source_context_schema'])
for source_context in source_context_list_scheme['sourceContextList']
]
# Check if the subresource is supported by the innermost source context.
innermost_source_context = selection['source_context_list'][-1]
supported_subresource = spec_json['source_context_schema'][
'supported_subresource'][innermost_source_context.source_context_type]
if supported_subresource != '*':
if selection['subresource'] not in supported_subresource:
raise util.ShouldSkip()
# Parse subresource policy deliveries.
selection[
'subresource_policy_deliveries'] = util.PolicyDelivery.list_from_json(
source_context_list_scheme['subresourcePolicyDeliveries'],
target_policy_delivery, spec_json['subresource_schema']
['supported_delivery_type'][selection['subresource']])
# Generate per-scenario test description.
selection['test_description'] = spec_json[
'test_description_template'] % selection
return selection
def generate_test_file(spec_directory, test_helper_filenames,
test_html_template_basename, test_filename, scenarios):
'''
Generates a test HTML file (and possibly its associated .headers file)
from `scenarios`.
'''
# Scenarios for the same file should have the same `source_context_list`,
# including the top-level one.
# Note: currently, non-top-level source contexts aren't necessarily required
# to be the same, but we set this requirement as it will be useful e.g. when
# we e.g. reuse a worker among multiple scenarios.
for scenario in scenarios:
assert (scenario['source_context_list'] == scenarios[0]
['source_context_list'])
# We process the top source context below, and do not include it in
# the JSON objects (i.e. `scenarios`) in generated HTML files.
top_source_context = scenarios[0]['source_context_list'].pop(0)
assert (top_source_context.source_context_type == 'top')
for scenario in scenarios[1:]:
assert (scenario['source_context_list'].pop(0) == top_source_context)
parameters = {}
# Sort scenarios, to avoid unnecessary diffs due to different orders in
# `scenarios`.
serialized_scenarios = sorted(
[dump_test_parameters(scenario) for scenario in scenarios])
parameters['scenarios'] = ",\n".join(serialized_scenarios).replace(
"\n", "\n" + " " * 10)
test_directory = os.path.dirname(test_filename)
parameters['helper_js'] = ""
for test_helper_filename in test_helper_filenames:
parameters['helper_js'] += ' <script src="%s"></script>\n' % (
os.path.relpath(test_helper_filename, test_directory))
parameters['sanity_checker_js'] = os.path.relpath(
os.path.join(spec_directory, 'generic', 'sanity-checker.js'),
test_directory)
parameters['spec_json_js'] = os.path.relpath(
os.path.join(spec_directory, 'generic', 'spec_json.js'),
test_directory)
test_headers_filename = test_filename + ".headers"
test_html_template = util.get_template(test_html_template_basename)
disclaimer_template = util.get_template('disclaimer.template')
html_template_filename = os.path.join(util.template_directory,
test_html_template_basename)
generated_disclaimer = disclaimer_template \
% {'generating_script_filename': os.path.relpath(sys.argv[0],
util.test_root_directory),
'spec_directory': os.path.relpath(spec_directory,
util.test_root_directory)}
# Adjust the template for the test invoking JS. Indent it to look nice.
parameters['generated_disclaimer'] = generated_disclaimer.rstrip()
# Directory for the test files.
try:
os.makedirs(test_directory)
except:
pass
delivery = handle_deliveries(top_source_context.policy_deliveries)
if len(delivery['headers']) > 0:
with open(test_headers_filename, "w") as f:
for header in delivery['headers']:
f.write('%s: %s\n' % (header, delivery['headers'][header]))
parameters['meta_delivery_method'] = delivery['meta']
# Obey the lint and pretty format.
if len(parameters['meta_delivery_method']) > 0:
parameters['meta_delivery_method'] = "\n " + \
parameters['meta_delivery_method']
# Write out the generated HTML file.
util.write_file(test_filename, test_html_template % parameters)
def generate_test_source_files(spec_directory, test_helper_filenames,
spec_json, target):
test_expansion_schema = spec_json['test_expansion_schema']
specification = spec_json['specification']
if target == "debug":
spec_json_js_template = util.get_template('spec_json.js.template')
util.write_file(
os.path.join(spec_directory, "generic", "spec_json.js"),
spec_json_js_template % {'spec_json': json.dumps(spec_json)})
util.write_file(
os.path.join(spec_directory, "generic",
"debug-output.spec.src.json"),
json.dumps(spec_json, indent=2, separators=(',', ': ')))
# Choose a debug/release template depending on the target.
html_template = "test.%s.html.template" % target
artifact_order = list(test_expansion_schema.keys())
artifact_order.remove('expansion')
excluded_selection_pattern = ''
for key in artifact_order:
excluded_selection_pattern += '%(' + key + ')s/'
# Create list of excluded tests.
exclusion_dict = set()
for excluded_pattern in spec_json['excluded_tests']:
excluded_expansion = \
expand_pattern(excluded_pattern, test_expansion_schema)
for excluded_selection in permute_expansion(excluded_expansion,
artifact_order):
excluded_selection['delivery_key'] = spec_json['delivery_key']
exclusion_dict.add(excluded_selection_pattern % excluded_selection)
# `scenarios[filename]` represents the list of scenario objects to be
# generated into `filename`.
scenarios = {}
for spec in specification:
# Used to make entries with expansion="override" override preceding
# entries with the same |selection_path|.
output_dict = {}
for expansion_pattern in spec['test_expansion']:
expansion = expand_pattern(expansion_pattern,
test_expansion_schema)
for selection in permute_expansion(expansion, artifact_order):
selection['delivery_key'] = spec_json['delivery_key']
selection_path = spec_json['selection_pattern'] % selection
if selection_path in output_dict:
if expansion_pattern['expansion'] != 'override':
print("Error: expansion is default in:")
print(dump_test_parameters(selection))
print("but overrides:")
print(dump_test_parameters(
output_dict[selection_path]))
sys.exit(1)
output_dict[selection_path] = copy.deepcopy(selection)
for selection_path in output_dict:
selection = output_dict[selection_path]
if (excluded_selection_pattern % selection) in exclusion_dict:
print('Excluding selection:', selection_path)
continue
try:
test_filename = get_test_filename(spec_directory, spec_json,
selection)
scenario = generate_selection(spec_json, selection)
scenarios[test_filename] = scenarios.get(test_filename,
[]) + [scenario]
except util.ShouldSkip:
continue
for filename in scenarios:
generate_test_file(spec_directory, test_helper_filenames,
html_template, filename, scenarios[filename])
def merge_json(base, child):
for key in child:
if key not in base:
base[key] = child[key]
continue
# `base[key]` and `child[key]` both exists.
if isinstance(base[key], list) and isinstance(child[key], list):
base[key].extend(child[key])
elif isinstance(base[key], dict) and isinstance(child[key], dict):
merge_json(base[key], child[key])
else:
base[key] = child[key]
def main():
parser = argparse.ArgumentParser(
description='Test suite generator utility')
parser.add_argument(
'-t',
'--target',
type=str,
choices=("release", "debug"),
default="release",
help='Sets the appropriate template for generating tests')
parser.add_argument(
'-s',
'--spec',
type=str,
default=os.getcwd(),
help='Specify a file used for describing and generating the tests')
# TODO(kristijanburnik): Add option for the spec_json file.
args = parser.parse_args()
spec_directory = os.path.abspath(args.spec)
# Read `spec.src.json` files, starting from `spec_directory`, and
# continuing to parent directories as long as `spec.src.json` exists.
spec_filenames = []
test_helper_filenames = []
spec_src_directory = spec_directory
while len(spec_src_directory) >= len(util.test_root_directory):
spec_filename = os.path.join(spec_src_directory, "spec.src.json")
if not os.path.exists(spec_filename):
break
spec_filenames.append(spec_filename)
test_filename = os.path.join(spec_src_directory, 'generic',
'test-case.sub.js')
assert (os.path.exists(test_filename))
test_helper_filenames.append(test_filename)
spec_src_directory = os.path.abspath(
os.path.join(spec_src_directory, ".."))
spec_filenames = list(reversed(spec_filenames))
test_helper_filenames = list(reversed(test_helper_filenames))
if len(spec_filenames) == 0:
print('Error: No spec.src.json is found at %s.' % spec_directory)
return
# Load the default spec JSON file, ...
default_spec_filename = os.path.join(util.script_directory,
'spec.src.json')
spec_json = collections.OrderedDict()
if os.path.exists(default_spec_filename):
spec_json = util.load_spec_json(default_spec_filename)
# ... and then make spec JSON files in subdirectories override the default.
for spec_filename in spec_filenames:
child_spec_json = util.load_spec_json(spec_filename)
merge_json(spec_json, child_spec_json)
spec_validator.assert_valid_spec_json(spec_json)
generate_test_source_files(spec_directory, test_helper_filenames,
spec_json, args.target)
if __name__ == '__main__':
main()
|