diff --git a/spot/entrypoints/background.ts b/spot/entrypoints/background.ts index c58a20828..d3259db49 100644 --- a/spot/entrypoints/background.ts +++ b/spot/entrypoints/background.ts @@ -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; 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 { 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 | 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; - responseHeaders: Record; - } & ( - | 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; - responseHeaders: Record; - } - const rawRequests: (TrackedRequest & { - startTs: number; - duration: number; - })[] = []; - function filterHeaders(headers: Record) { - const filteredHeaders: Record = {}; - 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: [""] }, - ["requestBody"], - ); - browser.webRequest.onBeforeSendHeaders.addListener( - trackOnHeaders, - { urls: [""] }, - ["requestHeaders"], - ); - browser.webRequest.onCompleted.addListener( - trackOnCompleted, - { - urls: [""], - }, - ["responseHeaders"], - ); - browser.webRequest.onErrorOccurred.addListener( - trackOnError, - { - 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; - } }); diff --git a/spot/package.json b/spot/package.json index c970539bd..87fe85db2 100644 --- a/spot/package.json +++ b/spot/package.json @@ -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", diff --git a/spot/utils/jwt.ts b/spot/utils/jwt.ts new file mode 100644 index 000000000..2afe2c7e7 --- /dev/null +++ b/spot/utils/jwt.ts @@ -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; +}; \ No newline at end of file diff --git a/spot/utils/networkTracking.ts b/spot/utils/networkTracking.ts new file mode 100644 index 000000000..b60bd5d71 --- /dev/null +++ b/spot/utils/networkTracking.ts @@ -0,0 +1,199 @@ +import { WebRequest } from "webextension-polyfill"; +export type TrackedRequest = { + statusCode: number; + requestHeaders: Record; + responseHeaders: Record; +} & ( + | 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; + responseHeaders: Record; +} +const rawRequests: (TrackedRequest & { + startTs: number; + duration: number; +})[] = []; +function filterHeaders(headers: Record) { + const filteredHeaders: Record = {}; + 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: [""] }, + ["requestBody"], + ); + browser.webRequest.onBeforeSendHeaders.addListener( + trackOnHeaders, + { urls: [""] }, + ["requestHeaders"], + ); + browser.webRequest.onCompleted.addListener( + trackOnCompleted, + { + urls: [""], + }, + ["responseHeaders"], + ); + browser.webRequest.onErrorOccurred.addListener( + trackOnError, + { + 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; +}