Project

General

Profile

« Previous | Next » 

Revision 9d825eaa

Added by koszko over 1 year ago

add new root content script

View differences:

background/patterns_query_manager.js
49 49
#FROM common/browser.js IMPORT browser
50 50
#ENDIF
51 51

  
52
let default_allow = {};
53
#EXPORT default_allow
54

  
52 55
let secret;
53 56

  
54 57
const tree = pqt.make();
......
72 75
	script_update_needed = false;
73 76

  
74 77
	const code = `\
75
this.haketilo_secret       = ${secret};
76
this.haketilo_pattern_tree = ${JSON.stringify(tree)};
78
this.haketilo_secret        = ${JSON.stringify(secret)};
79
this.haketilo_pattern_tree  = ${JSON.stringify(tree)};
80
this.haketilo_default_allow = ${JSON.stringify(default_allow.value)};
77 81
if (this.haketilo_content_script_main)
78 82
    haketilo_content_script_main();`;
79 83

  
......
151 155
    initial_mappings.forEach(m => register("mappings", m));
152 156
    initial_blocking.forEach(b => register("blocking", b));
153 157

  
158
    const set_allow_val = ch => default_allow.value = (ch.new_val || {}).value;
159
    const [setting_tracking, initial_settings] =
160
	  await haketilodb.track.settings(set_allow_val);
161
    for (const setting of initial_settings) {
162
	if (setting.name === "default_allow")
163
	    Object.assign(default_allow, setting);
164
    }
165

  
154 166
#IF MOZILLA || MV3
155 167
    script_update_needed = true;
156 168
    await update_content_script();
background/webrequest.js
50 50
#FROM common/misc.js    IMPORT is_privileged_url, csp_header_regex
51 51
#FROM common/policy.js  IMPORT decide_policy
52 52

  
53
#FROM background/patterns_query_manager.js IMPORT tree
53
#FROM background/patterns_query_manager.js IMPORT tree, default_allow
54 54

  
55 55
let secret;
56 56

  
57
let default_allow = false;
58

  
59
async function track_default_allow()
60
{
61
    const set_val = ch => default_allow = (ch.new_val || {}).value;
62
    const [tracking, settings] = await haketilodb.track.settings(set_val);
63
    for (const setting of settings) {
64
	if (setting.name === "default_allow")
65
	    default_allow = setting.value;
66
    }
67
}
68

  
69 57
function on_headers_received(details)
70 58
{
71 59
    const url = details.url;
......
74 62

  
75 63
    let headers = details.responseHeaders;
76 64

  
77
    const policy = decide_policy(tree, details.url, !!default_allow, secret);
65
    const policy =
66
	  decide_policy(tree, details.url, !!default_allow.value, secret);
78 67
    if (policy.allow)
79 68
	return;
80 69

  
common/misc.js
96 96
 * Check if url corresponds to a browser's special page (or a directory index in
97 97
 * case of `file://' protocol).
98 98
 */
99
const privileged_reg =
100
      /^(chrome(-extension)?|moz-extension):\/\/|^about:|^file:\/\/.*\/$/;
101
#EXPORT  url => privileged_reg.test(url)  AS is_privileged_url
99
#IF MOZILLA
100
const priv_reg = /^moz-extension:\/\/|^about:|^file:\/\/[^?#]*\/([?#]|$)/;
101
#ELIF CHROMIUM
102
const priv_reg = /^chrome(-extension)?:\/\/|^about:|^file:\/\/[^?#]*\/([?#]|$)/;
103
#ENDIF
104
#EXPORT  url => priv_reg.test(url)  AS is_privileged_url
102 105

  
103 106
/* Parse a CSP header */
104 107
function parse_csp(csp) {
content/content.js
1
/**
2
 * This file is part of Haketilo.
3
 *
4
 * Function: Content scripts - main script.
5
 *
6
 * Copyright (C) 2022 Wojtek Kosior
7
 *
8
 * This program is free software: you can redistribute it and/or modify
9
 * it under the terms of the GNU General Public License as published by
10
 * the Free Software Foundation, either version 3 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 * GNU General Public License for more details.
17
 *
18
 * As additional permission under GNU GPL version 3 section 7, you
19
 * may distribute forms of that code without the copy of the GNU
20
 * GPL normally required by section 4, provided you include this
21
 * license notice and, in case of non-source distribution, a URL
22
 * through which recipients can access the Corresponding Source.
23
 * If you modify file(s) with this exception, you may extend this
24
 * exception to your version of the file(s), but you are not
25
 * obligated to do so. If you do not wish to do so, delete this
26
 * exception statement from your version.
27
 *
28
 * As a special exception to the GPL, any HTML file which merely
29
 * makes function calls to this code, and for that purpose
30
 * includes it by reference shall be deemed a separate work for
31
 * copyright law purposes. If you modify this code, you may extend
32
 * this exception to your version of the code, but you are not
33
 * obligated to do so. If you do not wish to do so, delete this
34
 * exception statement from your version.
35
 *
36
 * You should have received a copy of the GNU General Public License
37
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
38
 *
39
 * I, Wojtek Kosior, thereby promise not to sue for violation of this file's
40
 * license. Although I request that you do not make use of this code in a
41
 * proprietary program, I am not going to enforce this in court.
42
 */
43

  
44
#IMPORT content/repo_query_cacher.js
45

  
46
#FROM common/browser.js           IMPORT browser
47
#FROM common/misc.js              IMPORT is_privileged_url
48
#FROM common/policy.js            IMPORT decide_policy
49
#FROM content/policy_enforcing.js IMPORT enforce_blocking
50

  
51
let already_run = false, page_info;
52

  
53
function on_page_info_request([type], sender, respond_cb) {
54
    if (type !== "page_info")
55
	return;
56

  
57
    respond_cb(page_info);
58
}
59

  
60
globalThis.haketilo_content_script_main = function() {
61
    if (already_run)
62
        return;
63

  
64
    already_run = true;
65

  
66
    if (is_privileged_url(document.URL))
67
	return;
68

  
69
    browser.runtime.onMessage.addListener(on_page_info_request);
70
    repo_query_cacher.start();
71

  
72
    const policy = decide_policy(globalThis.haketilo_pattern_tree,
73
				 document.URL,
74
				 globalThis.haketilo_defualt_allow,
75
				 globalThis.haketilo_secret);
76
    page_info = Object.assign({url: document.URL}, policy);
77
    ["csp", "nonce"].forEach(prop => delete page_info[prop]);
78

  
79
    enforce_blocking(policy);
80
}
81

  
82
function main() {
83
    if (globalThis.haketilo_pattern_tree !== undefined)
84
	globalThis.haketilo_content_script_main();
85
}
86

  
87
#IF !UNIT_TEST
88
main();
89
#ENDIF
html/default_blocking_policy.js
83 83
    toggle_policy_but.addEventListener("click", toggle_policy);
84 84
}
85 85

  
86
#IF !TEST_UNIT
86
#IF !UNIT_TEST
87 87
init_default_policy_dialog();
88 88
#ENDIF
html/popup.js
88 88
	    var mapping = `None (scripts ${scripts_fate} by a rule)`;
89 89
	else if (page_info.mapping)
90 90
	    var mapping = page_info.mapping;
91
	else
92
	    var mapping = `None (scripts ${scripts_fate} by default policy)`;
91
	else if (page_info.error)
92
	    var mapping = `None (error occured when determining policy)`;
93 93
	by_id("mapping_used").innerText = mapping;
94 94
    }
95 95
}
test/unit/test_content.py
1
# SPDX-License-Identifier: CC0-1.0
2

  
3
"""
4
Haketilo unit tests - main content script
5
"""
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
import pytest
21
import json
22
from selenium.webdriver.support.ui import WebDriverWait
23

  
24
from ..script_loader import load_script
25

  
26
# From:
27
# https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/contentScripts/register
28
# it is unclear whether the dynamically-registered content script is guaranteed
29
# to be always executed after statically-registered ones. We want to test both
30
# cases, so we'll make the mocked dynamic content script execute before
31
# content.js on http:// pages and after it on https:// pages.
32
dynamic_script = \
33
    ''';
34
    this.haketilo_secret        = "abracadabra";
35
    this.haketilo_pattern_tree  = {};
36
    this.haketilo_defualt_allow = false;
37

  
38
    if (this.haketilo_content_script_main)
39
        this.haketilo_content_script_main();
40
    '''
41

  
42
content_script = \
43
    '''
44
    /* Mock dynamic content script - case 'before'. */
45
    if (/#dynamic_before$/.test(document.URL)) {
46
        %s;
47
    }
48

  
49
    /* Place amalgamated content.js here. */
50
    %s;
51

  
52
    /* Rest of mocks */
53
    const data_to_verify = {};
54
    function data_set(prop, val) {
55
        data_to_verify[prop] = val;
56
        window.wrappedJSObject.data_to_verify = JSON.stringify(data_to_verify);
57
    }
58

  
59
    repo_query_cacher.start = () => data_set("cacher_started", true);
60

  
61
    enforce_blocking = policy => data_set("enforcing", policy);
62

  
63
    browser.runtime.onMessage.addListener = async function (listener_cb) {
64
        await new Promise(cb => setTimeout(cb, 0));
65

  
66
        /* Mock a good request. */
67
        const set_good = val => data_set("good_request_result", val);
68
        listener_cb(["page_info"], {}, val => set_good(val));
69

  
70
        /* Mock a bad request. */
71
        const set_bad = val => data_set("bad_request_result", val);
72
        listener_cb(["???"], {}, val => set_bad(val));
73
    }
74

  
75
    /* main() call - normally present in content.js, inside '#IF !UNIT_TEST'. */
76
    main();
77

  
78
    /* Mock dynamic content script - case 'after'. */
79
    if (/#dynamic_after$/.test(document.URL)) {
80
        %s;
81
    }
82

  
83
    data_set("script_run_without_errors", true);
84
    ''' % (dynamic_script, load_script('content/content.js'), dynamic_script)
85

  
86
@pytest.mark.ext_data({'content_script': content_script})
87
@pytest.mark.usefixtures('webextension')
88
@pytest.mark.parametrize('target', ['dynamic_before', 'dynamic_after'])
89
def test_content_unprivileged_page(driver, execute_in_page, target):
90
    """
91
    Test functioning of content.js on an page using unprivileged schema (e.g.
92
    'https://' and not 'about:').
93
    """
94
    driver.get(f'https://gotmyowndoma.in/index.html#{target}')
95
    data = json.loads(driver.execute_script('return window.data_to_verify;'))
96

  
97
    assert 'gotmyowndoma.in' in data['good_request_result']['url']
98
    assert 'bad_request_result' not in data
99

  
100
    assert data['cacher_started'] == True
101

  
102
    assert data['enforcing']['allow']  == False
103
    assert 'mapping' not in data['enforcing']
104
    assert 'error'   not in data['enforcing']
105

  
106
    assert data['script_run_without_errors'] == True
107

  
108
@pytest.mark.ext_data({'content_script': content_script})
109
@pytest.mark.usefixtures('webextension')
110
@pytest.mark.parametrize('target', ['dynamic_before', 'dynamic_after'])
111
def test_content_privileged_page(driver, execute_in_page, target):
112
    """
113
    Test functioning of content.js on an page considered privileged (e.g. a
114
    directory listing at 'file:///').
115
    """
116
    driver.get(f'file:///#{target}')
117
    data = json.loads(driver.execute_script('return window.data_to_verify;'))
118

  
119
    assert data == {'script_run_without_errors': True}
test/unit/test_patterns_query_manager.py
18 18
# CC0 1.0 Universal License for more details.
19 19

  
20 20
import pytest
21
import re
22 21
import json
23 22
from selenium.webdriver.support.ui import WebDriverWait
24 23

  
......
35 34
        'payloads': payloads
36 35
    }
37 36

  
38
content_script_tree_re = re.compile(r'this.haketilo_pattern_tree = (.*);')
39
def extract_tree_data(content_script_text):
40
    return json.loads(content_script_tree_re.search(content_script_text)[1])
41

  
42
content_script_mapping_re = re.compile(r'this.haketilo_mappings = (.*);')
43
def extract_mappings_data(content_script_text):
44
    return json.loads(content_script_mapping_re.search(content_script_text)[1])
37
def get_content_script_values(driver, content_script):
38
    """
39
    Allow easy extraction of 'this.something = ...' values from generated
40
    content script and verify the content script is syntactically correct.
41
    """
42
    return driver.execute_script(
43
        '''
44
        function value_holder() {
45
            %s;
46
            return this;
47
        }
48
        return value_holder.call({});
49
        ''' % content_script)
45 50

  
46 51
# Fields that are not relevant for testing are omitted from these mapping
47 52
# definitions.
......
87 92
    execute_in_page(
88 93
        '''
89 94
        const [initial_mappings, initial_blocking] = arguments.slice(0, 2);
90
        let mappingchange, blockingchange;
95
        let mappingchange, blockingchange, settingchange;
91 96

  
92 97
        haketilodb.track.mapping = function (cb) {
93 98
            mappingchange = cb;
......
99 104

  
100 105
            return [{}, initial_blocking];
101 106
        }
107
        haketilodb.track.settings = function (cb) {
108
            settingchange = cb;
109

  
110
            return [{}, [{name: "default_allow", value: true}]];
111
        }
102 112

  
103 113
        let last_script;
104 114
        let unregister_called = 0;
......
110 120
        }
111 121
        browser = {contentScripts: {register: register_mock}};
112 122

  
113
        returnval(start());
123
        returnval(start("abracadabra"));
114 124
        ''',
115 125
        sample_mappings[0:2], sample_blocking[0:2])
116 126

  
......
125 135
        dict([('~allow', 1),
126 136
              *[(f'inject-{fruit}', {'identifier': f'{fruit}-{best_pattern}'})
127 137
                for fruit in ('banana', 'orange')]])
128
    assert tree == extract_tree_data(content_script)
138
    cs_values = get_content_script_values(driver, content_script)
139
    assert cs_values['haketilo_secret']        == 'abracadabra'
140
    assert cs_values['haketilo_pattern_tree']  == tree
141
    assert cs_values['haketilo_default_allow'] == True
129 142
    assert deregistrations == 0
130 143

  
131 144
    def condition_all_added(driver):
132 145
        last_script = execute_in_page('returnval(last_script);')
146
        cs_values = get_content_script_values(driver, last_script)
133 147
        nums = [i for i in range(len(sample_blocking)) if i > 1]
134
        return (all([('gotmyown%sdoma' % i) in last_script for i in nums]) and
148
        return (cs_values['haketilo_default_allow'] == False and
149
                all([('gotmyown%sdoma' % i) in last_script for i in nums]) and
135 150
                all([m['identifier'] in last_script for m in sample_mappings]))
136 151

  
137 152
    execute_in_page(
138 153
        '''
154
        const new_setting_val = {name: "default_allow", value: false};
155
        settingchange({key: "default_allow", new_val: new_setting_val});
139 156
        for (const mapping of arguments[0])
140 157
            mappingchange({key: mapping.identifier, new_val: mapping});
141 158
        for (const blocking of arguments[1])
......
163 180

  
164 181
    def condition_all_removed(driver):
165 182
        content_script = execute_in_page('returnval(last_script);')
166
        return extract_tree_data(content_script) == {}
183
        cs_values = get_content_script_values(driver, content_script)
184
        return cs_values['haketilo_pattern_tree'] == {}
167 185

  
168 186
    execute_in_page(
169 187
        '''
test/unit/test_popup.py
62 62
        **unprivileged_page_info,
63 63
        'mapping': 'm1',
64 64
        'payload': {'identifier': 'res1'}
65
    },
66
    'error': {
67
        **unprivileged_page_info,
68
        'error': True
65 69
    }
66 70
}
67 71

  
......
143 147
    assert by_id['page_url'].text == mocked_page_infos[page_info_key]['url']
144 148
    assert not by_id['repo_query_container'].is_displayed()
145 149

  
146
    if 'blocked' in page_info_key or page_info_key == 'mapping':
150
    if 'blocked' in page_info_key or page_info_key in ('mapping', 'error'):
147 151
        assert by_id['scripts_blocked'].text.lower() == 'yes'
148 152
    elif 'allowed' in page_info_key:
149 153
        assert by_id['scripts_blocked'].text.lower() == 'no'
......
167 171
    elif 'default' in page_info_key:
168 172
        'by default_policy)' in mapping_text
169 173

  
174
    if page_info_key == 'error':
175
        assert mapping_text == 'None (error occured when determining policy)'
176

  
170 177
@pytest.mark.ext_data(popup_ext_data)
171 178
@pytest.mark.usefixtures('webextension')
172 179
def test_popup_repo_query(driver, execute_in_page):
test/unit/test_webrequest.py
30 30
            ''';
31 31
            // Mock pattern tree.
32 32
            tree = pqt.make();
33
            // Mock default allow.
34
            default_allow = {name: "default_allow", value: true};
33 35

  
34 36
            // Rule to block scripts.
35 37
            pqt.register(tree, "https://site.with.scripts.block.ed/***",
......
40 42
            pqt.register(tree, "https://site.with.paylo.ad/***",
41 43
                         "somemapping", {identifier: "someresource"});
42 44

  
43
            // Mock IndexedDB.
44
            haketilodb.track.settings =
45
                () => [{}, [{name: "default_allow", value: true}]];
46

  
47 45
            // Mock stream_filter.
48 46
            stream_filter.apply = (details, headers, policy) => headers;
49 47

  

Also available in: Unified diff