aboutsummaryrefslogtreecommitdiff
path: root/content/main.js
blob: 6c97350d01e4081bc4eecf3f457bd2e623eac065 (plain)
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
/**
 * Hachette main content script run in all frames
 *
 * Copyright (C) 2021 Wojtek Kosior
 * Copyright (C) 2021 jahoti
 * Redistribution terms are gathered in the `copyright' file.
 */

/*
 * IMPORTS_START
 * IMPORT handle_page_actions
 * IMPORT extract_signed
 * IMPORT sign_data
 * IMPORT gen_nonce
 * IMPORT is_privileged_url
 * IMPORT mozilla_suppress_scripts
 * IMPORT is_chrome
 * IMPORT is_mozilla
 * IMPORT start_activity_info_server
 * IMPORT modify_on_the_fly
 * IMPORTS_END
 */

function accept_node(node, parent)
{
    const clone = document.importNode(node, false);
    node.hachette_corresponding = clone;
    /*
     * TODO: Stop page's own issues like "Error parsing a meta element's
     * content:" from appearing as extension's errors.
     */
    parent.hachette_corresponding.appendChild(clone);
}

function extract_cookie_policy(cookie, min_time)
{
    let best_result = {time: -1};
    let policy = null;
    const extracted_signatures = [];

    for (const match of cookie.matchAll(/hachette-(\w*)=([^;]*)/g)) {
	const new_result = extract_signed(...match.slice(1, 3));
	if (new_result.fail)
	    continue;

	extracted_signatures.push(match[1]);

	if (new_result.time < Math.max(min_time, best_result.time))
	    continue;

	/* This should succeed - it's our self-produced valid JSON. */
	const new_policy = JSON.parse(decodeURIComponent(new_result.data));
	if (new_policy.url !== document.URL)
	    continue;

	best_result = new_result;
	policy = new_policy;
    }

    return [policy, extracted_signatures];
}

function extract_url_policy(url, min_time)
{
    const [base_url, payload, anchor] =
	  /^([^#]*)#?([^#]*)(#?.*)$/.exec(url).splice(1, 4);

    const match = /^hachette_([^_]+)_(.*)$/.exec(payload);
    if (!match)
	return [null, url];

    const result = extract_signed(...match.slice(1, 3));
    if (result.fail)
	return [null, url];

    const original_url = base_url + anchor;
    const policy = result.time < min_time ? null :
	  JSON.parse(decodeURIComponent(result.data));

    return [policy.url === original_url ? policy : null, original_url];
}

function employ_nonhttp_policy(policy)
{
    if (!policy.allow)
	return;

    policy.nonce = gen_nonce();
    const [base_url, target] = /^([^#]*)(#?.*)$/.exec(policy.url).slice(1, 3);
    const encoded_policy = encodeURIComponent(JSON.stringify(policy));
    const payload = "hachette_" +
	  sign_data(encoded_policy, new Date().getTime()).join("_");
    const resulting_url = `${base_url}#${payload}${target}`;
    location.href = resulting_url;
    location.reload();
}

if (!is_privileged_url(document.URL)) {
    let policy_received_callback = () => undefined;
    let policy;

    /* Signature valid for half an hour. */
    const min_time = new Date().getTime() - 1800 * 1000;

    if (/^https?:/.test(document.URL)) {
	let signatures;
	[policy, signatures] = extract_cookie_policy(document.cookie, min_time);
	for (const signature of signatures)
	    document.cookie = `hachette-${signature}=; Max-Age=-1;`;
    } else {
	const scheme = /^([^:]*)/.exec(document.URL)[1];
	const known_scheme = ["file", "ftp"].includes(scheme);

	if (!known_scheme)
	    console.warn(`Unknown url scheme: \`${scheme}'!`);

	let original_url;
	[policy, original_url] = extract_url_policy(document.URL, min_time);
	history.replaceState(null, "", original_url);

	if (known_scheme && !policy)
	    policy_received_callback = employ_nonhttp_policy;
    }

    if (!policy) {
	console.warn("Using default policy!");
	policy = {allow: false, nonce: gen_nonce()};
    }

    handle_page_actions(policy.nonce, policy_received_callback);

    if (!policy.allow) {
	if (is_mozilla) {
	    const script = document.querySelector("script");
	    if (script)
		script.textContent = "throw 'blocked';\n" + script.textContent;
	}
	const old_html = document.documentElement;
	const new_html = document.createElement("html");
	old_html.replaceWith(new_html);
	old_html.hachette_corresponding = new_html;

	const modify_end =
	      modify_on_the_fly(old_html, policy, {node_eater: accept_node});
	document.addEventListener("DOMContentLoaded", modify_end);
    }

    start_activity_info_server();
}