Project

General

Profile

« Previous | Next » 

Revision 9aa2d862

Added by koszko over 1 year ago

  • ID 9aa2d862334adbf66c64638697513280c4388584
  • Parent aa34ed46

don't double-modify response headers retrieved from cache

View differences:

background/webrequest.js
3 3
 *
4 4
 * Function: Modify HTTP traffic usng webRequest API.
5 5
 *
6
 * Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org>
6
 * Copyright (C) 2021, 2022 Wojtek Kosior <koszko@koszko.org>
7 7
 *
8 8
 * This program is free software: you can redistribute it and/or modify
9 9
 * it under the terms of the GNU General Public License as published by
......
41 41
 * proprietary program, I am not going to enforce this in court.
42 42
 */
43 43

  
44
#IMPORT common/indexeddb.js        AS haketilodb
44
#IMPORT common/indexeddb.js AS haketilodb
45

  
45 46
#IF MOZILLA
46 47
#IMPORT background/stream_filter.js
47 48
#ENDIF
48 49

  
49 50
#FROM common/browser.js IMPORT browser
50
#FROM common/misc.js    IMPORT is_privileged_url, csp_header_regex
51
#FROM common/misc.js    IMPORT is_privileged_url, csp_header_regex, \
52
                               sha256_async AS sha256
51 53
#FROM common/policy.js  IMPORT decide_policy
52 54

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

  
55 57
let secret;
56 58

  
57
function on_headers_received(details)
58
{
59
#IF MOZILLA
60
/*
61
 * Under Mozilla-based browsers, responses are cached together with headers as
62
 * they appear *after* modifications by Haketilo. This means Haketilo's CSP
63
 * script-blocking headers might be present in responses loaded from cache. In
64
 * the meantime the user might have changes Haketilo settings to instead allow
65
 * the scripts on the page in question. This causes a problem and creates the
66
 * need to somehow restore the response headers to the state in which they
67
 * arrived from the server.
68
 * To cope with this, Haketilo will inject some additional headers with private
69
 * data. Those will include a hard-to-guess value derived from extension's
70
 * internal ID. It is assumed the internal ID has a longer lifetime than cached
71
 * responses.
72
 */
73

  
74
const settings_page_url = browser.runtime.getURL("html/settings.html");
75
const header_prefix_prom = sha256(settings_page_url)
76
      .then(hash => `X-Haketilo-${hash}`);
77

  
78
/*
79
 * Mozilla, unlike Chrome, allows webRequest callbacks to return promises. Here
80
 * we leverage that to be able to use asynchronous sha256 computation.
81
 */
82
async function on_headers_received(details) {
83
#IF NEVER
84
} /* Help auto-indent in editors. */
85
#ENDIF
86
#ELSE
87
function on_headers_received(details) {
88
#ENDIF
59 89
    const url = details.url;
60 90
    if (is_privileged_url(details.url))
61 91
	return;
62 92

  
63 93
    let headers = details.responseHeaders;
64 94

  
95
#IF MOZILLA
96
    const prefix = await header_prefix_prom;
97

  
98
    /*
99
     * We assume that the original CSP headers of a response are always
100
     * preserved under names of the form:
101
     *     X-Haketilo-<some_secret>-<original_name>
102
     * In some cases the original response may contain no CSP headers. To still
103
     * be able to tell whether the headers we were provided were modified by
104
     * Haketilo in the past, all modifications are accompanied by addition of an
105
     * extra header with name:
106
     *     X-Haketilo-<some_secret>
107
     */
108

  
109
    const restore_old_headers = details.fromCache &&
110
	  !!headers.filter(h => h.name === prefix).length;
111

  
112
    if (restore_old_headers) {
113
	const restored_headers = [];
114

  
115
	for (const h of headers) {
116
	    if (csp_header_regex.test(h.name) || h.name === prefix)
117
		continue;
118

  
119
	    if (h.name.startsWith(prefix)) {
120
		restored_headers.push({
121
		    name:  h.name.substring(prefix.length + 1),
122
		    value: h.value
123
		});
124
	    } else {
125
		restored_headers.push(h);
126
	    }
127
	}
128

  
129
	headers = restored_headers;
130
    }
131
#ENDIF
132

  
65 133
    const policy =
66 134
	  decide_policy(tree, details.url, !!default_allow.value, secret);
67
    if (policy.allow)
68
	return;
69 135

  
70
    if (policy.payload)
136
    if (!policy.allow) {
137
#IF MOZILLA
138
	const to_append = [{name: prefix, value: ":)"}];
139

  
140
	for (const h of headers.filter(h => csp_header_regex.test(h.name))) {
141
	    if (!policy.payload)
142
		to_append.push(Object.assign({}, h));
143

  
144
	    h.name = `${prefix}-${h.name}`;
145
	}
146

  
147
	headers.push(...to_append);
148
#ELSE
71 149
	headers = headers.filter(h => !csp_header_regex.test(h.name));
150
#ENDIF
72 151

  
73
    headers.push({name: "Content-Security-Policy", value: policy.csp});
152
	headers.push({name: "Content-Security-Policy", value: policy.csp});
153
    }
74 154

  
75 155
#IF MOZILLA
76
    let skip = false;
77
    for (const header of headers) {
78
	if (header.name.toLowerCase().trim() !== "content-disposition")
79
	    continue;
80

  
81
	if (/^\s*attachment\s*(;.*)$/i.test(header.value)) {
82
		skip = true;
83
	} else {
84
	    skip = false;
85
	    break;
156
    /*
157
     * When page is meant to be viewed in the browser, use streamFilter to
158
     * inject a dummy <script> at the very beginning of it. This <script>
159
     * will cause extension's content scripts to run before page's first <meta>
160
     * tag is rendered so that they can prevent CSP rules inside <meta> tags
161
     * from blocking the payload we want to inject.
162
     */
163

  
164
    let use_stream_filter = !!policy.payload;
165
    if (use_stream_filter) {
166
	for (const header of headers) {
167
	    if (header.name.toLowerCase().trim() !== "content-disposition")
168
		continue;
169

  
170
	    if (/^\s*attachment\s*(;.*)$/i.test(header.value)) {
171
		use_stream_filter = false;
172
	    } else {
173
		use_stream_filter = true;
174
		break;
175
	    }
86 176
	}
87 177
    }
88
    skip = skip || (details.statusCode >= 300 && details.statusCode < 400);
178
    use_stream_filter = use_stream_filter &&
179
	(details.statusCode < 300 || details.statusCode >= 400);
89 180

  
90
    if (!skip)
181
    if (use_stream_filter)
91 182
	headers = stream_filter.apply(details, headers, policy);
92 183
#ENDIF
93 184

  

Also available in: Unified diff