Revision fd9f2fc4
Added by koszko over 1 year ago
Makefile.in | ||
---|---|---|
17 | 17 |
|
18 | 18 |
version = <<VERSION>> |
19 | 19 |
extension_files = background/ common/ content/ html/ licenses/ \ |
20 |
copyright default_settings.json manifest.json
|
|
20 |
icons/ copyright default_settings.json manifest.json
|
|
21 | 21 |
|
22 |
metafiles = build.sh configure Makefile.in process_html_file.sh README.txt \ |
|
22 |
|
|
23 |
metafiles = build.sh configure Makefile.in compute_scripts.awk README.txt \ |
|
23 | 24 |
re-generate_icons.sh shell_utils.sh upload_amo.sh write_makefile.sh |
24 | 25 |
|
25 | 26 |
# Configuration gets included here by write_makefile.sh |
... | ... | |
47 | 48 |
unpacked: $(default_target)-unpacked Makefile |
48 | 49 |
all-unpacked: mozilla-unpacked chromium-unpacked |
49 | 50 |
%-unpacked: $(extension_files) icons/haketilo16.png |
50 |
$(srcdir)/build.sh $* $(srcdir) $(UPDATE_URL)
|
|
51 |
"$(srcdir)/build.sh" $* $(srcdir) $(UPDATE_URL)
|
|
51 | 52 |
|
52 | 53 |
install install-strip: $(default_target)-unpacked |
53 | 54 |
cp -R $(default_target)-unpacked \ |
54 |
"$(DESTDIR)/{6fe13369-88e9-440f-b837-5012fb3bedec}" |
|
55 |
"$(DESTDIR)/{6fe13369-88e9-440f-b837-5012fb3bedec}"
|
|
55 | 56 |
|
56 | 57 |
uninstall: |
57 | 58 |
rm -r "$(DESTDIR)/{6fe13369-88e9-440f-b837-5012fb3bedec}" |
... | ... | |
59 | 60 |
%-build.zip: %-unpacked Makefile |
60 | 61 |
cd $< && zip -q -r ../$@ * |
61 | 62 |
|
62 |
test/: |
|
63 |
mkdir $@ |
|
64 |
|
|
65 |
test/certs/: | test/ |
|
63 |
certs/: |
|
66 | 64 |
mkdir $@ |
67 | 65 |
|
68 |
test/certs/%.key: | test/certs/
|
|
66 |
certs/%.key: | certs/
|
|
69 | 67 |
openssl genrsa -out $@ 2048 |
70 | 68 |
|
71 |
test/certs/rootCA.pem: test/certs/rootCA.key
|
|
69 |
certs/rootCA.pem: certs/rootCA.key
|
|
72 | 70 |
openssl req -x509 -new -nodes -key $< -days 1024 -out $@ \ |
73 | 71 |
-subj "/CN=Haketilo Test" |
74 | 72 |
|
75 |
test: test/certs/rootCA.pem test/certs/site.key $(default_target)-build.zip
|
|
76 |
MOZ_HEADLESS=whatever $(PYTHON) -m pytest
|
|
73 |
pytest.ini: pytest.ini.in
|
|
74 |
sed "s|<<SRCDIR>>|$(srcdir)|" <$< > $@
|
|
77 | 75 |
|
78 |
test-environment: test/certs/rootCA.pem test/certs/site.key |
|
79 |
python3 -m test |
|
76 |
test: certs/rootCA.pem certs/site.key $(default_target)-build.zip \ |
|
77 |
pytest.ini |
|
78 |
PYTHONPYCACHEPREFIX=$$(pwd)/test__pycache__ MOZ_HEADLESS=whatever \ |
|
79 |
"$(PYTHON)" -m pytest |
|
80 | 80 |
|
81 |
test-environment-with-haketilo: test/certs/rootCA.pem test/certs/site.key \ |
|
81 |
test-environment: certs/rootCA.pem certs/site.key |
|
82 |
"$(PYTHON)" -m test |
|
83 |
|
|
84 |
test-environment-with-haketilo: certs/rootCA.pem certs/site.key \ |
|
82 | 85 |
$(default_target)-build.zip |
83 |
python3 -m test --load-haketilo
|
|
86 |
"$(PYTHON)" -m test --load-haketilo
|
|
84 | 87 |
|
85 | 88 |
# helper targets |
86 | 89 |
clean mostlyclean: |
87 | 90 |
rm -rf mozilla-unpacked chromium-unpacked haketilo-[1-9]* |
88 | 91 |
rm -f mozilla-build.zip chromium-build.zip exports_init.js |
89 |
rm -rf test/certs |
|
90 |
rm -rf $$(find . -name geckodriver.log) |
|
91 |
rm -rf $$(find . -type d -name __pycache__) |
|
92 |
rm -rf $$(find . -type d -name injected_scripts) |
|
92 |
rm -rf pytest.ini certs injected_scripts geckodriver.log |
|
93 |
rm -rf certs/ test__pycache__/ .pytest_cache/ |
|
93 | 94 |
|
94 | 95 |
distclean: clean |
95 | 96 |
rm -f Makefile config.status record.conf |
... | ... | |
99 | 100 |
@echo 'deletes files that may need special tools to rebuild.' |
100 | 101 |
rm -f "$(srcdir)"/icons/*.png |
101 | 102 |
|
102 |
dist: $(extension_files) $(metafiles) icons/haketilo16.png |
|
103 |
test -d haketilo-$(version) || mkdir haketilo-$(version) |
|
104 |
for file in $(extension_files) $(metafiles) icons/; do \ |
|
105 |
cp -R "$(srcdir)"/$$file haketilo-$(version); \ |
|
106 |
done |
|
107 |
tar cf haketilo-$(version).tar haketilo-$(version) |
|
108 |
gzip haketilo-$(version).tar |
|
109 |
|
|
110 | 103 |
# Files for constructing the makefile |
111 | 104 |
Makefile: config.status Makefile.in record.conf |
112 | 105 |
./config.status |
... | ... | |
115 | 108 |
cp "$(srcdir)"/write_makefile.sh config.status |
116 | 109 |
|
117 | 110 |
# Unused GNU-specified targets |
111 |
dist: |
|
118 | 112 |
install-html: |
119 | 113 |
install-dvi: |
120 | 114 |
install-pdf: |
build.sh | ||
---|---|---|
15 | 15 |
|
16 | 16 |
set -e |
17 | 17 |
|
18 |
. ./shell_utils.sh |
|
19 |
|
|
20 | 18 |
print_usage() { |
21 | 19 |
printf 'usage: %s mozilla|chromium [source directory] [update url]\n' \ |
22 | 20 |
"$0" >&2 |
pytest.ini | ||
---|---|---|
1 |
#!/usr/bin/env pytest |
|
2 |
|
|
3 |
# This file is part of Haketilo |
|
4 |
# |
|
5 |
# Copyright (C) 2021, Wojtek Kosior |
|
6 |
# |
|
7 |
# This program is free software: you can redistribute it and/or modify |
|
8 |
# it under the terms of the CC0 1.0 Universal License as published by |
|
9 |
# the Creative Commons Corporation. |
|
10 |
# |
|
11 |
# This program is distributed in the hope that it will be useful, |
|
12 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
13 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
14 |
# CC0 1.0 Universal License for more details. |
|
15 |
|
|
16 |
[pytest] |
|
17 |
markers = |
|
18 |
ext_data: define a custom testing extension for `webextension` fixture. |
|
19 |
get_page: define a url the `driver` fixture should navigate the browser to. |
|
20 |
second_driver: tell `driver` fixture to spawn a separate browser instance fr this test. |
pytest.ini.in | ||
---|---|---|
1 |
#!/usr/bin/env pytest |
|
2 |
|
|
3 |
# This file is part of Haketilo |
|
4 |
# |
|
5 |
# Copyright (C) 2021, 2022 Wojtek Kosior <koszko@koszko.org> |
|
6 |
# |
|
7 |
# This program is free software: you can redistribute it and/or modify |
|
8 |
# it under the terms of the CC0 1.0 Universal License as published by |
|
9 |
# the Creative Commons Corporation. |
|
10 |
# |
|
11 |
# This program is distributed in the hope that it will be useful, |
|
12 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
13 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
14 |
# CC0 1.0 Universal License for more details. |
|
15 |
|
|
16 |
[pytest] |
|
17 |
markers = |
|
18 |
ext_data: define a custom testing extension for `webextension` fixture. |
|
19 |
get_page: define a url the `driver` fixture should navigate the browser to. |
|
20 |
second_driver: tell `driver` fixture to spawn a separate browser instance for this test. |
|
21 |
testpaths = |
|
22 |
<<SRCDIR>>/test/haketilo_test |
test/__init__.py | ||
---|---|---|
1 |
# SPDX-License-Identifier: CC0-1.0 |
|
2 |
# Copyright (C) 2021 Wojtek Kosior |
test/__main__.py | ||
---|---|---|
1 |
# SPDX-License-Identifier: AGPL-3.0-or-later |
|
2 |
|
|
3 |
""" |
|
4 |
Run a Firefox-type browser with WebDriver attached and Python console open |
|
5 |
""" |
|
6 |
|
|
7 |
# This file is part of Haketilo. |
|
8 |
# |
|
9 |
# Copyright (C) 2021 jahoti <jahoti@tilde.team> |
|
10 |
# Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org> |
|
11 |
# |
|
12 |
# This program is free software: you can redistribute it and/or modify |
|
13 |
# it under the terms of the GNU Affero General Public License as |
|
14 |
# published by the Free Software Foundation, either version 3 of the |
|
15 |
# License, or (at your option) any later version. |
|
16 |
# |
|
17 |
# This program is distributed in the hope that it will be useful, |
|
18 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
19 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
20 |
# GNU Affero General Public License for more details. |
|
21 |
# |
|
22 |
# You should have received a copy of the GNU Affero General Public License |
|
23 |
# along with this program. If not, see <https://www.gnu.org/licenses/>. |
|
24 |
# |
|
25 |
# |
|
26 |
# I, Wojtek Kosior, thereby promise not to sue for violation of this |
|
27 |
# file's license. Although I request that you do not make use of this code |
|
28 |
# in a proprietary program, I am not going to enforce this in court. |
|
29 |
|
|
30 |
import sys |
|
31 |
import time |
|
32 |
import code |
|
33 |
from rlcompleter import Completer |
|
34 |
import readline |
|
35 |
|
|
36 |
from .server import do_an_internet |
|
37 |
from .misc_constants import * |
|
38 |
from .profiles import firefox_safe_mode |
|
39 |
from .extension_crafting import get_extension_base_url |
|
40 |
|
|
41 |
def fail(msg, error_code): |
|
42 |
print('Error:', msg) |
|
43 |
print('Usage:', sys.argv[0], '[--load-haketilo]', '[certificates_directory] [proxy_port]') |
|
44 |
sys.exit(error_code) |
|
45 |
|
|
46 |
load_haketilo = False |
|
47 |
argv_idx = 1 |
|
48 |
if len(sys.argv) > argv_idx and sys.argv[argv_idx] == '--load-haketilo': |
|
49 |
load_haketilo = True |
|
50 |
argv_idx += 1 |
|
51 |
|
|
52 |
certdir = Path(sys.argv[argv_idx]).resolve() if len(sys.argv) > argv_idx \ |
|
53 |
else default_cert_dir |
|
54 |
|
|
55 |
if not certdir.is_dir(): |
|
56 |
fail('selected certificate directory does not exist.', 2) |
|
57 |
|
|
58 |
argv_idx += 1 |
|
59 |
|
|
60 |
port = sys.argv[argv_idx] if len(sys.argv) > argv_idx \ |
|
61 |
else str(default_proxy_port) |
|
62 |
|
|
63 |
if not port.isnumeric(): |
|
64 |
fail('port must be an integer.', 3) |
|
65 |
|
|
66 |
httpd = do_an_internet(certdir, int(port)) |
|
67 |
driver = firefox_safe_mode(proxy_port=int(port)) |
|
68 |
|
|
69 |
if load_haketilo: |
|
70 |
driver.install_addon(str(here.parent / 'mozilla-build.zip'), temporary=True) |
|
71 |
driver.get(get_extension_base_url(driver) + 'html/settings.html') |
|
72 |
|
|
73 |
print("You can now control the browser through 'driver' object") |
|
74 |
|
|
75 |
# Here we enable readline-enhanced editing: |
|
76 |
# https://stackoverflow.com/questions/35115208/is-there-any-way-to-combine-readline-rlcompleter-and-interactiveconsole-in-pytho#answer-35116399 |
|
77 |
readline.parse_and_bind('tab: complete'); |
|
78 |
console_locals = globals() |
|
79 |
readline.set_completer(Completer(console_locals).complete) |
|
80 |
code.InteractiveConsole(locals=globals()).interact() |
|
81 |
|
|
82 |
driver.quit() |
|
83 |
httpd.shutdown() |
test/conftest.py | ||
---|---|---|
1 |
# SPDX-License-Identifier: GPL-3.0-or-later |
|
2 |
|
|
3 |
""" |
|
4 |
Common fixtures for Haketilo unit tests |
|
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 GNU General Public License as published by |
|
13 |
# the Free Software Foundation, either version 3 of the License, or |
|
14 |
# (at your option) any later version. |
|
15 |
# |
|
16 |
# This program is distributed in the hope that it will be useful, |
|
17 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
18 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
19 |
# GNU General Public License for more details. |
|
20 |
# |
|
21 |
# You should have received a copy of the GNU General Public License |
|
22 |
# along with this program. If not, see <https://www.gnu.org/licenses/>. |
|
23 |
# |
|
24 |
# I, Wojtek Kosior, thereby promise not to sue for violation of this file's |
|
25 |
# license. Although I request that you do not make use of this code in a |
|
26 |
# proprietary program, I am not going to enforce this in court. |
|
27 |
|
|
28 |
import pytest |
|
29 |
from pathlib import Path |
|
30 |
from tempfile import TemporaryDirectory |
|
31 |
from selenium.webdriver.common.by import By |
|
32 |
from selenium.webdriver.support.ui import WebDriverWait |
|
33 |
from selenium.webdriver.support import expected_conditions as EC |
|
34 |
|
|
35 |
from .profiles import firefox_safe_mode |
|
36 |
from .server import do_an_internet |
|
37 |
from .extension_crafting import make_extension |
|
38 |
from .world_wide_library import start_serving_script, dump_scripts |
|
39 |
from .misc_constants import here |
|
40 |
|
|
41 |
@pytest.fixture(scope="session") |
|
42 |
def proxy(): |
|
43 |
httpd = do_an_internet() |
|
44 |
yield httpd |
|
45 |
httpd.shutdown() |
|
46 |
|
|
47 |
@pytest.fixture(scope="session") |
|
48 |
def _driver(proxy): |
|
49 |
with firefox_safe_mode() as driver: |
|
50 |
yield driver |
|
51 |
driver.quit() |
|
52 |
|
|
53 |
def close_all_but_one_window(driver): |
|
54 |
while len(driver.window_handles) > 1: |
|
55 |
driver.switch_to.window(driver.window_handles[-1]) |
|
56 |
driver.close() |
|
57 |
driver.switch_to.window(driver.window_handles[0]) |
|
58 |
|
|
59 |
@pytest.fixture() |
|
60 |
def driver(_driver, request): |
|
61 |
nav_target = request.node.get_closest_marker('get_page') |
|
62 |
nav_target = nav_target.args[0] if nav_target else 'about:blank' |
|
63 |
|
|
64 |
second_driver = request.node.get_closest_marker('second_driver') |
|
65 |
|
|
66 |
if second_driver: |
|
67 |
with firefox_safe_mode() as _driver: |
|
68 |
_driver.get(nav_target) |
|
69 |
yield _driver |
|
70 |
_driver.quit() |
|
71 |
else: |
|
72 |
close_all_but_one_window(_driver) |
|
73 |
_driver.get(nav_target) |
|
74 |
_driver.implicitly_wait(0) |
|
75 |
yield _driver |
|
76 |
|
|
77 |
@pytest.fixture() |
|
78 |
def webextension(driver, request): |
|
79 |
ext_data = request.node.get_closest_marker('ext_data') |
|
80 |
if ext_data is None: |
|
81 |
raise Exception('"webextension" fixture requires "ext_data" marker to be set') |
|
82 |
ext_data = ext_data.args[0].copy() |
|
83 |
|
|
84 |
navigate_to = ext_data.get('navigate_to') |
|
85 |
if navigate_to is not None: |
|
86 |
del ext_data['navigate_to'] |
|
87 |
|
|
88 |
driver.get('https://gotmyowndoma.in/') |
|
89 |
ext_path = make_extension(Path(driver.firefox_profile.path), **ext_data) |
|
90 |
addon_id = driver.install_addon(str(ext_path), temporary=True) |
|
91 |
get_url = lambda d: d.execute_script('return window.ext_page_url;') |
|
92 |
ext_page_url = WebDriverWait(driver, 10).until(get_url) |
|
93 |
driver.get(ext_page_url) |
|
94 |
|
|
95 |
if navigate_to is not None: |
|
96 |
driver.get(driver.current_url.replace('testpage.html', navigate_to)) |
|
97 |
|
|
98 |
yield |
|
99 |
|
|
100 |
# Unloading an extension might cause its windows to vanish. Make sure |
|
101 |
# there's at least one window navigated to some other page before |
|
102 |
# uninstalling the addon. Otherwise, we could be left with a windowless |
|
103 |
# browser :c |
|
104 |
driver.switch_to.window(driver.window_handles[-1]) |
|
105 |
driver.get('https://gotmyowndoma.in/') |
|
106 |
driver.uninstall_addon(addon_id) |
|
107 |
ext_path.unlink() |
|
108 |
|
|
109 |
@pytest.fixture() |
|
110 |
def haketilo(driver): |
|
111 |
addon_id = driver.install_addon(str(here.parent / 'mozilla-build.zip'), |
|
112 |
temporary=True) |
|
113 |
|
|
114 |
yield |
|
115 |
|
|
116 |
driver.uninstall_addon(addon_id) |
|
117 |
|
|
118 |
script_injector_script = '''\ |
|
119 |
/* |
|
120 |
* Selenium by default executes scripts in some weird one-time context. We want |
|
121 |
* separately-loaded scripts to be able to access global variables defined |
|
122 |
* before, including those declared with `const` or `let`. To achieve that, we |
|
123 |
* run our scripts by injecting them into the page with a <script> tag that runs |
|
124 |
* javascript served by our proxy. We use custom properties of the `window` |
|
125 |
* object to communicate with injected code. |
|
126 |
*/ |
|
127 |
const inject = async () => { |
|
128 |
delete window.haketilo_selenium_return_value; |
|
129 |
delete window.haketilo_selenium_exception; |
|
130 |
window.returnval = val => window.haketilo_selenium_return_value = val; |
|
131 |
|
|
132 |
const injectee = document.createElement('script'); |
|
133 |
injectee.src = arguments[0]; |
|
134 |
injectee.type = "application/javascript"; |
|
135 |
injectee.async = true; |
|
136 |
const prom = new Promise(cb => injectee.onload = cb); |
|
137 |
|
|
138 |
window.arguments = arguments[1]; |
|
139 |
document.body.append(injectee); |
|
140 |
|
|
141 |
await prom; |
|
142 |
|
|
143 |
/* |
|
144 |
* To ease debugging, we want this script to signal all exceptions from the |
|
145 |
* injectee. |
|
146 |
*/ |
|
147 |
if (window.haketilo_selenium_exception !== false) |
|
148 |
throw ['haketilo_selenium_error', |
|
149 |
'Error in injected script! Check your geckodriver.log and ./injected_scripts/!']; |
|
150 |
|
|
151 |
return window.haketilo_selenium_return_value; |
|
152 |
} |
|
153 |
return inject(); |
|
154 |
''' |
|
155 |
|
|
156 |
def _execute_in_page_context(driver, script, args): |
|
157 |
script = script + '\n;\nwindow.haketilo_selenium_exception = false;' |
|
158 |
script_url = start_serving_script(script) |
|
159 |
|
|
160 |
try: |
|
161 |
result = driver.execute_script(script_injector_script, script_url, args) |
|
162 |
if type(result) is list and len(result) == 2 and \ |
|
163 |
result[0] == 'haketilo_selenium_error': |
|
164 |
raise Exception(result[1]) |
|
165 |
return result |
|
166 |
except Exception as e: |
|
167 |
dump_scripts() |
|
168 |
raise e from None |
|
169 |
|
|
170 |
# Some fixtures here just define functions that operate on driver. We should |
|
171 |
# consider making them into webdriver wrapper class methods. |
|
172 |
|
|
173 |
@pytest.fixture() |
|
174 |
def execute_in_page(driver): |
|
175 |
def do_execute(script, *args): |
|
176 |
return _execute_in_page_context(driver, script, args) |
|
177 |
|
|
178 |
yield do_execute |
|
179 |
|
|
180 |
@pytest.fixture() |
|
181 |
def wait_elem_text(driver): |
|
182 |
def do_wait(id, text): |
|
183 |
WebDriverWait(driver, 10).until( |
|
184 |
EC.text_to_be_present_in_element((By.ID, id), text) |
|
185 |
) |
|
186 |
|
|
187 |
yield do_wait |
test/data/pages/gotmyowndomain.html | ||
---|---|---|
1 |
<!DOCTYPE html> |
|
2 |
<!-- |
|
3 |
SPDX-License-Identifier: AGPL-3.0-or-later |
|
4 |
|
|
5 |
Sample testing page |
|
6 |
|
|
7 |
This file is part of Haketilo. |
|
8 |
|
|
9 |
Copyright (C) 2021 jahoti <jahoti@tilde.team> |
|
10 |
|
|
11 |
This program is free software: you can redistribute it and/or modify |
|
12 |
it under the terms of the GNU Affero General Public License as |
|
13 |
published by the Free Software Foundation, either version 3 of the |
|
14 |
License, or (at your option) any later version. |
|
15 |
|
|
16 |
This program is distributed in the hope that it will be useful, |
|
17 |
but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
18 |
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
19 |
GNU Affero General Public License for more details. |
|
20 |
|
|
21 |
You should have received a copy of the GNU Affero General Public License |
|
22 |
along with this program. If not, see <https://www.gnu.org/licenses/>. |
|
23 |
--> |
|
24 |
<html> |
|
25 |
<head> |
|
26 |
<meta name=charset value="latin1"> |
|
27 |
<title>Schrodinger's Document</title> |
|
28 |
</head> |
|
29 |
<body> |
|
30 |
A nice, simple page for testing. |
|
31 |
<script> |
|
32 |
document.write('<p><b>Or so you thought...</b></p>'); |
|
33 |
</script> |
|
34 |
</body> |
|
35 |
</html> |
test/data/pages/gotmyowndomain_https.html | ||
---|---|---|
1 |
<!DOCTYPE html> |
|
2 |
<!-- |
|
3 |
SPDX-License-Identifier: AGPL-3.0-or-later |
|
4 |
|
|
5 |
Sample testing page to serve over HTTPS |
|
6 |
|
|
7 |
This file is part of Haketilo. |
|
8 |
|
|
9 |
Copyright (C) 2021 jahoti <jahoti@tilde.team> |
|
10 |
|
|
11 |
This program is free software: you can redistribute it and/or modify |
|
12 |
it under the terms of the GNU Affero General Public License as |
|
13 |
published by the Free Software Foundation, either version 3 of the |
|
14 |
License, or (at your option) any later version. |
|
15 |
|
|
16 |
This program is distributed in the hope that it will be useful, |
|
17 |
but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
18 |
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
19 |
GNU Affero General Public License for more details. |
|
20 |
|
|
21 |
You should have received a copy of the GNU Affero General Public License |
|
22 |
along with this program. If not, see <https://www.gnu.org/licenses/>. |
|
23 |
--> |
|
24 |
<html> |
|
25 |
<head> |
|
26 |
<meta name="charset" value="latin1"> |
|
27 |
<title>Schrodinger's Document</title> |
|
28 |
</head> |
|
29 |
<body> |
|
30 |
A nice, simple page for testing (using HTTPS). |
|
31 |
<script> |
|
32 |
document.write('<p><b>Or so you thought...</b></p>'); |
|
33 |
</script> |
|
34 |
</body> |
|
35 |
</html> |
test/data/pages/scripts_to_block_1.html | ||
---|---|---|
1 |
<!DOCTYPE html> |
|
2 |
<!-- |
|
3 |
SPDX-License-Identifier: CC0-1.0 |
|
4 |
|
|
5 |
A testing page with various scripts that need to get blocked. |
|
6 |
|
|
7 |
This file is part of Haketilo. |
|
8 |
|
|
9 |
Copyright (C) 2022 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 |
<html> |
|
21 |
<head> |
|
22 |
<meta name="charset" value="latin1"> |
|
23 |
<script> |
|
24 |
window.__run = [...(window.__run || []), 'inline']; |
|
25 |
</script> |
|
26 |
<!-- the one below shall not execute even when blocking is off... --> |
|
27 |
<script type="application/json"> |
|
28 |
window.__run = [...(window.__run || []), 'json']; |
|
29 |
</script> |
|
30 |
</head> |
|
31 |
<body> |
|
32 |
<button id="clickme1" |
|
33 |
onclick="window.__run = [...(window.__run || []), 'on'];"> |
|
34 |
Click Meee! |
|
35 |
</button> |
|
36 |
<a id="clickme2" |
|
37 |
href="javascript:window.__run = [...(window.__run || []), 'href'];void(0);"> |
|
38 |
Click Meee! |
|
39 |
</a> |
|
40 |
<iframe src="javascript:void(window.parent.__run = [...(window.parent.__run || []), 'src']);"> |
|
41 |
</iframe> |
|
42 |
<object data="javascript:window.__run = [...(window.__run || []), 'data'];"> |
|
43 |
</object> |
|
44 |
</body> |
|
45 |
</html> |
test/default_profiles/icecat_empty/extensions.json | ||
---|---|---|
1 |
{"schemaVersion":25,"addons":[{"id":"jid1-KtlZuoiikVfFew@jetpack","location":"app-global","userDisabled":true,"path":"/usr/lib/icecat/browser/extensions/jid1-KtlZuoiikVfFew@jetpack"},{"id":"uBlock0@raymondhill.net","location":"app-global","userDisabled":true,"path":"/usr/lib/icecat/browser/extensions/uBlock0@raymondhill.net.xpi"},{"id":"SubmitMe@0xbeef.coffee","location":"app-global","userDisabled":true,"path":"/usr/lib/icecat/browser/extensions/SubmitMe@0xbeef.coffee"},{"id":"FreeUSPS@0xbeef.coffee","location":"app-global","userDisabled":true,"path":"/usr/lib/icecat/browser/extensions/FreeUSPS@0xbeef.coffee"},{"id":"tortm-browser-button@jeremybenthum","location":"app-global","userDisabled":true,"path":"/usr/lib/icecat/browser/extensions/tortm-browser-button@jeremybenthum"},{"id":"tprb.addon@searxes.danwin1210.me","location":"app-global","userDisabled":true,"path":"/usr/lib/icecat/browser/extensions/tprb.addon@searxes.danwin1210.me"},{"id":"SimpleSumOfUs@0xbeef.coffee","location":"app-global","userDisabled":true,"path":"/usr/lib/icecat/browser/extensions/SimpleSumOfUs@0xbeef.coffee"}]} |
test/extension_crafting.py | ||
---|---|---|
1 |
# SPDX-License-Identifier: GPL-3.0-or-later |
|
2 |
|
|
3 |
""" |
|
4 |
Making temporary WebExtensions for use in the test suite |
|
5 |
""" |
|
6 |
|
|
7 |
# This file is part of Haketilo. |
|
8 |
# |
|
9 |
# Copyright (C) 2021, 2022 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 GNU General Public License as published by |
|
13 |
# the Free Software Foundation, either version 3 of the License, or |
|
14 |
# (at your option) any later version. |
|
15 |
# |
|
16 |
# This program is distributed in the hope that it will be useful, |
|
17 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
18 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
19 |
# GNU General Public License for more details. |
|
20 |
# |
|
21 |
# You should have received a copy of the GNU General Public License |
|
22 |
# along with this program. If not, see <https://www.gnu.org/licenses/>. |
|
23 |
# |
|
24 |
# I, Wojtek Kosior, thereby promise not to sue for violation of this file's |
|
25 |
# license. Although I request that you do not make use of this code in a |
|
26 |
# proprietary program, I am not going to enforce this in court. |
|
27 |
|
|
28 |
import json |
|
29 |
import zipfile |
|
30 |
import re |
|
31 |
import shutil |
|
32 |
import subprocess |
|
33 |
|
|
34 |
from pathlib import Path |
|
35 |
from uuid import uuid4 |
|
36 |
from tempfile import TemporaryDirectory |
|
37 |
|
|
38 |
from selenium.webdriver.support.ui import WebDriverWait |
|
39 |
from selenium.common.exceptions import NoSuchElementException |
|
40 |
|
|
41 |
from .misc_constants import * |
|
42 |
|
|
43 |
class ManifestTemplateValueToFill: |
|
44 |
pass |
|
45 |
|
|
46 |
def manifest_template(): |
|
47 |
return { |
|
48 |
'manifest_version': 2, |
|
49 |
'name': 'Haketilo test extension', |
|
50 |
'version': '1.0', |
|
51 |
'applications': { |
|
52 |
'gecko': { |
|
53 |
'id': ManifestTemplateValueToFill(), |
|
54 |
'strict_min_version': '60.0' |
|
55 |
} |
|
56 |
}, |
|
57 |
'permissions': [ |
|
58 |
'contextMenus', |
|
59 |
'webRequest', |
|
60 |
'webRequestBlocking', |
|
61 |
'activeTab', |
|
62 |
'notifications', |
|
63 |
'sessions', |
|
64 |
'storage', |
|
65 |
'tabs', |
|
66 |
'<all_urls>', |
|
67 |
'unlimitedStorage' |
|
68 |
], |
|
69 |
'content_security_policy': "object-src 'none'; script-src 'self' https://serve.scrip.ts;", |
|
70 |
'web_accessible_resources': ['testpage.html'], |
|
71 |
'options_ui': { |
|
72 |
'page': 'testpage.html', |
|
73 |
'open_in_tab': True |
|
74 |
}, |
|
75 |
'background': { |
|
76 |
'persistent': True, |
|
77 |
'scripts': ['__open_test_page.js', 'background.js'] |
|
78 |
}, |
|
79 |
'content_scripts': [ |
|
80 |
{ |
|
81 |
'run_at': 'document_start', |
|
82 |
'matches': ['<all_urls>'], |
|
83 |
'match_about_blank': True, |
|
84 |
'all_frames': True, |
|
85 |
'js': ['content.js'] |
|
86 |
} |
|
87 |
] |
|
88 |
} |
|
89 |
|
|
90 |
class ExtraHTML: |
|
91 |
def __init__(self, html_path, append={}, wrap_into_htmldoc=True): |
|
92 |
self.html_path = html_path |
|
93 |
self.append = append |
|
94 |
self.wrap_into_htmldoc = wrap_into_htmldoc |
|
95 |
|
|
96 |
def add_to_xpi(self, xpi, tmpdir=None): |
|
97 |
if tmpdir is None: |
|
98 |
with TemporaryDirectory() as tmpdir: |
|
99 |
return self.add_to_xpi(xpi, tmpdir) |
|
100 |
|
|
101 |
append_flags = [] |
|
102 |
for filename, code in self.append.items(): |
|
103 |
append_flags.extend(['-A', f'{filename}:{code}']) |
|
104 |
|
|
105 |
awk = subprocess.run( |
|
106 |
['awk', '-f', awk_script_name, '--', *unit_test_defines, |
|
107 |
*append_flags, '-H', self.html_path, '--write-js-deps', |
|
108 |
'--output=files-to-copy', f'--output-dir={tmpdir}'], |
|
109 |
stdout=subprocess.PIPE, cwd=script_root, check=True |
|
110 |
) |
|
111 |
|
|
112 |
for path in filter(None, awk.stdout.decode().split('\n')): |
|
113 |
xpi.write(script_root / path, path) |
|
114 |
|
|
115 |
tmpdir = Path(tmpdir) |
|
116 |
for path in tmpdir.rglob('*'): |
|
117 |
relpath = str(path.relative_to(tmpdir)) |
|
118 |
if not path.is_dir() and relpath != self.html_path: |
|
119 |
xpi.write(path, relpath) |
|
120 |
|
|
121 |
with open(tmpdir / self.html_path, 'rt') as html_file: |
|
122 |
html = html_file.read() |
|
123 |
if self.wrap_into_htmldoc: |
|
124 |
html = f'<!DOCTYPE html><html><body>{html}</body></html>' |
|
125 |
xpi.writestr(self.html_path, html) |
|
126 |
|
|
127 |
default_background_script = '' |
|
128 |
default_content_script = '' |
|
129 |
default_test_page = ''' |
|
130 |
<!DOCTYPE html> |
|
131 |
<html> |
|
132 |
<head> |
|
133 |
<title>Extension's options page for testing</title> |
|
134 |
</head> |
|
135 |
<body> |
|
136 |
<h1>Extension's options page for testing</h1> |
|
137 |
</body> |
|
138 |
</html> |
|
139 |
''' |
|
140 |
|
|
141 |
open_test_page_script = '''(() => { |
|
142 |
const page_url = browser.runtime.getURL("testpage.html"); |
|
143 |
const execute_details = { |
|
144 |
code: `window.wrappedJSObject.ext_page_url=${JSON.stringify(page_url)};` |
|
145 |
}; |
|
146 |
browser.tabs.query({currentWindow: true, active: true}) |
|
147 |
.then(t => browser.tabs.executeScript(t.id, execute_details)); |
|
148 |
})();''' |
|
149 |
|
|
150 |
def make_extension(destination_dir, |
|
151 |
background_script=default_background_script, |
|
152 |
content_script=default_content_script, |
|
153 |
test_page=default_test_page, |
|
154 |
extra_files={}, extra_html=[]): |
|
155 |
if not hasattr(extra_html, '__iter__'): |
|
156 |
extra_html = [extra_html] |
|
157 |
manifest = manifest_template() |
|
158 |
extension_id = '{%s}' % uuid4() |
|
159 |
manifest['applications']['gecko']['id'] = extension_id |
|
160 |
files = { |
|
161 |
'manifest.json' : json.dumps(manifest), |
|
162 |
'__open_test_page.js': open_test_page_script, |
|
163 |
'background.js' : background_script, |
|
164 |
'content.js' : content_script, |
|
165 |
'testpage.html' : test_page, |
|
166 |
**extra_files |
|
167 |
} |
|
168 |
destination_path = destination_dir / f'{extension_id}.xpi' |
|
169 |
with zipfile.ZipFile(destination_path, 'x') as xpi: |
|
170 |
for filename, contents in files.items(): |
|
171 |
if hasattr(contents, '__call__'): |
|
172 |
contents = contents() |
|
173 |
xpi.writestr(filename, contents) |
|
174 |
for html in extra_html: |
|
175 |
html.add_to_xpi(xpi) |
|
176 |
|
|
177 |
return destination_path |
|
178 |
|
|
179 |
extract_base_url_re = re.compile(r'^(.*)manifest.json$') |
|
180 |
|
|
181 |
def get_extension_base_url(driver): |
|
182 |
""" |
|
183 |
Extension's internall UUID is not directly exposed in Selenium. Instead, we |
|
184 |
can navigate to about:debugging and inspect the manifest URL present there |
|
185 |
to get the base url like: |
|
186 |
moz-extension://b225c78f-d108-4caa-8406-f38b37d8dee5/ |
|
187 |
which can then be used to navigate to extension-bundled pages. |
|
188 |
""" |
|
189 |
# For newer Firefoxes |
|
190 |
driver.get('about:debugging#/runtime/this-firefox') |
|
191 |
|
|
192 |
def get_manifest_link_newer_ff(driver): |
|
193 |
try: |
|
194 |
return driver.find_element_by_class_name('qa-manifest-url') |
|
195 |
except NoSuchElementException: |
|
196 |
pass |
|
197 |
|
|
198 |
try: |
|
199 |
details = driver.find_element_by_class_name('error-page-details') |
|
200 |
except NoSuchElementException: |
|
201 |
return False |
|
202 |
|
|
203 |
if '#/runtime/this-firefox' in details.text: |
|
204 |
return "not_newer_ff" |
|
205 |
|
|
206 |
manifest_link = WebDriverWait(driver, 10).until(get_manifest_link_newer_ff) |
|
207 |
|
|
208 |
if manifest_link == "not_newer_ff": |
|
209 |
driver.get("about:debugging#addons") |
|
210 |
driver.implicitly_wait(10) |
|
211 |
manifest_link = driver.find_element_by_class_name('manifest-url') |
|
212 |
driver.implicitly_wait(0) |
|
213 |
|
|
214 |
manifest_url = manifest_link.get_attribute('href') |
|
215 |
return extract_base_url_re.match(manifest_url).group(1) |
test/haketilo_test/__init__.py | ||
---|---|---|
1 |
# SPDX-License-Identifier: CC0-1.0 |
|
2 |
# Copyright (C) 2021 Wojtek Kosior |
test/haketilo_test/__main__.py | ||
---|---|---|
1 |
# SPDX-License-Identifier: AGPL-3.0-or-later |
|
2 |
|
|
3 |
""" |
|
4 |
Run a Firefox-type browser with WebDriver attached and Python console open |
|
5 |
""" |
|
6 |
|
|
7 |
# This file is part of Haketilo. |
|
8 |
# |
|
9 |
# Copyright (C) 2021 jahoti <jahoti@tilde.team> |
|
10 |
# Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org> |
|
11 |
# |
|
12 |
# This program is free software: you can redistribute it and/or modify |
|
13 |
# it under the terms of the GNU Affero General Public License as |
|
14 |
# published by the Free Software Foundation, either version 3 of the |
|
15 |
# License, or (at your option) any later version. |
|
16 |
# |
|
17 |
# This program is distributed in the hope that it will be useful, |
|
18 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
19 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
20 |
# GNU Affero General Public License for more details. |
|
21 |
# |
|
22 |
# You should have received a copy of the GNU Affero General Public License |
|
23 |
# along with this program. If not, see <https://www.gnu.org/licenses/>. |
|
24 |
# |
|
25 |
# |
|
26 |
# I, Wojtek Kosior, thereby promise not to sue for violation of this |
|
27 |
# file's license. Although I request that you do not make use of this code |
|
28 |
# in a proprietary program, I am not going to enforce this in court. |
|
29 |
|
|
30 |
import sys |
|
31 |
import time |
|
32 |
import code |
|
33 |
from rlcompleter import Completer |
|
34 |
import readline |
|
35 |
|
|
36 |
from .server import do_an_internet |
|
37 |
from .misc_constants import * |
|
38 |
from .profiles import firefox_safe_mode |
|
39 |
from .extension_crafting import get_extension_base_url |
|
40 |
|
|
41 |
def fail(msg, error_code): |
|
42 |
print('Error:', msg) |
|
43 |
print('Usage:', sys.argv[0], '[--load-haketilo]', '[certificates_directory] [proxy_port]') |
|
44 |
sys.exit(error_code) |
|
45 |
|
|
46 |
load_haketilo = False |
|
47 |
argv_idx = 1 |
|
48 |
if len(sys.argv) > argv_idx and sys.argv[argv_idx] == '--load-haketilo': |
|
49 |
load_haketilo = True |
|
50 |
argv_idx += 1 |
|
51 |
|
|
52 |
certdir = Path(sys.argv[argv_idx]).resolve() if len(sys.argv) > argv_idx \ |
|
53 |
else default_cert_dir |
|
54 |
|
|
55 |
if not certdir.is_dir(): |
|
56 |
fail('selected certificate directory does not exist.', 2) |
|
57 |
|
|
58 |
argv_idx += 1 |
|
59 |
|
|
60 |
port = sys.argv[argv_idx] if len(sys.argv) > argv_idx \ |
|
61 |
else str(default_proxy_port) |
|
62 |
|
|
63 |
if not port.isnumeric(): |
|
64 |
fail('port must be an integer.', 3) |
|
65 |
|
|
66 |
httpd = do_an_internet(certdir, int(port)) |
|
67 |
driver = firefox_safe_mode(proxy_port=int(port)) |
|
68 |
|
|
69 |
if load_haketilo: |
|
70 |
driver.install_addon(str(here.parent / 'mozilla-build.zip'), temporary=True) |
|
71 |
driver.get(get_extension_base_url(driver) + 'html/settings.html') |
|
72 |
|
|
73 |
print("You can now control the browser through 'driver' object") |
|
74 |
|
|
75 |
# Here we enable readline-enhanced editing: |
|
76 |
# https://stackoverflow.com/questions/35115208/is-there-any-way-to-combine-readline-rlcompleter-and-interactiveconsole-in-pytho#answer-35116399 |
|
77 |
readline.parse_and_bind('tab: complete'); |
|
78 |
console_locals = globals() |
|
79 |
readline.set_completer(Completer(console_locals).complete) |
|
80 |
code.InteractiveConsole(locals=globals()).interact() |
|
81 |
|
|
82 |
driver.quit() |
|
83 |
httpd.shutdown() |
test/haketilo_test/conftest.py | ||
---|---|---|
1 |
# SPDX-License-Identifier: GPL-3.0-or-later |
|
2 |
|
|
3 |
""" |
|
4 |
Common fixtures for Haketilo unit tests |
|
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 GNU General Public License as published by |
|
13 |
# the Free Software Foundation, either version 3 of the License, or |
|
14 |
# (at your option) any later version. |
|
15 |
# |
|
16 |
# This program is distributed in the hope that it will be useful, |
|
17 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
18 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
19 |
# GNU General Public License for more details. |
|
20 |
# |
|
21 |
# You should have received a copy of the GNU General Public License |
|
22 |
# along with this program. If not, see <https://www.gnu.org/licenses/>. |
|
23 |
# |
|
24 |
# I, Wojtek Kosior, thereby promise not to sue for violation of this file's |
|
25 |
# license. Although I request that you do not make use of this code in a |
|
26 |
# proprietary program, I am not going to enforce this in court. |
|
27 |
|
|
28 |
import pytest |
|
29 |
from pathlib import Path |
|
30 |
from tempfile import TemporaryDirectory |
|
31 |
from selenium.webdriver.common.by import By |
|
32 |
from selenium.webdriver.support.ui import WebDriverWait |
|
33 |
from selenium.webdriver.support import expected_conditions as EC |
|
34 |
|
|
35 |
from .profiles import firefox_safe_mode |
|
36 |
from .server import do_an_internet |
|
37 |
from .extension_crafting import make_extension |
|
38 |
from .world_wide_library import start_serving_script, dump_scripts |
|
39 |
from .misc_constants import proj_root |
|
40 |
|
|
41 |
@pytest.fixture(scope="session") |
|
42 |
def proxy(): |
|
43 |
httpd = do_an_internet() |
|
44 |
yield httpd |
|
45 |
httpd.shutdown() |
|
46 |
|
|
47 |
@pytest.fixture(scope="session") |
|
48 |
def _driver(proxy): |
|
49 |
with firefox_safe_mode() as driver: |
|
50 |
yield driver |
|
51 |
driver.quit() |
|
52 |
|
|
53 |
def close_all_but_one_window(driver): |
|
54 |
while len(driver.window_handles) > 1: |
|
55 |
driver.switch_to.window(driver.window_handles[-1]) |
|
56 |
driver.close() |
|
57 |
driver.switch_to.window(driver.window_handles[0]) |
|
58 |
|
|
59 |
@pytest.fixture() |
|
60 |
def driver(_driver, request): |
|
61 |
nav_target = request.node.get_closest_marker('get_page') |
|
62 |
nav_target = nav_target.args[0] if nav_target else 'about:blank' |
|
63 |
|
|
64 |
second_driver = request.node.get_closest_marker('second_driver') |
|
65 |
|
|
66 |
if second_driver: |
|
67 |
with firefox_safe_mode() as _driver: |
|
68 |
_driver.get(nav_target) |
|
69 |
yield _driver |
|
70 |
_driver.quit() |
|
71 |
else: |
|
72 |
close_all_but_one_window(_driver) |
|
73 |
_driver.get(nav_target) |
|
74 |
_driver.implicitly_wait(0) |
|
75 |
yield _driver |
|
76 |
|
|
77 |
@pytest.fixture() |
|
78 |
def webextension(driver, request): |
|
79 |
ext_data = request.node.get_closest_marker('ext_data') |
|
80 |
if ext_data is None: |
|
81 |
raise Exception('"webextension" fixture requires "ext_data" marker to be set') |
|
82 |
ext_data = ext_data.args[0].copy() |
|
83 |
|
|
84 |
navigate_to = ext_data.get('navigate_to') |
|
85 |
if navigate_to is not None: |
|
86 |
del ext_data['navigate_to'] |
|
87 |
|
|
88 |
driver.get('https://gotmyowndoma.in/') |
|
89 |
ext_path = make_extension(Path(driver.firefox_profile.path), **ext_data) |
|
90 |
addon_id = driver.install_addon(str(ext_path), temporary=True) |
|
91 |
get_url = lambda d: d.execute_script('return window.ext_page_url;') |
|
92 |
ext_page_url = WebDriverWait(driver, 10).until(get_url) |
|
93 |
driver.get(ext_page_url) |
|
94 |
|
|
95 |
if navigate_to is not None: |
|
96 |
driver.get(driver.current_url.replace('testpage.html', navigate_to)) |
|
97 |
|
|
98 |
yield |
|
99 |
|
|
100 |
# Unloading an extension might cause its windows to vanish. Make sure |
|
101 |
# there's at least one window navigated to some other page before |
|
102 |
# uninstalling the addon. Otherwise, we could be left with a windowless |
|
103 |
# browser :c |
|
104 |
driver.switch_to.window(driver.window_handles[-1]) |
|
105 |
driver.get('https://gotmyowndoma.in/') |
|
106 |
driver.uninstall_addon(addon_id) |
|
107 |
ext_path.unlink() |
|
108 |
|
|
109 |
@pytest.fixture() |
|
110 |
def haketilo(driver): |
|
111 |
addon_id = driver.install_addon(str(Path.cwd() / 'mozilla-build.zip'), |
|
112 |
temporary=True) |
|
113 |
|
|
114 |
yield |
|
115 |
|
|
116 |
driver.uninstall_addon(addon_id) |
|
117 |
|
|
118 |
script_injector_script = '''\ |
|
119 |
/* |
|
120 |
* Selenium by default executes scripts in some weird one-time context. We want |
|
121 |
* separately-loaded scripts to be able to access global variables defined |
|
122 |
* before, including those declared with `const` or `let`. To achieve that, we |
|
123 |
* run our scripts by injecting them into the page with a <script> tag that runs |
|
124 |
* javascript served by our proxy. We use custom properties of the `window` |
|
125 |
* object to communicate with injected code. |
|
126 |
*/ |
|
127 |
const inject = async () => { |
|
128 |
delete window.haketilo_selenium_return_value; |
|
129 |
delete window.haketilo_selenium_exception; |
|
130 |
window.returnval = val => window.haketilo_selenium_return_value = val; |
|
131 |
|
|
132 |
const injectee = document.createElement('script'); |
|
133 |
injectee.src = arguments[0]; |
|
134 |
injectee.type = "application/javascript"; |
|
135 |
injectee.async = true; |
|
136 |
const prom = new Promise(cb => injectee.onload = cb); |
|
137 |
|
|
138 |
window.arguments = arguments[1]; |
|
139 |
document.body.append(injectee); |
|
140 |
|
|
141 |
await prom; |
|
142 |
|
|
143 |
/* |
|
144 |
* To ease debugging, we want this script to signal all exceptions from the |
|
145 |
* injectee. |
|
146 |
*/ |
|
147 |
if (window.haketilo_selenium_exception !== false) |
|
148 |
throw ['haketilo_selenium_error', |
|
149 |
'Error in injected script! Check your geckodriver.log and ./injected_scripts/!']; |
|
150 |
|
|
151 |
return window.haketilo_selenium_return_value; |
|
152 |
} |
|
153 |
return inject(); |
|
154 |
''' |
|
155 |
|
|
156 |
def _execute_in_page_context(driver, script, args): |
|
157 |
script = script + '\n;\nwindow.haketilo_selenium_exception = false;' |
|
158 |
script_url = start_serving_script(script) |
|
159 |
|
|
160 |
try: |
|
161 |
result = driver.execute_script(script_injector_script, script_url, args) |
|
162 |
if type(result) is list and len(result) == 2 and \ |
|
163 |
result[0] == 'haketilo_selenium_error': |
|
164 |
raise Exception(result[1]) |
|
165 |
return result |
|
166 |
except Exception as e: |
|
167 |
dump_scripts() |
|
168 |
raise e from None |
|
169 |
|
|
170 |
# Some fixtures here just define functions that operate on driver. We should |
|
171 |
# consider making them into webdriver wrapper class methods. |
|
172 |
|
|
173 |
@pytest.fixture() |
|
174 |
def execute_in_page(driver): |
|
175 |
def do_execute(script, *args): |
|
176 |
return _execute_in_page_context(driver, script, args) |
|
177 |
|
|
178 |
yield do_execute |
|
179 |
|
|
180 |
@pytest.fixture() |
|
181 |
def wait_elem_text(driver): |
|
182 |
def do_wait(id, text): |
|
183 |
WebDriverWait(driver, 10).until( |
|
184 |
EC.text_to_be_present_in_element((By.ID, id), text) |
|
185 |
) |
|
186 |
|
|
187 |
yield do_wait |
test/haketilo_test/data/pages/gotmyowndomain.html | ||
---|---|---|
1 |
<!DOCTYPE html> |
|
2 |
<!-- |
|
3 |
SPDX-License-Identifier: AGPL-3.0-or-later |
|
4 |
|
|
5 |
Sample testing page |
|
6 |
|
|
7 |
This file is part of Haketilo. |
|
8 |
|
|
9 |
Copyright (C) 2021 jahoti <jahoti@tilde.team> |
|
10 |
|
|
11 |
This program is free software: you can redistribute it and/or modify |
|
12 |
it under the terms of the GNU Affero General Public License as |
|
13 |
published by the Free Software Foundation, either version 3 of the |
|
14 |
License, or (at your option) any later version. |
|
15 |
|
|
16 |
This program is distributed in the hope that it will be useful, |
|
17 |
but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
18 |
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
19 |
GNU Affero General Public License for more details. |
|
20 |
|
|
21 |
You should have received a copy of the GNU Affero General Public License |
|
22 |
along with this program. If not, see <https://www.gnu.org/licenses/>. |
|
23 |
--> |
|
24 |
<html> |
|
25 |
<head> |
|
26 |
<meta name=charset value="latin1"> |
|
27 |
<title>Schrodinger's Document</title> |
|
28 |
</head> |
|
29 |
<body> |
|
30 |
A nice, simple page for testing. |
|
31 |
<script> |
|
32 |
document.write('<p><b>Or so you thought...</b></p>'); |
|
33 |
</script> |
|
34 |
</body> |
|
35 |
</html> |
test/haketilo_test/data/pages/gotmyowndomain_https.html | ||
---|---|---|
1 |
<!DOCTYPE html> |
|
2 |
<!-- |
|
3 |
SPDX-License-Identifier: AGPL-3.0-or-later |
|
4 |
|
|
5 |
Sample testing page to serve over HTTPS |
|
6 |
|
|
7 |
This file is part of Haketilo. |
|
8 |
|
|
9 |
Copyright (C) 2021 jahoti <jahoti@tilde.team> |
|
10 |
|
|
11 |
This program is free software: you can redistribute it and/or modify |
|
12 |
it under the terms of the GNU Affero General Public License as |
|
13 |
published by the Free Software Foundation, either version 3 of the |
|
14 |
License, or (at your option) any later version. |
|
15 |
|
|
16 |
This program is distributed in the hope that it will be useful, |
|
17 |
but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
18 |
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
19 |
GNU Affero General Public License for more details. |
|
20 |
|
|
21 |
You should have received a copy of the GNU Affero General Public License |
|
22 |
along with this program. If not, see <https://www.gnu.org/licenses/>. |
|
23 |
--> |
|
24 |
<html> |
|
25 |
<head> |
|
26 |
<meta name="charset" value="latin1"> |
|
27 |
<title>Schrodinger's Document</title> |
|
28 |
</head> |
|
29 |
<body> |
|
30 |
A nice, simple page for testing (using HTTPS). |
|
31 |
<script> |
|
32 |
document.write('<p><b>Or so you thought...</b></p>'); |
|
33 |
</script> |
|
34 |
</body> |
|
35 |
</html> |
test/haketilo_test/data/pages/scripts_to_block_1.html | ||
---|---|---|
1 |
<!DOCTYPE html> |
|
2 |
<!-- |
|
3 |
SPDX-License-Identifier: CC0-1.0 |
|
4 |
|
|
5 |
A testing page with various scripts that need to get blocked. |
|
6 |
|
|
7 |
This file is part of Haketilo. |
|
8 |
|
|
9 |
Copyright (C) 2022 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 |
<html> |
|
21 |
<head> |
|
22 |
<meta name="charset" value="latin1"> |
|
23 |
<script> |
|
24 |
window.__run = [...(window.__run || []), 'inline']; |
|
25 |
</script> |
|
26 |
<!-- the one below shall not execute even when blocking is off... --> |
|
27 |
<script type="application/json"> |
|
28 |
window.__run = [...(window.__run || []), 'json']; |
|
29 |
</script> |
|
30 |
</head> |
|
31 |
<body> |
|
32 |
<button id="clickme1" |
|
33 |
onclick="window.__run = [...(window.__run || []), 'on'];"> |
|
34 |
Click Meee! |
|
35 |
</button> |
|
36 |
<a id="clickme2" |
|
37 |
href="javascript:window.__run = [...(window.__run || []), 'href'];void(0);"> |
|
38 |
Click Meee! |
|
39 |
</a> |
|
40 |
<iframe src="javascript:void(window.parent.__run = [...(window.parent.__run || []), 'src']);"> |
|
41 |
</iframe> |
|
42 |
<object data="javascript:window.__run = [...(window.__run || []), 'data'];"> |
|
43 |
</object> |
|
44 |
</body> |
|
45 |
</html> |
test/haketilo_test/default_profiles/icecat_empty/extensions.json | ||
---|---|---|
1 |
{"schemaVersion":25,"addons":[{"id":"jid1-KtlZuoiikVfFew@jetpack","location":"app-global","userDisabled":true,"path":"/usr/lib/icecat/browser/extensions/jid1-KtlZuoiikVfFew@jetpack"},{"id":"uBlock0@raymondhill.net","location":"app-global","userDisabled":true,"path":"/usr/lib/icecat/browser/extensions/uBlock0@raymondhill.net.xpi"},{"id":"SubmitMe@0xbeef.coffee","location":"app-global","userDisabled":true,"path":"/usr/lib/icecat/browser/extensions/SubmitMe@0xbeef.coffee"},{"id":"FreeUSPS@0xbeef.coffee","location":"app-global","userDisabled":true,"path":"/usr/lib/icecat/browser/extensions/FreeUSPS@0xbeef.coffee"},{"id":"tortm-browser-button@jeremybenthum","location":"app-global","userDisabled":true,"path":"/usr/lib/icecat/browser/extensions/tortm-browser-button@jeremybenthum"},{"id":"tprb.addon@searxes.danwin1210.me","location":"app-global","userDisabled":true,"path":"/usr/lib/icecat/browser/extensions/tprb.addon@searxes.danwin1210.me"},{"id":"SimpleSumOfUs@0xbeef.coffee","location":"app-global","userDisabled":true,"path":"/usr/lib/icecat/browser/extensions/SimpleSumOfUs@0xbeef.coffee"}]} |
test/haketilo_test/extension_crafting.py | ||
---|---|---|
1 |
# SPDX-License-Identifier: GPL-3.0-or-later |
|
2 |
|
|
3 |
""" |
|
4 |
Making temporary WebExtensions for use in the test suite |
|
5 |
""" |
|
6 |
|
|
7 |
# This file is part of Haketilo. |
|
8 |
# |
|
9 |
# Copyright (C) 2021, 2022 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 GNU General Public License as published by |
|
13 |
# the Free Software Foundation, either version 3 of the License, or |
|
14 |
# (at your option) any later version. |
|
15 |
# |
|
16 |
# This program is distributed in the hope that it will be useful, |
|
17 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
18 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
19 |
# GNU General Public License for more details. |
|
20 |
# |
|
21 |
# You should have received a copy of the GNU General Public License |
|
22 |
# along with this program. If not, see <https://www.gnu.org/licenses/>. |
|
23 |
# |
|
24 |
# I, Wojtek Kosior, thereby promise not to sue for violation of this file's |
|
25 |
# license. Although I request that you do not make use of this code in a |
|
26 |
# proprietary program, I am not going to enforce this in court. |
|
27 |
|
|
28 |
import json |
|
29 |
import zipfile |
|
30 |
import re |
|
31 |
import shutil |
|
32 |
import subprocess |
|
33 |
|
|
34 |
from pathlib import Path |
|
35 |
from uuid import uuid4 |
|
36 |
from tempfile import TemporaryDirectory |
|
37 |
|
|
38 |
from selenium.webdriver.support.ui import WebDriverWait |
|
39 |
from selenium.common.exceptions import NoSuchElementException |
|
40 |
|
|
41 |
from .misc_constants import * |
|
42 |
|
|
43 |
class ManifestTemplateValueToFill: |
|
44 |
pass |
|
45 |
|
|
46 |
def manifest_template(): |
|
47 |
return { |
|
48 |
'manifest_version': 2, |
|
49 |
'name': 'Haketilo test extension', |
|
50 |
'version': '1.0', |
|
51 |
'applications': { |
|
52 |
'gecko': { |
|
53 |
'id': ManifestTemplateValueToFill(), |
|
54 |
'strict_min_version': '60.0' |
|
55 |
} |
|
56 |
}, |
|
57 |
'permissions': [ |
|
58 |
'contextMenus', |
|
59 |
'webRequest', |
|
60 |
'webRequestBlocking', |
|
61 |
'activeTab', |
|
62 |
'notifications', |
|
63 |
'sessions', |
|
64 |
'storage', |
|
65 |
'tabs', |
|
66 |
'<all_urls>', |
|
67 |
'unlimitedStorage' |
|
68 |
], |
|
69 |
'content_security_policy': "object-src 'none'; script-src 'self' https://serve.scrip.ts;", |
|
70 |
'web_accessible_resources': ['testpage.html'], |
|
71 |
'options_ui': { |
|
72 |
'page': 'testpage.html', |
|
73 |
'open_in_tab': True |
|
74 |
}, |
|
75 |
'background': { |
|
76 |
'persistent': True, |
|
77 |
'scripts': ['__open_test_page.js', 'background.js'] |
|
78 |
}, |
|
79 |
'content_scripts': [ |
|
80 |
{ |
|
81 |
'run_at': 'document_start', |
|
82 |
'matches': ['<all_urls>'], |
|
83 |
'match_about_blank': True, |
|
84 |
'all_frames': True, |
|
85 |
'js': ['content.js'] |
|
86 |
} |
|
87 |
] |
|
88 |
} |
|
89 |
|
|
90 |
class ExtraHTML: |
|
91 |
def __init__(self, html_path, append={}, wrap_into_htmldoc=True): |
|
92 |
self.html_path = html_path |
|
93 |
self.append = append |
|
94 |
self.wrap_into_htmldoc = wrap_into_htmldoc |
|
95 |
|
|
96 |
def add_to_xpi(self, xpi, tmpdir=None): |
|
97 |
if tmpdir is None: |
|
98 |
with TemporaryDirectory() as tmpdir: |
|
99 |
return self.add_to_xpi(xpi, tmpdir) |
|
100 |
|
|
101 |
append_flags = [] |
|
102 |
for filename, code in self.append.items(): |
|
103 |
append_flags.extend(['-A', f'{filename}:{code}']) |
|
104 |
|
|
105 |
awk = subprocess.run( |
|
106 |
['awk', '-f', awk_script_name, '--', *unit_test_defines, |
|
107 |
*append_flags, '-H', self.html_path, '--write-js-deps', |
|
108 |
'--output=files-to-copy', f'--output-dir={tmpdir}'], |
|
109 |
stdout=subprocess.PIPE, cwd=proj_root, check=True |
|
110 |
) |
|
111 |
|
|
112 |
for path in filter(None, awk.stdout.decode().split('\n')): |
|
113 |
xpi.write(proj_root / path, path) |
|
114 |
|
|
115 |
tmpdir = Path(tmpdir) |
|
116 |
for path in tmpdir.rglob('*'): |
|
117 |
relpath = str(path.relative_to(tmpdir)) |
|
118 |
if not path.is_dir() and relpath != self.html_path: |
|
119 |
xpi.write(path, relpath) |
|
120 |
|
|
121 |
with open(tmpdir / self.html_path, 'rt') as html_file: |
|
122 |
html = html_file.read() |
|
123 |
if self.wrap_into_htmldoc: |
|
124 |
html = f'<!DOCTYPE html><html><body>{html}</body></html>' |
|
125 |
xpi.writestr(self.html_path, html) |
|
126 |
|
|
127 |
default_background_script = '' |
|
128 |
default_content_script = '' |
|
129 |
default_test_page = ''' |
|
130 |
<!DOCTYPE html> |
|
131 |
<html> |
|
132 |
<head> |
|
133 |
<title>Extension's options page for testing</title> |
|
134 |
</head> |
|
135 |
<body> |
|
136 |
<h1>Extension's options page for testing</h1> |
|
137 |
</body> |
|
138 |
</html> |
|
139 |
''' |
|
140 |
|
|
141 |
open_test_page_script = '''(() => { |
|
142 |
const page_url = browser.runtime.getURL("testpage.html"); |
|
143 |
const execute_details = { |
|
144 |
code: `window.wrappedJSObject.ext_page_url=${JSON.stringify(page_url)};` |
|
145 |
}; |
|
146 |
browser.tabs.query({currentWindow: true, active: true}) |
|
147 |
.then(t => browser.tabs.executeScript(t.id, execute_details)); |
|
148 |
})();''' |
|
149 |
|
|
150 |
def make_extension(destination_dir, |
|
151 |
background_script=default_background_script, |
|
152 |
content_script=default_content_script, |
|
153 |
test_page=default_test_page, |
|
154 |
extra_files={}, extra_html=[]): |
|
155 |
if not hasattr(extra_html, '__iter__'): |
|
156 |
extra_html = [extra_html] |
|
157 |
manifest = manifest_template() |
|
158 |
extension_id = '{%s}' % uuid4() |
|
159 |
manifest['applications']['gecko']['id'] = extension_id |
|
160 |
files = { |
|
161 |
'manifest.json' : json.dumps(manifest), |
|
162 |
'__open_test_page.js': open_test_page_script, |
|
163 |
'background.js' : background_script, |
|
164 |
'content.js' : content_script, |
|
165 |
'testpage.html' : test_page, |
|
166 |
**extra_files |
|
167 |
} |
|
168 |
destination_path = destination_dir / f'{extension_id}.xpi' |
|
169 |
with zipfile.ZipFile(destination_path, 'x') as xpi: |
|
170 |
for filename, contents in files.items(): |
|
171 |
if hasattr(contents, '__call__'): |
|
172 |
contents = contents() |
|
173 |
xpi.writestr(filename, contents) |
|
174 |
for html in extra_html: |
|
175 |
html.add_to_xpi(xpi) |
|
176 |
|
|
177 |
return destination_path |
|
178 |
|
|
179 |
extract_base_url_re = re.compile(r'^(.*)manifest.json$') |
|
180 |
|
|
181 |
def get_extension_base_url(driver): |
|
182 |
""" |
|
183 |
Extension's internall UUID is not directly exposed in Selenium. Instead, we |
|
184 |
can navigate to about:debugging and inspect the manifest URL present there |
|
185 |
to get the base url like: |
|
186 |
moz-extension://b225c78f-d108-4caa-8406-f38b37d8dee5/ |
|
187 |
which can then be used to navigate to extension-bundled pages. |
|
188 |
""" |
|
189 |
# For newer Firefoxes |
|
190 |
driver.get('about:debugging#/runtime/this-firefox') |
|
191 |
|
|
192 |
def get_manifest_link_newer_ff(driver): |
|
193 |
try: |
|
194 |
return driver.find_element_by_class_name('qa-manifest-url') |
|
195 |
except NoSuchElementException: |
|
196 |
pass |
|
197 |
|
|
198 |
try: |
|
199 |
details = driver.find_element_by_class_name('error-page-details') |
|
200 |
except NoSuchElementException: |
|
201 |
return False |
|
202 |
|
|
203 |
if '#/runtime/this-firefox' in details.text: |
|
204 |
return "not_newer_ff" |
|
205 |
|
|
206 |
manifest_link = WebDriverWait(driver, 10).until(get_manifest_link_newer_ff) |
|
207 |
|
|
208 |
if manifest_link == "not_newer_ff": |
|
209 |
driver.get("about:debugging#addons") |
|
210 |
driver.implicitly_wait(10) |
|
211 |
manifest_link = driver.find_element_by_class_name('manifest-url') |
|
212 |
driver.implicitly_wait(0) |
|
213 |
|
|
214 |
manifest_url = manifest_link.get_attribute('href') |
|
215 |
return extract_base_url_re.match(manifest_url).group(1) |
test/haketilo_test/misc_constants.py | ||
---|---|---|
1 |
# SPDX-License-Identifier: AGPL-3.0-or-later |
|
2 |
|
|
3 |
""" |
|
4 |
Miscellaneous data that were found useful |
|
5 |
""" |
|
6 |
|
|
7 |
# This file is part of Haketilo. |
|
8 |
# |
|
9 |
# Copyright (C) 2021 jahoti <jahoti@tilde.team> |
|
10 |
# Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org> |
Also available in: Unified diff
fix out-of-source builds