1
|
# SPDX-License-Identifier: CC0-1.0
|
2
|
|
3
|
"""
|
4
|
Haketilo unit tests - modifying requests using webRequest API
|
5
|
"""
|
6
|
|
7
|
# This file is part of Haketilo
|
8
|
#
|
9
|
# Copyright (C) 2021, Wojtek Kosior <koszko@koszko.org>
|
10
|
#
|
11
|
# This program is free software: you can redistribute it and/or modify
|
12
|
# it under the terms of the CC0 1.0 Universal License as published by
|
13
|
# the Creative Commons Corporation.
|
14
|
#
|
15
|
# This program is distributed in the hope that it will be useful,
|
16
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
17
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
18
|
# CC0 1.0 Universal License for more details.
|
19
|
|
20
|
import re
|
21
|
from hashlib import sha256
|
22
|
import pytest
|
23
|
|
24
|
from ..script_loader import load_script
|
25
|
from .utils import are_scripts_allowed
|
26
|
|
27
|
allowed_url = 'https://site.with.scripts.allow.ed/'
|
28
|
blocked_url = 'https://site.with.scripts.block.ed/'
|
29
|
payload_url = 'https://site.with.paylo.ad/'
|
30
|
|
31
|
def webrequest_js():
|
32
|
return (load_script('background/webrequest.js',
|
33
|
'#IMPORT common/patterns_query_tree.js AS pqt') +
|
34
|
''';
|
35
|
// Mock pattern tree.
|
36
|
tree = pqt.make();
|
37
|
// Mock default allow.
|
38
|
default_allow = {name: "default_allow", value: true};
|
39
|
|
40
|
// Rule to block scripts.
|
41
|
pqt.register(tree, "%(blocked)s***",
|
42
|
"~allow", 0);
|
43
|
|
44
|
// Rule to allow scripts, but overridden by payload assignment.
|
45
|
pqt.register(tree, "%(payload)s***", "~allow", 1);
|
46
|
pqt.register(tree, "%(payload)s***", "somemapping",
|
47
|
{identifier: "someresource"});
|
48
|
|
49
|
// Mock stream_filter.
|
50
|
stream_filter.apply = (details, headers, policy) => headers;
|
51
|
''' % {'blocked': blocked_url, 'payload': payload_url})
|
52
|
|
53
|
def webrequest_js_start_called():
|
54
|
return webrequest_js() + ';\nstart("somesecret");'
|
55
|
|
56
|
ext_url = 'moz-extension://49de6ce9-49fc-49e1-8102-7ef35286389c/html/settings.html'
|
57
|
prefix = 'X-Haketilo-' + sha256(ext_url.encode()).digest().hex()
|
58
|
|
59
|
# Prepare a list of headers as could be sent by a website.
|
60
|
sample_csp_header = {
|
61
|
'name': 'Content-Security-Policy',
|
62
|
'value': "script-src 'self';"
|
63
|
}
|
64
|
sample_csp_header_idx = 7
|
65
|
|
66
|
sample_headers = [
|
67
|
{'name': 'Content-Type', 'value': 'text/html;charset=utf-8'},
|
68
|
{'name': 'Content-Length', 'value': '61954'},
|
69
|
{'name': 'Content-Language', 'value': 'en'},
|
70
|
{'name': 'Expires', 'value': 'Mon, 12 Mar 2012 11:04...'},
|
71
|
{'name': 'Last-Modified', 'value': 'Fri, 26 Jul 2013 22:50...'},
|
72
|
{'name': 'Cache-Control', 'value': 'max-age=0, s-maxage=86...'},
|
73
|
{'name': 'Age', 'value': '224'},
|
74
|
{'name': 'Server', 'value': 'nginx/1.1.19'},
|
75
|
{'name': 'Date', 'value': 'Thu, 10 Mar 2022 12:09...'}
|
76
|
]
|
77
|
|
78
|
sample_headers.insert(sample_csp_header_idx, sample_csp_header)
|
79
|
|
80
|
# Prepare a list of headers as would be crafted by Haketilo when there is a
|
81
|
# payload to inject.
|
82
|
nonce_source = f'somemapping:someresource:{payload_url}:somesecret'.encode()
|
83
|
nonce = f'nonce-{sha256(nonce_source).digest().hex()}'
|
84
|
|
85
|
payload_csp_header = {
|
86
|
'name': f'Content-Security-Policy',
|
87
|
'value': ("prefetch-src 'none'; script-src-attr 'none'; "
|
88
|
f"script-src '{nonce}' 'unsafe-eval'; script-src-elem '{nonce}';")
|
89
|
}
|
90
|
|
91
|
sample_payload_headers = [
|
92
|
*sample_headers,
|
93
|
{'name': prefix, 'value': ':)'},
|
94
|
payload_csp_header
|
95
|
]
|
96
|
|
97
|
sample_payload_headers[sample_csp_header_idx] = {
|
98
|
**sample_csp_header,
|
99
|
'name': f'{prefix}-{sample_csp_header["name"]}',
|
100
|
}
|
101
|
|
102
|
# Prepare a list of headers as would be crafted by Haketilo when scripts are
|
103
|
# blocked.
|
104
|
sample_blocked_headers = [*sample_payload_headers]
|
105
|
sample_blocked_headers.pop()
|
106
|
sample_blocked_headers.append(sample_csp_header)
|
107
|
sample_blocked_headers.append({
|
108
|
'name': f'Content-Security-Policy',
|
109
|
'value': ("prefetch-src 'none'; script-src-attr 'none'; "
|
110
|
"script-src 'none' 'unsafe-eval'; script-src-elem 'none';")
|
111
|
})
|
112
|
|
113
|
@pytest.mark.get_page('https://gotmyowndoma.in')
|
114
|
@pytest.mark.parametrize('params', [
|
115
|
(sample_headers, allowed_url),
|
116
|
(sample_blocked_headers, blocked_url),
|
117
|
(sample_payload_headers, payload_url),
|
118
|
])
|
119
|
def test_webrequest_on_headers_received(driver, execute_in_page, params):
|
120
|
"""Unit-test the on_headers_received() function."""
|
121
|
headers_out, url = params
|
122
|
|
123
|
execute_in_page(
|
124
|
'''{
|
125
|
// Mock browser object.
|
126
|
const url = arguments[0];
|
127
|
this.browser = {runtime: {getURL: () => url}};
|
128
|
}''',
|
129
|
ext_url)
|
130
|
|
131
|
execute_in_page(webrequest_js())
|
132
|
|
133
|
execute_in_page('secret = "somesecret";')
|
134
|
|
135
|
for headers_in in [
|
136
|
sample_headers,
|
137
|
sample_blocked_headers,
|
138
|
sample_payload_headers
|
139
|
]:
|
140
|
details = {'url': url, 'responseHeaders': headers_in, 'fromCache': True}
|
141
|
res = execute_in_page('returnval(on_headers_received(arguments[0]));',
|
142
|
details)
|
143
|
|
144
|
assert res == {'responseHeaders': headers_out}
|
145
|
|
146
|
@pytest.mark.ext_data({'background_script': webrequest_js_start_called})
|
147
|
@pytest.mark.usefixtures('webextension')
|
148
|
def test_webrequest_real_pages(driver, execute_in_page):
|
149
|
"""
|
150
|
Test webRequest-based header modifications by loading actual pages and
|
151
|
attempting to run scripts within them.
|
152
|
"""
|
153
|
for attempt in range(10):
|
154
|
driver.get('https://site.with.scripts.block.ed/')
|
155
|
|
156
|
if not are_scripts_allowed(driver):
|
157
|
break
|
158
|
assert attempt != 9
|
159
|
|
160
|
driver.get(allowed_url)
|
161
|
assert are_scripts_allowed(driver)
|
162
|
|
163
|
driver.get(payload_url)
|
164
|
assert not are_scripts_allowed(driver)
|
165
|
source = 'somemapping:someresource:https://site.with.paylo.ad/index.html:somesecret'
|
166
|
assert are_scripts_allowed(driver, sha256(source.encode()).digest().hex())
|