Skip to content

Commit

Permalink
session client reconnect (#289)
Browse files Browse the repository at this point in the history
  • Loading branch information
turbocrime authored Feb 19, 2025
1 parent 04eef12 commit 5c99283
Show file tree
Hide file tree
Showing 6 changed files with 47 additions and 34 deletions.
17 changes: 1 addition & 16 deletions apps/extension/src/content-scripts/injected-listeners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,36 +11,27 @@ const requestFailureMessage = (failure?: unknown): PraxMessage<PenumbraRequestFa
: { [PRAX]: PenumbraRequestFailure.BadResponse };

const praxRequest = async (req: PraxConnection.Connect | PraxConnection.Disconnect) => {
console.debug('praxRequest req', req);
const res = await chrome.runtime
.sendMessage<
PraxConnection.Connect | PraxConnection.Disconnect,
null | PenumbraRequestFailure
>(req)
.catch(e => {
console.debug('praxRequest error', e);
return PenumbraRequestFailure.NotHandled;
});
console.debug('praxRequest res', res);
.catch(() => PenumbraRequestFailure.NotHandled);
return res;
};

const praxDocumentListener = (ev: MessageEvent<unknown>) => {
if (ev.origin === window.origin && isPraxMessageEvent(ev)) {
const req = unwrapPraxMessageEvent(ev);
if (typeof req === 'string' && req in PraxConnection) {
console.debug('window event', req);

void (async () => {
let response: unknown;

switch (req as PraxConnection) {
case PraxConnection.Connect:
console.debug('using window event', PraxConnection.Connect);
response = await praxRequest(PraxConnection.Connect);
break;
case PraxConnection.Disconnect:
console.debug('using window event', PraxConnection.Disconnect);
response = await praxRequest(PraxConnection.Disconnect);
break;
default: // message is not for this handler
Expand All @@ -50,11 +41,9 @@ const praxDocumentListener = (ev: MessageEvent<unknown>) => {
// response should be null, or content for a failure message
if (response != null) {
// failure, send failure message
console.debug('window event failure', response);
window.postMessage(requestFailureMessage(response), '/');
} else {
// success, no response
console.debug('window event success');
}
})();
}
Expand All @@ -67,17 +56,13 @@ const praxExtensionListener = (
ok: (no?: never) => void,
) => {
if (sender.id === PRAX && typeof req === 'string' && req in PraxConnection) {
console.debug('extension event', req);

switch (req as PraxConnection) {
case PraxConnection.Init: {
console.debug('using extension event', req);
const port = CRSessionClient.init(PRAX);
window.postMessage(portMessage(port), '/', [port]);
break;
}
case PraxConnection.End: {
console.debug('using extension event', req);
window.postMessage(endMessage, '/');
break;
}
Expand Down
3 changes: 2 additions & 1 deletion apps/extension/src/senders/approve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import { OriginApproval, PopupType } from '../message/popup';
import { popup } from '../popup';
import { UserChoice } from '@penumbra-zone/types/user-choice';
import { getOriginRecord, upsertOriginRecord } from '../storage/origin';
import { ValidSender } from './validate';

/**
* Obtain approval status from storage, as boolean.
*
* @param validSender A sender that has already been validated
* @returns true if an existing record indicates this sender is approved
*/
export const alreadyApprovedSender = async (validSender: { origin: string }): Promise<boolean> =>
export const alreadyApprovedSender = async (validSender: ValidSender): Promise<boolean> =>
getOriginRecord(validSender.origin).then(r => r?.choice === UserChoice.Approved);

/**
Expand Down
16 changes: 14 additions & 2 deletions apps/extension/src/senders/internal.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,14 @@
export const isInternalSender = (sender: chrome.runtime.MessageSender): boolean =>
sender.origin === origin && sender.id === chrome.runtime.id;
export const isInternalSender = (
sender?: chrome.runtime.MessageSender,
): sender is InternalSender => {
if (sender?.origin && sender.id === chrome.runtime.id) {
const senderUrl = new URL(sender.origin);
return senderUrl.protocol === 'chrome-extension:' && senderUrl.host === chrome.runtime.id;
}
return false;
};

export type InternalSender = chrome.runtime.MessageSender & {
origin: string;
id: string;
};
16 changes: 16 additions & 0 deletions apps/extension/src/senders/session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { alreadyApprovedSender } from './approve';
import { assertValidSender, type ValidSender } from './validate';
import { type InternalSender, isInternalSender } from './internal';

export const assertValidSessionPort = async (port: chrome.runtime.Port) => {
if (isInternalSender(port.sender)) {
return port as chrome.runtime.Port & { sender: InternalSender };
}

const validSender = assertValidSender(port.sender);
if (await alreadyApprovedSender(validSender)) {
return port as chrome.runtime.Port & { sender: ValidSender };
}

throw new Error('Session sender is not approved', { cause: port.sender?.origin });
};
20 changes: 9 additions & 11 deletions apps/extension/src/senders/validate.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
enum ValidProtocol {
'https:' = 'https:',
}
const validProtocols = ['https:'];

type ValidSender = chrome.runtime.MessageSender & {
export type ValidSender = chrome.runtime.MessageSender & {
frameId: 0;
documentId: string;
tab: chrome.tabs.Tab & { id: number };
Expand All @@ -13,7 +11,7 @@ type ValidSender = chrome.runtime.MessageSender & {
const isHttpLocalhost = (url: URL): boolean =>
url.protocol === 'http:' && url.hostname === 'localhost';

export const assertValidSender = (sender?: chrome.runtime.MessageSender) => {
export const assertValidSender = (sender?: chrome.runtime.MessageSender): ValidSender => {
if (!sender) {
throw new Error('Sender undefined');
}
Expand All @@ -35,8 +33,12 @@ export const assertValidSender = (sender?: chrome.runtime.MessageSender) => {
throw new Error('Sender origin is invalid');
}

if (!(parsedOrigin.protocol in ValidProtocol || isHttpLocalhost(parsedOrigin))) {
throw new Error(`Sender protocol is not ${Object.values(ValidProtocol).join(',')}`);
if (!validProtocols.includes(parsedOrigin.protocol)) {
if (isHttpLocalhost(parsedOrigin)) {
console.warn('Allowing http at localhost', parsedOrigin);
} else {
throw new Error(`Sender protocol is not ${validProtocols.join(',')}`);
}
}

if (!sender.url) {
Expand All @@ -50,9 +52,5 @@ export const assertValidSender = (sender?: chrome.runtime.MessageSender) => {
throw new Error('Sender URL has unexpected origin');
}

// TODO: externally_connectable can use more sender data
//if (!sender.tlsChannelId) throw new Error('Sender has no tlsChannelId');
//if (!sender.id) throw new Error('Sender has no crx id');

return sender as ValidSender;
};
9 changes: 5 additions & 4 deletions apps/extension/src/service-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { ConnectRouter, createContextValues, PromiseClient } from '@connectrpc/c
import { jsonOptions } from '@penumbra-zone/protobuf';
import { CRSessionManager } from '@penumbra-zone/transport-chrome/session-manager';
import { connectChannelAdapter } from '@penumbra-zone/transport-dom/adapter';
import { assertValidSessionPort } from './senders/session';

// context
import { approverCtx } from '@penumbra-zone/services/ctx/approver';
Expand All @@ -27,19 +28,19 @@ import { servicesCtx } from '@penumbra-zone/services/ctx/prax';
import { skCtx } from '@penumbra-zone/services/ctx/spend-key';
import { approveTransaction } from './approve-transaction';
import { getFullViewingKey } from './ctx/full-viewing-key';
import { getWalletId } from './ctx/wallet-id';
import { getSpendKey } from './ctx/spend-key';
import { getWalletId } from './ctx/wallet-id';

// context clients
import { StakeService, CustodyService } from '@penumbra-zone/protobuf';
import { CustodyService, StakeService } from '@penumbra-zone/protobuf';
import { custodyClientCtx } from '@penumbra-zone/services/ctx/custody-client';
import { stakeClientCtx } from '@penumbra-zone/services/ctx/stake-client';
import { createDirectClient } from '@penumbra-zone/transport-dom/direct';
import { internalTransportOptions } from './transport-options';

// idb, querier, block processor
import { startWalletServices } from './wallet-services';
import { walletIdCtx } from '@penumbra-zone/services/ctx/wallet-id';
import { startWalletServices } from './wallet-services';

import { backOff } from 'exponential-backoff';

Expand Down Expand Up @@ -95,7 +96,7 @@ const handler = await backOff(() => initHandler(), {
},
});

CRSessionManager.init(PRAX, handler);
CRSessionManager.init(PRAX, handler, assertValidSessionPort);

// https://developer.chrome.com/docs/extensions/reference/api/alarms
void chrome.alarms.create('blockSync', {
Expand Down

0 comments on commit 5c99283

Please sign in to comment.