Skip to content
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

feat: extension connection logic #25

Merged
merged 2 commits into from
Aug 29, 2024
Merged
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
3 changes: 3 additions & 0 deletions extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
"lint": "eslint . --cache --ext .ts,.tsx"
},
"dependencies": {
"async-mutex": "^0.5.0",
"level": "^8.0.1",
"polished": "^4.2.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand All @@ -15,6 +17,7 @@
"devDependencies": {
"@craco/craco": "^7.1.0",
"@svgr/webpack": "^8.1.0",
"@types/chrome": "^0.0.270",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.18",
"@types/rebass": "^4.0.14",
Expand Down
33 changes: 33 additions & 0 deletions extension/src/entries/Background/db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Level } from 'level'

import mutex from './mutex'

const db = new Level('./ext-db', {
valueEncoding: 'json',
})
const connectionDb = db.sublevel<string, boolean>('connections', {
valueEncoding: 'json',
})

export async function setConnection(origin: string) {
if (await getConnection(origin)) return null
await connectionDb.put(origin, true)
return true
}

export async function deleteConnection(origin: string) {
return mutex.runExclusive(async () => {
if (await getConnection(origin)) {
await connectionDb.del(origin)
}
})
}

export async function getConnection(origin: string) {
try {
const existing = await connectionDb.get(origin)
return existing
} catch (e) {
return null
}
}
14 changes: 10 additions & 4 deletions extension/src/entries/Background/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
// eslint-disable-next-line import/no-unused-modules

// @ts-ignore
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch((error: any) => console.error(error))
import { initRPC } from './rpc'
;(async () => {
try {
// @ts-ignore
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch((error: any) => console.error(error))
initRPC()
} catch (error) {
console.error('Error when initializing RPC:', error)
}
})()
5 changes: 5 additions & 0 deletions extension/src/entries/Background/mutex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Mutex } from 'async-mutex'

const mutex = new Mutex()

export default mutex
82 changes: 82 additions & 0 deletions extension/src/entries/Background/rpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { deferredPromise } from 'src/utils/promise'

import { deleteConnection, getConnection, setConnection } from './db'

export enum BackgroundActionType {
connect_request = 'connect_request',
disconnect_request = 'disconnect_request',
extract_data_request = 'extract_data_request',
}

type BackgroundAction = {
type: BackgroundActionType
data?: any
meta?: any
error?: boolean
}

export const initRPC = () => {
// @ts-ignore
chrome.runtime.onMessage.addListener((request: BackgroundAction, _sender, sendResponse) => {
switch (request.type) {
case BackgroundActionType.extract_data_request:
handleExtractData(request, sendResponse)
return true
case BackgroundActionType.connect_request:
handleConnect(request, sendResponse)
return true
case BackgroundActionType.disconnect_request:
handleDisconnect(request, sendResponse)
return true
default:
sendResponse({ error: 'Unknown action type' })
break
}
})
}

async function handleConnect(request: BackgroundAction, sendResponse: (data?: any) => void) {
try {
const connection = await getConnection(request.data.origin)

if (!connection) {
const defer = deferredPromise()
// Can be set to true/false if the user accepts/rejects the connection
defer.resolve(true)
await setConnection(request.data.origin)

sendResponse(true)
}
} catch (error) {
sendResponse({ error: 'Failed to connect' })
}
}

async function handleDisconnect(request: BackgroundAction, sendResponse: (data?: any) => void) {
try {
const connection = await getConnection(request.data.origin)

if (connection) {
await deleteConnection(request.data.origin)
sendResponse(true)
} else {
sendResponse({ error: 'No active connection found' })
}
} catch (error) {
sendResponse({ error: 'Failed to disconnect' })
}
}

function handleExtractData(request: BackgroundAction, sendResponse: (data?: any) => void) {
if (!request.data.key) {
return sendResponse({ error: 'Key is required' })
}
// Do something with the request.data.key
// For now, just return a response
// But you can use setters and getters to send public data to the content script
let responseData = 'extension public data'
if (request.data.key === 'secret') {
responseData = 'extension secret data'
}
sendResponse({ response: responseData })
}
34 changes: 34 additions & 0 deletions extension/src/entries/Content/content.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ContentScriptTypes, RPCClient } from './rpc'

const client = new RPCClient()

class Zkramp {
async extractData(key: string) {
const resp = await client.call(ContentScriptTypes.extract_data, {
key,
})

return resp
}

async disconnect() {
const resp = await client.call(ContentScriptTypes.disconnect)

return resp
}
}

const connect = async () => {
const resp = await client.call(ContentScriptTypes.connect)

if (resp) {
return new Zkramp()
}

return null
}

// @ts-ignore
window.zkramp = {
connect,
}
78 changes: 78 additions & 0 deletions extension/src/entries/Content/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { BackgroundActionType } from '../Background/rpc'
import { ContentScriptRequest, ContentScriptTypes, RPCServer } from './rpc'
;(async () => {
loadScript('content.js')
const server = new RPCServer()

const sendMessageAsync = (message: any) => {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage(message, (response) => {
if (chrome.runtime.lastError) {
reject(chrome.runtime.lastError)
} else {
resolve(response)
}
})
})
}

server.on(ContentScriptTypes.connect, async () => {
// @ts-ignore
const connected = await sendMessageAsync({
type: BackgroundActionType.connect_request,
data: {
...getOriginData(),
},
})

if (!connected) throw new Error('user rejected.')

return connected
})

server.on(ContentScriptTypes.disconnect, async () => {
// @ts-ignore
const disconnected = await sendMessageAsync({
type: BackgroundActionType.disconnect_request,
data: {
...getOriginData(),
},
})

if (!disconnected) throw new Error('error.')

return disconnected
})

server.on(ContentScriptTypes.extract_data, async (request: ContentScriptRequest<{ key: string }>) => {
const { key } = request.params || {}

if (!key) throw new Error('params must include key of the request')

// @ts-ignore
const response = await chrome.runtime.sendMessage({
type: BackgroundActionType.extract_data_request,
data: {
...getOriginData(),
key,
},
})

return response
})
})()

function loadScript(filename: string) {
//@ts-ignore
const url = chrome.runtime.getURL(filename)
const script = document.createElement('script')
script.setAttribute('type', 'text/javascript')
script.setAttribute('src', url)
document.body.appendChild(script)
}

function getOriginData() {
return {
origin: window.origin,
}
}
102 changes: 102 additions & 0 deletions extension/src/entries/Content/rpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { deferredPromise, PromiseResolvers } from '../../utils/promise'

export enum ContentScriptTypes {
connect = 'zkramp/cs/connect',
disconnect = 'zkramp/cs/disconnect',
extract_data = 'zkramp/cs/extract_data',
}

export type ContentScriptRequest<params> = {
zkramprpc: string
} & RPCRequest<ContentScriptTypes, params>

type ContentScriptResponse = {
zkramprpc: string
} & RPCResponse

type RPCRequest<method, params> = {
id: number
method: method
params?: params
}

type RPCResponse = {
id: number
result?: never
error?: never
}

export class RPCServer {
#handlers: Map<ContentScriptTypes, (message: ContentScriptRequest<any>) => Promise<any>> = new Map()

constructor() {
window.addEventListener('message', async (event: MessageEvent<ContentScriptRequest<never>>) => {
const data = event.data

if (data.zkramprpc !== '1.0') return
if (!data.method) return

const handler = this.#handlers.get(data.method)

if (handler) {
try {
const result = await handler(data)
window.postMessage({
zkramprpc: '1.0',
id: data.id,
result,
})
} catch (error) {
window.postMessage({
zkramprpc: '1.0',
id: data.id,
error,
})
}
} else {
throw new Error(`unknown method - ${data.method}`)
}
})
}

on(method: ContentScriptTypes, handler: (message: ContentScriptRequest<any>) => Promise<any>) {
this.#handlers.set(method, handler)
}
}

export class RPCClient {
#requests: Map<number, PromiseResolvers> = new Map()
#id = 0

get id() {
return this.#id++
}

constructor() {
window.addEventListener('message', (event: MessageEvent<ContentScriptResponse>) => {
const data = event.data

if (data.zkramprpc !== '1.0') return

const promise = this.#requests.get(data.id)

if (promise) {
if (typeof data.result !== 'undefined') {
promise.resolve(data.result)
this.#requests.delete(data.id)
} else if (typeof data.error !== 'undefined') {
promise.reject(data.error)
this.#requests.delete(data.id)
}
}
})
}

async call(method: ContentScriptTypes, params?: any): Promise<never> {
const request = { zkramprpc: '1.0', id: this.id, method, params }
const defer = deferredPromise()
this.#requests.set(request.id, defer)
window.postMessage(request, '*')
return defer.promise
}
}
23 changes: 14 additions & 9 deletions extension/src/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,18 @@
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';"
},
"permissions": [
"offscreen",
"storage",
"webRequest",
"activeTab",
"sidePanel",
"tabs",
"scripting"
]
"content_scripts": [
{
"matches": ["http://*/*", "https://*/*", "<all_urls>"],
"js": ["contentScript.js"],
"css": []
}
],
"web_accessible_resources": [
{
"resources": ["content.js"],
"matches": ["http://*/*", "https://*/*", "<all_urls>"]
}
],
"permissions": ["offscreen", "storage", "webRequest", "activeTab", "sidePanel", "tabs", "scripting"]
}
Loading
Loading