Skip to content

Add an example to search for multi-byte pattern in an array #586

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions http-response/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

## 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

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
208 changes: 195 additions & 13 deletions http-response/background.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,204 @@
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();
"use strict";

function splice(arr, start, deleteCount, elements = []) {
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 splicedArray = new Uint8Array(newSize);
splicedArray.set(arr.subarray(0, start));
if (len) {
splicedArray.set(elements, start);
}
splicedArray.set(arr.subarray(start + deleteCount), start + len);
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");
const encoder = new TextEncoder();

const url = new URL(details.url);
if (url.hostname === "example.com") {
const bytes = encoder.encode("</html>");
filter.ondata = event => {
const data = new Uint8Array(event.data);
// Check if this is the last chunk of the response data
let stream = false;
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;
}
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.replaceAll("Example", "WebExtension Example");
filter.write(encoder.encode(str));
filter.disconnect();
};
} else {
const elements = encoder.encode("WebExtension Test");
const bytes = encoder.encode("Test");
const oldData = null;

filter.ondata = event => {
let data = new Uint8Array(event.data);

if (oldData) {
data = mergeTypedArrays(oldData, data);
oldData = null;
}

const res = search(bytes, data);
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. "<h1>Tes"
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 = data.slice(foundIndex);
data = data.slice(0, foundIndex);
}
}
filter.write(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;
}
10 changes: 7 additions & 3 deletions http-response/manifest.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
{

"description": "Altering HTTP responses",
"manifest_version": 2,
"manifest_version": 3,
"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", "webRequestFilterResponse"
],

"host_permissions": [
"https://example.com/*", "https://www.w3.org/*"
],

"background": {
Expand Down