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

abort deapproved transports, use timeouts #116

Merged
merged 5 commits into from
Jul 25, 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
4 changes: 2 additions & 2 deletions apps/extension/src/clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { ViewService } from '@penumbra-zone/protobuf';
import { createPromiseClient } from '@connectrpc/connect';
import { createChannelTransport } from '@penumbra-zone/transport-dom/create';
import { CRSessionClient } from '@penumbra-zone/transport-chrome/session-client';
import { jsonOptions } from '@penumbra-zone/protobuf';
import { internalTransportOptions } from './transport-options';

const port = CRSessionClient.init(PRAX);

const extensionPageTransport = createChannelTransport({
jsonOptions,
...internalTransportOptions,
getPort: () => Promise.resolve(port),
});

Expand Down
2 changes: 2 additions & 0 deletions apps/extension/src/listeners/chrome-message-listener.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
declare type ChromeExtensionMessageEventListener =
chrome.runtime.ExtensionMessageEvent extends chrome.events.Event<infer L> ? L : never;
29 changes: 29 additions & 0 deletions apps/extension/src/listeners/content-script/disconnect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { PraxConnection } from '../../message/prax';
import { alreadyApprovedSender } from '../../senders/approve';
import { revokeOrigin } from '../../senders/revoke';
import { assertValidSender } from '../../senders/validate';

// listen for page requests for disconnect
export const praxDisconnectListener: ChromeExtensionMessageEventListener = (
req,
unvalidatedSender,
// this handler will only ever send an empty response
respond: (no?: never) => void,
) => {
if (req !== PraxConnection.Disconnect) {
// boolean return in handlers signals intent to respond
return false;
}

const validSender = assertValidSender(unvalidatedSender);
void alreadyApprovedSender(validSender).then(hasApproval => {
if (!hasApproval) {
throw new Error('Sender does not possess approval');
}
revokeOrigin(validSender.origin);
});
respond();

// boolean return in handlers signals intent to respond
return true;
};
35 changes: 35 additions & 0 deletions apps/extension/src/listeners/content-script/init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { PraxConnection } from '../../message/prax';
import { alreadyApprovedSender } from '../../senders/approve';
import { assertValidSender } from '../../senders/validate';

// listen for page init
export const praxInitListener: ChromeExtensionMessageEventListener = (
req,
unvalidatedSender,
// this handler will only ever send an empty response
resepond: (no?: never) => void,
) => {
if (req !== PraxConnection.Init) {
// boolean return in handlers signals intent to respond
return false;
}

const validSender = assertValidSender(unvalidatedSender);

void (async () => {
const alreadyApproved = await alreadyApprovedSender(validSender);
if (alreadyApproved) {
void chrome.tabs.sendMessage(validSender.tab.id, PraxConnection.Init, {
// init only the specific document
frameId: validSender.frameId,
documentId: validSender.documentId,
});
}
})();

// handler is done
resepond();

// boolean return in handlers signals intent to respond
return true;
};
58 changes: 58 additions & 0 deletions apps/extension/src/listeners/content-script/request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Code, ConnectError } from '@connectrpc/connect';
import { PenumbraRequestFailure } from '@penumbra-zone/client';
import { UserChoice } from '@penumbra-zone/types/user-choice';
import { PraxConnection } from '../../message/prax';
import { approveSender } from '../../senders/approve';
import { assertValidSender } from '../../senders/validate';

// listen for page requests for approval
export const praxRequestListener: ChromeExtensionMessageEventListener = (
req,
unvalidatedSender,
// this handler responds with nothing, or an enumerated failure reason
respond: (failure?: PenumbraRequestFailure) => void,
) => {
if (req !== PraxConnection.Request) {
// boolean return in handlers signals intent to respond
return false;
}

const validSender = assertValidSender(unvalidatedSender);

void approveSender(validSender).then(
status => {
// origin is already known, or popup choice was made
if (status === UserChoice.Approved) {
void chrome.tabs.sendMessage(validSender.tab.id, PraxConnection.Init, {
// init only the specific document
frameId: validSender.frameId,
documentId: validSender.documentId,
});
// handler is done
respond();
} else {
// any other choice is a denial
respond(PenumbraRequestFailure.Denied);
}
},
e => {
// something is wrong. user may not have seen a popup
if (globalThis.__DEV__) {
console.warn('Connection request listener failed:', e);
}

if (e instanceof ConnectError && e.code === Code.Unauthenticated) {
// the website should instruct the user to log in
respond(PenumbraRequestFailure.NeedsLogin);
} else {
// something strange is happening. either storage is broken, the popup
// returned an error, the sender is invalid, or someone's misbehaving.
// obfuscate this rejection with a random delay 2-12 secs
setTimeout(() => respond(PenumbraRequestFailure.Denied), 2000 + Math.random() * 10000);
}
},
);

// boolean return in handlers signals intent to respond
return true;
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// this is temporary code to use the externally_connectable permission,
// also providing an easter egg for curious users
chrome.runtime.onMessageExternal.addListener((_, __, response) => {
export const praxEasterEgg: ChromeExtensionMessageEventListener = (_, __, response) => {
response('penumbra is the key');
return true;
});
};
22 changes: 18 additions & 4 deletions apps/extension/src/listeners/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,18 @@
import './message-external';
import './message-prax-disconnect';
import './message-prax-init';
import './message-prax-request';
import { praxDisconnectListener } from './content-script/disconnect';
import { praxInitListener } from './content-script/init';
import { praxRequestListener } from './content-script/request';

import { praxRevokeListener } from './internal/revoke';

import { praxEasterEgg } from './external/message-external';

// content-script messages
chrome.runtime.onMessage.addListener(praxInitListener);
chrome.runtime.onMessage.addListener(praxDisconnectListener);
chrome.runtime.onMessage.addListener(praxRequestListener);

// internal messages
chrome.runtime.onMessage.addListener(praxRevokeListener);

// external messages
chrome.runtime.onMessageExternal.addListener(praxEasterEgg);
17 changes: 17 additions & 0 deletions apps/extension/src/listeners/internal/revoke.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { isPraxRevoke } from '../../message/prax';
import { revokeOrigin } from '../../senders/revoke';
import { isInternalSender } from '../../senders/internal';

// listen for internal revoke commands
export const praxRevokeListener: ChromeExtensionMessageEventListener = (
req,
sender,
respond: (no?: never) => void,
) => {
if (!isInternalSender(sender) || !isPraxRevoke(req)) {
return false;
}
revokeOrigin(req.revoke);
respond();
return true;
};
25 changes: 0 additions & 25 deletions apps/extension/src/listeners/message-prax-disconnect.ts

This file was deleted.

37 changes: 0 additions & 37 deletions apps/extension/src/listeners/message-prax-init.ts

This file was deleted.

60 changes: 0 additions & 60 deletions apps/extension/src/listeners/message-prax-request.ts

This file was deleted.

7 changes: 7 additions & 0 deletions apps/extension/src/message/prax.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,10 @@ export enum PraxConnection {
Request = 'Request',
Disconnect = 'Disconnect',
}

export interface PraxRevoke {
revoke: string;
}

export const isPraxRevoke = (req: unknown): req is PraxRevoke =>
req != null && typeof req === 'object' && 'revoke' in req && typeof req.revoke === 'string';
21 changes: 0 additions & 21 deletions apps/extension/src/senders/disconnect.ts

This file was deleted.

2 changes: 2 additions & 0 deletions apps/extension/src/senders/internal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const isInternalSender = (sender: chrome.runtime.MessageSender): boolean =>
sender.origin === origin && sender.id === chrome.runtime.id;
11 changes: 11 additions & 0 deletions apps/extension/src/senders/revoke.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { removeOriginRecord } from '../storage/origin';
import { CRSessionManager } from '@penumbra-zone/transport-chrome/session-manager';

/**
* Immediately remove any origin record from storage, and immediately kill any
* active connection to the origin.
*/
export const revokeOrigin = (targetOrigin: string) => {
void removeOriginRecord(targetOrigin);
CRSessionManager.killOrigin(targetOrigin);
};
5 changes: 3 additions & 2 deletions apps/extension/src/service-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { StakeService, CustodyService } 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';
Expand All @@ -61,8 +62,8 @@ const initHandler = async () => {
const contextValues = req.contextValues ?? createContextValues();

// initialize or reuse context clients
custodyClient ??= createDirectClient(CustodyService, handler, { jsonOptions });
stakeClient ??= createDirectClient(StakeService, handler, { jsonOptions });
custodyClient ??= createDirectClient(CustodyService, handler, internalTransportOptions);
stakeClient ??= createDirectClient(StakeService, handler, internalTransportOptions);
contextValues.set(custodyClientCtx, custodyClient);
contextValues.set(stakeClientCtx, stakeClient);

Expand Down
1 change: 1 addition & 0 deletions apps/extension/src/state/connected-sites.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const createConnectedSitesSlice =
known => known.origin !== siteToDiscard.origin,
);
await local.set('knownSites', knownSitesWithoutDiscardedSite);
void chrome.runtime.sendMessage({ revoke: siteToDiscard.origin });
},
});

Expand Down
6 changes: 6 additions & 0 deletions apps/extension/src/transport-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { jsonOptions } from '@penumbra-zone/protobuf';

export const internalTransportOptions = {
defaultTimeoutMs: 0,
jsonOptions,
};
Loading