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: e2e api #102

Merged
merged 9 commits into from
Jul 6, 2022
110 changes: 73 additions & 37 deletions src/background/dapp-session.manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,48 @@ type LocalStorage = {
get: (storeValues: string[], callback: (result: { [key: string]: unknown }) => void) => void
}

class DappSecurityContext {
private readonly storage: LocalStorage
private readonly storePrefix: string
abstract class DappSecurityContext {
protected readonly storage: LocalStorage
protected readonly storePrefix: string

constructor(storePrefix: string) {
this.storage = chrome.storage.local
this.storePrefix = storePrefix
}

public abstract isValid(sender: chrome.runtime.MessageSender): boolean

/** STORAGE FUNCTIONS */

public async setStorageItem(keyName: string, keyValue: unknown): Promise<void> {
const key = this.enrichStorageKey(keyName)

await new Promise(resolve => this.storage.set({ [key]: keyValue }, () => resolve(true)))
}

public getStorageItem(keyName: string): Promise<unknown> {
const key = this.enrichStorageKey(keyName)

return new Promise(resolve =>
this.storage.get([key], result => {
resolve(result[key])
}),
)
}

private enrichStorageKey(keyName: string): string {
return `${this.storePrefix}-${keyName}`
}
}

class TabDappSecurityContext extends DappSecurityContext {
public constructor(
private tabId: number,
private frameId: number,
private frameContentRoot: string,
private originContentRoot: string,
) {
this.storage = chrome.storage.local
this.storePrefix = this.frameContentRoot || this.originContentRoot
super(frameContentRoot || originContentRoot)
}

public isValidTabId(tabId: number): boolean {
Expand All @@ -33,26 +64,30 @@ class DappSecurityContext {
return originContentRoot === this.originContentRoot
}

/** STORAGE FUNCTIONS */
public isValid(sender: chrome.runtime.MessageSender): boolean {
const tabId = senderTabId(sender)
const frameId = senderFrameId(sender)
const frameContentRoot = senderFrameOrigin(sender)
const originContentRoot = senderContentOrigin(sender)

public async setStorageItem(keyName: string, keyValue: unknown): Promise<void> {
const key = this.enrichStorageKey(keyName)
console.log(`tabid ${tabId} frameId ${frameId} frameconte ${frameContentRoot} originCon ${originContentRoot}`)

await new Promise(resolve => this.storage.set({ [key]: keyValue }, () => resolve(true)))
return (
this.isValidTabId(tabId) &&
this.isValidFrameId(frameId) &&
this.isFrameContentRoot(frameContentRoot) &&
this.isValidOriginContentRoot(originContentRoot)
)
}
}

public getStorageItem(keyName: string): Promise<unknown> {
const key = this.enrichStorageKey(keyName)

return new Promise(resolve =>
this.storage.get([key], result => {
resolve(result[key])
}),
)
class ExtensionDappSecurityContext extends DappSecurityContext {
constructor(private extensionId: string) {
super(extensionId)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe this will cause problem for allowed the storage key length. can you check what is the limit for that? Maybe would be better, if we transform the ID here to some prefixed and trimmed ID

}

private enrichStorageKey(keyName: string): string {
return `${this.storePrefix}-${keyName}`
public isValid(sender: chrome.runtime.MessageSender): boolean {
return sender.id === this.extensionId
}
}

Expand All @@ -63,12 +98,20 @@ export class DappSessionManager {
this.securityContexts = {}
}
public register(sessionId: string, sender: chrome.runtime.MessageSender): void {
const tabId = senderTabId(sender)
const frameId = senderFrameId(sender)
const frameContentRoot = senderFrameOrigin(sender)
const originContentRoot = senderContentOrigin(sender)
let context: DappSecurityContext | null = null

this.securityContexts[sessionId] = new DappSecurityContext(tabId, frameId, frameContentRoot, originContentRoot)
if (this.isSenderExtension(sender)) {
context = new ExtensionDappSecurityContext(String(sessionId))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here the sessionId is the extension ID?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm telling you because the ExtensionDappSecurityContext has extensionId parameter naming

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
context = new ExtensionDappSecurityContext(String(sessionId))
context = new ExtensionDappSecurityContext(sessionId)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, this sessionId/extensionId is not needed at all.

} else {
const tabId = senderTabId(sender)
const frameId = senderFrameId(sender)
const frameContentRoot = senderFrameOrigin(sender)
const originContentRoot = senderContentOrigin(sender)

context = new TabDappSecurityContext(tabId, frameId, frameContentRoot, originContentRoot)
}

this.securityContexts[sessionId] = context
console.log(`dApp session "${sessionId}" has been initialized`, this.securityContexts[sessionId])
}

Expand All @@ -80,19 +123,7 @@ export class DappSessionManager {

if (!context) return false

const tabId = senderTabId(sender)
const frameId = senderFrameId(sender)
const frameContentRoot = senderFrameOrigin(sender)
const originContentRoot = senderContentOrigin(sender)

console.log(`tabid ${tabId} frameId ${frameId} frameconte ${frameContentRoot} originCon ${originContentRoot}`)

return (
context.isValidTabId(tabId) &&
context.isValidFrameId(frameId) &&
context.isFrameContentRoot(frameContentRoot) &&
context.isValidOriginContentRoot(originContentRoot)
)
return context.isValid(sender)
}

public getStorageItem(
Expand Down Expand Up @@ -122,4 +153,9 @@ export class DappSessionManager {

return securityContext
}

private isSenderExtension(sender: chrome.runtime.MessageSender): boolean {
// TODO If support for other browsers is needed, then this function should be extended
return Boolean(sender.url?.startsWith('chrome-extension://'))
}
}
194 changes: 194 additions & 0 deletions src/background/feeder/e2e-session.feeder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import { nanoid } from 'nanoid'
import { bzzProtocolToFakeUrl } from '../../contentscript/swarm-library/bzz-link'
import { fakeUrl } from '../../utils/fake-url'
import { appendSwarmSessionIdToUrl } from '../../utils/swarm-session-id'
import { DappSessionManager } from '../dapp-session.manager'

enum Action {
REGISTER = 'register',
LOCAL_STORAGE_GET = 'localStorage.getItem',
LOCAL_STORAGE_SET = 'localStorage.setItem',
BZZ_LINK_PROTOCOL_TO_FAKE_URL = 'bzzLink.bzzProtocolToFakeUrl',
BZZ_LINK_LINK_URL_TO_FAKE_URL = 'bzzLink.bzzLinkUrlToFakeUrl',
BZZ_LINK_URL_TO_FAKE_URL = 'bzzLink.urlToFakeUrl',
WEB2_HELPER_FAKE_BEE_API_ADDRESS = 'web2Helper.fakeBeeApiAddress',
WEB2_HELPER_FAKE_BZZ_ADDRESS = 'web2Helper.fakeBzzAddress',
}

interface Request<A extends Action, P> {
action: A
sessionId: string
parameters: P
}

interface Response {
data?: any
error?: string
}

type RegisterRequest = Request<Action.REGISTER, void>
type LocalStorageGetRequest = Request<Action.LOCAL_STORAGE_GET, { name: string }>
type LocalStorageSetRequest = Request<Action.LOCAL_STORAGE_SET, { name: string; value: unknown }>
type BzzLinkProtocolToFakeUrlRequest = Request<Action.BZZ_LINK_PROTOCOL_TO_FAKE_URL, { url: string; newPage: boolean }>
type BzzLinkLinkUrlToFakeUrlRequest = Request<
Action.BZZ_LINK_LINK_URL_TO_FAKE_URL,
{ bzzLinkUrl: string; newPage: boolean }
>
type BzzLinkUrlToFakeUrlRequest = Request<Action.BZZ_LINK_URL_TO_FAKE_URL, { url: string; newPage: boolean }>
type Web2HelperFakeBeeApiAddressRequesst = Request<Action.WEB2_HELPER_FAKE_BEE_API_ADDRESS, void>
type Web2HelprFakeBzzAddressRequest = Request<Action.WEB2_HELPER_FAKE_BZZ_ADDRESS, { reference: string }>

type RequestType =
| RegisterRequest
| LocalStorageGetRequest
| LocalStorageSetRequest
| BzzLinkProtocolToFakeUrlRequest
| BzzLinkLinkUrlToFakeUrlRequest
| BzzLinkUrlToFakeUrlRequest
| Web2HelperFakeBeeApiAddressRequesst
| Web2HelprFakeBzzAddressRequest

export class E2ESessionFeeder {
constructor(private manager: DappSessionManager) {
this.serveEvents()
}

serveEvents(): void {
console.log('Register E2ESessionFeeder event listeners...')

// register dapp session id
chrome.runtime.onMessageExternal.addListener(
async (
request: RequestType,
sender: chrome.runtime.MessageSender,
sendResponse: (response?: Response) => void,
) => {
const { action } = request || {}
const senderId = sender.id as string
const response: Response = {}

try {
if (!senderId) {
throw new Error('Invalid extension ID')
}

if (action === Action.REGISTER) {
response.data = this.handleRegistration(sender)
} else if (action === Action.LOCAL_STORAGE_GET) {
response.data = await this.handleLocalStorageGet(request)
} else if (action === Action.LOCAL_STORAGE_SET) {
await this.handleLocalStorageSet(request)
} else if (action === Action.WEB2_HELPER_FAKE_BEE_API_ADDRESS) {
response.data = this.handleWeb2FakeBeeApiAddress(request)
} else if (action === Action.WEB2_HELPER_FAKE_BZZ_ADDRESS) {
response.data = this.handleWeb2FakeBzzAddress(request)
} else if (action === Action.BZZ_LINK_PROTOCOL_TO_FAKE_URL) {
response.data = this.handleBzzLinkProtocolToFakeUrl(request)
} else if (action === Action.BZZ_LINK_LINK_URL_TO_FAKE_URL) {
response.data = this.handleBzzLinkUrlToFakeUrl(request)
} else if (action === Action.BZZ_LINK_URL_TO_FAKE_URL) {
response.data = this.handleBzzUrlToFakeUrl(request)
} else {
throw new Error(`Unknown action ${action}`)
}
} catch (error) {
response.error = String(error)
}

this.sendResponse(response, action, senderId, sendResponse)
},
)
}

private sendResponse(
response: Response | null,
action: Action,
senderId: string,
sendResponse: (response?: Response) => void,
) {
if (response?.error) {
console.warn(
`Sending error response for action '${action}' to the extension with ID '${senderId}'.`,
response.error,
)
} else {
console.log(`The extension with ID '${senderId}' successfully invoked action '${action}'.`)
}
sendResponse(response || {})
}

private handleRegistration(sender: chrome.runtime.MessageSender): string {
const sessionId = nanoid()
this.manager.register(sessionId, sender)

return sessionId
}

private handleLocalStorageGet(request: RequestType): Promise<unknown> {
const {
sessionId,
parameters: { name },
} = request as LocalStorageGetRequest

if (!name) {
throw new Error('Cannot get item from localStorage. Item name required')
}

return this.manager.getStorageItem(sessionId, name)
}

private handleLocalStorageSet(request: RequestType): Promise<void> {
const {
sessionId,
parameters: { name, value },
} = request as LocalStorageSetRequest

if (!name) {
throw new Error('Cannot get item from localStorage. Item name required')
}

return this.manager.setStorageItem(sessionId, name, value)
}

private handleBzzLinkProtocolToFakeUrl(request: RequestType): string | null {
const {
sessionId,
parameters: { url, newPage },
} = request as BzzLinkProtocolToFakeUrlRequest

return bzzProtocolToFakeUrl(url, sessionId, newPage)
}

private handleBzzLinkUrlToFakeUrl(request: RequestType): string | null {
const {
sessionId,
parameters: { bzzLinkUrl, newPage },
} = request as BzzLinkLinkUrlToFakeUrlRequest

return bzzProtocolToFakeUrl(bzzLinkUrl, sessionId, newPage)
}

private handleBzzUrlToFakeUrl(request: RequestType): string | null {
const {
sessionId,
parameters: { url, newPage },
} = request as BzzLinkUrlToFakeUrlRequest

return bzzProtocolToFakeUrl(url, sessionId, newPage)
}

private handleWeb2FakeBeeApiAddress(request: RequestType): string {
const { sessionId } = request

return appendSwarmSessionIdToUrl(fakeUrl.beeApiAddress, sessionId)
}

private handleWeb2FakeBzzAddress(request: RequestType): string {
const {
sessionId,
parameters: { reference },
} = request as Web2HelprFakeBzzAddressRequest

return appendSwarmSessionIdToUrl(`${fakeUrl.bzzProtocol}/${reference}`, sessionId)
}
Comment on lines +125 to +165
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic could be completely on the other extension's side and you could save the async calls.

}
2 changes: 2 additions & 0 deletions src/background/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { StoreObserver } from '../utils/storage'
import { DappSessionManager } from './dapp-session.manager'
import { DappSessionFeeder } from './feeder/dapp-session.feeder'
import { E2ESessionFeeder } from './feeder/e2e-session.feeder'
import { LocalStorageFeeder } from './feeder/local-storage.feeder'
import { Web2HelperFeeder } from './feeder/web2-helper.feeder'
import { BeeApiListener } from './listener/bee-api.listener'
Expand All @@ -12,6 +13,7 @@ const storeObserver = new StoreObserver()
const beeApiListener = new BeeApiListener(storeObserver)
const dappSessionManager = new DappSessionManager()
new DappSessionFeeder(dappSessionManager)
new E2ESessionFeeder(dappSessionManager)
new LocalStorageFeeder(dappSessionManager)
new DebugListener()
new Web2HelperFeeder(beeApiListener)
Expand Down
21 changes: 21 additions & 0 deletions src/contentscript/swarm-library/bzz-link-page-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {
bzzProtocolToFakeUrl as bzzProtocolToFakeUrlFn,
bzzLinkUrlToFakeUrl as bzzLinkUrlToFakeUrlFn,
urlToFakeUrl as urlToFakeUrlFn,
} from './bzz-link'

/** gives back the fake URL of the BZZ protocol reference or null if the first parameter is not a valid BZZ protocol reference */
export function bzzProtocolToFakeUrl(url: string, newPage = false): string | null {
return bzzProtocolToFakeUrlFn(url, window.swarm.sessionId, newPage)
}

/** gives back the fake URL of the bzz.link or null if the first parameter is not a valid bzz.link reference */
export function bzzLinkUrlToFakeUrl(bzzLinkUrl: string, newPage = false): string | null {
return bzzLinkUrlToFakeUrlFn(bzzLinkUrl, window.swarm.sessionId, newPage)
}

/** transform the given URL to FakeURL or return null if it is not possible */
export function urlToFakeUrl(url: string, newPage = false): string | null {
return urlToFakeUrlFn(url, window.swarm.sessionId, newPage)
}
export * from '../../utils/bzz-link'
Loading