From 845e124e460aa395c9d90cf5497d01e8f6679c34 Mon Sep 17 00:00:00 2001 From: def00111 Date: Sat, 7 Jun 2025 22:12:08 +0200 Subject: [PATCH 01/11] Add an example to search for multi-byte pattern in an array --- http-response/README.md | 2 +- http-response/background.js | 163 +++++++++++++++++++++++++++++++++--- http-response/manifest.json | 4 +- 3 files changed, 153 insertions(+), 16 deletions(-) diff --git a/http-response/README.md b/http-response/README.md index 40c23380..99bf1416 100755 --- a/http-response/README.md +++ b/http-response/README.md @@ -2,7 +2,7 @@ ## What it does -Listens to HTTP Responses from example.com and changes the body of the response as it comes through. So that the word "Example" on https://example.com becomes "WebExtension Example". +Listens to HTTP Responses from example.com and w3.org and changes the body of the response as it comes through. So that the word "Example" on https://example.com becomes "WebExtension Example" and the word "Test" on https://www.w3.org/2006/11/mwbp-tests/ becomes "WebExtension Test". ## What it shows diff --git a/http-response/background.js b/http-response/background.js index b7a78800..d3692936 100755 --- a/http-response/background.js +++ b/http-response/background.js @@ -1,22 +1,159 @@ +"use strict"; + function listener(details) { - let filter = browser.webRequest.filterResponseData(details.requestId); - let decoder = new TextDecoder("utf-8"); - let encoder = new TextEncoder(); - - filter.ondata = event => { - let str = decoder.decode(event.data, {stream: true}); - // Just change any instance of Example in the HTTP response - // to WebExtension Example. - str = str.replace(/Example/g, 'WebExtension Example'); - filter.write(encoder.encode(str)); - filter.disconnect(); - } + const filter = browser.webRequest.filterResponseData(details.requestId); + const decoder = new TextDecoder("utf-8"); + const encoder = new TextEncoder(); + + const url = new URL(details.url); + if (url.hostname != "www.w3.org") { + filter.ondata = event => { + let str = decoder.decode(event.data, {stream: true}); + // Just change any instance of Example in the HTTP response + // to WebExtension Example. + str = str.replace(/Example/g, 'WebExtension Example'); + filter.write(encoder.encode(str)); + filter.disconnect(); + }; + } else { + const elements = encoder.encode("WebExtension "); + const bytes = encoder.encode("Test"); + const oldData = []; + filter.ondata = event => { + let data = event.data; + data = new Uint8Array(data); + data = Array.from(data); + + if (oldData.length) { + data = oldData.concat(data); + oldData.length = 0; + } + + let len = 0; + const res = search(bytes, data); + for (const i of res) { + // Insert "WebExtension " at the given index + data.splice(i + len, 0, ...elements); + len += elements.length; + } + + // Check if the word "Example" is cropped at the end, e.g. "

Exampl" + const n = data.length; + const m = bytes.length; + + let i = n; + let j = 1; + let foundIndex = -1; + + while (--i > n - m) { + if (bytes[0] === data[i]) { + foundIndex = i; + break; + } + } + + if (foundIndex != -1) { + let found = true; + while (j < n - foundIndex) { + if (data[++i] !== bytes[j++]) { + found = false; + break; + } + } + + if (found) { + oldData.push(...data.slice(foundIndex)); + data = data.slice(0, foundIndex); + } + } + filter.write(new Uint8Array(data)); + }; + + filter.onstop = () => { + filter.close(); + }; + } + return {}; } browser.webRequest.onBeforeRequest.addListener( listener, - {urls: ["https://example.com/*"], types: ["main_frame"]}, + {urls: ["https://example.com/*", "https://www.w3.org/2006/11/mwbp-tests/*"], types: ["main_frame"]}, ["blocking"] ); + +// JavaScript program to search the pattern in given array +// using KMP Algorithm + +function constructLps(pat, lps) { + // len stores the length of longest prefix which + // is also a suffix for the previous index + let len = 0; + + // lps[0] is always 0 + lps[0] = 0; + + let i = 1; + while (i < pat.length) { + // If characters match, increment the size of lps + if (pat[i] === pat[len]) { + lps[i++] = ++len; + } + // If there is a mismatch + else { + if (len !== 0) { + // Update len to the previous lps value + // to avoid redundant comparisons + len = lps[len - 1]; + } else { + // If no matching prefix found, set lps[i] to 0 + lps[i++] = 0; + } + } + } +} + +function search(pat, arr) { + const n = arr.length; + const m = pat.length; + + const lps = new Array(m); + const res = []; + + constructLps(pat, lps); + + // Pointers i and j, for traversing + // the array and pattern + let i = 0; + let j = 0; + + while (i < n) { + // If characters match, move both pointers forward + if (arr[i] === pat[j]) { + i++; + j++; + + // If the entire pattern is matched + // store the start index in result + if (j === m) { + res.push(i - j); + // Use LPS of previous index to + // skip unnecessary comparisons + j = lps[j - 1]; + } + } + // If there is a mismatch + else { + // Use lps value of previous index + // to avoid redundant comparisons + if (j !== 0) { + j = lps[j - 1]; + } else { + i++; + } + } + } + return res; +} diff --git a/http-response/manifest.json b/http-response/manifest.json index 3e3d75cd..b7cdbe8d 100755 --- a/http-response/manifest.json +++ b/http-response/manifest.json @@ -3,14 +3,14 @@ "description": "Altering HTTP responses", "manifest_version": 2, "name": "http-response-filter", - "version": "1.0", + "version": "2.0", "homepage_url": "https://github.com/mdn/webextensions-examples/tree/master/http-response", "icons": { "48": "pen.svg" }, "permissions": [ - "webRequest", "webRequestBlocking", "https://example.com/*" + "webRequest", "webRequestBlocking", "https://example.com/*", "https://www.w3.org/2006/11/mwbp-tests/*" ], "background": { From 0bcbc58563210dda6bd9bb29443a1ba8a8da7636 Mon Sep 17 00:00:00 2001 From: def00111 Date: Sat, 7 Jun 2025 23:06:02 +0200 Subject: [PATCH 02/11] Update background.js --- http-response/background.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/http-response/background.js b/http-response/background.js index d3692936..6b6d7b46 100755 --- a/http-response/background.js +++ b/http-response/background.js @@ -37,7 +37,7 @@ function listener(details) { len += elements.length; } - // Check if the word "Example" is cropped at the end, e.g. "

Exampl" + // Check if the word "Test" is cropped at the end, e.g. "

Tes" const n = data.length; const m = bytes.length; From 76ca274bc779a7423d02e6822d31837379591c92 Mon Sep 17 00:00:00 2001 From: def00111 Date: Fri, 13 Jun 2025 11:25:57 +0200 Subject: [PATCH 03/11] Update example --- http-response/README.md | 2 +- http-response/background.js | 21 +++++++++++++++++---- http-response/manifest.json | 8 ++++++-- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/http-response/README.md b/http-response/README.md index 99bf1416..1e89a884 100755 --- a/http-response/README.md +++ b/http-response/README.md @@ -6,6 +6,6 @@ Listens to HTTP Responses from example.com and w3.org and changes the body of th ## What it shows -How to use the response parser on bytes. +How to use the response parser on bytes and how to change the response on a non-UTF-8 page without using an encoding library. Icon is from: https://www.iconfinder.com/icons/763339/draw_edit_editor_pen_pencil_tool_write_icon#size=128 diff --git a/http-response/background.js b/http-response/background.js index 6b6d7b46..2aefb7be 100755 --- a/http-response/background.js +++ b/http-response/background.js @@ -6,12 +6,25 @@ function listener(details) { const encoder = new TextEncoder(); const url = new URL(details.url); - if (url.hostname != "www.w3.org") { + if (url.hostname === "example.com") { + const bytes = encoder.encode("\n"); + const m = bytes.length; filter.ondata = event => { - let str = decoder.decode(event.data, {stream: true}); + const data = new Uint8Array(event.data); + const n = data.length; + // Check if this is the last chunk of the response data + let stream = false; + for (let i = n - m, j = 0; i < n; i++, j++) { + if (bytes[j] !== data[i]) { + // This is not the last chunk of the response data + stream = true; + break; + } + } + let str = decoder.decode(event.data, {stream}); // Just change any instance of Example in the HTTP response // to WebExtension Example. - str = str.replace(/Example/g, 'WebExtension Example'); + str = str.replaceAll("Example", 'WebExtension Example'); filter.write(encoder.encode(str)); filter.disconnect(); }; @@ -53,7 +66,7 @@ function listener(details) { } } - if (foundIndex != -1) { + if (foundIndex !== -1) { let found = true; while (j < n - foundIndex) { if (data[++i] !== bytes[j++]) { diff --git a/http-response/manifest.json b/http-response/manifest.json index b7cdbe8d..d07e825e 100755 --- a/http-response/manifest.json +++ b/http-response/manifest.json @@ -1,7 +1,7 @@ { "description": "Altering HTTP responses", - "manifest_version": 2, + "manifest_version": 3, "name": "http-response-filter", "version": "2.0", "homepage_url": "https://github.com/mdn/webextensions-examples/tree/master/http-response", @@ -10,7 +10,11 @@ }, "permissions": [ - "webRequest", "webRequestBlocking", "https://example.com/*", "https://www.w3.org/2006/11/mwbp-tests/*" + "webRequest", "webRequestBlocking", "webRequestFilterResponse" + ], + + "host_permissions": [ + "https://example.com/*", "https://www.w3.org/*" ], "background": { From e6d7edcbd1f6b6c4ab36be32de8c7bfca467cad9 Mon Sep 17 00:00:00 2001 From: def00111 Date: Fri, 13 Jun 2025 13:17:32 +0200 Subject: [PATCH 04/11] Update example --- http-response/background.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/http-response/background.js b/http-response/background.js index 2aefb7be..8c78d596 100755 --- a/http-response/background.js +++ b/http-response/background.js @@ -7,15 +7,20 @@ function listener(details) { const url = new URL(details.url); if (url.hostname === "example.com") { - const bytes = encoder.encode("\n"); + const bytes = encoder.encode(""); const m = bytes.length; filter.ondata = event => { const data = new Uint8Array(event.data); const n = data.length; // Check if this is the last chunk of the response data let stream = false; - for (let i = n - m, j = 0; i < n; i++, j++) { - if (bytes[j] !== data[i]) { + for (let i = n - 1, j = m - 1; i > n - m; i--) { + if (data[i] === 0x20 || data[i] === 0xA || + data[i] === 0xD) { + // Ignore whitespace and newline chars + continue; + } + if (bytes[j--] !== data[i]) { // This is not the last chunk of the response data stream = true; break; From 7a7a508d0deb52840314d5bb20d8d5dbc5379699 Mon Sep 17 00:00:00 2001 From: def00111 Date: Fri, 13 Jun 2025 14:12:33 +0200 Subject: [PATCH 05/11] Update example --- http-response/background.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/http-response/background.js b/http-response/background.js index 8c78d596..7c9bc7d2 100755 --- a/http-response/background.js +++ b/http-response/background.js @@ -14,7 +14,7 @@ function listener(details) { const n = data.length; // Check if this is the last chunk of the response data let stream = false; - for (let i = n - 1, j = m - 1; i > n - m; i--) { + for (let i = n - 1, j = m - 1; i >= 0 && j >= 0; i--) { if (data[i] === 0x20 || data[i] === 0xA || data[i] === 0xD) { // Ignore whitespace and newline chars From a7b59cd2d3508856c65eea0d29bb8ebe6da962db Mon Sep 17 00:00:00 2001 From: def00111 Date: Fri, 13 Jun 2025 14:39:03 +0200 Subject: [PATCH 06/11] Update example --- http-response/background.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/http-response/background.js b/http-response/background.js index 7c9bc7d2..83105b59 100755 --- a/http-response/background.js +++ b/http-response/background.js @@ -15,9 +15,8 @@ function listener(details) { // Check if this is the last chunk of the response data let stream = false; for (let i = n - 1, j = m - 1; i >= 0 && j >= 0; i--) { - if (data[i] === 0x20 || data[i] === 0xA || - data[i] === 0xD) { - // Ignore whitespace and newline chars + if (data[i] === 0xA || data[i] === 0xD) { + // Ignore newline chars continue; } if (bytes[j--] !== data[i]) { From 7a730b9b80a27e9ba86838ab63f28ff9418fcd81 Mon Sep 17 00:00:00 2001 From: def00111 Date: Fri, 13 Jun 2025 19:09:37 +0200 Subject: [PATCH 07/11] Update example --- http-response/background.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/http-response/background.js b/http-response/background.js index 83105b59..ce3a2c15 100755 --- a/http-response/background.js +++ b/http-response/background.js @@ -8,13 +8,15 @@ function listener(details) { const url = new URL(details.url); if (url.hostname === "example.com") { const bytes = encoder.encode(""); - const m = bytes.length; filter.ondata = event => { const data = new Uint8Array(event.data); - const n = data.length; // Check if this is the last chunk of the response data let stream = false; - for (let i = n - 1, j = m - 1; i >= 0 && j >= 0; i--) { + for ( + let i = data.length - 1, j = bytes.length - 1; + i >= 0 && j >= 0; + i-- + ) { if (data[i] === 0xA || data[i] === 0xD) { // Ignore newline chars continue; @@ -28,7 +30,7 @@ function listener(details) { let str = decoder.decode(event.data, {stream}); // Just change any instance of Example in the HTTP response // to WebExtension Example. - str = str.replaceAll("Example", 'WebExtension Example'); + str = str.replaceAll("Example", "WebExtension Example"); filter.write(encoder.encode(str)); filter.disconnect(); }; From 8e5c7bc684ca498f6afd430c1a4e8b4dc05274ac Mon Sep 17 00:00:00 2001 From: def00111 Date: Thu, 19 Jun 2025 19:35:10 +0200 Subject: [PATCH 08/11] Update example --- http-response/background.js | 57 ++++++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/http-response/background.js b/http-response/background.js index ce3a2c15..b98c3f49 100755 --- a/http-response/background.js +++ b/http-response/background.js @@ -1,5 +1,27 @@ "use strict"; +function splice(arr, starting, deleteCount, elements = []) { + if (arguments.length === 1) { + return arr; + } + starting = Math.max(starting, 0); + deleteCount = Math.max(deleteCount, 0); + + const newSize = arr.length - deleteCount + elements.length; + const splicedArray = new Uint8Array(newSize); + splicedArray.set(arr.subarray(0, starting)); + splicedArray.set(elements, starting); + splicedArray.set(arr.subarray(starting + deleteCount), starting + elements.length); + return splicedArray; +} + +function mergeTypedArrays(a, b) { + const c = new Uint8Array(a.length + b.length); + c.set(a); + c.set(b, a.length); + return c; +} + function listener(details) { const filter = browser.webRequest.filterResponseData(details.requestId); const decoder = new TextDecoder("utf-8"); @@ -35,25 +57,26 @@ function listener(details) { filter.disconnect(); }; } else { - const elements = encoder.encode("WebExtension "); + const elements = encoder.encode("WebExtension Test"); const bytes = encoder.encode("Test"); - const oldData = []; + const oldData = null; + filter.ondata = event => { - let data = event.data; - data = new Uint8Array(data); - data = Array.from(data); + let data = new Uint8Array(event.data); - if (oldData.length) { - data = oldData.concat(data); - oldData.length = 0; + if (oldData) { + data = mergeTypedArrays(oldData, data); + oldData = null; } - let len = 0; const res = search(bytes, data); - for (const i of res) { - // Insert "WebExtension " at the given index - data.splice(i + len, 0, ...elements); - len += elements.length; + if (res.length) { + let len = 0; + for (const i of res) { + // Replace "Test" with "WebExtension Test" at the given index + data = splice(data, i + len, bytes.length, elements); + len += elements.length - bytes.length; + } } // Check if the word "Test" is cropped at the end, e.g. "

Tes" @@ -82,11 +105,13 @@ function listener(details) { } if (found) { - oldData.push(...data.slice(foundIndex)); - data = data.slice(0, foundIndex); + const part = data.subarray(foundIndex); + oldData = new Uint8Array(part.length); + oldData.set(part); + data = data.subarray(0, foundIndex); } } - filter.write(new Uint8Array(data)); + filter.write(data); }; filter.onstop = () => { From 5866ebfd2c62eca7649354913bb37a13d9472d8c Mon Sep 17 00:00:00 2001 From: def00111 Date: Sun, 22 Jun 2025 11:10:20 +0200 Subject: [PATCH 09/11] Update example --- http-response/background.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/http-response/background.js b/http-response/background.js index b98c3f49..50b525c9 100755 --- a/http-response/background.js +++ b/http-response/background.js @@ -10,7 +10,9 @@ function splice(arr, starting, deleteCount, elements = []) { const newSize = arr.length - deleteCount + elements.length; const splicedArray = new Uint8Array(newSize); splicedArray.set(arr.subarray(0, starting)); - splicedArray.set(elements, starting); + if (elements.length) { + splicedArray.set(elements, starting); + } splicedArray.set(arr.subarray(starting + deleteCount), starting + elements.length); return splicedArray; } @@ -105,10 +107,8 @@ function listener(details) { } if (found) { - const part = data.subarray(foundIndex); - oldData = new Uint8Array(part.length); - oldData.set(part); - data = data.subarray(0, foundIndex); + oldData = data.slice(foundIndex); + data = data.slice(0, foundIndex); } } filter.write(data); From 6c51af0a28f44b02d95344e7e2ac18924ee9d71d Mon Sep 17 00:00:00 2001 From: def00111 Date: Sun, 22 Jun 2025 11:51:03 +0200 Subject: [PATCH 10/11] Update example --- http-response/background.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/http-response/background.js b/http-response/background.js index 50b525c9..70c7b431 100755 --- a/http-response/background.js +++ b/http-response/background.js @@ -1,19 +1,20 @@ "use strict"; -function splice(arr, starting, deleteCount, elements = []) { +function splice(arr, start, deleteCount, elements = []) { if (arguments.length === 1) { return arr; } - starting = Math.max(starting, 0); + start = Math.max(start, 0); deleteCount = Math.max(deleteCount, 0); - const newSize = arr.length - deleteCount + elements.length; + const len = elements.length; + const newSize = arr.length - deleteCount + len; const splicedArray = new Uint8Array(newSize); - splicedArray.set(arr.subarray(0, starting)); - if (elements.length) { - splicedArray.set(elements, starting); + splicedArray.set(arr.subarray(0, start)); + if (len) { + splicedArray.set(elements, start); } - splicedArray.set(arr.subarray(starting + deleteCount), starting + elements.length); + splicedArray.set(arr.subarray(start + deleteCount), start + len); return splicedArray; } From 05172de53a7ef65e17c55ff6c8d57e82d960901a Mon Sep 17 00:00:00 2001 From: def00111 Date: Sat, 28 Jun 2025 15:29:30 +0200 Subject: [PATCH 11/11] Update example --- http-response/background.js | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/http-response/background.js b/http-response/background.js index 70c7b431..f46546b6 100755 --- a/http-response/background.js +++ b/http-response/background.js @@ -1,20 +1,22 @@ "use strict"; -function splice(arr, start, deleteCount, elements = []) { +function splice(arr, start, deleteCount, ...items) { if (arguments.length === 1) { return arr; } start = Math.max(start, 0); deleteCount = Math.max(deleteCount, 0); - const len = elements.length; - const newSize = arr.length - deleteCount + len; + const combinedLength = items.reduce((acc, item) => acc + item.length, 0); + const newSize = arr.length - deleteCount + combinedLength; const splicedArray = new Uint8Array(newSize); splicedArray.set(arr.subarray(0, start)); - if (len) { - splicedArray.set(elements, start); + let writeOffset = 0; + for (const item of items) { + splicedArray.set(item, start + writeOffset); + writeOffset += item.length; } - splicedArray.set(arr.subarray(start + deleteCount), start + len); + splicedArray.set(arr.subarray(start + deleteCount), start + combinedLength); return splicedArray; } @@ -60,7 +62,10 @@ function listener(details) { filter.disconnect(); }; } else { - const elements = encoder.encode("WebExtension Test"); + const element1 = encoder.encode("WebExtension"); + const element2 = encoder.encode(" "); + const element3 = encoder.encode("Test"); + const total = element1.length + element2.length + element3.length; const bytes = encoder.encode("Test"); const oldData = null; @@ -77,8 +82,8 @@ function listener(details) { let len = 0; for (const i of res) { // Replace "Test" with "WebExtension Test" at the given index - data = splice(data, i + len, bytes.length, elements); - len += elements.length - bytes.length; + data = splice(data, i + len, bytes.length, element1, element2, element3); + len += total - bytes.length; } }