some spot refactoring

This commit is contained in:
nick-delirium 2024-09-17 18:09:11 +02:00
parent 4b3563cacd
commit 2e536a2232
No known key found for this signature in database
GPG key ID: 93ABD695DF5FDBA0
4 changed files with 326 additions and 299 deletions

View file

@ -1,9 +1,17 @@
import { WebRequest } from "webextension-polyfill";
import {
createSpotNetworkRequest,
stopTrackingNetwork,
startTrackingNetwork,
SpotNetworkRequest,
} from "../utils/networkTracking";
import {
isTokenExpired
} from '../utils/jwt'
export default defineBackground(() => {
const CHECK_INT = 60 * 1000;
const PING_INT = 30 * 1000;
const VER = '1.0.3';
const VER = "1.0.7";
const messages = {
popup: {
@ -101,6 +109,12 @@ export default defineBackground(() => {
stopped: "stopped",
};
const defaultSettings = {
openInNewTab: true,
consoleLogs: true,
networkLogs: true,
ingestPoint: "https://app.openreplay.com",
};
const defaultSpotObj = {
name: "",
comment: "",
@ -126,12 +140,7 @@ export default defineBackground(() => {
let finalReady = false;
let finalSpotObj: SpotObj = defaultSpotObj;
let onStop: (() => void) | null = null;
let settings = {
openInNewTab: true,
consoleLogs: true,
networkLogs: true,
ingestPoint: "https://app.openreplay.com",
};
let settings = defaultSettings;
let recordingState = {
activeTabId: null,
area: null,
@ -139,6 +148,8 @@ export default defineBackground(() => {
audioPerm: 0,
} as Record<string, any>;
let jwtToken = "";
let refreshInt: any;
let pingInt: any;
function setJWTToken(token: string) {
jwtToken = token;
@ -147,11 +158,28 @@ export default defineBackground(() => {
void browser.runtime.sendMessage({
type: messages.popup.loginExist,
});
if (!refreshInt) {
refreshInt = setInterval(() => {
void refreshToken();
}, CHECK_INT);
}
if (!pingInt) {
pingInt = setInterval(() => {
void pingJWT();
}, PING_INT);
}
} else {
void browser.storage.local.remove("jwtToken");
void browser.runtime.sendMessage({
type: messages.popup.to.noLogin,
});
if (refreshInt) {
clearInterval(refreshInt);
}
if (pingInt) {
clearInterval(pingInt);
}
}
}
@ -167,21 +195,35 @@ export default defineBackground(() => {
}
let slackChannels: { name: string; webhookId: number }[] = [];
const refreshToken = async () => {
const data = await browser.storage.local.get(["jwtToken", "settings"]);
void checkTokenValidity();
browser.storage.local.get("settings").then(async (data: any) => {
if (!data.settings) {
void browser.storage.local.set({ settings });
return;
}
settings = Object.assign(settings, data.settings);
});
async function refreshToken() {
const data = await browser.storage.local.get(["jwtToken", "settings"]);
if (!data.settings) {
await browser.storage.local.set({ defaultSettings });
data.settings = defaultSettings;
}
if (!data.jwtToken) {
setJWTToken("");
}
const { jwtToken, settings } = data;
const ingest = safeApiUrl(settings.ingestPoint);
const refreshUrl = safeApiUrl(`${ingest}/api`);
const refreshUrl = `${safeApiUrl(settings.ingestPoint)}/api/spot/refresh`
console.log(settings.ingestPoint, refreshUrl);
if (!isTokenExpired(jwtToken) || !jwtToken) {
if (refreshInt) {
clearInterval(refreshInt);
}
return true;
}
const resp = await fetch(`${refreshUrl}/spot/refresh`, {
const resp = await fetch(refreshUrl, {
method: "GET",
headers: {
Authorization: `Bearer ${jwtToken}`,
@ -199,7 +241,7 @@ export default defineBackground(() => {
const refreshedJwt = dataObj.jwt;
setJWTToken(refreshedJwt);
return true;
};
}
const fetchSlackChannels = async (token: string, ingest: string) => {
await refreshToken();
@ -217,51 +259,15 @@ export default defineBackground(() => {
}));
}
};
let refreshInt: any;
let pingInt: any;
browser.storage.local
.get(["jwtToken", "settings"])
.then(async (data: any) => {
if (!data.settings) {
void browser.storage.local.set({ settings });
return;
}
settings = Object.assign(settings, data.settings);
if (!data.jwtToken) {
console.error("No JWT token found in storage");
void browser.runtime.sendMessage({
type: messages.popup.to.noLogin,
});
return;
}
const url = safeApiUrl(`${data.settings.ingestPoint}/api`);
const ok = await refreshToken();
if (ok) {
fetchSlackChannels(data.jwtToken, url).catch((e) => {
console.error(e);
void refreshToken();
});
if (!refreshInt) {
refreshInt = setInterval(() => {
void refreshToken();
}, CHECK_INT);
}
if (!pingInt) {
pingInt = setInterval(() => {
void pingJWT();
}, PING_INT);
}
}
});
async function pingJWT(): Promise<void> {
const data = await browser.storage.local.get(["jwtToken", "settings"]);
if (!data.settings) {
return;
await browser.storage.local.set({ defaultSettings });
data.settings = defaultSettings;
}
if (!data.jwtToken) {
setJWTToken("");
}
const { jwtToken, settings } = data;
const ingest = safeApiUrl(settings.ingestPoint);
@ -277,7 +283,7 @@ export default defineBackground(() => {
method: "GET",
headers: {
Authorization: `Bearer ${jwtToken}`,
'Ext-Version': VER
"Ext-Version": VER,
},
});
if (!r.ok) {
@ -289,6 +295,30 @@ export default defineBackground(() => {
}
let lastReq: Record<string, any> | null = null;
async function checkTokenValidity() {
const data = await browser.storage.local.get("jwtToken");
console.log(data)
if (!data.jwtToken) {
void browser.runtime.sendMessage({
type: messages.popup.to.noLogin,
});
return;
}
const ok = await refreshToken();
if (ok) {
if (!refreshInt) {
refreshInt = setInterval(() => {
void refreshToken();
}, CHECK_INT);
}
if (!pingInt) {
pingInt = setInterval(() => {
void pingJWT();
}, PING_INT);
}
}
}
// @ts-ignore
browser.runtime.onMessage.addListener((request, sender, respond) => {
if (request.type === messages.content.from.contentReady) {
@ -819,7 +849,7 @@ export default defineBackground(() => {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${jwtToken}`,
'Ext-Version': VER
"Ext-Version": VER,
},
})
.then((r) => {
@ -916,208 +946,25 @@ export default defineBackground(() => {
void browser.runtime.setUninstallURL("https://forms.gle/sMo8da2AvrPg5o7YA");
browser.runtime.onInstalled.addListener(async ({ reason }) => {
// Also fired on update and browser_update
if (reason !== "install") return;
await browser.tabs.create({
url: "https://www.openreplay.com/spot/welcome?ref=extension",
active: true,
});
if (reason === "install") {
await browser.tabs.create({
url: "https://www.openreplay.com/spot/welcome?ref=extension",
active: true,
});
}
// in future:
// const tabs = await browser.tabs.query({}) as chrome.tabs.Tab[]
// for (const tab of tabs) {
// if (tab.id) {
// this will require more permissions, do we even want this?
// void chrome.tabs.executeScript(tab.id, {file: "content"});
// }
// }
await checkTokenValidity();
await initializeOffscreenDocument();
});
void initializeOffscreenDocument();
type TrackedRequest = {
statusCode: number;
requestHeaders: Record<string, string>;
responseHeaders: Record<string, string>;
} & (
| WebRequest.OnBeforeRequestDetailsType
| WebRequest.OnBeforeSendHeadersDetailsType
| WebRequest.OnCompletedDetailsType
| WebRequest.OnErrorOccurredDetailsType
| WebRequest.OnResponseStartedDetailsType
);
interface SpotNetworkRequest {
encodedBodySize: number;
responseBodySize: number;
duration: number;
method: TrackedRequest["method"];
type: string;
time: TrackedRequest["timeStamp"];
statusCode: number;
error?: string;
url: TrackedRequest["url"];
fromCache: boolean;
body: string;
requestHeaders: Record<string, string>;
responseHeaders: Record<string, string>;
}
const rawRequests: (TrackedRequest & {
startTs: number;
duration: number;
})[] = [];
function filterHeaders(headers: Record<string, string>) {
const filteredHeaders: Record<string, string> = {};
const privateHs = [
"x-api-key",
"www-authenticate",
"x-csrf-token",
"x-requested-with",
"x-forwarded-for",
"x-real-ip",
"cookie",
"authorization",
"auth",
"proxy-authorization",
"set-cookie",
];
if (Array.isArray(headers)) {
headers.forEach(({ name, value }) => {
if (privateHs.includes(name.toLowerCase())) {
return;
} else {
filteredHeaders[name] = value;
}
});
} else {
for (const [key, value] of Object.entries(headers)) {
if (!privateHs.includes(key.toLowerCase())) {
filteredHeaders[key] = value;
}
}
}
return filteredHeaders;
}
function createSpotNetworkRequest(
trackedRequest: TrackedRequest,
trackedTab?: number,
) {
if (trackedRequest.tabId === -1) {
return;
}
if (trackedTab && trackedTab !== trackedRequest.tabId) {
return;
}
if (
["ping", "beacon", "image", "script", "font"].includes(
trackedRequest.type,
)
) {
if (!trackedRequest.statusCode || trackedRequest.statusCode < 400) {
return;
}
}
const type = ["stylesheet", "script", "image", "media", "font"].includes(
trackedRequest.type,
)
? "resource"
: trackedRequest.type;
const requestHeaders = trackedRequest.requestHeaders
? filterHeaders(trackedRequest.requestHeaders)
: {};
const responseHeaders = trackedRequest.responseHeaders
? filterHeaders(trackedRequest.responseHeaders)
: {};
const reqSize = trackedRequest.reqBody
? trackedRequest.requestSize || trackedRequest.reqBody.length
: 0;
const status = getRequestStatus(trackedRequest);
const request: SpotNetworkRequest = {
method: trackedRequest.method,
type,
body: trackedRequest.reqBody,
requestHeaders,
responseHeaders,
time: trackedRequest.timeStamp,
statusCode: status,
error: trackedRequest.error,
url: trackedRequest.url,
fromCache: trackedRequest.fromCache || false,
encodedBodySize: reqSize,
responseBodySize: trackedRequest.responseSize,
duration: trackedRequest.duration,
};
return request;
}
function modifyOnSpot(request: TrackedRequest) {
const id = request.requestId;
const index = rawRequests.findIndex((r) => r.requestId === id);
const ts = Date.now();
const start = rawRequests[index]?.startTs ?? ts;
rawRequests[index] = {
...rawRequests[index],
...request,
duration: ts - start,
};
}
const trackOnBefore = (
details: WebRequest.OnBeforeRequestDetailsType & { reqBody: string },
) => {
if (details.method === "POST" && details.requestBody) {
const requestBody = details.requestBody;
if (requestBody.formData) {
details.reqBody = JSON.stringify(requestBody.formData);
} else if (requestBody.raw) {
const raw = requestBody.raw[0]?.bytes;
if (raw) {
details.reqBody = new TextDecoder("utf-8").decode(raw);
}
}
}
rawRequests.push({ ...details, startTs: Date.now(), duration: 0 });
};
const trackOnCompleted = (details: WebRequest.OnCompletedDetailsType) => {
modifyOnSpot(details);
};
const trackOnHeaders = (
details: WebRequest.OnBeforeSendHeadersDetailsType,
) => {
modifyOnSpot(details);
};
const trackOnError = (details: WebRequest.OnErrorOccurredDetailsType) => {
modifyOnSpot(details);
};
function startTrackingNetwork() {
rawRequests.length = 0;
browser.webRequest.onBeforeRequest.addListener(
// @ts-ignore
trackOnBefore,
{ urls: ["<all_urls>"] },
["requestBody"],
);
browser.webRequest.onBeforeSendHeaders.addListener(
trackOnHeaders,
{ urls: ["<all_urls>"] },
["requestHeaders"],
);
browser.webRequest.onCompleted.addListener(
trackOnCompleted,
{
urls: ["<all_urls>"],
},
["responseHeaders"],
);
browser.webRequest.onErrorOccurred.addListener(
trackOnError,
{
urls: ["<all_urls>"],
},
["extraHeaders"],
);
}
function stopTrackingNetwork() {
browser.webRequest.onBeforeRequest.removeListener(trackOnBefore);
browser.webRequest.onCompleted.removeListener(trackOnCompleted);
browser.webRequest.onErrorOccurred.removeListener(trackOnError);
}
async function initializeOffscreenDocument() {
const existingContexts = await browser.runtime.getContexts({});
let recording = false;
@ -1125,9 +972,8 @@ export default defineBackground(() => {
const offscreenDocument = existingContexts.find(
(c: { contextType: string }) => c.contextType === "OFFSCREEN_DOCUMENT",
);
if (offscreenDocument) {
await browser.offscreen.closeDocument()
await browser.offscreen.closeDocument();
}
await browser.offscreen.createDocument({
@ -1153,22 +999,15 @@ export default defineBackground(() => {
}
let activeTab = activeTabs[0];
const sendTo = message.activeTabId || activeTab.id!;
if (!contentArmy[sendTo]) {
let tries = 0;
const exist = await new Promise((res) => {
const interval = setInterval(() => {
if (contentArmy[sendTo] || tries < 500) {
clearInterval(interval);
res(tries < 500);
}
}, 100);
});
if (!exist) throw new Error("Can't find required tab");
let attempts = 0;
while (!contentArmy[sendTo] && attempts < 100) {
await new Promise((resolve) => setTimeout(resolve, 100));
attempts++;
}
try {
void browser.tabs.sendMessage(sendTo, message);
} catch (e) {
console.error("Sending to active tab", e, message);
if (contentArmy[sendTo]) {
await browser.tabs.sendMessage(sendTo, message);
} else {
console.error("Content script not ready in tab", sendTo);
}
}
@ -1381,29 +1220,4 @@ export default defineBackground(() => {
console.error("Error starting recording", e, activeTab, activeTabId);
}
}
const decodeJwt = (jwt: string): any => {
const base64Url = jwt.split(".")[1];
if (!base64Url) {
return { exp: 0 };
}
const base64 = base64Url.replace("-", "+").replace("_", "/");
return JSON.parse(atob(base64));
};
const isTokenExpired = (token: string): boolean => {
const decoded: any = decodeJwt(token);
const currentTime = Date.now() / 1000;
return decoded.exp < currentTime;
};
function getRequestStatus(request: any): number {
if (request.statusCode) {
return request.statusCode;
}
if (request.error) {
return 0;
}
return 200;
}
});

View file

@ -2,7 +2,7 @@
"name": "wxt-starter",
"description": "manifest.json description",
"private": true,
"version": "1.0.6",
"version": "1.0.7",
"type": "module",
"scripts": {
"dev": "wxt",

14
spot/utils/jwt.ts Normal file
View file

@ -0,0 +1,14 @@
export const decodeJwt = (jwt: string): any => {
const base64Url = jwt.split(".")[1];
if (!base64Url) {
return { exp: 0 };
}
const base64 = base64Url.replace("-", "+").replace("_", "/");
return JSON.parse(atob(base64));
};
export const isTokenExpired = (token: string): boolean => {
const decoded: any = decodeJwt(token);
const currentTime = Date.now() / 1000;
return decoded.exp < currentTime;
};

View file

@ -0,0 +1,199 @@
import { WebRequest } from "webextension-polyfill";
export type TrackedRequest = {
statusCode: number;
requestHeaders: Record<string, string>;
responseHeaders: Record<string, string>;
} & (
| WebRequest.OnBeforeRequestDetailsType
| WebRequest.OnBeforeSendHeadersDetailsType
| WebRequest.OnCompletedDetailsType
| WebRequest.OnErrorOccurredDetailsType
| WebRequest.OnResponseStartedDetailsType
);
export interface SpotNetworkRequest {
encodedBodySize: number;
responseBodySize: number;
duration: number;
method: TrackedRequest["method"];
type: string;
time: TrackedRequest["timeStamp"];
statusCode: number;
error?: string;
url: TrackedRequest["url"];
fromCache: boolean;
body: string;
requestHeaders: Record<string, string>;
responseHeaders: Record<string, string>;
}
const rawRequests: (TrackedRequest & {
startTs: number;
duration: number;
})[] = [];
function filterHeaders(headers: Record<string, string>) {
const filteredHeaders: Record<string, string> = {};
const privateHs = [
"x-api-key",
"www-authenticate",
"x-csrf-token",
"x-requested-with",
"x-forwarded-for",
"x-real-ip",
"cookie",
"authorization",
"auth",
"proxy-authorization",
"set-cookie",
];
if (Array.isArray(headers)) {
headers.forEach(({ name, value }) => {
if (privateHs.includes(name.toLowerCase())) {
return;
} else {
filteredHeaders[name] = value;
}
});
} else {
for (const [key, value] of Object.entries(headers)) {
if (!privateHs.includes(key.toLowerCase())) {
filteredHeaders[key] = value;
}
}
}
return filteredHeaders;
}
export function createSpotNetworkRequest(
trackedRequest: TrackedRequest,
trackedTab?: number,
) {
if (trackedRequest.tabId === -1) {
return;
}
if (trackedTab && trackedTab !== trackedRequest.tabId) {
return;
}
if (
["ping", "beacon", "image", "script", "font"].includes(trackedRequest.type)
) {
if (!trackedRequest.statusCode || trackedRequest.statusCode < 400) {
return;
}
}
const type = ["stylesheet", "script", "image", "media", "font"].includes(
trackedRequest.type,
)
? "resource"
: trackedRequest.type;
const requestHeaders = trackedRequest.requestHeaders
? filterHeaders(trackedRequest.requestHeaders)
: {};
const responseHeaders = trackedRequest.responseHeaders
? filterHeaders(trackedRequest.responseHeaders)
: {};
const reqSize = trackedRequest.reqBody
? trackedRequest.requestSize || trackedRequest.reqBody.length
: 0;
const status = getRequestStatus(trackedRequest);
const request: SpotNetworkRequest = {
method: trackedRequest.method,
type,
body: trackedRequest.reqBody,
requestHeaders,
responseHeaders,
time: trackedRequest.timeStamp,
statusCode: status,
error: trackedRequest.error,
url: trackedRequest.url,
fromCache: trackedRequest.fromCache || false,
encodedBodySize: reqSize,
responseBodySize: trackedRequest.responseSize,
duration: trackedRequest.duration,
};
return request;
}
function modifyOnSpot(request: TrackedRequest) {
const id = request.requestId;
const index = rawRequests.findIndex((r) => r.requestId === id);
const ts = Date.now();
const start = rawRequests[index]?.startTs ?? ts;
rawRequests[index] = {
...rawRequests[index],
...request,
duration: ts - start,
};
}
const trackOnBefore = (
details: WebRequest.OnBeforeRequestDetailsType & { reqBody: string },
) => {
if (details.method === "POST" && details.requestBody) {
const requestBody = details.requestBody;
if (requestBody.formData) {
details.reqBody = JSON.stringify(requestBody.formData);
} else if (requestBody.raw) {
const raw = requestBody.raw[0]?.bytes;
if (raw) {
details.reqBody = new TextDecoder("utf-8").decode(raw);
}
}
}
rawRequests.push({ ...details, startTs: Date.now(), duration: 0 });
};
const trackOnCompleted = (details: WebRequest.OnCompletedDetailsType) => {
modifyOnSpot(details);
};
const trackOnHeaders = (details: WebRequest.OnBeforeSendHeadersDetailsType) => {
modifyOnSpot(details);
};
const trackOnError = (details: WebRequest.OnErrorOccurredDetailsType) => {
modifyOnSpot(details);
};
export function startTrackingNetwork() {
rawRequests.length = 0;
browser.webRequest.onBeforeRequest.addListener(
// @ts-ignore
trackOnBefore,
{ urls: ["<all_urls>"] },
["requestBody"],
);
browser.webRequest.onBeforeSendHeaders.addListener(
trackOnHeaders,
{ urls: ["<all_urls>"] },
["requestHeaders"],
);
browser.webRequest.onCompleted.addListener(
trackOnCompleted,
{
urls: ["<all_urls>"],
},
["responseHeaders"],
);
browser.webRequest.onErrorOccurred.addListener(
trackOnError,
{
urls: ["<all_urls>"],
},
["extraHeaders"],
);
}
export function stopTrackingNetwork() {
browser.webRequest.onBeforeRequest.removeListener(trackOnBefore);
browser.webRequest.onCompleted.removeListener(trackOnCompleted);
browser.webRequest.onErrorOccurred.removeListener(trackOnError);
}
function getRequestStatus(request: any): number {
if (request.statusCode) {
return request.statusCode;
}
if (request.error) {
return 0;
}
return 200;
}