From d42dadca04bd2c40cfe836b3f2bbe0ffbf176eda Mon Sep 17 00:00:00 2001 From: Wojtek Kosior Date: Fri, 23 Jul 2021 17:32:31 +0200 Subject: extract observables implementation from storage.js --- background/storage.js | 24 ++++++++++-------------- common/observable.js | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 14 deletions(-) create mode 100644 common/observable.js diff --git a/background/storage.js b/background/storage.js index 682f933..c2160b0 100644 --- a/background/storage.js +++ b/background/storage.js @@ -16,6 +16,7 @@ * IMPORT make_once * IMPORT browser * IMPORT is_chrome + * IMPORT observables * IMPORTS_END */ @@ -98,7 +99,8 @@ async function list(prefix) for (let item of await get_list_var(name)) map.set(item, await get(prefix + item)); - return {map, prefix, name, listeners : new Set(), lock : make_lock()}; + return {map, prefix, name, observable: observables.make(), + lock: make_lock()}; } var list_by_prefix = {}; @@ -121,7 +123,7 @@ exports.add_change_listener = function (cb, prefixes=list_prefixes) prefixes = [prefixes]; for (let prefix of prefixes) - list_by_prefix[prefix].listeners.add(cb); + observables.subscribe(list_by_prefix[prefix].observable, cb); } exports.remove_change_listener = function (cb, prefixes=list_prefixes) @@ -130,13 +132,7 @@ exports.remove_change_listener = function (cb, prefixes=list_prefixes) prefixes = [prefixes]; for (let prefix of prefixes) - list_by_prefix[prefix].listeners.delete(cb); -} - -function broadcast_change(change, list) -{ - for (let listener_callback of list.listeners) - listener_callback(change); + observables.unsubscribe(list_by_prefix[prefix].observable, cb); } /* Prepare some hepler functions to get elements of a list */ @@ -198,7 +194,7 @@ async function _set_item(item, value, list) new_val : value }; - broadcast_change(change, list); + observables.broadcast(list.observable, change); return old_val; } @@ -236,7 +232,7 @@ async function _remove_item(item, list) new_val : undefined }; - broadcast_change(change, list); + observables.broadcast(list.observable, change); return old_val; } @@ -282,7 +278,7 @@ async function _replace_item(old_item, new_item, list, new_val=undefined) new_val : undefined }; - broadcast_change(change, list); + observables.broadcast(list.observable, change); list.map.set(new_item, new_val); @@ -290,7 +286,7 @@ async function _replace_item(old_item, new_item, list, new_val=undefined) change.old_val = undefined; change.new_val = new_val; - broadcast_change(change, list); + observables.broadcast(list.observable, change); return old_val; } @@ -375,7 +371,7 @@ exports.clear = async function () for (let [item, val] of list_entries_it(list)) { change.item = item; change.old_val = val; - broadcast_change(change, list); + observables.broadcast(list.observable, change); } list.map = new Map(); diff --git a/common/observable.js b/common/observable.js new file mode 100644 index 0000000..1fb0b0a --- /dev/null +++ b/common/observable.js @@ -0,0 +1,36 @@ +/** + * part of Hachette + * Facilitate listening to events + * + * Copyright (C) 2021 Wojtek Kosior + * Redistribution terms are gathered in the `copyright' file. + */ + +function make() +{ + return new Set(); +} + +function subscribe(observable, cb) +{ + observable.add(cb); +} + +function unsubscribe(observable, cb) +{ + observable.delete(cb); +} + +function broadcast(observable, event) +{ + for (const callback of observable) + callback(event); +} + +const observables = {make, subscribe, unsubscribe, broadcast}; + +/* + * EXPORTS_START + * EXPORT observables + * EXPORTS_END + */ -- cgit v1.2.3 From 57e4ed2b06d15747b20737bad14bcdd2d73fd8a6 Mon Sep 17 00:00:00 2001 From: jahoti Date: Wed, 21 Jul 2021 00:00:00 +0000 Subject: Remove unnecessary imports of url_item and add a CSP header-parsing function The parsing function isn't used yet; however, it will eventually be as a less destructive alternative to handling headers as indivisible units. --- background/policy_injector.js | 1 - common/misc.js | 20 ++++++++++++++++++++ content/main.js | 1 - 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/background/policy_injector.js b/background/policy_injector.js index 01da094..9a994f8 100644 --- a/background/policy_injector.js +++ b/background/policy_injector.js @@ -16,7 +16,6 @@ * IMPORT gen_unique * IMPORT gen_nonce * IMPORT is_privileged_url - * IMPORT url_item * IMPORT url_extract_target * IMPORT sign_policy * IMPORT get_query_best diff --git a/common/misc.js b/common/misc.js index a59ec14..6af2327 100644 --- a/common/misc.js +++ b/common/misc.js @@ -154,6 +154,25 @@ function sign_policy(policy, now, hours_offset) { return gen_unique(time + policy); } +/* Parse a CSP header */ +function parse_csp(csp) { + let directive, directive_array; + let directives = {}; + for (directive of csp.split(';')) { + directive = directive.trim; + if (directive === '') + continue; + + directive_array = directive.split(/\s+/); + directive = directive_array.shift(); + /* The "true" case should never occur; nevertheless... */ + directives[directive] = directive in directives ? + directives[directive].concat(directive_array) : + directive_array; + } + return directives; +} + /* * EXPORTS_START * EXPORT gen_nonce @@ -165,5 +184,6 @@ function sign_policy(policy, now, hours_offset) { * EXPORT nice_name * EXPORT open_in_settings * EXPORT is_privileged_url + * EXPORT parse_csp * EXPORTS_END */ diff --git a/content/main.js b/content/main.js index 8f8375e..9ed557c 100644 --- a/content/main.js +++ b/content/main.js @@ -9,7 +9,6 @@ /* * IMPORTS_START * IMPORT handle_page_actions - * IMPORT url_item * IMPORT url_extract_target * IMPORT gen_unique * IMPORT gen_nonce -- cgit v1.2.3 From fba4820bec6714115ef03bd4bdfd714ba485ac2c Mon Sep 17 00:00:00 2001 From: jahoti Date: Wed, 21 Jul 2021 00:00:00 +0000 Subject: [UNTESTED- will test] Use more nuanced CSP filtering CSP headers are now parsed and processed, rather than treated as simple units. This allows us to ensure policies delivered as HTTP headers do not interfere with our script filtering, as well as to preserve useful protections while removing the ones that could be problematic. Additionally, prefetching should now be blocked on pages where native scripts aren't allowed, and all reporting of CSP violations has been stripped (is this appropriate?). --- background/policy_injector.js | 72 +++++++++++++++++++++++++++++++------------ 1 file changed, 53 insertions(+), 19 deletions(-) diff --git a/background/policy_injector.js b/background/policy_injector.js index 9a994f8..a67b4e3 100644 --- a/background/policy_injector.js +++ b/background/policy_injector.js @@ -12,14 +12,13 @@ * IMPORT get_storage * IMPORT browser * IMPORT is_chrome - * IMPORT is_mozilla * IMPORT gen_unique * IMPORT gen_nonce * IMPORT is_privileged_url * IMPORT url_extract_target * IMPORT sign_policy * IMPORT get_query_best - * IMPORT csp_rule + * IMPORT parse_csp * IMPORTS_END */ @@ -32,11 +31,19 @@ const csp_header_names = { "x-content-security-policy" : true }; +const unwanted_csp_directives = { + "report-to" : true, + "report-uri" : true, + "script-src" : true, + "script-src-elem" : true, + "prefetch-src": true +}; + const header_name = "content-security-policy"; -function is_csp_header(header) +function not_csp_header(header) { - return !!csp_header_names[header.name.toLowerCase()]; + return !csp_header_names[header.name.toLowerCase()]; } function url_inject(details) @@ -82,21 +89,48 @@ function headers_inject(details) if (!targets.current) return {cancel: true}; - const rule = csp_rule(targets.policy.nonce); - var headers = details.responseHeaders; - - /* - * Chrome doesn't have the buggy behavior of caching headers - * we injected. Firefox does and we have to remove it there. - */ - if (!targets.policy.allow || is_mozilla) - headers = headers.filter(h => !is_csp_header(h)); - - if (!targets.policy.allow) { - headers.push({ - name : header_name, - value : rule - }); + const headers = []; + for (let header of details.responseHeaders) { + if (not_csp_header(header)) { + /* Retain all non-snitching headers */ + if (header.name.toLowerCase() !== + 'content-security-policy-report-only') + headers.push(header); + + continue; + } + + const csp = parse_csp(header.value); + const rule = `'nonce-${targets.policy.nonce}'` + + /* TODO: confirm deleting non-existent things is OK everywhere */ + /* No snitching or prefetching/rendering */ + delete csp['report-to']; + delete csp['report-uri']; + + if (!target.policy.allow) { + delete csp['script-src']; + delete csp['script-src-elem']; + csp['script-src-attr'] = ["'none'"]; + csp['prefetch-src'] = ["'none'"]; + } + + if ('script-src' in csp) + csp['script-src'].push(rule); + else + csp['script-src'] = rule; + + if ('script-src-elem' in csp) + csp['script-src-elem'].push(rule); + else + csp['script-src-elem'] = rule; + + /* TODO: is this safe */ + let new_policy = Object.entries(csp).map( + i => i[0] + ' ' + i[1].join(' ') + ';' + ); + + headers.push({name: header.name, value: new_policy.join('')}); } return {responseHeaders: headers}; -- cgit v1.2.3 From e402e0363cd55f7f849c20c1acd96de548ebc9a6 Mon Sep 17 00:00:00 2001 From: jahoti Date: Thu, 22 Jul 2021 00:00:00 +0000 Subject: Fix some bugs in the refined CSP handling --- background/policy_injector.js | 6 +++--- common/misc.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/background/policy_injector.js b/background/policy_injector.js index a67b4e3..90c65bd 100644 --- a/background/policy_injector.js +++ b/background/policy_injector.js @@ -108,7 +108,7 @@ function headers_inject(details) delete csp['report-to']; delete csp['report-uri']; - if (!target.policy.allow) { + if (!targets.policy.allow) { delete csp['script-src']; delete csp['script-src-elem']; csp['script-src-attr'] = ["'none'"]; @@ -118,12 +118,12 @@ function headers_inject(details) if ('script-src' in csp) csp['script-src'].push(rule); else - csp['script-src'] = rule; + csp['script-src'] = [rule]; if ('script-src-elem' in csp) csp['script-src-elem'].push(rule); else - csp['script-src-elem'] = rule; + csp['script-src-elem'] = [rule]; /* TODO: is this safe */ let new_policy = Object.entries(csp).map( diff --git a/common/misc.js b/common/misc.js index 6af2327..0d8466e 100644 --- a/common/misc.js +++ b/common/misc.js @@ -159,7 +159,7 @@ function parse_csp(csp) { let directive, directive_array; let directives = {}; for (directive of csp.split(';')) { - directive = directive.trim; + directive = directive.trim(); if (directive === '') continue; -- cgit v1.2.3 From 97b8e30fadf0f1e1e0aeb9078ac333026d735270 Mon Sep 17 00:00:00 2001 From: jahoti Date: Sun, 25 Jul 2021 00:00:00 +0000 Subject: Squash more CSP-filtering bugs On Firefox, original CSP headers are now smuggled (signed) in an x-orig-csp header to prevent re-processing issues with caching. Additionally, a default header is added for non-whitelisted domains in case there are no existing headers we can attach to. --- background/policy_injector.js | 133 +++++++++++++++++++++++++++++++----------- 1 file changed, 98 insertions(+), 35 deletions(-) diff --git a/background/policy_injector.js b/background/policy_injector.js index 90c65bd..f58fb71 100644 --- a/background/policy_injector.js +++ b/background/policy_injector.js @@ -12,6 +12,7 @@ * IMPORT get_storage * IMPORT browser * IMPORT is_chrome + * IMPORT is_mozilla * IMPORT gen_unique * IMPORT gen_nonce * IMPORT is_privileged_url @@ -39,7 +40,7 @@ const unwanted_csp_directives = { "prefetch-src": true }; -const header_name = "content-security-policy"; +const report_only = "content-security-policy-report-only"; function not_csp_header(header) { @@ -82,6 +83,38 @@ function url_inject(details) }; } +function process_csp_header(header, rule, block) +{ + const csp = parse_csp(header.value); + + /* No snitching */ + delete csp['report-to']; + delete csp['report-uri']; + + if (block) { + delete csp['script-src']; + delete csp['script-src-elem']; + csp['script-src-attr'] = ["'none'"]; + csp['prefetch-src'] = ["'none'"]; + } + + if ('script-src' in csp) + csp['script-src'].push(rule); + else + csp['script-src'] = [rule]; + + if ('script-src-elem' in csp) + csp['script-src-elem'].push(rule); + else + csp['script-src-elem'] = [rule]; + + const new_policy = Object.entries(csp).map( + i => i[0] + ' ' + i[1].join(' ') + ';' + ); + + return {name: header.name, value: new_policy.join('')} +} + function headers_inject(details) { const targets = url_extract_target(details.url); @@ -89,48 +122,78 @@ function headers_inject(details) if (!targets.current) return {cancel: true}; - const headers = []; + let orig_csp_headers = is_chrome ? null : []; + let headers = []; + let csp_headers = is_chrome ? headers : []; + + const rule = `'nonce-${targets.policy.nonce}'`; + const block = !targets.policy.allow; + for (let header of details.responseHeaders) { if (not_csp_header(header)) { /* Retain all non-snitching headers */ - if (header.name.toLowerCase() !== - 'content-security-policy-report-only') + if (header.name.toLowerCase() !== report_only) { headers.push(header); + + /* If these are the original CSP headers, use them instead */ + /* Test based on url_extract_target() in misc.js */ + if (is_mozilla && header.name === "x-orig-csp") { + let index = header.value.indexOf('%5B'); + if (index === -1) + continue; + + let sig = header.value.substring(0, index); + let data = header.value.substring(index); + if (sig !== sign_policy(data, 0)) + continue; + + /* Confirmed- it's the originals, smuggled in! */ + try { + data = JSON.parse(decodeURIComponent(data)); + } catch (e) { + /* This should not be reached - + it's our self-produced valid JSON. */ + console.log("Unexpected internal error - invalid JSON smuggled!", e); + } + + orig_csp_headers = csp_headers = null; + for (let header of data) + headers.push(process_csp_header(header, rule, block)); + } + } continue; } - - const csp = parse_csp(header.value); - const rule = `'nonce-${targets.policy.nonce}'` - - /* TODO: confirm deleting non-existent things is OK everywhere */ - /* No snitching or prefetching/rendering */ - delete csp['report-to']; - delete csp['report-uri']; - - if (!targets.policy.allow) { - delete csp['script-src']; - delete csp['script-src-elem']; - csp['script-src-attr'] = ["'none'"]; - csp['prefetch-src'] = ["'none'"]; - } - - if ('script-src' in csp) - csp['script-src'].push(rule); - else - csp['script-src'] = [rule]; - - if ('script-src-elem' in csp) - csp['script-src-elem'].push(rule); - else - csp['script-src-elem'] = [rule]; - - /* TODO: is this safe */ - let new_policy = Object.entries(csp).map( - i => i[0] + ' ' + i[1].join(' ') + ';' - ); + if (is_mozilla && !orig_csp_headers) + continue; - headers.push({name: header.name, value: new_policy.join('')}); + csp_headers.push(process_csp_header(header, rule, block)); + if (is_mozilla) + orig_csp_headers.push(header); + } + + if (orig_csp_headers) { + /** Smuggle in the original CSP headers for future use. + * These are signed with a time of 0, as it's not clear there + * is a limit on how long Firefox might retain these headers in + * the cache. + */ + orig_csp_headers = encodeURIComponent(JSON.stringify(orig_csp_headers)); + headers.push({ + name: "x-orig-csp", + value: sign_policy(orig_csp_headers, 0) + orig_csp_headers + }); + + headers = headers.concat(csp_headers); + } + + /* To ensure there is a CSP header if required */ + if (block) { + headers.push({ + name: "content-security-policy", + value: `script-src ${rule}; script-src-elem ${rule}; ` + + "script-src-attr 'none'; prefetch-src 'none';" + }); } return {responseHeaders: headers}; -- cgit v1.2.3 From 5fcc980828047e966c24fde8a80e7f819a457e36 Mon Sep 17 00:00:00 2001 From: Wojtek Kosior Date: Mon, 26 Jul 2021 12:10:13 +0200 Subject: code maintenance --- background/policy_injector.js | 58 ++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/background/policy_injector.js b/background/policy_injector.js index f58fb71..386cf22 100644 --- a/background/policy_injector.js +++ b/background/policy_injector.js @@ -26,27 +26,23 @@ var storage; var query_best; -const csp_header_names = { - "content-security-policy" : true, - "x-webkit-csp" : true, - "x-content-security-policy" : true -}; - -const unwanted_csp_directives = { - "report-to" : true, - "report-uri" : true, - "script-src" : true, - "script-src-elem" : true, - "prefetch-src": true -}; +const csp_header_names = new Set([ + "content-security-policy", + "x-webkit-csp", + "x-content-security-policy" +]); + +/* TODO: variable no longer in use; remove if not needed */ +const unwanted_csp_directives = new Set([ + "report-to", + "report-uri", + "script-src", + "script-src-elem", + "prefetch-src" +]); const report_only = "content-security-policy-report-only"; -function not_csp_header(header) -{ - return !csp_header_names[header.name.toLowerCase()]; -} - function url_inject(details) { if (is_privileged_url(details.url)) @@ -86,18 +82,18 @@ function url_inject(details) function process_csp_header(header, rule, block) { const csp = parse_csp(header.value); - + /* No snitching */ delete csp['report-to']; delete csp['report-uri']; - + if (block) { delete csp['script-src']; delete csp['script-src-elem']; csp['script-src-attr'] = ["'none'"]; csp['prefetch-src'] = ["'none'"]; } - + if ('script-src' in csp) csp['script-src'].push(rule); else @@ -107,12 +103,12 @@ function process_csp_header(header, rule, block) csp['script-src-elem'].push(rule); else csp['script-src-elem'] = [rule]; - + const new_policy = Object.entries(csp).map( - i => i[0] + ' ' + i[1].join(' ') + ';' + i => `${i[0]} ${i[1].join(' ')};` ); - - return {name: header.name, value: new_policy.join('')} + + return {name: header.name, value: new_policy.join('')}; } function headers_inject(details) @@ -128,13 +124,13 @@ function headers_inject(details) const rule = `'nonce-${targets.policy.nonce}'`; const block = !targets.policy.allow; - - for (let header of details.responseHeaders) { - if (not_csp_header(header)) { + + for (const header of details.responseHeaders) { + if (!csp_header_names.has(header)) { /* Retain all non-snitching headers */ if (header.name.toLowerCase() !== report_only) { headers.push(header); - + /* If these are the original CSP headers, use them instead */ /* Test based on url_extract_target() in misc.js */ if (is_mozilla && header.name === "x-orig-csp") { @@ -157,7 +153,7 @@ function headers_inject(details) } orig_csp_headers = csp_headers = null; - for (let header of data) + for (const header of data) headers.push(process_csp_header(header, rule, block)); } } @@ -166,7 +162,7 @@ function headers_inject(details) } if (is_mozilla && !orig_csp_headers) continue; - + csp_headers.push(process_csp_header(header, rule, block)); if (is_mozilla) orig_csp_headers.push(header); -- cgit v1.2.3 From 64afd5b9415d62c1f178ca78a8358bd3503d5855 Mon Sep 17 00:00:00 2001 From: Wojtek Kosior Date: Mon, 26 Jul 2021 13:37:05 +0200 Subject: provide a facility to sanitize externally-obtained JSON --- common/sanitize_JSON.js | 412 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 412 insertions(+) create mode 100644 common/sanitize_JSON.js diff --git a/common/sanitize_JSON.js b/common/sanitize_JSON.js new file mode 100644 index 0000000..3fc5007 --- /dev/null +++ b/common/sanitize_JSON.js @@ -0,0 +1,412 @@ +/** + * part of Hachette + * Powerful, full-blown format enforcer for externally-obtained JSON + * + * Copyright (C) 2021 Wojtek Kosior + * Redistribution terms are gathered in the `copyright' file. + */ + +var error_path; +var invalid_schema; + +function parse_json_with_schema(schema, json_string) +{ + error_path = []; + invalid_schema = false; + + try { + return sanitize_unknown(schema, JSON.parse(json_string)); + } catch (e) { + throw `Invalid JSON${invalid_schema ? " schema" : ""}: ${e}.`; + } finally { + /* Allow garbage collection. */ + error_path = undefined; + } +} + +function error_message(cause) +{ + return `object${error_path.join("")} ${cause}`; +} + +function sanitize_unknown(schema, item) +{ + console.log(`sanitize_unknown ${JSON.stringify(schema)}`); + let error_msg = undefined; + let schema_options = []; + let has_default = false; + let _default = undefined; + + if (!Array.isArray(schema) || schema[1] === "matchentry" || + schema.length < 2 || !["ordefault", "or"].includes(schema)) + return sanitize_unknown_no_alternatives(schema, item); + + if ((schema.length & 1) !== 1) { + invalid_schema = true; + throw error_message("was not understood"); + } + + for (let i = 0; i < schema.length; i++) { + if ((i & 1) !== 1) { + schema_options.push(schema[i]); + continue; + } + + if (schema[i] === "or") + continue; + if (schema[i] === "ordefault" && schema.length === i + 2) { + has_default = true; + _default = schema[i + 1]; + break; + } + + invalid_schema = true; + throw error_message("was not understood"); + } + + for (const schema_option of schema_options) { + try { + return sanitize_unknown_no_alternatives(schema_option, item); + } catch (e) { + if (invalid_schema) + throw e; + + if (has_default) + continue; + + if (error_msg === undefined) + error_msg = e; + else + error_msg = `${error_msg}, or ${e}`; + } + } + + if (has_default) + return _default; + + throw error_msg; +} + +function sanitize_unknown_no_alternatives(schema, item) +{ + console.log(`sanitize_unknown_no_alternatives ${JSON.stringify(schema)}`); + for (const [schema_check, item_check, sanitizer, type_name] of checks) { + console.log(`checking ${type_name}`); + if (schema_check(schema)) { + if (item_check(item)) + return sanitizer(schema, item); + throw error_message(`should be ${type_name} but is not`); + } + } + + invalid_schema = true; + throw error_message("was not understood"); +} + +function key_error_path_segment(key) +{ + return /^[a-zA-Z_][a-zA-Z_0-9]*$/.exec(key) ? + `.${key}` : `[${JSON.stringify(key)}]`; +} + +/* + * Generic object - one that can contain arbitrary keys (in addition to ones + * specified explicitly in the schema). + */ +function sanitize_genobj(schema, object) +{ + let max_matched_entries = Infinity; + let min_matched_entries = 0; + let matched_entries = 0; + const entry_schemas = []; + schema = [...schema]; + + if (schema[2] === "minentries") { + if (schema.length < 4) { + invalid_schema = true; + throw error_message("was not understood"); + } + + min_matched_entries = schema[3]; + schema.splice(2, 2); + } + + if (min_matched_entries < 0) { + invalid_schema = true; + throw error_message('specifies invalid "minentries" (should be a non-negative number)'); + } + + if (schema[2] === "maxentries") { + if (schema.length < 4) { + invalid_schema = true; + throw error_message("was not understood"); + } + + max_matched_entries = schema[3]; + schema.splice(2, 2); + } + + if (max_matched_entries < 0) { + invalid_schema = true; + throw error_message('specifies invalid "maxentries" (should be a non-negative number)'); + } + + while (schema.length > 2) { + let regex = /.+/; + + if (schema.length > 3) { + regex = schema[2]; + schema.splice(2, 1); + } + + if (typeof regex === "string") + regex = new RegExp(regex); + + entry_schemas.push([regex, schema[2]]); + schema.splice(2, 1); + } + + const result = sanitize_object(schema[0], object); + + for (const [key, entry] of Object.entries(object)) { + if (result.hasOwnProperty(key)) + continue; + + matched_entries += 1; + if (matched_entries > max_matched_entries) + throw error_message(`has more than ${max_matched_entries} matched entr${max_matched_entries === 1 ? "y" : "ies"}`); + + error_path.push(key_error_path_segment(key)); + + let match = false; + for (const [key_regex, entry_schema] of entry_schemas) { + if (!key_regex.exec(key)) + continue; + + match = true; + + sanitize_object_entry(result, key, entry_schema, object); + break; + } + + if (!match) { + const regex_list = entry_schemas.map(i => i[0]).join(", "); + throw error_message(`does not match any of key regexes: [${regex_list}]`); + } + + error_path.pop(); + } + + if (matched_entries < min_matched_entries) + throw error_message(`has less than ${min_matched_entries} matched entr${min_matched_entries === 1 ? "y" : "ies"}`); + + return result; +} + +function sanitize_array(schema, array) +{ + console.log(`sanitize_array ${JSON.stringify(schema)}`); + let min_length = 0; + let max_length = Infinity; + let repeat_length = 1; + let i = 0; + const result = []; + + schema = [...schema]; + if (schema[schema.length - 2] === "maxlen") { + max_length = schema[schema.length - 1]; + schema.splice(schema.length - 2); + } + + if (schema[schema.length - 2] === "minlen") { + min_length = schema[schema.length - 1]; + schema.splice(schema.length - 2); + } + + if (["repeat", "repeatfull"].includes(schema[schema.length - 2])) + repeat_length = schema.pop(); + if (repeat_length < 1) { + invalid_schema = true; + throw error_message('specifies invalid "${schema[schema.length - 2]}" (should be number greater than 1)'); + } + if (["repeat", "repeatfull"].includes(schema[schema.length - 1])) { + var repeat_directive = schema.pop(); + repeat = schema.splice(schema.length - repeat_length); + } else if (schema.length !== array.length) { + throw error_message(`does not not have exactly ${schema.length} items`); + } + + if (repeat_directive === "repeatfull" && + (array.length - schema.length) % repeat_length !== 0) + throw error_message(`does not not contain a full number of item group repetitions`); + + if (array.length < min_length) + throw error_message(`has less than ${min_length} element${min_length === 1 ? "" : "s"}`); + + if (array.length > max_length) + throw error_message(`has more than ${max_length} element${max_length === 1 ? "" : "s"}`); + + console.log(schema, repeat); + + for (const item of array) { + if (i >= schema.length) { + i = 0; + schema = repeat; + } + + error_path.push(`[${i}]`); + const sanitized = sanitize_unknown(schema[i], item); + if (sanitized !== discard) + result.push(sanitized); + error_path.pop(); + + i++; + } + + return result; +} + +function sanitize_regex(schema, string) +{ + console.log(`sanitize_regex ${schema}`); + if (schema.test(string)) + return string; + + throw error_message(`does not match regex ${schema}`); +} + +const string_spec_regex = /^string(:(.*))?$/; + +function sanitize_string(schema, string) +{ + console.log(`sanitize_string ${JSON.stringify(schema)}`); + const regex = string_spec_regex.exec(schema)[2]; + + if (regex === undefined) + return string; + + return sanitize_regex(new RegExp(regex), string); +} + +function sanitize_object(schema, object) +{ + console.log(`sanitize_object ${JSON.stringify(schema)}`); + const result = {}; + + for (let [key, entry_schema] of Object.entries(schema)) { + error_path.push(key_error_path_segment(key)); + sanitize_object_entry(result, key, entry_schema, object); + error_path.pop(); + } + + return result; +} + +function sanitize_object_entry(result, key, entry_schema, object) +{ + console.log(`sanitize_object_entry ${JSON.stringify(entry_schema)}`); + let optional = false; + let has_default = false; + let _default = undefined; + + if (Array.isArray(entry_schema) && entry_schema.length > 1) { + if (entry_schema[0] === "optional") { + optional = true; + entry_schema = [...entry_schema].splice(1); + + const idx_def = entry_schema.length - (entry_schema.length & 1) - 1; + if (entry_schema[idx_def] === "default") { + has_default = true; + _default = entry_schema[idx_def + 1]; + entry_schema.splice(idx_def); + } else if ((entry_schema.length & 1) !== 1) { + invalid_schema = true; + throw error_message("was not understood"); + } + + if (entry_schema.length < 2) + entry_schema = entry_schema[0]; + } + } + + let unsanitized_value = object[key]; + if (unsanitized_value === undefined) { + if (!optional) + throw error_message("is missing"); + + if (has_default) + result[key] = _default; + + return; + } + + const sanitized = sanitize_unknown(entry_schema, unsanitized_value); + if (sanitized !== discard) + result[key] = sanitized; +} + +function take_literal(schema, item) +{ + console.log(`take_literal ${JSON.stringify(schema)}`); + return item; +} + +/* + * This function is used like a symbol. Other parts of code do sth like + * `item === discard` to check if item was returned by this function. + */ +function discard(schema, item) +{ + console.log(`discard ${JSON.stringify(schema)}`); + return discard; +} + +/* + * The following are some helper functions to categorize various + * schema item specifiers (used in the array below). + */ + +function is_genobj_spec(item) +{ + return Array.isArray(item) && item[1] === "matchentry"; +} + +function is_regex(item) +{ + return typeof item === "object" && typeof item.test === "function"; +} + +function is_string_spec(item) +{ + return typeof item === "string" && string_spec_regex.test(item); +} + +function is_object(item) +{ + return typeof item === "object"; +} + +function eq(what) +{ + return i => i === what; +} + +/* Array and null checks must go before object check. */ +const checks = [ + [is_genobj_spec, is_object, sanitize_genobj, "an object"], + [Array.isArray, Array.isArray, sanitize_array, "an array"], + [eq(null), i => i === null, take_literal, "null"], + [is_regex, i => typeof i === "string", sanitize_regex, "a string"], + [is_string_spec, i => typeof i === "string", sanitize_string, "a string"], + [is_object, is_object, sanitize_object, "an object"], + [eq("number"), i => typeof i === "number", take_literal, "a number"], + [eq("boolean"), i => typeof i === "boolean", take_literal, "a boolean"], + [eq("anything"), i => true, take_literal, "dummy"], + [eq("discard"), i => true, discard, "dummy"] +]; + +/* + * EXPORTS_START + * EXPORT parse_json_with_schema + * EXPORTS_END + */ -- cgit v1.2.3 From 2fa41a54acfa5e25b5ccad5b3c91210cc42ce00d Mon Sep 17 00:00:00 2001 From: Wojtek Kosior Date: Tue, 27 Jul 2021 11:41:43 +0200 Subject: validate settings on import --- common/sanitize_JSON.js | 18 +++--------------- html/options_main.js | 44 ++++++++++++++++++++++++++++++-------------- 2 files changed, 33 insertions(+), 29 deletions(-) diff --git a/common/sanitize_JSON.js b/common/sanitize_JSON.js index 3fc5007..8268d3e 100644 --- a/common/sanitize_JSON.js +++ b/common/sanitize_JSON.js @@ -31,7 +31,6 @@ function error_message(cause) function sanitize_unknown(schema, item) { - console.log(`sanitize_unknown ${JSON.stringify(schema)}`); let error_msg = undefined; let schema_options = []; let has_default = false; @@ -89,9 +88,7 @@ function sanitize_unknown(schema, item) function sanitize_unknown_no_alternatives(schema, item) { - console.log(`sanitize_unknown_no_alternatives ${JSON.stringify(schema)}`); for (const [schema_check, item_check, sanitizer, type_name] of checks) { - console.log(`checking ${type_name}`); if (schema_check(schema)) { if (item_check(item)) return sanitizer(schema, item); @@ -205,7 +202,6 @@ function sanitize_genobj(schema, object) function sanitize_array(schema, array) { - console.log(`sanitize_array ${JSON.stringify(schema)}`); let min_length = 0; let max_length = Infinity; let repeat_length = 1; @@ -231,14 +227,14 @@ function sanitize_array(schema, array) } if (["repeat", "repeatfull"].includes(schema[schema.length - 1])) { var repeat_directive = schema.pop(); - repeat = schema.splice(schema.length - repeat_length); + var repeat = schema.splice(schema.length - repeat_length); } else if (schema.length !== array.length) { - throw error_message(`does not not have exactly ${schema.length} items`); + throw error_message(`does not have exactly ${schema.length} items`); } if (repeat_directive === "repeatfull" && (array.length - schema.length) % repeat_length !== 0) - throw error_message(`does not not contain a full number of item group repetitions`); + throw error_message(`does not contain a full number of item group repetitions`); if (array.length < min_length) throw error_message(`has less than ${min_length} element${min_length === 1 ? "" : "s"}`); @@ -246,8 +242,6 @@ function sanitize_array(schema, array) if (array.length > max_length) throw error_message(`has more than ${max_length} element${max_length === 1 ? "" : "s"}`); - console.log(schema, repeat); - for (const item of array) { if (i >= schema.length) { i = 0; @@ -268,7 +262,6 @@ function sanitize_array(schema, array) function sanitize_regex(schema, string) { - console.log(`sanitize_regex ${schema}`); if (schema.test(string)) return string; @@ -279,7 +272,6 @@ const string_spec_regex = /^string(:(.*))?$/; function sanitize_string(schema, string) { - console.log(`sanitize_string ${JSON.stringify(schema)}`); const regex = string_spec_regex.exec(schema)[2]; if (regex === undefined) @@ -290,7 +282,6 @@ function sanitize_string(schema, string) function sanitize_object(schema, object) { - console.log(`sanitize_object ${JSON.stringify(schema)}`); const result = {}; for (let [key, entry_schema] of Object.entries(schema)) { @@ -304,7 +295,6 @@ function sanitize_object(schema, object) function sanitize_object_entry(result, key, entry_schema, object) { - console.log(`sanitize_object_entry ${JSON.stringify(entry_schema)}`); let optional = false; let has_default = false; let _default = undefined; @@ -347,7 +337,6 @@ function sanitize_object_entry(result, key, entry_schema, object) function take_literal(schema, item) { - console.log(`take_literal ${JSON.stringify(schema)}`); return item; } @@ -357,7 +346,6 @@ function take_literal(schema, item) */ function discard(schema, item) { - console.log(`discard ${JSON.stringify(schema)}`); return discard; } diff --git a/html/options_main.js b/html/options_main.js index 026b9ba..6aed8bb 100644 --- a/html/options_main.js +++ b/html/options_main.js @@ -12,6 +12,7 @@ * IMPORT TYPE_NAME * IMPORT list_prefixes * IMPORT nice_name + * IMPORT parse_json_with_schema * IMPORTS_END */ @@ -223,6 +224,7 @@ function reset_work_bag_li(ul, item, components) ul.work_li.insertBefore(bag_components_ul, old_components_ul); ul.work_li.removeChild(old_components_ul); + console.log("bag components", components); add_bag_components(components); } @@ -543,6 +545,32 @@ function read_file(file) _read_file(file, resolve, reject)); } +const url_regex = /^[a-z0-9]+:\/\/[^/]+\.[^/]{2}(\/[^?#]*)?$/; +const sha256_regex = /^[0-9a-f]{64}$/; +const component_schema = [ + new RegExp(`^[${TYPE_PREFIX.SCRIPT}${TYPE_PREFIX.BAG}]$`), + /.+/ +]; + +const settings_schema = [ + [{}, "matchentry", "minentries", 1, + new RegExp(`^${TYPE_PREFIX.SCRIPT}`), { + /* script data */ + "url": ["optional", url_regex], + "sha256": ["optional", sha256_regex], + "text": ["optional", "string"] + }, + new RegExp(`^${TYPE_PREFIX.BAG}`), [ + "optional", + [component_schema, "repeat"], + "default", undefined + ], + new RegExp(`^${TYPE_PREFIX.PAGE}`), { + /* page data */ + "components": ["optional", component_schema] + }], "repeat" +] + async function import_from_file(event) { let files = event.target.files; @@ -555,30 +583,18 @@ async function import_from_file(event) let result = undefined; try { - result = JSON.parse(await read_file(files[0])); + result = parse_json_with_schema(settings_schema, + await read_file(files[0])); } catch(e) { bad_file_errormsg.textContent = "" + e; import_failed_radio.checked = true; return; } - let errormsg = validate_settings(result); - if (errormsg !== false) { - bad_file_errormsg.textContent = errormsg; - import_failed_radio.checked = true; - return; - } - populate_import_list(result); import_selection_radio.checked = true; } -function validate_settings(settings) -{ - // TODO - return false; -} - function import_li_id(prefix, item) { return `ili_${prefix}_${item}`; -- cgit v1.2.3 From 25817b68c03b25c9b4fcaba2b96ab65f2edfd63c Mon Sep 17 00:00:00 2001 From: jahoti Date: Wed, 28 Jul 2021 00:00:00 +0000 Subject: Rationalize CSP violation report blocking. Report blocking now applies iff scripts are blocked. --- background/policy_injector.js | 76 +++++++++++++++++++++---------------------- 1 file changed, 37 insertions(+), 39 deletions(-) diff --git a/background/policy_injector.js b/background/policy_injector.js index 386cf22..f573d48 100644 --- a/background/policy_injector.js +++ b/background/policy_injector.js @@ -83,13 +83,15 @@ function process_csp_header(header, rule, block) { const csp = parse_csp(header.value); - /* No snitching */ - delete csp['report-to']; - delete csp['report-uri']; if (block) { + /* No snitching */ + delete csp['report-to']; + delete csp['report-uri']; + delete csp['script-src']; delete csp['script-src-elem']; + csp['script-src-attr'] = ["'none'"]; csp['prefetch-src'] = ["'none'"]; } @@ -127,45 +129,41 @@ function headers_inject(details) for (const header of details.responseHeaders) { if (!csp_header_names.has(header)) { - /* Retain all non-snitching headers */ - if (header.name.toLowerCase() !== report_only) { - headers.push(header); - - /* If these are the original CSP headers, use them instead */ - /* Test based on url_extract_target() in misc.js */ - if (is_mozilla && header.name === "x-orig-csp") { - let index = header.value.indexOf('%5B'); - if (index === -1) - continue; - - let sig = header.value.substring(0, index); - let data = header.value.substring(index); - if (sig !== sign_policy(data, 0)) - continue; - - /* Confirmed- it's the originals, smuggled in! */ - try { - data = JSON.parse(decodeURIComponent(data)); - } catch (e) { - /* This should not be reached - - it's our self-produced valid JSON. */ - console.log("Unexpected internal error - invalid JSON smuggled!", e); - } - - orig_csp_headers = csp_headers = null; - for (const header of data) - headers.push(process_csp_header(header, rule, block)); + /* Remove headers that only snitch on us */ + if (header.name.toLowerCase() === report_only && block) + continue; + headers.push(header); + + /* If these are the original CSP headers, use them instead */ + /* Test based on url_extract_target() in misc.js */ + if (is_mozilla && header.name === "x-orig-csp") { + let index = header.value.indexOf('%5B'); + if (index === -1) + continue; + + let sig = header.value.substring(0, index); + let data = header.value.substring(index); + if (sig !== sign_policy(data, 0)) + continue; + + /* Confirmed- it's the originals, smuggled in! */ + try { + data = JSON.parse(decodeURIComponent(data)); + } catch (e) { + /* This should not be reached - + it's our self-produced valid JSON. */ + console.log("Unexpected internal error - invalid JSON smuggled!", e); } - } - continue; + orig_csp_headers = csp_headers = null; + for (const header of data) + headers.push(process_csp_header(header, rule, block)); + } + } else if (is_chrome || !orig_csp_headers) { + csp_headers.push(process_csp_header(header, rule, block)); + if (is_mozilla) + orig_csp_headers.push(header); } - if (is_mozilla && !orig_csp_headers) - continue; - - csp_headers.push(process_csp_header(header, rule, block)); - if (is_mozilla) - orig_csp_headers.push(header); } if (orig_csp_headers) { -- cgit v1.2.3 From 5b419aedd564e6506aa2fc8bddcaa5d601888f17 Mon Sep 17 00:00:00 2001 From: jahoti Date: Mon, 2 Aug 2021 00:00:00 +0000 Subject: [UNTESTED- will test] Add filtering for http-equiv CSP headers --- background/policy_injector.js | 40 +++------------------------------------- common/misc.js | 36 +++++++++++++++++++++++++++++++++++- content/main.js | 27 +++++++++++++++++++-------- 3 files changed, 57 insertions(+), 46 deletions(-) diff --git a/background/policy_injector.js b/background/policy_injector.js index f573d48..80a0e3b 100644 --- a/background/policy_injector.js +++ b/background/policy_injector.js @@ -19,7 +19,7 @@ * IMPORT url_extract_target * IMPORT sign_policy * IMPORT get_query_best - * IMPORT parse_csp + * IMPORT sanitize_csp_header * IMPORTS_END */ @@ -79,40 +79,6 @@ function url_inject(details) }; } -function process_csp_header(header, rule, block) -{ - const csp = parse_csp(header.value); - - - if (block) { - /* No snitching */ - delete csp['report-to']; - delete csp['report-uri']; - - delete csp['script-src']; - delete csp['script-src-elem']; - - csp['script-src-attr'] = ["'none'"]; - csp['prefetch-src'] = ["'none'"]; - } - - if ('script-src' in csp) - csp['script-src'].push(rule); - else - csp['script-src'] = [rule]; - - if ('script-src-elem' in csp) - csp['script-src-elem'].push(rule); - else - csp['script-src-elem'] = [rule]; - - const new_policy = Object.entries(csp).map( - i => `${i[0]} ${i[1].join(' ')};` - ); - - return {name: header.name, value: new_policy.join('')}; -} - function headers_inject(details) { const targets = url_extract_target(details.url); @@ -157,10 +123,10 @@ function headers_inject(details) orig_csp_headers = csp_headers = null; for (const header of data) - headers.push(process_csp_header(header, rule, block)); + headers.push(sanitize_csp_header(header, rule, block)); } } else if (is_chrome || !orig_csp_headers) { - csp_headers.push(process_csp_header(header, rule, block)); + csp_headers.push(sanitize_csp_header(header, rule, block)); if (is_mozilla) orig_csp_headers.push(header); } diff --git a/common/misc.js b/common/misc.js index 0d8466e..d046b65 100644 --- a/common/misc.js +++ b/common/misc.js @@ -173,6 +173,40 @@ function parse_csp(csp) { return directives; } +/* Make CSP headers do our bidding, not interfere */ +function sanitize_csp_header(header, rule, block) +{ + const csp = parse_csp(header.value); + + if (block) { + /* No snitching */ + delete csp['report-to']; + delete csp['report-uri']; + + delete csp['script-src']; + delete csp['script-src-elem']; + + csp['script-src-attr'] = ["'none'"]; + csp['prefetch-src'] = ["'none'"]; + } + + if ('script-src' in csp) + csp['script-src'].push(rule); + else + csp['script-src'] = [rule]; + + if ('script-src-elem' in csp) + csp['script-src-elem'].push(rule); + else + csp['script-src-elem'] = [rule]; + + const new_policy = Object.entries(csp).map( + i => `${i[0]} ${i[1].join(' ')};` + ); + + return {name: header.name, value: new_policy.join('')}; +} + /* * EXPORTS_START * EXPORT gen_nonce @@ -184,6 +218,6 @@ function parse_csp(csp) { * EXPORT nice_name * EXPORT open_in_settings * EXPORT is_privileged_url - * EXPORT parse_csp + * EXPORT sanitize_csp_header * EXPORTS_END */ diff --git a/content/main.js b/content/main.js index 9ed557c..5edb8a6 100644 --- a/content/main.js +++ b/content/main.js @@ -19,6 +19,7 @@ * IMPORT is_chrome * IMPORT is_mozilla * IMPORT start_activity_info_server + * IMPORT sanitize_csp_header * IMPORTS_END */ @@ -65,6 +66,17 @@ function block_node(node) block_script(node); return; } + + else if (node.tagName === 'META' && + node.getAttribute('http-equiv') === 'content-security-policy') { + + node.content = sanitize_csp_header( + {value: node.content}, + `'nonce-${nonce}'`, + !policy.allow + ).value; + return; + } sanitize_attributes(node); @@ -114,14 +126,13 @@ if (!is_privileged_url(document.URL)) { if (!policy.allow) { block_nodes_recursively(document.documentElement); - if (is_chrome) { - var observer = new MutationObserver(handle_mutation); - observer.observe(document.documentElement, { - attributes: true, - childList: true, - subtree: true - }); - } + /* Now needed on Mozilla as well to sanitize CSP header */ + var observer = new MutationObserver(handle_mutation); + observer.observe(document.documentElement, { + attributes: true, + childList: true, + subtree: true + }); if (is_mozilla) addEventListener('beforescriptexecute', mozilla_suppress_scripts, true); -- cgit v1.2.3 From 5957fbeeb47bb2c519d34ae4d2eada2433dd1e09 Mon Sep 17 00:00:00 2001 From: Wojtek Kosior Date: Wed, 4 Aug 2021 22:01:01 +0200 Subject: make settings_query.js use storage object passed as an argument --- background/page_actions_server.js | 6 ++-- background/page_info_server.js | 6 ++-- background/policy_injector.js | 6 ++-- background/settings_query.js | 65 --------------------------------------- common/settings_query.js | 52 +++++++++++++++++++++++++++++++ 5 files changed, 58 insertions(+), 77 deletions(-) delete mode 100644 background/settings_query.js create mode 100644 common/settings_query.js diff --git a/background/page_actions_server.js b/background/page_actions_server.js index a7a44c1..58a0073 100644 --- a/background/page_actions_server.js +++ b/background/page_actions_server.js @@ -13,18 +13,17 @@ * IMPORT browser * IMPORT listen_for_connection * IMPORT sha256 - * IMPORT get_query_best + * IMPORT query_best * IMPORT make_ajax_request * IMPORTS_END */ var storage; -var query_best; var handler; function send_actions(url, port) { - const [pattern, settings] = query_best(url); + const [pattern, settings] = query_best(storage, url); const repos = storage.get_all(TYPE_PREFIX.REPO); port.postMessage(["settings", [pattern, settings, repos]]); @@ -126,7 +125,6 @@ function new_connection(port) async function start_page_actions_server() { storage = await get_storage(); - query_best = await get_query_best(); listen_for_connection(CONNECTION_TYPE.PAGE_ACTIONS, new_connection); } diff --git a/background/page_info_server.js b/background/page_info_server.js index 6f02750..e915011 100644 --- a/background/page_info_server.js +++ b/background/page_info_server.js @@ -10,7 +10,7 @@ * IMPORTS_START * IMPORT listen_for_connection * IMPORT get_storage - * IMPORT get_query_all + * IMPORT query_all * IMPORT TYPE_PREFIX * IMPORT CONNECTION_TYPE * IMPORT url_matches @@ -18,7 +18,6 @@ */ var storage; -var query_all; function handle_change(connection_data, change) { @@ -37,7 +36,7 @@ async function handle_subscription(connection_data, message) } connection_data.subscribed.add(url); - connection_data.port.postMessage(["new_url", query_all(url)]); + connection_data.port.postMessage(["new_url", query_all(storage, url)]); } function new_connection(port) @@ -62,7 +61,6 @@ function new_connection(port) async function start_page_info_server() { storage = await get_storage(); - query_all = await get_query_all(); listen_for_connection(CONNECTION_TYPE.PAGE_INFO, new_connection); } diff --git a/background/policy_injector.js b/background/policy_injector.js index 01da094..b3d85e8 100644 --- a/background/policy_injector.js +++ b/background/policy_injector.js @@ -19,13 +19,12 @@ * IMPORT url_item * IMPORT url_extract_target * IMPORT sign_policy - * IMPORT get_query_best + * IMPORT query_best * IMPORT csp_rule * IMPORTS_END */ var storage; -var query_best; const csp_header_names = { "content-security-policy" : true, @@ -53,7 +52,7 @@ function url_inject(details) if (targets.policy) targets.target = ""; - let [pattern, settings] = query_best(targets.base_url); + let [pattern, settings] = query_best(storage, targets.base_url); /* Defaults */ if (!pattern) settings = {}; @@ -106,7 +105,6 @@ function headers_inject(details) async function start_policy_injector() { storage = await get_storage(); - query_best = await get_query_best(); let extra_opts = ["blocking", "responseHeaders"]; if (is_chrome) diff --git a/background/settings_query.js b/background/settings_query.js deleted file mode 100644 index d0d9511..0000000 --- a/background/settings_query.js +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Hachette querying page settings with regard to wildcard records - * - * Copyright (C) 2021 Wojtek Kosior - * Redistribution terms are gathered in the `copyright' file. - */ - -/* - * IMPORTS_START - * IMPORT make_once - * IMPORT get_storage - * IMPORT TYPE_PREFIX - * IMPORT for_each_possible_pattern - * IMPORTS_END - */ - -var storage; - -async function init(fun) -{ - storage = await get_storage(); - - return fun; -} - -function check_pattern(pattern, multiple, matched) -{ - const settings = storage.get(TYPE_PREFIX.PAGE, pattern); - - if (settings === undefined) - return; - - matched.push([pattern, settings]); - - if (!multiple) - return false; -} - -function query(url, multiple) -{ - const matched = []; - for_each_possible_pattern(url, p => check_pattern(p, multiple, matched)); - - return multiple ? matched : (matched[0] || [undefined, undefined]); -} - -function query_best(url) -{ - return query(url, false); -} - -function query_all(url) -{ - return query(url, true); -} - -const get_query_best = make_once(() => init(query_best)); -const get_query_all = make_once(() => init(query_all)); - -/* - * EXPORTS_START - * EXPORT get_query_best - * EXPORT get_query_all - * EXPORTS_END - */ diff --git a/common/settings_query.js b/common/settings_query.js new file mode 100644 index 0000000..e85ae63 --- /dev/null +++ b/common/settings_query.js @@ -0,0 +1,52 @@ +/** + * Hachette querying page settings with regard to wildcard records + * + * Copyright (C) 2021 Wojtek Kosior + * Redistribution terms are gathered in the `copyright' file. + */ + +/* + * IMPORTS_START + * IMPORT TYPE_PREFIX + * IMPORT for_each_possible_pattern + * IMPORTS_END + */ + +function check_pattern(storage, pattern, multiple, matched) +{ + const settings = storage.get(TYPE_PREFIX.PAGE, pattern); + + if (settings === undefined) + return; + + matched.push([pattern, settings]); + + if (!multiple) + return false; +} + +function query(storage, url, multiple) +{ + const matched = []; + const cb = p => check_pattern(storage, p, multiple, matched); + for_each_possible_pattern(url, cb); + + return multiple ? matched : (matched[0] || [undefined, undefined]); +} + +function query_best(storage, url) +{ + return query(storage, url, false); +} + +function query_all(storage, url) +{ + return query(storage, url, true); +} + +/* + * EXPORTS_START + * EXPORT query_best + * EXPORT query_all + * EXPORTS_END + */ -- cgit v1.2.3 From 90896bcfeb4e55c78d9a15700a6a4580f0df6365 Mon Sep 17 00:00:00 2001 From: Wojtek Kosior Date: Thu, 5 Aug 2021 20:44:25 +0200 Subject: enable modularization of html files --- build.sh | 41 +++++++---------------------------------- copyright | 2 +- process_html_file.sh | 33 +++++++++++++++++++++++++++++++++ shell_utils.sh | 39 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 35 deletions(-) create mode 100755 process_html_file.sh create mode 100644 shell_utils.sh diff --git a/build.sh b/build.sh index 60c1863..d940176 100755 --- a/build.sh +++ b/build.sh @@ -3,36 +3,7 @@ # Copyright (C) 2021 Wojtek Kosior # Redistribution terms are gathered in the `copyright' file. -ENDL=" -" - -errcho() { - echo "$@" >&2 -} - -map_set_instr() { - echo "$1__$2='$3'" -} - -map_set() { - eval "$(map_set_instr "$@")" -} - -map_get() { - eval "echo \"\$$1__$2\"" -} - -map_del_instr() { - echo "unset $1__$2" -} - -map_del() { - eval "$(map_del_instr "$@")" -} - -sanitize() { - echo "$1" | tr /.- _ -} +. ./shell_utils.sh handle_export_line() { if [ "x$1" = "xEXPORTS_START" ]; then @@ -259,11 +230,13 @@ s^_BGSCRIPTS_^$BGSCRIPTS^ s^_CONTENTSCRIPTS_^$CONTENTSCRIPTS^" \ < manifest.json > $BUILDDIR/manifest.json - sed "s^_POPUPSCRIPTS_^$POPUPSCRIPTS^" \ - < html/display-panel.html > $BUILDDIR/html/display-panel.html + ./process_html_file.sh html/display-panel.html | + sed "s^_POPUPSCRIPTS_^$POPUPSCRIPTS^" \ + > $BUILDDIR/html/display-panel.html - sed "s^_OPTIONSSCRIPTS_^$OPTIONSSCRIPTS^" \ - < html/options.html > $BUILDDIR/html/options.html + ./process_html_file.sh html/options.html | + sed "s^_OPTIONSSCRIPTS_^$OPTIONSSCRIPTS^" \ + > $BUILDDIR/html/options.html for FILE in $SCRIPTS; do FILEKEY=$(sanitize "$FILE") diff --git a/copyright b/copyright index 36340e4..96de092 100644 --- a/copyright +++ b/copyright @@ -6,7 +6,7 @@ Files: * Copyright: 2021 Wojtek Kosior License: GPL-3+-javascript or Alicense-1.0 -Files: re-generate_icons.sh build.sh +Files: *.sh Copyright: 2021 Wojtek Kosior 2021 jahoti License: CC0 diff --git a/process_html_file.sh b/process_html_file.sh new file mode 100755 index 0000000..1ed0295 --- /dev/null +++ b/process_html_file.sh @@ -0,0 +1,33 @@ +#!/bin/sh + +# Copyright (C) 2021 Wojtek Kosior +# Redistribution terms are gathered in the `copyright' file. + +# Call like: +# ./process_html_file.sh html/options.html + +. ./shell_utils.sh + +FILE="$1" +FILEKEY=$(sanitize "$FILE") + +if [ "x$(map_get HTML_FILENAMES $FILEKEY)" = "xyes" ]; then + errcho "import loop on $FILE" + exit 1 +fi + +map_set_export HTML_FILENAMES $FILEKEY yes + +awk '\ +!/^[\t\r ]*[\t\r ]*$/{ + print $0; +} +/^[\t\r ]*[\t\r ]*$/{ + indent = substr($0, 1, index($0, "<") - 1); + command = "./process_html_file.sh " $2; + while (command | getline) { + print indent $0; + } + if (close(command) != 0) + exit 1; +}' < "$FILE" diff --git a/shell_utils.sh b/shell_utils.sh new file mode 100644 index 0000000..95e0d4e --- /dev/null +++ b/shell_utils.sh @@ -0,0 +1,39 @@ +# Copyright (C) 2021 Wojtek Kosior +# Redistribution terms are gathered in the `copyright' file. + +# This file is meant to be sourced in sh. + +ENDL=" +" + +errcho() { + echo "$@" >&2 +} + +map_set_instr() { + echo "$1__$2='$3'" +} + +map_set() { + eval "$(map_set_instr "$@")" +} + +map_set_export() { + eval "export $(map_set_instr "$@")" +} + +map_get() { + eval "echo \"\$$1__$2\"" +} + +map_del_instr() { + echo "unset $1__$2" +} + +map_del() { + eval "$(map_del_instr "$@")" +} + +sanitize() { + echo "$1" | tr /.- _ +} -- cgit v1.2.3 From 792fbe187bdffca4a748e88d66ea29f8936ae5c8 Mon Sep 17 00:00:00 2001 From: Wojtek Kosior Date: Fri, 6 Aug 2021 17:17:45 +0200 Subject: Facilitate installation of scripts from the repository This commit includes: * removal of page_info_server * running of storage client in popup context * extraction of some common CSS to a separate file * extraction of scripts import view to a separate file * addition of a facility to conveniently clone complex structures from DOM (in DOM_helpers.js) * addition of hydrilla repo url to default settings * other minor changes and of course changes related to the actual installation of scripts from the repo --- background/main.js | 2 - background/page_info_server.js | 72 -------- build.sh | 1 + common/connection_types.js | 3 +- common/misc.js | 14 ++ common/sanitize_JSON.js | 2 +- common/stored_types.js | 4 +- content/activity_info_server.js | 32 +++- content/main.js | 3 - content/repo_query.js | 111 ++++++++---- copyright | 4 +- default_settings.json | 3 + html/DOM_helpers.js | 41 +++++ html/base.css | 44 +++++ html/display-panel.html | 123 +++++++------ html/display-panel.js | 392 ++++++++++++++++++++++++++++++++++------ html/import_frame.html | 27 +++ html/import_frame.js | 163 +++++++++++++++++ html/options.html | 47 +---- html/options_main.js | 160 +++------------- 20 files changed, 830 insertions(+), 418 deletions(-) delete mode 100644 background/page_info_server.js create mode 100644 html/DOM_helpers.js create mode 100644 html/base.css create mode 100644 html/import_frame.html create mode 100644 html/import_frame.js diff --git a/background/main.js b/background/main.js index ffa814e..7c50fd5 100644 --- a/background/main.js +++ b/background/main.js @@ -12,7 +12,6 @@ * IMPORT start_storage_server * IMPORT start_page_actions_server * IMPORT start_policy_injector - * IMPORT start_page_info_server * IMPORT browser * IMPORTS_END */ @@ -20,7 +19,6 @@ start_storage_server(); start_page_actions_server(); start_policy_injector(); -start_page_info_server(); async function init_ext(install_details) { diff --git a/background/page_info_server.js b/background/page_info_server.js deleted file mode 100644 index e915011..0000000 --- a/background/page_info_server.js +++ /dev/null @@ -1,72 +0,0 @@ -/** - * part of Hachette - * Serving of storage data corresponding to requested urls (server side). - * - * Copyright (C) 2021 Wojtek Kosior - * Redistribution terms are gathered in the `copyright' file. - */ - -/* - * IMPORTS_START - * IMPORT listen_for_connection - * IMPORT get_storage - * IMPORT query_all - * IMPORT TYPE_PREFIX - * IMPORT CONNECTION_TYPE - * IMPORT url_matches - * IMPORTS_END - */ - -var storage; - -function handle_change(connection_data, change) -{ - if (change.prefix !== TYPE_PREFIX.PAGE) - return; - - connection_data.port.postMessage(["change", change]); -} - -async function handle_subscription(connection_data, message) -{ - const [action, url] = message; - if (action === "unsubscribe") { - connection_data.subscribed.delete(url); - return; - } - - connection_data.subscribed.add(url); - connection_data.port.postMessage(["new_url", query_all(storage, url)]); -} - -function new_connection(port) -{ - console.log("new page info connection!"); - - const connection_data = { - subscribed : new Set(), - port - }; - - let _handle_change = change => handle_change(connection_data, change); - - storage.add_change_listener(_handle_change); - - port.onMessage.addListener(m => handle_subscription(connection_data, m)); - port.onDisconnect.addListener( - () => storage.remove_change_listener(_handle_change) - ); -} - -async function start_page_info_server() -{ - storage = await get_storage(); - - listen_for_connection(CONNECTION_TYPE.PAGE_INFO, new_connection); -} - -/* - * EXPORTS_START - * EXPORT start_page_info_server - * EXPORTS_END - */ diff --git a/build.sh b/build.sh index d940176..675dc2c 100755 --- a/build.sh +++ b/build.sh @@ -273,6 +273,7 @@ $(map_get EXPORTCODES $FILEKEY) fi cp -r copyright licenses/ $BUILDDIR + cp html/*.css $BUILDDIR/html mkdir $BUILDDIR/icons cp icons/*.png $BUILDDIR/icons } diff --git a/common/connection_types.js b/common/connection_types.js index 41bde75..88c6964 100644 --- a/common/connection_types.js +++ b/common/connection_types.js @@ -13,8 +13,7 @@ const CONNECTION_TYPE = { REMOTE_STORAGE : "0", PAGE_ACTIONS : "1", - PAGE_INFO : "2", - ACTIVITY_INFO : "3" + ACTIVITY_INFO : "2" }; /* diff --git a/common/misc.js b/common/misc.js index a59ec14..7158d32 100644 --- a/common/misc.js +++ b/common/misc.js @@ -12,6 +12,7 @@ * IMPORT browser * IMPORT is_chrome * IMPORT TYPE_NAME + * IMPORT TYPE_PREFIX * IMPORTS_END */ @@ -154,6 +155,18 @@ function sign_policy(policy, now, hours_offset) { return gen_unique(time + policy); } +/* Regexes and objest to use as/in schemas for parse_json_with_schema(). */ +const nonempty_string_matcher = /.+/; + +const matchers = { + sha256: /^[0-9a-f]{64}$/, + nonempty_string: nonempty_string_matcher, + component: [ + new RegExp(`^[${TYPE_PREFIX.SCRIPT}${TYPE_PREFIX.BAG}]$`), + nonempty_string_matcher + ] +}; + /* * EXPORTS_START * EXPORT gen_nonce @@ -165,5 +178,6 @@ function sign_policy(policy, now, hours_offset) { * EXPORT nice_name * EXPORT open_in_settings * EXPORT is_privileged_url + * EXPORT matchers * EXPORTS_END */ diff --git a/common/sanitize_JSON.js b/common/sanitize_JSON.js index 8268d3e..8b86d2d 100644 --- a/common/sanitize_JSON.js +++ b/common/sanitize_JSON.js @@ -37,7 +37,7 @@ function sanitize_unknown(schema, item) let _default = undefined; if (!Array.isArray(schema) || schema[1] === "matchentry" || - schema.length < 2 || !["ordefault", "or"].includes(schema)) + schema.length < 2 || !["ordefault", "or"].includes(schema[1])) return sanitize_unknown_no_alternatives(schema, item); if ((schema.length & 1) !== 1) { diff --git a/common/stored_types.js b/common/stored_types.js index 304842b..bfceba6 100644 --- a/common/stored_types.js +++ b/common/stored_types.js @@ -18,7 +18,9 @@ const TYPE_PREFIX = { PAGE : "p", BAG : "b", SCRIPT : "s", - VAR : "_" + VAR : "_", + /* Url prefix is not used in stored settings. */ + URL : "u" }; const TYPE_NAME = { diff --git a/content/activity_info_server.js b/content/activity_info_server.js index a1384e9..81a25fb 100644 --- a/content/activity_info_server.js +++ b/content/activity_info_server.js @@ -11,14 +11,20 @@ * IMPORTS_START * IMPORT listen_for_connection * IMPORT CONNECTION_TYPE - * IMPORT set_repo_query_repos - * IMPORT set_repo_query_callback + * IMPORT repo_query + * IMPORT subscribe_repo_query_results + * IMPORT unsubscribe_repo_query_results * IMPORTS_END */ var activities = []; var ports = new Set(); +function report_activity_oneshot(name, data, port) +{ + port.postMessage([name, data]); +} + function report_activity(name, data) { const activity = [name, data]; @@ -35,20 +41,23 @@ function report_script(script_data) function report_settings(settings) { - const [pattern, settings_data, repos] = settings; - set_repo_query_repos(repos); - report_activity("settings", settings); } -function report_repo_query_result(result) +function report_repo_query_action(update, port) { - report_activity("repo_query_result", result); + report_activity_oneshot("repo_query_action", update, port); } -function trigger_repo_query() +function trigger_repo_query(query_specifier) { - set_repo_query_callback(report_repo_query_result); + repo_query(...query_specifier); +} + +function handle_disconnect(port, report_action) +{ + ports.delete(port) + unsubscribe_repo_query_results(report_action); } function new_connection(port) @@ -60,13 +69,16 @@ function new_connection(port) for (const activity of activities) port.postMessage(activity); + const report_action = u => report_repo_query_action(u, port); + subscribe_repo_query_results(report_action); + /* * So far the only thing we expect to receive is repo query order. Once more * possibilities arrive, we will need to complicate this listener. */ port.onMessage.addListener(trigger_repo_query); - port.onDisconnect.addListener(() => ports.delete(port)); + port.onDisconnect.addListener(() => handle_disconnect(port, report_action)); } function start_activity_info_server() diff --git a/content/main.js b/content/main.js index 437a32b..8f8375e 100644 --- a/content/main.js +++ b/content/main.js @@ -20,7 +20,6 @@ * IMPORT is_chrome * IMPORT is_mozilla * IMPORT start_activity_info_server - * IMPORT set_repo_query_url * IMPORTS_END */ @@ -130,6 +129,4 @@ if (!is_privileged_url(document.URL)) { } start_activity_info_server(); - - set_repo_query_url(document.URL); } diff --git a/content/repo_query.js b/content/repo_query.js index b8c8ed9..3708108 100644 --- a/content/repo_query.js +++ b/content/repo_query.js @@ -9,72 +9,105 @@ /* * IMPORTS_START * IMPORT make_ajax_request + * IMPORT observables + * IMPORT TYPE_PREFIX + * IMPORT parse_json_with_schema + * IMPORT matchers * IMPORTS_END */ -var query_started = false; +const paths = { + [TYPE_PREFIX.PAGE]: "/pattern", + [TYPE_PREFIX.BAG]: "/bag", + [TYPE_PREFIX.SCRIPT]: "/script", + [TYPE_PREFIX.URL]: "/query" +}; -var url = undefined; -var repos = undefined; -var callback = undefined; +const queried_items = new Map(); +const observable = observables.make(); -async function query(repo) +function repo_query(prefix, item, repo_urls) { - const [repo_url, data] = repo; + const key = prefix + item; - let response = "Query failed"; - const query_url = `${repo_url}/query?n=${encodeURIComponent(url)}`; + const results = queried_items.get(key) || {}; + queried_items.set(key, results); - try { - let xhttp = await make_ajax_request("GET", query_url); - if (xhttp.status === 200) - response = xhttp.responseText; - console.log(xhttp); - } catch (e) { - console.log(e); - } + for (const repo_url of repo_urls) + perform_query_against(key, repo_url, results); +} - callback([repo_url, response]); +const page_schema = { + pattern: matchers.nonempty_string, + payload: ["optional", matchers.component, "default", undefined] +}; +const bag_schema = { + name: matchers.nonempty_string, + components: ["optional", [matchers.component, "repeat"], "default", []] +}; +const script_schema = { + name: matchers.nonempty_string, + location: matchers.nonempty_string, + sha256: matchers.sha256, +}; +const search_result_schema = [page_schema, "repeat"]; + +const schemas = { + [TYPE_PREFIX.PAGE]: page_schema, + [TYPE_PREFIX.BAG]: bag_schema, + [TYPE_PREFIX.SCRIPT]: script_schema, + [TYPE_PREFIX.URL]: search_result_schema } -function start_query() +async function perform_query_against(key, repo_url, results) { - if (query_started || !url || !repos || !callback) + if (results[repo_url] !== undefined) return; - query_started = true; + const prefix = key[0]; + const item = key.substring(1); + const result = {state: "started"}; + results[repo_url] = result; - console.log(`about to query ${url} from ${repos}`); + const broadcast_msg = {prefix, item, results: {[repo_url]: result}}; + observables.broadcast(observable, broadcast_msg); - for (const repo of repos) - query(repo); -} + let state = "connection_error"; + const query_url = + `${repo_url}${paths[prefix]}?n=${encodeURIComponent(item)}`; -function set_repo_query_url(_url) -{ - url = _url; + try { + let xhttp = await make_ajax_request("GET", query_url); + if (xhttp.status === 200) { + state = "parse_error"; + result.response = + parse_json_with_schema(schemas[prefix], xhttp.responseText); + state = "completed"; + } + } catch (e) { + console.log(e); + } - start_query(); + result.state = state; + observables.broadcast(observable, broadcast_msg); } -function set_repo_query_repos(_repos) +function subscribe_repo_query_results(cb) { - repos = _repos; - - start_query(); + observables.subscribe(observable, cb); + for (const [key, results] of queried_items.entries()) + cb({prefix: key[0], item: key.substring(1), results}); } -function set_repo_query_callback(_callback) +function unsubscribe_repo_query_results(cb) { - callback = _callback; - - start_query(); + observables.unsubscribe(observable, cb); } /* * EXPORTS_START - * EXPORT set_repo_query_url - * EXPORT set_repo_query_repos - * EXPORT set_repo_query_callback + * EXPORT repo_query + * EXPORT subscribe_repo_query_results + * EXPORT unsubscribe_repo_query_results * EXPORTS_END */ diff --git a/copyright b/copyright index 96de092..05a16aa 100644 --- a/copyright +++ b/copyright @@ -20,11 +20,11 @@ Copyright: 2021 Wojtek Kosior 2021 jahoti License: GPL-3+-javascript or Alicense-1.0 -Files: README.txt copyright +Files: *.html README.txt copyright Copyright: 2021 Wojtek Kosior License: GPL-3+ or Alicense-1.0 or CC-BY-SA-4.0 -Files: *.html +Files: html/base.css Copyright: 2021 Wojtek Kosior 2021 Nicholas Johnson License: GPL-3+ or Alicense-1.0 or CC-BY-SA-4.0 diff --git a/default_settings.json b/default_settings.json index 8656381..14856fe 100644 --- a/default_settings.json +++ b/default_settings.json @@ -43,5 +43,8 @@ "phttps://www.worldcat.org/title/**": { "components": ["s", "worldcat (library holdings)"] } + }, + { + "rhttps://hydrilla.koszko.org": {} } ] diff --git a/html/DOM_helpers.js b/html/DOM_helpers.js new file mode 100644 index 0000000..2bff966 --- /dev/null +++ b/html/DOM_helpers.js @@ -0,0 +1,41 @@ +/** + * Hachette operations on DOM elements + * + * Copyright (C) 2021 Wojtek Kosior + * Redistribution terms are gathered in the `copyright' file. + */ + +function by_id(id) +{ + return document.getElementById(id); +} + +function clone_template(template_id) +{ + const clone = document.getElementById(template_id).cloneNode(true); + const result_object = {}; + const to_process = [clone]; + + while (to_process.length > 0) { + const element = to_process.pop(); + const template_key = element.getAttribute("data-template"); + + if (template_key) + result_object[template_key] = element; + + element.removeAttribute("id"); + element.removeAttribute("template_key"); + + for (const child of element.children) + to_process.push(child); + } + + return result_object; +} + +/* + * EXPORTS_START + * EXPORT by_id + * EXPORT clone_template + * EXPORTS_END + */ diff --git a/html/base.css b/html/base.css new file mode 100644 index 0000000..2256833 --- /dev/null +++ b/html/base.css @@ -0,0 +1,44 @@ +/** + * Hachette base styles + * + * Copyright (C) 2021 Wojtek Kosior + * Copyright (C) 2021 Nicholas Johnson + * Redistribution terms are gathered in the `copyright' file. + */ + +input[type="checkbox"], input[type="radio"], .hide { + display: none; +} + +.show_next:not(:checked)+* { + display: none; +} + +.show_hide_next2:not(:checked)+* { + display: none; +} + +.show_hide_next2:checked+*+* { + display: none; +} + +button, .button { + background-color: #4CAF50; + border: none; + border-radius: 8px; + color: white; + text-align: center; + text-decoration: none; + display: inline-block; + padding: 6px 12px; + margin: 2px 0px; +} + +button.slimbutton, .button.slimbutton { + padding: 2px 4px; + margin: 0; +} + +button:hover, .button:hover { + box-shadow: 0 6px 8px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19); +} diff --git a/html/display-panel.html b/html/display-panel.html index d8d7f5d..1b9c77b 100644 --- a/html/display-panel.html +++ b/html/display-panel.html @@ -7,35 +7,25 @@ Hachette - page settings + @@ -118,11 +98,6 @@ -
  • - - - -
  • @@ -243,25 +218,7 @@ diff --git a/html/options_main.js b/html/options_main.js index 6aed8bb..830c860 100644 --- a/html/options_main.js +++ b/html/options_main.js @@ -13,26 +13,23 @@ * IMPORT list_prefixes * IMPORT nice_name * IMPORT parse_json_with_schema + * IMPORT by_id + * IMPORT matchers + * IMPORT get_import_frame * IMPORTS_END */ var storage; -function by_id(id) -{ - return document.getElementById(id); -} const item_li_template = by_id("item_li_template"); const bag_component_li_template = by_id("bag_component_li_template"); const chbx_component_li_template = by_id("chbx_component_li_template"); const radio_component_li_template = by_id("radio_component_li_template"); -const import_li_template = by_id("import_li_template"); /* Make sure they are later cloned without id. */ item_li_template.removeAttribute("id"); bag_component_li_template.removeAttribute("id"); chbx_component_li_template.removeAttribute("id"); radio_component_li_template.removeAttribute("id"); -import_li_template.removeAttribute("id"); function item_li_id(prefix, item) { @@ -224,7 +221,6 @@ function reset_work_bag_li(ul, item, components) ul.work_li.insertBefore(bag_components_ul, old_components_ul); ul.work_li.removeChild(old_components_ul); - console.log("bag components", components); add_bag_components(components); } @@ -519,12 +515,6 @@ const ul_by_prefix = { } } -const import_window = by_id("import_window"); -const import_loading_radio = by_id("import_loading_radio"); -const import_failed_radio = by_id("import_failed_radio"); -const import_selection_radio = by_id("import_selection_radio"); -const bad_file_errormsg = by_id("bad_file_errormsg"); - /* * Newer browsers could utilise `text' method of File objects. * Older ones require FileReader. @@ -545,31 +535,30 @@ function read_file(file) _read_file(file, resolve, reject)); } -const url_regex = /^[a-z0-9]+:\/\/[^/]+\.[^/]{2}(\/[^?#]*)?$/; -const sha256_regex = /^[0-9a-f]{64}$/; -const component_schema = [ - new RegExp(`^[${TYPE_PREFIX.SCRIPT}${TYPE_PREFIX.BAG}]$`), - /.+/ -]; +const url_regex = /^[a-z0-9]+:\/\/[^/]+\.[^/]{2,}(\/[^?#]*)?$/; +const empty_regex = /^$/; const settings_schema = [ [{}, "matchentry", "minentries", 1, new RegExp(`^${TYPE_PREFIX.SCRIPT}`), { /* script data */ - "url": ["optional", url_regex], - "sha256": ["optional", sha256_regex], + "url": ["optional", url_regex, "or", empty_regex], + "sha256": ["optional", matchers.sha256, "or", empty_regex], "text": ["optional", "string"] }, new RegExp(`^${TYPE_PREFIX.BAG}`), [ "optional", - [component_schema, "repeat"], + [matchers.component, "repeat"], "default", undefined ], new RegExp(`^${TYPE_PREFIX.PAGE}`), { /* page data */ - "components": ["optional", component_schema] + "components": ["optional", matchers.component] }], "repeat" -] +]; + +const import_window = by_id("import_window"); +let import_frame; async function import_from_file(event) { @@ -578,86 +567,17 @@ async function import_from_file(event) return; import_window.classList.remove("hide"); - import_loading_radio.checked = true; - - let result = undefined; + import_frame.show_loading(); try { - result = parse_json_with_schema(settings_schema, - await read_file(files[0])); + const file = await read_file(files[0]); + var result = parse_json_with_schema(settings_schema, file); } catch(e) { - bad_file_errormsg.textContent = "" + e; - import_failed_radio.checked = true; + import_frame.show_error("Bad file :(", "" + e); return; } - populate_import_list(result); - import_selection_radio.checked = true; -} - -function import_li_id(prefix, item) -{ - return `ili_${prefix}_${item}`; -} - -let import_ul = by_id("import_ul"); -let import_chbxs_colliding = undefined; -let settings_import_map = undefined; - -function populate_import_list(settings) -{ - let old_children = import_ul.children; - while (old_children[0] !== undefined) - import_ul.removeChild(old_children[0]); - - import_chbxs_colliding = []; - settings_import_map = new Map(); - - for (let setting of settings) { - let [key, value] = Object.entries(setting)[0]; - let prefix = key[0]; - let name = key.substring(1); - add_import_li(prefix, name); - settings_import_map.set(key, value); - } -} - -function add_import_li(prefix, name) -{ - let li = import_li_template.cloneNode(true); - let name_span = li.firstElementChild; - let chbx = name_span.nextElementSibling; - let warning_span = chbx.nextElementSibling; - - li.setAttribute("data-prefix", prefix); - li.setAttribute("data-name", name); - li.id = import_li_id(prefix, name); - name_span.textContent = nice_name(prefix, name); - - if (storage.get(prefix, name) !== undefined) { - import_chbxs_colliding.push(chbx); - warning_span.textContent = "(will overwrite existing setting!)"; - } - - import_ul.appendChild(li); -} - -function check_all_imports() -{ - for (let li of import_ul.children) - li.firstElementChild.nextElementSibling.checked = true; -} - -function uncheck_all_imports() -{ - for (let li of import_ul.children) - li.firstElementChild.nextElementSibling.checked = false; -} - -function uncheck_colliding_imports() -{ - for (let chbx of import_chbxs_colliding) - chbx.checked = false; + import_frame.show_selection(result); } const file_opener_form = by_id("file_opener_form"); @@ -665,9 +585,6 @@ const file_opener_form = by_id("file_opener_form"); function hide_import_window() { import_window.classList.add("hide"); - /* Let GC free some memory */ - import_chbxs_colliding = undefined; - settings_import_map = undefined; /* * Reset file . Without this, a second attempt to import the same @@ -676,43 +593,16 @@ function hide_import_window() file_opener_form.reset(); } -function commit_import() -{ - for (let li of import_ul.children) { - let chbx = li.firstElementChild.nextElementSibling; - - if (!chbx.checked) - continue; - - let prefix = li.getAttribute("data-prefix"); - let name = li.getAttribute("data-name"); - let key = prefix + name; - let value = settings_import_map.get(key); - storage.set(prefix, name, value); - } - - hide_import_window(); -} - -function initialize_import_facility() +async function initialize_import_facility() { let import_but = by_id("import_but"); let file_opener = by_id("file_opener"); - let import_failok_but = by_id("import_failok_but"); - let check_all_import_but = by_id("check_all_import_but"); - let uncheck_all_import_but = by_id("uncheck_all_import_but"); - let uncheck_existing_import_but = by_id("uncheck_existing_import_but"); - let commit_import_but = by_id("commit_import_but"); - let cancel_import_but = by_id("cancel_import_but"); + import_but.addEventListener("click", () => file_opener.click()); file_opener.addEventListener("change", import_from_file); - import_failok_but.addEventListener("click", hide_import_window); - check_all_import_but.addEventListener("click", check_all_imports); - uncheck_all_import_but.addEventListener("click", uncheck_all_imports); - uncheck_colliding_import_but - .addEventListener("click", uncheck_colliding_imports); - commit_import_but.addEventListener("click", commit_import); - cancel_import_but.addEventListener("click", hide_import_window); + + import_frame = await get_import_frame(); + import_frame.onclose = hide_import_window; } /* @@ -784,9 +674,9 @@ async function main() jump_to_item(document.URL); - initialize_import_facility(); - storage.add_change_listener(handle_change); + + await initialize_import_facility(); } function handle_change(change) -- cgit v1.2.3 From 2fbab2f07d3cebde1fba0e801df4f3e9129e463b Mon Sep 17 00:00:00 2001 From: Wojtek Kosior Date: Tue, 10 Aug 2021 20:18:58 +0200 Subject: change default repository URL --- default_settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/default_settings.json b/default_settings.json index 14856fe..44fbca0 100644 --- a/default_settings.json +++ b/default_settings.json @@ -45,6 +45,6 @@ } }, { - "rhttps://hydrilla.koszko.org": {} + "rhttps://api-demo.hachette-hydrilla.org": {} } ] -- cgit v1.2.3 From 6fda8ea58575e52e8957e6b5bfbdcde8b71d0106 Mon Sep 17 00:00:00 2001 From: jahoti Date: Sat, 14 Aug 2021 00:00:00 +0000 Subject: Revert changes to content/main.js to commit 25817b68c* It turns out modifying the CSP headers in meta tags has no effect. --- content/main.js | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/content/main.js b/content/main.js index 5edb8a6..9ed557c 100644 --- a/content/main.js +++ b/content/main.js @@ -19,7 +19,6 @@ * IMPORT is_chrome * IMPORT is_mozilla * IMPORT start_activity_info_server - * IMPORT sanitize_csp_header * IMPORTS_END */ @@ -66,17 +65,6 @@ function block_node(node) block_script(node); return; } - - else if (node.tagName === 'META' && - node.getAttribute('http-equiv') === 'content-security-policy') { - - node.content = sanitize_csp_header( - {value: node.content}, - `'nonce-${nonce}'`, - !policy.allow - ).value; - return; - } sanitize_attributes(node); @@ -126,13 +114,14 @@ if (!is_privileged_url(document.URL)) { if (!policy.allow) { block_nodes_recursively(document.documentElement); - /* Now needed on Mozilla as well to sanitize CSP header */ - var observer = new MutationObserver(handle_mutation); - observer.observe(document.documentElement, { - attributes: true, - childList: true, - subtree: true - }); + if (is_chrome) { + var observer = new MutationObserver(handle_mutation); + observer.observe(document.documentElement, { + attributes: true, + childList: true, + subtree: true + }); + } if (is_mozilla) addEventListener('beforescriptexecute', mozilla_suppress_scripts, true); -- cgit v1.2.3 From ae1844f9ac935eb3c89314cd402b4ec2c3d0f537 Mon Sep 17 00:00:00 2001 From: Wojtek Kosior Date: Sat, 14 Aug 2021 09:54:03 +0200 Subject: merge csp-PoC --- common/misc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/misc.js b/common/misc.js index d046b65..fac88bb 100644 --- a/common/misc.js +++ b/common/misc.js @@ -182,7 +182,7 @@ function sanitize_csp_header(header, rule, block) /* No snitching */ delete csp['report-to']; delete csp['report-uri']; - + delete csp['script-src']; delete csp['script-src-elem']; -- cgit v1.2.3