Skip to content

Commit

Permalink
Merge pull request #25 from AlecBlance/feat/referrer
Browse files Browse the repository at this point in the history
  • Loading branch information
AlecBlance authored Oct 20, 2024
2 parents e2924d4 + 436da0f commit 5d6aa67
Show file tree
Hide file tree
Showing 10 changed files with 181 additions and 169 deletions.
7 changes: 7 additions & 0 deletions .changeset/small-fans-wonder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"s3bucketlist": minor
---

- Add origin of s3 bucket request
- Fixes on duplicate bucket checks
- Style adjustment on scroll display
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 5 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,10 @@ function App() {
</div>
</div>
<Separator />
<div className="grow overflow-y-auto">
<div
className="grow overflow-y-auto"
style={{ scrollbarGutter: "stable" }}
>
{types.map((type) => {
return (
<TabBuckets
Expand All @@ -89,7 +92,7 @@ function App() {
Alec Blance
</a>
</p>
<p>v3.0.1</p>
<p>v3.2.0</p>
</div>
</div>
</Tabs>
Expand Down
193 changes: 31 additions & 162 deletions src/background.ts
Original file line number Diff line number Diff line change
@@ -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<string, string[]>;
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: ["<all_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<string, any>[];
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: ["<all_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<string, any>) => {
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);
7 changes: 6 additions & 1 deletion src/components/Bucket.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,12 @@ const Bucket = ({
</div>
</AccordionTrigger>
{isPermPresent && (
<AccordionContent className="bg-secondary p-2 text-xs">
<AccordionContent className="space-y-2 bg-secondary p-2 text-xs">
{info.origin && (
<p>
<b>Origin:</b> {info.origin}
</p>
)}
<div className="flex flex-col gap-y-2">
{Object.entries(info.permissions).map(([key, value]) => (
<div key={key} className="flex flex-col gap-y-1">
Expand Down
106 changes: 106 additions & 0 deletions src/lib/recorder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import * as cheerio from "cheerio";
import { addNumber } from "@/utils/helper.utils";
import { IBucketType } from "@/types";

const storage = chrome.storage.local;
const preRecord = new Set();

/**
* Gets the permissions from the offscreen page
*
* @param $ the cheerio object
*/
const getPerms = ($: cheerio.CheerioAPI, hostname: string) => {
const permissions = {} as Record<string, string[]>;
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("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 || 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
if (isPresent) return;
const bucketInfo = await getBucketInfo(bucketHostname);
if (!bucketInfo) return;
storage.set({
buckets: {
...buckets,
[bucketInfo.type]: [
{ ...bucketInfo.info, origin: response.initiator },
...buckets[bucketInfo.type],
],
},
});
if (bucketInfo.type === "good") addNumber(buckets);
};
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export interface IBucketInfo {
permissions: Record<string, string[]>;
date: number;
hostname: string;
origin: string;
}
export type ILastSeen = Record<string, number>;

Expand Down
Loading

0 comments on commit 5d6aa67

Please sign in to comment.