From 841f816c1d901e20ab9a8378e43a3003f838da5f Mon Sep 17 00:00:00 2001 From: Alec Blance <17378596+AlecBlance@users.noreply.github.com> Date: Sun, 20 Oct 2024 07:27:56 +0800 Subject: [PATCH 1/6] refactor: :recycle: better readability --- src/background.ts | 193 ++++++-------------------------------- src/lib/recorder.ts | 104 ++++++++++++++++++++ src/utils/helper.utils.ts | 22 +++++ tsconfig.app.tsbuildinfo | 2 +- 4 files changed, 158 insertions(+), 163 deletions(-) create mode 100644 src/lib/recorder.ts diff --git a/src/background.ts b/src/background.ts index 58485b7..16cbd46 100644 --- a/src/background.ts +++ b/src/background.ts @@ -1,178 +1,47 @@ -import * as cheerio from "cheerio"; -import { IBucketType } from "./types"; import { getStorageKeyValue } from "@/utils/helper.utils"; +import { recordBuckets } from "./lib/recorder"; const requests = chrome.webRequest.onHeadersReceived; const storage = chrome.storage.local; -/** - * Gets the buckets stored in the session storage - */ -const getBuckets = async () => { - const { buckets } = await storage.get("buckets"); - return buckets as IBucketType; +// To make async function work, and also the ability to remove listener +const dummyFunc = (detail: chrome.webRequest.WebResponseHeadersDetails) => { + recordBuckets(detail); }; -/** - * Changes the badge text to the number of buckets - * - * @param buckets list of buckets from the storage - */ -const addNumber = async (buckets: IBucketType) => { - const { lastSeen } = await storage.get("lastSeen"); - const bucketsListed = - buckets.good.filter((bucket) => bucket.date > lastSeen.good).length + 1; - chrome.action.setBadgeText({ - text: bucketsListed.toString(), - }); - chrome.action.setBadgeBackgroundColor({ - color: "green", - }); -}; - -/** - * Gets the permissions from the offscreen page - * - * @param $ the cheerio object - */ -const getPerms = ($: cheerio.CheerioAPI, hostname: string) => { - const permissions = {} as Record; - const hasUri = $("URI"); - const hasCode = $("Code"); - const hasListBucket = $("ListBucketResult"); - let type = ""; - const date = new Date().getTime(); - try { - if (!hasUri.length && !hasCode.length && hasListBucket) - throw new Error("No permissions"); - if (hasUri.length) { - hasUri.toArray().map((elem) => { - const title = $(elem).text().split("/").pop()!; - const perm = $(elem).parent().next().text(); - permissions[title] = [...(permissions[title] || []), perm]; - }); - type = "good"; - } - if (hasListBucket.length) { - permissions["ListBucket"] = ["True"]; - type = "good"; - } else if (hasCode.length) { - const elem = hasCode[0]; - const title = $(elem).text(); - const perm = $(elem).next().text(); - type = "bad"; - permissions[title] = [...(permissions[title] || []), perm]; - } - } catch (e) { - type = "error"; - console.log(e); - } - return { type, info: { permissions, date, hostname } }; +const listener = async (toRecord: boolean) => { + if (toRecord) + requests.addListener( + dummyFunc, + { + urls: [""], + }, + ["responseHeaders"], + ); + else requests.removeListener(dummyFunc); }; -/** - * Gets the bucket info from the offscreen page - * - * @param hostname the hostname of the bucket - */ -const getBucketInfo = async (hostname: string, buckets: IBucketType) => { - try { - const text = await Promise.all([ - fetch("http://" + hostname + "/?acl").then((res) => res.text()), - fetch("http://" + hostname + "/").then((res) => res.text()), - ]).then(([aclText, listBucketText]) => aclText + listBucketText); - const permissions = getPerms(cheerio.load(text), hostname); +(async () => { + const [{ buckets }, { record }] = (await getStorageKeyValue([ + "buckets", + "record", + ])) as Record[]; + if (!buckets || !Object.keys(buckets).length) { storage.set({ - buckets: { - ...buckets, - [permissions.type]: [permissions.info, ...buckets[permissions.type]], - }, + buckets: { good: [], bad: [], error: [] }, + record: true, + lastSeen: { good: 0, bad: 0, error: 0 }, }); - return permissions; - } catch (e) { - console.log(e); - return false; } -}; + listener(record ?? true); +})(); -/** - * Records the buckets from the network requests - * - * @param response the response from the network request - */ -const recordBuckets = ( - response: chrome.webRequest.WebResponseHeadersDetails, -): void => { - const s3 = response.responseHeaders?.some( - (header) => header.name == "x-amz-request-id", - ); - if (!s3) return; - getBuckets().then((buckets) => { - const url = new URL(response.url); - let hostname = url.hostname; - const pathname = url.pathname; - if (hostname === "s3.amazonaws.com") { - hostname += "/" + pathname.split("/")[1]; - } - const isPresent = Object.values(buckets) - .flat() - .some((bucket) => bucket.hostname === hostname); - const noFavicon = !hostname.includes("favicon.ico"); - if (!isPresent && noFavicon) { - getBucketInfo(hostname, buckets).then((permissions) => { - if (permissions && permissions.type === "good") addNumber(buckets); - }); - } +chrome.runtime.onMessage.addListener((toRecord: boolean) => { + listener(toRecord); + chrome.action.setBadgeText({ + text: toRecord ? "" : "!", }); -}; - -/** - * Listens for the network requests - */ -const listener = async () => { - requests.addListener( - recordBuckets, - { - urls: [""], - }, - ["responseHeaders"], - ); -}; - -/** - * Handles messages from the popup - * - * @param toRecord whether to record the buckets - * @param _sender the sender of the message - * @param sendResponse the response to send back - */ -const fromPopup = (toRecord: boolean) => { - if (toRecord) { - listener(); - chrome.action.setBadgeText({ - text: "", - }); - } else { - requests.removeListener(recordBuckets); - chrome.action.setBadgeText({ - text: "!", - }); - chrome.action.setBadgeBackgroundColor({ - color: "orange", - }); - } -}; - -listener(); - -getStorageKeyValue("buckets").then((response: Record) => { - if (Object.keys(response).length && Object.keys(response.buckets).length) - return; - storage.set({ - buckets: { good: [], bad: [], error: [] }, - record: true, - lastSeen: { good: 0, bad: 0, error: 0 }, + chrome.action.setBadgeBackgroundColor({ + color: toRecord ? "green" : "orange", }); }); - -chrome.runtime.onMessage.addListener(fromPopup); diff --git a/src/lib/recorder.ts b/src/lib/recorder.ts new file mode 100644 index 0000000..5a8d524 --- /dev/null +++ b/src/lib/recorder.ts @@ -0,0 +1,104 @@ +import * as cheerio from "cheerio"; +import { addNumber } from "@/utils/helper.utils"; +import { IBucketType } from "@/types"; + +const storage = chrome.storage.local; +//const preRecorded = new Set(); + +/** + * Gets the permissions from the offscreen page + * + * @param $ the cheerio object + */ +const getPerms = ($: cheerio.CheerioAPI, hostname: string) => { + const permissions = {} as Record; + const hasUri = $("URI"); + const hasCode = $("Code"); + const hasListBucket = $("ListBucketResult"); + let type = ""; + const date = new Date().getTime(); + console.log("Date", date); + console.log("Hostname", hostname); + + try { + if (!hasUri.length && !hasCode.length && hasListBucket) + throw new Error("No permissions"); + if (hasUri.length) { + hasUri.toArray().map((elem) => { + const title = $(elem).text().split("/").pop()!; + const perm = $(elem).parent().next().text(); + permissions[title] = [...(permissions[title] || []), perm]; + }); + type = "good"; + } + if (hasListBucket.length) { + permissions["ListBucket"] = ["True"]; + type = "good"; + } else if (hasCode.length) { + const elem = hasCode[0]; + const title = $(elem).text(); + const perm = $(elem).next().text(); + type = "bad"; + permissions[title] = [...(permissions[title] || []), perm]; + } + } catch (e) { + type = "error"; + console.log("Error in getPerms", e); + } + return { type, info: { permissions, date, hostname } }; +}; + +/** + * Gets the bucket info from the offscreen page + * + * @param hostname the hostname of the bucket + */ +const getBucketInfo = async (hostname: string) => { + try { + // Combining results to have a single text to search + const text = await Promise.all([ + fetch("http://" + hostname + "/?acl").then((res) => res.text()), + fetch("http://" + hostname + "/").then((res) => res.text()), + ]).then(([aclText, listBucketText]) => aclText + listBucketText); + return getPerms(cheerio.load(text), hostname); + } catch (e) { + console.log(e); + return false; + } +}; + +/** + * Records the buckets from the network requests + * + * @param response the response from the network request + */ +export const recordBuckets = async ( + response: chrome.webRequest.WebResponseHeadersDetails, +) => { + const s3 = response.responseHeaders?.some( + (header) => header.name == "x-amz-request-id", + ); + if (!s3) return; + const { buckets }: { buckets: IBucketType } = await storage.get("buckets"); + const { hostname, pathname } = new URL(response.url); + // There are instances that the bucket name is a path + const bucketHostname = + hostname === "s3.amazonaws.com" + ? `${hostname}/${pathname.split("/")[1]}` + : hostname; + const isPresent = Object.values(buckets) + .flat() + .some((bucket) => bucket.hostname === bucketHostname); + // Double requests that might be recorded that has favicon.ico + const hasFavicon = bucketHostname.includes("favicon.ico"); + if (isPresent || hasFavicon) return; + const bucketInfo = await getBucketInfo(bucketHostname); + if (!bucketInfo) return; + storage.set({ + buckets: { + ...buckets, + [bucketInfo.type]: [bucketInfo.info, ...buckets[bucketInfo.type]], + }, + }); + bucketInfo.type === "good" && addNumber(buckets); +}; diff --git a/src/utils/helper.utils.ts b/src/utils/helper.utils.ts index beec3b4..3a29c74 100644 --- a/src/utils/helper.utils.ts +++ b/src/utils/helper.utils.ts @@ -1,3 +1,7 @@ +import { IBucketType } from "@/types"; + +const storage = chrome.storage.local; + /** * This gets the local storage key value or the keys within the local storage key object * @@ -52,3 +56,21 @@ export const editStorageKeyValue = async ( }), ); }; + +/** + * Changes the badge text to the number of buckets + * + * @param buckets list of buckets from the storage + */ +export const addNumber = async (buckets: IBucketType) => { + const { lastSeen } = await storage.get("lastSeen"); + // We only care for the good buckets + const bucketsListed = + buckets.good.filter((bucket) => bucket.date > lastSeen.good).length + 1; + chrome.action.setBadgeText({ + text: bucketsListed.toString(), + }); + chrome.action.setBadgeBackgroundColor({ + color: "green", + }); +}; diff --git a/tsconfig.app.tsbuildinfo b/tsconfig.app.tsbuildinfo index 9879bfb..07b7d25 100644 --- a/tsconfig.app.tsbuildinfo +++ b/tsconfig.app.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/App.tsx","./src/background.ts","./src/main.tsx","./src/vite-env.d.ts","./src/components/Bucket.tsx","./src/components/CustomSwitch.tsx","./src/components/Menu.tsx","./src/components/TabBuckets.tsx","./src/components/TabButton.tsx","./src/components/ui/accordion.tsx","./src/components/ui/badge.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/separator.tsx","./src/components/ui/switch.tsx","./src/components/ui/tabs.tsx","./src/lib/utils.ts","./src/store/useBuckets.store.ts","./src/store/useLastSeen.store.ts","./src/types/index.ts","./src/utils/helper.utils.ts"],"version":"5.6.2"} \ No newline at end of file +{"root":["./src/App.tsx","./src/background.ts","./src/main.tsx","./src/vite-env.d.ts","./src/components/Bucket.tsx","./src/components/CustomSwitch.tsx","./src/components/Menu.tsx","./src/components/TabBuckets.tsx","./src/components/TabButton.tsx","./src/components/ui/accordion.tsx","./src/components/ui/badge.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/separator.tsx","./src/components/ui/switch.tsx","./src/components/ui/tabs.tsx","./src/lib/recorder.ts","./src/lib/utils.ts","./src/store/useBuckets.store.ts","./src/store/useLastSeen.store.ts","./src/types/index.ts","./src/utils/helper.utils.ts"],"version":"5.6.2"} \ No newline at end of file From 3cad410cc77fde654b7e2d80f3b5173029038abd Mon Sep 17 00:00:00 2001 From: Alec Blance <17378596+AlecBlance@users.noreply.github.com> Date: Sun, 20 Oct 2024 07:40:55 +0800 Subject: [PATCH 2/6] fix: :bug: duplicate requests created better filtering using sets, and sync checks --- src/lib/recorder.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/lib/recorder.ts b/src/lib/recorder.ts index 5a8d524..f011330 100644 --- a/src/lib/recorder.ts +++ b/src/lib/recorder.ts @@ -3,7 +3,7 @@ import { addNumber } from "@/utils/helper.utils"; import { IBucketType } from "@/types"; const storage = chrome.storage.local; -//const preRecorded = new Set(); +const preRecord = new Set(); /** * Gets the permissions from the offscreen page @@ -78,20 +78,22 @@ export const recordBuckets = async ( const s3 = response.responseHeaders?.some( (header) => header.name == "x-amz-request-id", ); - if (!s3) return; - const { buckets }: { buckets: IBucketType } = await storage.get("buckets"); + if (!s3 || response.frameId < 0) return; const { hostname, pathname } = new URL(response.url); // There are instances that the bucket name is a path const bucketHostname = hostname === "s3.amazonaws.com" ? `${hostname}/${pathname.split("/")[1]}` : hostname; + const hasFavicon = bucketHostname.includes("favicon.ico"); + if (hasFavicon) return; + if (!preRecord.add(bucketHostname)) return; + const { buckets }: { buckets: IBucketType } = await storage.get("buckets"); const isPresent = Object.values(buckets) .flat() .some((bucket) => bucket.hostname === bucketHostname); // Double requests that might be recorded that has favicon.ico - const hasFavicon = bucketHostname.includes("favicon.ico"); - if (isPresent || hasFavicon) return; + if (isPresent) return; const bucketInfo = await getBucketInfo(bucketHostname); if (!bucketInfo) return; storage.set({ From f7bbf7fc7e37467aaa8285d21bb872e0e2274a6a Mon Sep 17 00:00:00 2001 From: Alec Blance <17378596+AlecBlance@users.noreply.github.com> Date: Sun, 20 Oct 2024 07:44:36 +0800 Subject: [PATCH 3/6] style: :lipstick: reserve space for scrollbar --- src/App.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index 4b94508..5cb571d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -66,7 +66,10 @@ function App() { -
+
{types.map((type) => { return ( Date: Sun, 20 Oct 2024 08:02:53 +0800 Subject: [PATCH 4/6] fix: :rotating_light: linter warning --- src/lib/recorder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/recorder.ts b/src/lib/recorder.ts index f011330..6c48509 100644 --- a/src/lib/recorder.ts +++ b/src/lib/recorder.ts @@ -102,5 +102,5 @@ export const recordBuckets = async ( [bucketInfo.type]: [bucketInfo.info, ...buckets[bucketInfo.type]], }, }); - bucketInfo.type === "good" && addNumber(buckets); + if (bucketInfo.type === "good") addNumber(buckets); }; From 3779fe3263efc456950a198885e444f3920c4aa3 Mon Sep 17 00:00:00 2001 From: Alec Blance <17378596+AlecBlance@users.noreply.github.com> Date: Sun, 20 Oct 2024 12:16:21 +0800 Subject: [PATCH 5/6] feat: :sparkles: referrer / origin website of s3 buckets --- src/components/Bucket.tsx | 7 ++++++- src/lib/recorder.ts | 8 ++++---- src/types/index.ts | 1 + 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/components/Bucket.tsx b/src/components/Bucket.tsx index 2169ef5..bc985ad 100644 --- a/src/components/Bucket.tsx +++ b/src/components/Bucket.tsx @@ -93,7 +93,12 @@ const Bucket = ({
{isPermPresent && ( - + + {info.origin && ( +

+ Origin: {info.origin} +

+ )}
{Object.entries(info.permissions).map(([key, value]) => (
diff --git a/src/lib/recorder.ts b/src/lib/recorder.ts index 6c48509..e1155c7 100644 --- a/src/lib/recorder.ts +++ b/src/lib/recorder.ts @@ -17,9 +17,6 @@ const getPerms = ($: cheerio.CheerioAPI, hostname: string) => { const hasListBucket = $("ListBucketResult"); let type = ""; const date = new Date().getTime(); - console.log("Date", date); - console.log("Hostname", hostname); - try { if (!hasUri.length && !hasCode.length && hasListBucket) throw new Error("No permissions"); @@ -99,7 +96,10 @@ export const recordBuckets = async ( storage.set({ buckets: { ...buckets, - [bucketInfo.type]: [bucketInfo.info, ...buckets[bucketInfo.type]], + [bucketInfo.type]: [ + { ...bucketInfo.info, origin: response.initiator }, + ...buckets[bucketInfo.type], + ], }, }); if (bucketInfo.type === "good") addNumber(buckets); diff --git a/src/types/index.ts b/src/types/index.ts index 1f56e99..700ba05 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -4,6 +4,7 @@ export interface IBucketInfo { permissions: Record; date: number; hostname: string; + origin: string; } export type ILastSeen = Record; From 436da0f52f75d64f9db97dacf791e3c69959f676 Mon Sep 17 00:00:00 2001 From: Alec Blance <17378596+AlecBlance@users.noreply.github.com> Date: Sun, 20 Oct 2024 12:27:19 +0800 Subject: [PATCH 6/6] chore: :bookmark: bump version to v3.2.0 --- .changeset/small-fans-wonder.md | 7 +++++++ README.md | 3 +-- public/manifest.json | 2 +- src/App.tsx | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 .changeset/small-fans-wonder.md diff --git a/.changeset/small-fans-wonder.md b/.changeset/small-fans-wonder.md new file mode 100644 index 0000000..e0869dd --- /dev/null +++ b/.changeset/small-fans-wonder.md @@ -0,0 +1,7 @@ +--- +"s3bucketlist": minor +--- + +- Add origin of s3 bucket request +- Fixes on duplicate bucket checks +- Style adjustment on scroll display diff --git a/README.md b/README.md index 70e8fb8..dfbd69e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# S3BucketList v3.0.1 +# S3BucketList v3.2.0 Search, lists, and checks S3 Buckets found in network requests while you are browsing. @@ -17,7 +17,6 @@ You can install this extension from the [Chrome Web Store](https://chromewebstor ## Roadmap -- Add CI/CD for releases and versioning - Add blacklisting ## Tech Stack diff --git a/public/manifest.json b/public/manifest.json index db8d125..9222526 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "S3BucketList", "description": "Search, lists, and checks S3 Buckets found in network requests", - "version": "3.0.1", + "version": "3.2.0", "icons": { "16": "images/bucket-16.png", "32": "images/bucket-32.png", diff --git a/src/App.tsx b/src/App.tsx index 5cb571d..f06c4e6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -92,7 +92,7 @@ function App() { Alec Blance

-

v3.0.1

+

v3.2.0