Compare commits

..

2 Commits

Author SHA1 Message Date
Codetrauma
fdaf6ca815 refactor: linting + deleting clogs 2024-06-14 14:36:44 -07:00
Codetrauma
511272a451 feat: adding content-length header to notarization 2024-06-14 14:18:33 -07:00
76 changed files with 11442 additions and 7956 deletions

View File

@@ -42,7 +42,7 @@ at your option.
```
$ git clone https://github.com/novnc/websockify && cd websockify
$ ./docker/build.sh
$ docker run -it --rm -p 55688:80 novnc/websockify 80 api.x.com:443
$ docker run -it --rm -p 55688:80 novnc/websockify 80 api.twitter.com:443
```
## Running Websockify Docker Image

6533
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "tlsn-extension",
"version": "0.1.0.700",
"version": "0.1.0.5",
"license": "MIT",
"repository": {
"type": "git",
@@ -27,7 +27,6 @@
"fast-deep-equal": "^3.1.3",
"fuse.js": "^6.6.2",
"level": "^8.0.0",
"minimatch": "^9.0.4",
"node-cache": "^5.1.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@@ -38,8 +37,7 @@
"redux-logger": "^3.0.6",
"redux-thunk": "^2.4.2",
"tailwindcss": "^3.3.3",
"tlsn-js": "0.1.0-alpha.7.1",
"tlsn-js-v5": "npm:tlsn-js@0.1.0-alpha.5.4"
"tlsn-js": "0.1.0-alpha.5.3"
},
"devDependencies": {
"@babel/core": "^7.20.12",

48
plugins/README.md Normal file
View File

@@ -0,0 +1,48 @@
# Plugin Development for the TLSNotary Browser Extension
This folder is dedicated to the development of plugins for the TLSNotary browser extension, utilizing the Extism framework. Currently, the folder includes a TypeScript-based plugin example, `twitter_profile`, with plans to add more plugins showcasing different programming languages and functionalities.
## Installation of Extism-js
1. **Download and Install Extism-js**: Begin by setting up `extism-js`, which enables you to compile and manage your plugins. Run these commands to download and install it:
```sh
curl -O https://raw.githubusercontent.com/extism/js-pdk/main/install.sh
sh install.sh
```
This script installs the Extism JavaScript Plugin Development Kit from its GitHub repository, preparing your environment for plugin compilation.
## Building the Twitter Profile Plugin
Navigate to the `twitter_profile` directory within this folder and run the following command to build the plugin:
```sh
extism-js index.js -i index.d.ts -o index.wasm
```
This command compiles the TypeScript code in index.js into a WebAssembly module, ready for integration with the TLSNotary extension.
### Running the Twitter Plugin Example:
1. Build the `twitter_profile` plugin as explained above.
2. Build and install the `tlsn-extension` as documented in the [main README.md](../README.md).
3. [Run a local notary server](https://github.com/tlsnotary/tlsn/blob/main/notary-server/README.md), ensuring `TLS` is disabled in the [config file](https://github.com/tlsnotary/tlsn/blob/main/notary-server/config/config.yaml#L18).
4. Install the plugin: Click the **Add a Plugin (+)** button and select the `index.wasm` file you built in step 1. A **Twitter Profile** button should then appear below the default buttons.
5. Click the **Twitter Profile** button. This action opens the Twitter webpage along with a TLSNotary sidebar.
6. Follow the steps in the TLSNotary sidebar.
7. Access the TLSNotary results by clicking the **History** button in the TLSNotary extension.
## Future Plugins
This directory will be expanded with more plugins designed to demonstrate the functionality of the TLSNotary extension. Plugins enable flexible use of the TLSNotary across a broad range of applications. The use of Extism facilitates plugin development in various languages, further enhancing flexibility.
## Create an icon
1. resize to 320x320 pixels:
```sh
convert icon.png -resize 320x320! icon_320.png
```
2. convert to base64
```sh
base64 -i icon_320.png -o icon_320.txt
```

4
plugins/hello/hello.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
declare module 'main' {
// Extism exports take no params and return an I32
export function hello(): I32;
}

6
plugins/hello/hello.js Normal file
View File

@@ -0,0 +1,6 @@
function hello() {
const name = Host.inputString();
Host.outputString(`Hello, ${name}`);
}
module.exports = { hello };

BIN
plugins/hello/hello.wasm Normal file

Binary file not shown.

14
plugins/reddit/index.d.ts vendored Normal file
View File

@@ -0,0 +1,14 @@
declare module 'main' {
// Extism exports take no params and return an I32
export function start(): I32;
export function two(): I32;
export function three(): I32;
export function config(): I32;
}
declare module 'extism:host' {
interface user {
redirect(ptr: I64): void;
notarize(ptr: I64): I64;
}
}

94
plugins/reddit/index.js Normal file

File diff suppressed because one or more lines are too long

14
plugins/twitter_profile/index.d.ts vendored Normal file
View File

@@ -0,0 +1,14 @@
declare module 'main' {
// Extism exports take no params and return an I32
export function start(): I32;
export function two(): I32;
export function three(): I32;
export function config(): I32;
}
declare module 'extism:host' {
interface user {
redirect(ptr: I64): void;
notarize(ptr: I64): I64;
}
}

File diff suppressed because one or more lines are too long

25
pnpm-lock.yaml generated
View File

@@ -38,9 +38,6 @@ dependencies:
level:
specifier: ^8.0.0
version: 8.0.1
minimatch:
specifier: ^9.0.4
version: 9.0.4
node-cache:
specifier: ^5.1.2
version: 5.1.2
@@ -72,11 +69,8 @@ dependencies:
specifier: ^3.3.3
version: 3.4.3
tlsn-js:
specifier: 0.1.0-alpha.7.1
version: 0.1.0-alpha.7.1
tlsn-js-v5:
specifier: npm:tlsn-js@0.1.0-alpha.5.4
version: /tlsn-js@0.1.0-alpha.5.4
specifier: 0.1.0-alpha.5.3
version: 0.1.0-alpha.5.3
devDependencies:
'@babel/core':
@@ -7974,24 +7968,13 @@ packages:
resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==}
dev: true
/tlsn-js@0.1.0-alpha.5.4:
resolution: {integrity: sha512-qbqaDjApXarohn/XMJrxMsNHYTCzy+pw0Fc8gtPPN17PLE+1DwwLTXAAhnxYqYQyo3+Hmy+ODd4C02+KCwRWmg==}
/tlsn-js@0.1.0-alpha.5.3:
resolution: {integrity: sha512-eTSCQ6MaH8mN2oCfLsQDe/mdfTlIq74MbKVesV6M01C2SRE0FJcVlTWMsnT3L+wOpvOlvsiBeCWV7T9m/YMzew==}
engines: {node: '>= 16.20.2'}
dependencies:
comlink: 4.4.1
dev: false
/tlsn-js@0.1.0-alpha.7.1:
resolution: {integrity: sha512-EWdRp1VQBfdre8jehJgmDjtDvt01ZL1JWbcscctnFTLIIwMYS7IBxU07UYG0NMZFXeTE8PlrUDEVwEl1+vla+g==}
engines: {node: '>= 16.20.2'}
dependencies:
tlsn-wasm: 0.1.0-alpha.7.2
dev: false
/tlsn-wasm@0.1.0-alpha.7.2:
resolution: {integrity: sha512-NzrDfOxmFtMHDb4lmMsx6RaS6F+IVXEHxK0zow8jpnx+NryuJ+qnp4380Lq0uj61w/Yuq+yzOhzFe6Bpeo59dA==}
dev: false
/tmp@0.0.33:
resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
engines: {node: '>=0.6.0'}

Binary file not shown.

View File

@@ -1,70 +0,0 @@
import React, { useCallback, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import {
useActiveTabUrl,
setConnection,
useIsConnected,
} from '../../reducers/requests';
import Modal, { ModalHeader, ModalContent } from '../../components/Modal/Modal';
import { deleteConnection, getConnection } from '../../entries/Background/db';
const ConnectionDetailsModal = (props: {
showConnectionDetails: boolean;
setShowConnectionDetails: (show: boolean) => void;
}) => {
const dispatch = useDispatch();
const activeTabOrigin = useActiveTabUrl();
const connected = useIsConnected();
useEffect(() => {
(async () => {
if (activeTabOrigin) {
const isConnected: boolean | null = await getConnection(
activeTabOrigin.origin,
);
dispatch(setConnection(!!isConnected));
}
})();
}, [activeTabOrigin, dispatch]);
const handleDisconnect = useCallback(async () => {
if (activeTabOrigin?.origin) {
await deleteConnection(activeTabOrigin.origin);
props.setShowConnectionDetails(false);
dispatch(setConnection(false));
}
}, [activeTabOrigin?.origin, dispatch, props]);
return (
<Modal
onClose={() => props.setShowConnectionDetails(false)}
className="flex flex-col gap-2 items-center text-base cursor-default justify-center mx-4 min-h-24"
>
<ModalHeader
className="w-full rounded-t-lg pb-0 border-b-0"
onClose={() => props.setShowConnectionDetails(false)}
>
<span className="text-lg font-semibold">
{activeTabOrigin?.hostname || 'Connections'}
</span>
</ModalHeader>
<ModalContent className="w-full gap-2 flex-grow flex flex-col items-center justify-between px-4 pt-0 pb-4">
<div className="flex flex-row gap-2 items-start w-full text-xs font-semibold text-slate-800">
{connected
? 'TLSN Extension is connected to this site.'
: 'TLSN Extension is not connected to this site. To connect to this site, find and click the connect button.'}
</div>
{connected && (
<button
className="button disabled:opacity-50 self-end"
onClick={handleDisconnect}
>
Disconnect
</button>
)}
</ModalContent>
</Modal>
);
};
export default ConnectionDetailsModal;

View File

@@ -1,115 +0,0 @@
import React, {
MouseEventHandler,
ReactElement,
ReactNode,
useCallback,
useState,
} from 'react';
import Icon from '../Icon';
import browser from 'webextension-polyfill';
import classNames from 'classnames';
import { useNavigate } from 'react-router';
import PluginUploadInfo from '../PluginInfo';
export function MenuIcon(): ReactElement {
const [opened, setOpen] = useState(false);
const toggleMenu = useCallback(() => {
setOpen(!opened);
}, [opened]);
return (
<div className="relative">
{opened && (
<>
<div
className="fixed top-0 left-0 w-screen h-screen z-10"
onClick={toggleMenu}
/>
<Menu opened={opened} setOpen={setOpen} />
</>
)}
<Icon
fa="fa-solid fa-bars"
className="text-slate-500 hover:text-slate-700 active:text-slate-900 cursor-pointer z-20"
onClick={toggleMenu}
/>
</div>
);
}
export default function Menu(props: {
opened: boolean;
setOpen: (opened: boolean) => void;
}): ReactElement {
const navigate = useNavigate();
const openExtensionInPage = () => {
props.setOpen(false);
browser.tabs.create({
url: `chrome-extension://${chrome.runtime.id}/popup.html`,
});
};
return (
<div className="absolute top-[100%] right-0 rounded-md z-20">
<div className="flex flex-col bg-slate-200 w-40 shadow rounded-md py">
<MenuRow
fa="fa-solid fa-plus"
className="relative"
onClick={() => {
props.setOpen(false);
}}
>
<PluginUploadInfo />
<span>Install Plugin</span>
</MenuRow>
<MenuRow
fa="fa-solid fa-toolbox"
className="border-b border-slate-300"
onClick={() => {
props.setOpen(false);
navigate('/plugins');
}}
>
Plugins
</MenuRow>
<MenuRow
className="lg:hidden"
fa="fa-solid fa-up-right-and-down-left-from-center"
onClick={openExtensionInPage}
>
Expand
</MenuRow>
<MenuRow
fa="fa-solid fa-gear"
onClick={() => {
props.setOpen(false);
navigate('/options');
}}
>
Options
</MenuRow>
</div>
</div>
);
}
function MenuRow(props: {
fa: string;
children?: ReactNode;
onClick?: MouseEventHandler;
className?: string;
}): ReactElement {
return (
<div
className={classNames(
'flex flex-row items-center py-3 px-4 gap-2 hover:bg-slate-300 cursor-pointer text-slate-800 hover:text-slate-900 font-semibold',
props.className,
)}
onClick={props.onClick}
>
<Icon size={0.875} fa={props.fa} />
{props.children}
</div>
);
}

View File

@@ -37,19 +37,13 @@ export default function Modal(props: Props): ReactElement {
}
type HeaderProps = {
className?: string;
onClose?: () => void;
children?: ReactNode;
children: ReactNode;
};
export function ModalHeader(props: HeaderProps): ReactElement {
return (
<div
className={classNames(
'border-b modal__header border-gray-100',
props.className,
)}
>
<div className={classNames('border-b modal__header border-gray-100')}>
<div className="modal__header__title">{props.children}</div>
<div className="modal__header__content">
{props.onClose && (

View File

@@ -1,20 +0,0 @@
.custom-modal {
height: 100%;
max-width: 800px;
max-height: 100vh;
display: flex;
margin: 0 auto;
flex-direction: column;
}
.custom-modal-content {
flex-grow: 2;
overflow-y: auto;
max-height: 90%;
}
.modal__overlay {
display: flex;
align-items: center;
justify-content: center;
}

View File

@@ -1,208 +0,0 @@
import React, {
ChangeEvent,
Children,
MouseEventHandler,
ReactElement,
ReactNode,
useCallback,
useState,
} from 'react';
import { makePlugin, getPluginConfig } from '../../utils/misc';
import { addPlugin } from '../../utils/rpc';
import Modal, {
ModalHeader,
ModalContent,
ModalFooter,
} from '../../components/Modal/Modal';
import type { PluginConfig } from '../../utils/misc';
import './index.scss';
import logo from '../../assets/img/icon-128.png';
import {
HostFunctionsDescriptions,
MultipleParts,
PermissionDescription,
} from '../../utils/plugins';
import { ErrorModal } from '../ErrorModal';
import classNames from 'classnames';
import DefaultPluginIcon from '../../assets/img/default-plugin-icon.png';
export default function PluginUploadInfo(): ReactElement {
const [error, showError] = useState('');
const [pluginBuffer, setPluginBuffer] = useState<ArrayBuffer | any>(null);
const [pluginContent, setPluginContent] = useState<PluginConfig | null>(null);
const onAddPlugin = useCallback(
async (evt: React.MouseEvent<HTMLButtonElement>) => {
try {
await addPlugin(Buffer.from(pluginBuffer).toString('hex'));
setPluginContent(null);
} catch (e: any) {
showError(e?.message || 'Invalid Plugin');
}
},
[pluginContent, pluginBuffer],
);
const onPluginInfo = useCallback(
async (evt: ChangeEvent<HTMLInputElement>) => {
if (!evt.target.files) return;
try {
const [file] = evt.target.files;
const arrayBuffer = await file.arrayBuffer();
const plugin = await makePlugin(arrayBuffer);
setPluginContent(await getPluginConfig(plugin));
setPluginBuffer(arrayBuffer);
} catch (e: any) {
showError(e?.message || 'Invalid Plugin');
} finally {
evt.target.value = '';
}
},
[setPluginContent, setPluginBuffer],
);
const onClose = useCallback(() => {
setPluginContent(null);
setPluginBuffer(null);
}, []);
return (
<>
<input
className="opacity-0 absolute top-0 right-0 h-full w-full cursor-pointer"
type="file"
onChange={onPluginInfo}
/>
{error && <ErrorModal onClose={() => showError('')} message={error} />}
{pluginContent && (
<PluginInfoModal
pluginContent={pluginContent}
onClose={onClose}
onAddPlugin={onAddPlugin}
/>
)}
</>
);
}
export function PluginInfoModalHeader(props: {
className?: string;
children: ReactNode | ReactNode[];
}) {
return <div className={props.className}>{props.children}</div>;
}
export function PluginInfoModalContent(props: {
className?: string;
children: ReactNode | ReactNode[];
}) {
return <div className={props.className}>{props.children}</div>;
}
export function PluginInfoModal(props: {
pluginContent: PluginConfig;
onClose: () => void;
onAddPlugin?: MouseEventHandler;
children?: ReactNode | ReactNode[];
}) {
const { pluginContent, onClose, onAddPlugin, children } = props;
const header = Children.toArray(children).filter(
(c: any) => c.type.name === 'PluginInfoModalHeader',
)[0];
const content = Children.toArray(children).filter(
(c: any) => c.type.name === 'PluginInfoModalContent',
)[0];
return (
<Modal
onClose={onClose}
className="custom-modal !rounded-none flex items-center justify-center gap-4 cursor-default"
>
<ModalHeader className="w-full p-2 border-gray-200 text-gray-500">
{header || (
<div className="flex flex-row items-end justify-start gap-2">
<img className="h-5" src={logo || DefaultPluginIcon} alt="logo" />
<span className="font-semibold">{`Installing ${pluginContent.title}`}</span>
</div>
)}
</ModalHeader>
<ModalContent className="flex flex-col flex-grow-0 flex-shrink-0 items-center px-8 py-2 gap-2 w-full max-h-none">
{content || (
<>
<img
className="w-12 h-12"
src={pluginContent.icon || DefaultPluginIcon}
alt="Plugin Icon"
/>
<span className="text-3xl text-center">
<span>
<span className="text-blue-600 font-semibold">
{pluginContent.title}
</span>{' '}
wants access to your browser
</span>
</span>
</>
)}
</ModalContent>
<div className="flex-grow flex-shrink overflow-y-auto w-full px-8">
<PluginPermissions pluginContent={pluginContent} />
</div>
<ModalFooter className="flex justify-end gap-2 p-4">
<button className="button" onClick={onClose}>
Cancel
</button>
{onAddPlugin && (
<button className="button button--primary" onClick={onAddPlugin}>
Allow
</button>
)}
</ModalFooter>
</Modal>
);
}
export function PluginPermissions({
pluginContent,
className,
}: {
pluginContent: PluginConfig;
className?: string;
}) {
return (
<div className={classNames('flex flex-col p-2 gap-5', className)}>
{pluginContent.hostFunctions?.map((hostFunction: string) => {
const HFComponent = HostFunctionsDescriptions[hostFunction];
return <HFComponent key={hostFunction} {...pluginContent} />;
})}
{pluginContent.cookies && (
<PermissionDescription fa="fa-solid fa-cookie-bite">
<span className="cursor-default">
<span className="mr-1">Access cookies from</span>
<MultipleParts parts={pluginContent.cookies} />
</span>
</PermissionDescription>
)}
{pluginContent.headers && (
<PermissionDescription fa="fa-solid fa-envelope">
<span className="cursor-default">
<span className="mr-1">Access headers from</span>
<MultipleParts parts={pluginContent.headers} />
</span>
</PermissionDescription>
)}
{pluginContent.requests && (
<PermissionDescription fa="fa-solid fa-globe">
<span className="cursor-default">
<span className="mr-1">Submit network requests to</span>
<MultipleParts
parts={pluginContent?.requests.map(({ url }) => url)}
/>
</span>
</PermissionDescription>
)}
</div>
);
}

View File

@@ -6,7 +6,6 @@
padding: 0;
overflow: hidden;
transition: 200ms opacity;
}
&:hover {
@@ -21,25 +20,4 @@
}
}
}
}
.custom-modal {
width: 100vw;
height: 100vh;
max-width: 800px;
max-height: 90vh;
display: flex;
margin: 1rem auto;
flex-direction: column;
}
.custom-modal-content {
flex-grow: 2;
overflow-y: auto;
max-height: 90%;
}
.modal__overlay {
display: flex;
align-items: center;
justify-content: center;
}
}

View File

@@ -1,11 +1,17 @@
import React, {
ChangeEvent,
MouseEventHandler,
ReactElement,
useCallback,
useEffect,
useState,
} from 'react';
import { fetchPluginHashes, removePlugin, runPlugin } from '../../utils/rpc';
import {
fetchPluginHashes,
removePlugin,
fetchPluginConfigByHash,
runPlugin,
} from '../../utils/rpc';
import { usePluginHashes } from '../../reducers/plugins';
import { PluginConfig } from '../../utils/misc';
import DefaultPluginIcon from '../../assets/img/default-plugin-icon.png';
@@ -14,12 +20,6 @@ import Icon from '../Icon';
import './index.scss';
import browser from 'webextension-polyfill';
import { ErrorModal } from '../ErrorModal';
import {
PluginInfoModal,
PluginInfoModalContent,
PluginInfoModalHeader,
} from '../PluginInfo';
import { getPluginConfigByHash } from '../../entries/Background/db';
export function PluginList(props: { className?: string }): ReactElement {
const hashes = usePluginHashes();
@@ -29,9 +29,7 @@ export function PluginList(props: { className?: string }): ReactElement {
}, []);
return (
<div
className={classNames('flex flex-col flex-nowrap gap-1', props.className)}
>
<div className={classNames('flex flex-col flex-nowrap', props.className)}>
{!hashes.length && (
<div className="flex flex-col items-center justify-center text-slate-400 cursor-default select-none">
<div>No available plugins</div>
@@ -50,11 +48,9 @@ export function Plugin(props: {
}): ReactElement {
const [error, showError] = useState('');
const [config, setConfig] = useState<PluginConfig | null>(null);
const [pluginInfo, showPluginInfo] = useState(false);
const [remove, showRemove] = useState(false);
const onClick = useCallback(async () => {
if (!config || remove) return;
if (!config) return;
try {
await runPlugin(props.hash, 'start');
@@ -73,11 +69,11 @@ export function Plugin(props: {
} catch (e: any) {
showError(e.message);
}
}, [props.hash, config, remove]);
}, [props.hash, config]);
useEffect(() => {
(async function () {
setConfig(await getPluginConfigByHash(props.hash));
setConfig(await fetchPluginConfigByHash(props.hash));
})();
}, [props.hash]);
@@ -85,128 +81,33 @@ export function Plugin(props: {
(e) => {
e.stopPropagation();
removePlugin(props.hash);
showRemove(false);
},
[props.hash, remove],
);
const onConfirmRemove: MouseEventHandler = useCallback(
(e) => {
e.stopPropagation();
showRemove(true);
},
[props.hash, remove],
);
const onPluginInfo: MouseEventHandler = useCallback(
(e) => {
e.stopPropagation();
showPluginInfo(true);
},
[props.hash, pluginInfo],
[props.hash],
);
if (!config) return <></>;
return (
<div
<button
className={classNames(
'flex flex-row justify-center border rounded border-slate-300 p-2 gap-2 plugin-box',
'flex flex-row border rounded border-slate-300 p-2 gap-2 plugin-box',
'cursor-pointer hover:bg-slate-100 hover:border-slate-400 active:bg-slate-200',
)}
onClick={onClick}
>
{!!error && <ErrorModal onClose={() => showError('')} message={error} />}
{!remove ? (
<div className="flex flex-row w-full gap-2">
<img className="w-12 h-12" src={config.icon || DefaultPluginIcon} />
<div className="flex flex-col w-full items-start">
<div className="font-bold flex flex-row h-6 items-center justify-between w-full">
{config.title}
<div className="flex flex-row items-center justify-center">
<Icon
fa="fa-solid fa-circle-info"
className="flex flex-row items-center justify-center cursor-pointer plugin-box__remove-icon"
onClick={onPluginInfo}
/>
<Icon
fa="fa-solid fa-xmark"
className="flex flex-row items-center justify-center cursor-pointer text-red-500 bg-red-200 rounded-full plugin-box__remove-icon"
onClick={onConfirmRemove}
/>
</div>
</div>
<div>{config.description}</div>
</div>
<img className="w-12 h-12" src={config.icon || DefaultPluginIcon} />
<div className="flex flex-col w-full items-start">
<div className="font-bold flex flex-row h-6 items-center justify-between w-full">
{config.title}
<Icon
fa="fa-solid fa-xmark"
className="flex flex-row items-center justify-center cursor-pointer text-red-500 bg-red-200 rounded-full plugin-box__remove-icon"
onClick={onRemove}
/>
</div>
) : (
<RemovePlugin
onRemove={onRemove}
showRemove={showRemove}
config={config}
/>
)}
{pluginInfo && (
<PluginInfoModal
pluginContent={config}
onClose={() => showPluginInfo(false)}
>
<PluginInfoModalHeader>
<div className="flex flex-row items-end justify-start gap-2">
<Icon
className="text-slate-500 hover:text-slate-700 cursor-pointer"
size={1}
fa="fa-solid fa-caret-left"
onClick={() => showPluginInfo(false)}
/>
</div>
</PluginInfoModalHeader>
<PluginInfoModalContent className="flex flex-col items-center cursor-default">
<img
className="w-12 h-12 mb-2"
src={config.icon || DefaultPluginIcon}
alt="Plugin Icon"
/>
<span className="text-3xl text-blue-600 font-semibold">
{config.title}
</span>
<div className="text-slate-500 text-lg">{config.description}</div>
</PluginInfoModalContent>
</PluginInfoModal>
)}
</div>
);
}
function RemovePlugin(props: {
onRemove: MouseEventHandler;
showRemove: (show: boolean) => void;
config: PluginConfig;
}): ReactElement {
const { onRemove, showRemove, config } = props;
const onCancel: MouseEventHandler = useCallback((e) => {
e.stopPropagation();
showRemove(false);
}, []);
return (
<div className="flex flex-col items-center w-full gap-1">
<div className="font-bold text-red-700">
{`Are you sure you want to remove "${config.title}" plugin?`}
</div>
<div className="mb-1">Warning: this cannot be undone.</div>
<div className="flex flex-row w-full gap-1">
<button className="flex-grow button p-1" onClick={onCancel}>
Cancel
</button>
<button
className="flex-grow font-bold bg-red-500 hover:bg-red-600 text-white rounded p-1"
onClick={onRemove}
>
Remove
</button>
</div>
</div>
<div>{config.description}</div>
</div>
</button>
);
}

View File

@@ -1,154 +0,0 @@
import React, { useCallback } from 'react';
import c from 'classnames';
export function InputBody(props: {
body: string;
setBody: (body: string) => void;
}) {
return (
<textarea
className="textarea h-[90%] w-full resize-none"
value={props.body}
onChange={(e) => props.setBody(e.target.value)}
/>
);
}
export function FormBodyTable(props: {
formBody: [string, string, boolean?][];
setFormBody: (formBody: [string, string, boolean?][]) => void;
}) {
const toggleKV = useCallback(
(index: number) => {
const newFormBody = [...props.formBody];
newFormBody[index][2] = !newFormBody[index][2];
props.setFormBody(newFormBody);
},
[props.formBody],
);
const setKV = useCallback(
(index: number, key: string, value: string) => {
const newFormBody = [...props.formBody];
newFormBody[index] = [key, value];
props.setFormBody(newFormBody);
if (index === props.formBody.length - 1 && (key || value)) {
props.setFormBody([...newFormBody, ['', '', true]]);
}
},
[props.formBody],
);
const last = props.formBody.length - 1;
return (
<table className="border border-slate-300 border-collapse table-fixed w-full">
<tbody>
{props.formBody.map(([key, value, silent], i) => (
<tr
key={i}
className={c('border-b border-slate-200', {
'opacity-30': !!silent,
})}
>
<td className="w-8 text-center pt-2">
{last !== i && (
<input
type="checkbox"
onChange={() => toggleKV(i)}
checked={!silent}
/>
)}
</td>
<td className="border border-slate-300 font-bold align-top break-all w-fit">
<input
className="input py-1 px-2 w-full"
type="text"
value={key}
placeholder="Key"
onChange={(e) => {
setKV(i, e.target.value, value);
}}
/>
</td>
<td className="border border-slate-300 break-all align-top">
<input
className="input py-1 px-2 w-full"
type="text"
value={value}
placeholder="Value"
onChange={(e) => {
setKV(i, key, e.target.value);
}}
/>
</td>
</tr>
))}
</tbody>
</table>
);
}
export function formatForRequest(
input: string | [string, string, boolean?][],
type: string,
): string {
try {
let pairs: [string, string][] = [];
if (typeof input === 'string') {
const lines = input.split('\n').filter((line) => line.trim() !== '');
pairs = lines.map((line) => {
const [key, value] = line.split('=').map((part) => part.trim());
return [key, value];
});
} else {
pairs = input
.filter(([, , silent]) => silent !== true)
.map(([key, value]) => [key, value]);
}
if (type === 'text/plain') {
return JSON.stringify(input as string);
}
if (type === 'application/json') {
const jsonObject = JSON.parse(input as string);
return JSON.stringify(jsonObject);
}
if (type === 'application/x-www-form-urlencoded') {
const searchParams = new URLSearchParams();
pairs.forEach(([key, value]) => {
searchParams.append(key, value);
});
return searchParams.toString();
}
return pairs.map(([key, value]) => `${key}=${value}`).join('&');
} catch (e) {
console.error('Error formatting for request:', e);
return '';
}
}
export async function parseResponse(contentType: string, res: Response) {
const parsedResponseData = {
json: '',
text: '',
img: '',
headers: Array.from(res.headers.entries()),
};
if (contentType?.includes('application/json')) {
parsedResponseData.json = await res.json();
} else if (contentType?.includes('text')) {
parsedResponseData.text = await res.text();
} else if (contentType?.includes('image')) {
const blob = await res.blob();
parsedResponseData.img = URL.createObjectURL(blob);
} else {
parsedResponseData.text = await res.text();
}
return parsedResponseData;
}

View File

@@ -5,9 +5,8 @@ import React, {
useEffect,
useState,
} from 'react';
import { notarizeRequest, useRequest } from '../../reducers/requests';
import { useRequest } from '../../reducers/requests';
import classNames from 'classnames';
import { useDispatch } from 'react-redux';
import {
Route,
Routes,
@@ -19,7 +18,6 @@ import Icon from '../Icon';
import NavigateWithParams from '../NavigateWithParams';
import {
set,
get,
MAX_SENT_LS_KEY,
MAX_RECEIVED_LS_KEY,
getMaxRecv,

View File

@@ -1,21 +1,13 @@
import React, {
ReactElement,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import React, { ReactElement, useCallback, useState } from 'react';
import { BackgroundActiontype, RequestLog } from '../../entries/Background/rpc';
import { useNavigate } from 'react-router';
import Fuse from 'fuse.js';
import Icon from '../Icon';
import { useDispatch } from 'react-redux';
import { setRequests } from '../../reducers/requests';
import classNames from 'classnames';
type Props = {
requests: RequestLog[];
shouldFix?: boolean;
};
export default function RequestTable(props: Props): ReactElement {
@@ -55,14 +47,7 @@ export default function RequestTable(props: Props): ReactElement {
return (
<div className="flex flex-col flex-nowrap flex-grow">
<div
className={classNames(
'flex flex-row flex-nowrap bg-slate-300 py-1 px-2 gap-2',
{
'fixed top-[4.5rem] w-full shadow': props.shouldFix,
},
)}
>
<div className="flex flex-row flex-nowrap bg-slate-300 py-1 px-2 gap-2">
<input
className="input w-full"
type="text"
@@ -76,7 +61,7 @@ export default function RequestTable(props: Props): ReactElement {
onClick={reset}
/>
</div>
<div className="flex-grow">
<div className="flex-grow overflow-y-auto h-0">
<table className="border border-slate-300 border-collapse table-fixed w-full">
<thead className="bg-slate-200">
<tr>

View File

@@ -1,15 +1,63 @@
import classNames from 'classnames';
import React, { ReactElement } from 'react';
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
export default function ResponseDetail(props: {
responseData: {
json: any | null;
text: string | null;
img: string | null;
headers: [string, string][] | null;
} | null;
response: Response | null;
className?: string;
}): ReactElement {
const [json, setJSON] = useState<any | null>(null);
const [text, setText] = useState<string | null>(null);
const [img, setImg] = useState<string | null>(null);
const [formData, setFormData] = useState<URLSearchParams | null>(null);
useEffect(() => {
const resp = props.response;
if (!resp) return;
const contentType =
resp.headers.get('content-type') || resp.headers.get('Content-Type');
if (contentType?.includes('application/json')) {
resp
.json()
.then((json) => {
if (json) {
setJSON(json);
}
})
.catch();
} else if (contentType?.includes('text')) {
resp
.text()
.then((_text) => {
if (_text) {
setText(_text);
}
})
.catch();
} else if (contentType?.includes('image')) {
resp
.blob()
.then((blob) => {
if (blob) {
setImg(URL.createObjectURL(blob));
}
})
.catch();
} else {
resp
.blob()
.then((blob) => blob.text())
.then((_text) => {
if (_text) {
setText(_text);
}
})
.catch();
}
}, [props.response]);
return (
<div
className={classNames(
@@ -18,7 +66,7 @@ export default function ResponseDetail(props: {
)}
>
<table className="border border-slate-300 border-collapse table-fixed w-full">
{!!props.responseData?.json && (
{!!json && (
<>
<thead className="bg-slate-200">
<tr>
@@ -32,13 +80,13 @@ export default function ResponseDetail(props: {
<textarea
rows={16}
className="w-full bg-slate-100 text-slate-600 p-2 text-xs break-all h-full outline-none font-mono"
value={JSON.stringify(props.responseData.json, null, 2)}
value={JSON.stringify(json, null, 2)}
></textarea>
</td>
</tr>
</>
)}
{!!props.responseData?.text && (
{!!text && (
<>
<thead className="bg-slate-200">
<tr>
@@ -52,13 +100,13 @@ export default function ResponseDetail(props: {
<textarea
rows={16}
className="w-full bg-slate-100 text-slate-600 p-2 text-xs break-all h-full outline-none font-mono"
value={props.responseData.text}
value={text}
></textarea>
</td>
</tr>
</>
)}
{!!props.responseData?.img && (
{!!img && (
<>
<thead className="bg-slate-200">
<tr>
@@ -69,12 +117,12 @@ export default function ResponseDetail(props: {
</thead>
<tr>
<td className="bg-slate-100" colSpan={2}>
<img src={props.responseData.img} />
<img src={img} />
</td>
</tr>
</>
)}
{!!props.responseData?.headers && (
{!!props.response?.headers && (
<>
<thead className="bg-slate-200">
<tr>
@@ -84,18 +132,20 @@ export default function ResponseDetail(props: {
</tr>
</thead>
<tbody>
{props.responseData?.headers.map(([name, value]) => {
return (
<tr className="border-b border-slate-200">
<td className="border border-slate-300 font-bold align-top py-1 px-2 whitespace-nowrap">
{name}
</td>
<td className="border border-slate-300 break-all align-top py-1 px-2">
{value}
</td>
</tr>
);
})}
{Array.from(props.response.headers.entries()).map(
([name, value]) => {
return (
<tr className="border-b border-slate-200">
<td className="border border-slate-300 font-bold align-top py-1 px-2 whitespace-nowrap">
{name}
</td>
<td className="border border-slate-300 break-all align-top py-1 px-2">
{value}
</td>
</tr>
);
},
)}
</tbody>
</>
)}

View File

@@ -4,6 +4,14 @@ let RequestsLogs: {
[tabId: string]: NodeCache;
} = {};
let HeadersStore: {
[hostname: string]: NodeCache;
} = {};
let CookieStore: {
[hostname: string]: NodeCache;
} = {};
export const deleteCacheByTabId = (tabId: number) => {
delete RequestsLogs[tabId];
};
@@ -19,10 +27,50 @@ export const getCacheByTabId = (tabId: number): NodeCache => {
return RequestsLogs[tabId];
};
export const deleteHeadersByHost = (hostname: string) => {
delete HeadersStore[hostname];
};
export const getHeaderStoreByHost = (hostname: string): NodeCache => {
HeadersStore[hostname] =
HeadersStore[hostname] ||
new NodeCache({
stdTTL: 60 * 5, // default 5m TTL
maxKeys: 1000000,
});
return HeadersStore[hostname];
};
export const deleteCookiesByHost = (hostname: string) => {
delete CookieStore[hostname];
};
export const getCookieStoreByHost = (hostname: string): NodeCache => {
CookieStore[hostname] =
CookieStore[hostname] ||
new NodeCache({
stdTTL: 60 * 5, // default 5m TTL
maxKeys: 1000000,
});
return CookieStore[hostname];
};
export const clearRequestCache = () => {
RequestsLogs = {};
};
export const clearHeaderCache = () => {
HeadersStore = {};
};
export const clearCookieCache = () => {
CookieStore = {};
};
export const clearCache = () => {
clearRequestCache();
clearHeaderCache();
clearCookieCache();
};

View File

@@ -1,10 +1,9 @@
import { Level } from 'level';
import type { RequestHistory } from './rpc';
import { PluginConfig, PluginMetadata, sha256 } from '../../utils/misc';
import mutex from './mutex';
import { PluginConfig, sha256 } from '../../utils/misc';
const charwise = require('charwise');
export const db = new Level('./ext-db', {
const db = new Level('./ext-db', {
valueEncoding: 'json',
});
const historyDb = db.sublevel<string, RequestHistory>('history', {
@@ -16,24 +15,6 @@ const pluginDb = db.sublevel<string, string>('plugin', {
const pluginConfigDb = db.sublevel<string, PluginConfig>('pluginConfig', {
valueEncoding: 'json',
});
const pluginMetadataDb = db.sublevel<string, PluginMetadata>('pluginMetadata', {
valueEncoding: 'json',
});
const connectionDb = db.sublevel<string, boolean>('connections', {
valueEncoding: 'json',
});
const cookiesDb = db.sublevel<string, boolean>('cookies', {
valueEncoding: 'json',
});
const headersDb = db.sublevel<string, boolean>('headers', {
valueEncoding: 'json',
});
const appDb = db.sublevel<string, any>('app', {
valueEncoding: 'json',
});
enum AppDatabaseKey {
DefaultPluginsInstalled = 'DefaultPluginsInstalled',
}
export async function addNotaryRequest(
now = Date.now(),
@@ -146,12 +127,14 @@ export async function getNotaryRequests(): Promise<RequestHistory[]> {
export async function getNotaryRequest(
id: string,
): Promise<RequestHistory | null> {
return historyDb.get(id).catch(() => null);
return historyDb.get(id);
}
export async function getPluginHashes(): Promise<string[]> {
const retVal: string[] = [];
for await (const [key] of pluginDb.iterator()) {
// pluginDb.del(key);
// pluginConfigDb.del(key);
retVal.push(key);
}
return retVal;
@@ -222,59 +205,6 @@ export async function removePluginConfig(
return existing;
}
export async function getPlugins(): Promise<
(PluginConfig & { hash: string; metadata: PluginMetadata })[]
> {
const hashes = await getPluginHashes();
const ret: (PluginConfig & { hash: string; metadata: PluginMetadata })[] = [];
for (const hash of hashes) {
const config = await getPluginConfigByHash(hash);
const metadata = await getPluginMetadataByHash(hash);
if (config) {
ret.push({
...config,
hash,
metadata: metadata || {
filePath: '',
origin: '',
},
});
}
}
return ret;
}
export async function getPluginMetadataByHash(
hash: string,
): Promise<PluginMetadata | null> {
try {
const metadata = await pluginMetadataDb.get(hash);
return metadata;
} catch (e) {
return null;
}
}
export async function addPluginMetadata(
hash: string,
metadata: PluginMetadata,
): Promise<PluginMetadata | null> {
await pluginMetadataDb.put(hash, metadata);
return metadata;
}
export async function removePluginMetadata(
hash: string,
): Promise<PluginMetadata | null> {
const existing = await pluginMetadataDb.get(hash);
if (!existing) return null;
await pluginMetadataDb.del(hash);
return existing;
}
export async function setNotaryRequestCid(
id: string,
cid: string,
@@ -292,105 +222,3 @@ export async function setNotaryRequestCid(
return newReq;
}
export async function setConnection(origin: string) {
if (await getConnection(origin)) return null;
await connectionDb.put(origin, true);
return true;
}
export async function setCookies(host: string, name: string, value: string) {
return mutex.runExclusive(async () => {
await cookiesDb.sublevel(host).put(name, value);
return true;
});
}
export async function clearCookies(host: string) {
return mutex.runExclusive(async () => {
await cookiesDb.sublevel(host).clear();
return true;
});
}
export async function getCookies(host: string, name: string) {
try {
const existing = await cookiesDb.sublevel(host).get(name);
return existing;
} catch (e) {
return null;
}
}
export async function getCookiesByHost(host: string) {
const ret: { [key: string]: string } = {};
for await (const [key, value] of cookiesDb.sublevel(host).iterator()) {
ret[key] = value;
}
return ret;
}
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;
}
}
export async function setHeaders(host: string, name: string, value?: string) {
if (!value) return null;
return mutex.runExclusive(async () => {
await headersDb.sublevel(host).put(name, value);
return true;
});
}
export async function clearHeaders(host: string) {
return mutex.runExclusive(async () => {
await headersDb.sublevel(host).clear();
return true;
});
}
export async function getHeaders(host: string, name: string) {
try {
const existing = await headersDb.sublevel(host).get(name);
return existing;
} catch (e) {
return null;
}
}
export async function getHeadersByHost(host: string) {
const ret: { [key: string]: string } = {};
for await (const [key, value] of headersDb.sublevel(host).iterator()) {
ret[key] = value;
}
return ret;
}
async function getDefaultPluginsInstalled(): Promise<boolean> {
return appDb.get(AppDatabaseKey.DefaultPluginsInstalled).catch(() => false);
}
export async function setDefaultPluginsInstalled(installed = false) {
return mutex.runExclusive(async () => {
await appDb.put(AppDatabaseKey.DefaultPluginsInstalled, installed);
});
}
export async function getAppState() {
return {
defaultPluginsInstalled: await getDefaultPluginsInstalled(),
};
}

View File

@@ -1,10 +1,13 @@
import { getCacheByTabId } from './cache';
import {
getCacheByTabId,
getCookieStoreByHost,
getHeaderStoreByHost,
} from './cache';
import { BackgroundActiontype, RequestLog } from './rpc';
import mutex from './mutex';
import browser from 'webextension-polyfill';
import { addRequest } from '../../reducers/requests';
import { urlify } from '../../utils/misc';
import { setCookies, setHeaders } from './db';
export const onSendHeaders = (
details: browser.WebRequest.OnSendHeadersDetailsType,
@@ -18,21 +21,23 @@ export const onSendHeaders = (
const { hostname } = urlify(details.url) || {};
if (hostname && details.requestHeaders) {
const headerStore = getHeaderStoreByHost(hostname);
details.requestHeaders.forEach((header) => {
const { name, value } = header;
if (/^cookie$/i.test(name) && value) {
const cookieStore = getCookieStoreByHost(hostname);
value
.split(';')
.map((v) => v.split('='))
.forEach((cookie) => {
setCookies(hostname, cookie[0].trim(), cookie[1]);
cookieStore.set(cookie[0].trim(), cookie[1]);
});
} else {
setHeaders(hostname, name, value);
headerStore.set(name, value);
}
});
}
cache.set(requestId, {
...existing,
method: details.method as 'GET' | 'POST',
@@ -54,7 +59,6 @@ export const onBeforeRequest = (
const { method, requestBody, tabId, requestId } = details;
if (method === 'OPTIONS') return;
if (requestBody) {
const cache = getCacheByTabId(tabId);
const existing = cache.get<RequestLog>(requestId);
@@ -89,7 +93,6 @@ export const onResponseStarted = (
if (method === 'OPTIONS') return;
const cache = getCacheByTabId(tabId);
const existing = cache.get<RequestLog>(requestId);
const newLog: RequestLog = {
requestHeaders: [],

View File

@@ -1,8 +1,6 @@
import { onBeforeRequest, onResponseStarted, onSendHeaders } from './handlers';
import { deleteCacheByTabId } from './cache';
import browser from 'webextension-polyfill';
import { getAppState, setDefaultPluginsInstalled } from './db';
import { installPlugin } from './plugins/utils';
(async () => {
browser.webRequest.onSendHeaders.addListener(
@@ -33,19 +31,6 @@ import { installPlugin } from './plugins/utils';
deleteCacheByTabId(tabId);
});
const { defaultPluginsInstalled } = await getAppState();
if (!defaultPluginsInstalled) {
try {
const twitterProfileUrl = browser.runtime.getURL('twitter_profile.wasm');
const discordDmUrl = browser.runtime.getURL('discord_dm.wasm');
await installPlugin(twitterProfileUrl);
await installPlugin(discordDmUrl);
} finally {
await setDefaultPluginsInstalled(true);
}
}
const { initRPC } = await import('./rpc');
await createOffscreenDocument();
initRPC();

View File

@@ -1,29 +0,0 @@
import { addPlugin, addPluginConfig, addPluginMetadata } from '../db';
import { getPluginConfig } from '../../../utils/misc';
export async function installPlugin(
urlOrBuffer: ArrayBuffer | string,
origin = '',
filePath = '',
metadata: {[key: string]: string} = {},
) {
let arrayBuffer;
if (typeof urlOrBuffer === 'string') {
const resp = await fetch(urlOrBuffer);
arrayBuffer = await resp.arrayBuffer();
} else {
arrayBuffer = urlOrBuffer;
}
const config = await getPluginConfig(arrayBuffer);
const hex = Buffer.from(arrayBuffer).toString('hex');
const hash = await addPlugin(hex);
await addPluginConfig(hash!, config);
await addPluginMetadata(hash!, {
...metadata,
origin,
filePath,
});
return hash;
}

View File

@@ -1,6 +1,11 @@
import browser from 'webextension-polyfill';
import { clearCache, getCacheByTabId } from './cache';
import { addRequestHistory, setRequests } from '../../reducers/history';
import {
clearCache,
getCacheByTabId,
getCookieStoreByHost,
getHeaderStoreByHost,
} from './cache';
import { addRequestHistory } from '../../reducers/history';
import {
addNotaryRequest,
addNotaryRequestProofs,
@@ -17,23 +22,14 @@ import {
addPluginConfig,
getPluginConfigByHash,
removePluginConfig,
getConnection,
setConnection,
deleteConnection,
addPluginMetadata,
getPlugins,
getCookiesByHost,
getHeadersByHost,
getAppState,
setDefaultPluginsInstalled,
} from './db';
import { addOnePlugin, removeOnePlugin } from '../../reducers/plugins';
import {
devlog,
extractBodyFromResponse,
getPluginConfig,
hexToArrayBuffer,
makePlugin,
PluginConfig,
} from '../../utils/misc';
import {
getLoggingFilter,
@@ -42,10 +38,6 @@ import {
getNotaryApi,
getProxyApi,
} from '../../utils/storage';
import { deferredPromise } from '../../utils/promise';
import { minimatch } from 'minimatch';
import { OffscreenActionTypes } from '../Offscreen/types';
import { SidePanelActionTypes } from '../SidePanel/types';
const charwise = require('charwise');
@@ -72,23 +64,6 @@ export enum BackgroundActiontype {
get_plugin_hashes = 'get_plugin_hashes',
open_popup = 'open_popup',
change_route = 'change_route',
connect_request = 'connect_request',
connect_response = 'connect_response',
get_history_request = 'get_history_request',
get_history_response = 'get_history_response',
get_proof_request = 'get_proof_request',
get_proof_response = 'get_proof_response',
notarize_request = 'notarize_request',
notarize_response = 'notarize_response',
install_plugin_request = 'install_plugin_request',
install_plugin_response = 'install_plugin_response',
get_plugins_request = 'get_plugins_request',
get_plugins_response = 'get_plugins_response',
run_plugin_request = 'run_plugin_request',
run_plugin_response = 'run_plugin_response',
get_logging_level = 'get_logging_level',
get_app_state = 'get_app_state',
set_default_plugins_installed = 'set_default_plugins_installed',
}
export type BackgroundAction = {
@@ -119,6 +94,7 @@ export type RequestHistory = {
method: string;
headers: { [key: string]: string };
body?: string;
maxTranscriptSize: number;
maxSentData: number;
maxRecvData: number;
notaryUrl: string;
@@ -134,14 +110,11 @@ export type RequestHistory = {
secretHeaders?: string[];
secretResps?: string[];
cid?: string;
metadata?: {
[k: string]: string;
};
};
export const initRPC = () => {
browser.runtime.onMessage.addListener(
(request, sender, sendResponse): any => {
async (request, sender, sendResponse) => {
switch (request.type) {
case BackgroundActiontype.get_requests:
return handleGetRequests(request, sendResponse);
@@ -153,7 +126,8 @@ export const initRPC = () => {
case BackgroundActiontype.finish_prove_request:
return handleFinishProveRequest(request, sendResponse);
case BackgroundActiontype.delete_prove_request:
return removeNotaryRequest(request.data);
await removeNotaryRequest(request.data);
return sendResponse();
case BackgroundActiontype.retry_prove_request:
return handleRetryProveReqest(request, sendResponse);
case BackgroundActiontype.prove_request_start:
@@ -178,29 +152,6 @@ export const initRPC = () => {
return handleExecPluginProver(request);
case BackgroundActiontype.open_popup:
return handleOpenPopup(request);
case BackgroundActiontype.connect_request:
return handleConnect(request);
case BackgroundActiontype.get_history_request:
return handleGetHistory(request);
case BackgroundActiontype.get_proof_request:
return handleGetProof(request);
case BackgroundActiontype.notarize_request:
return handleNotarizeRequest(request);
case BackgroundActiontype.install_plugin_request:
return handleInstallPluginRequest(request);
case BackgroundActiontype.get_plugins_request:
return handleGetPluginsRequest(request);
case BackgroundActiontype.run_plugin_request:
return handleRunPluginCSRequest(request);
case BackgroundActiontype.get_logging_level:
getLoggingFilter().then(sendResponse);
return true;
case BackgroundActiontype.get_app_state:
getAppState().then(sendResponse);
return true;
case BackgroundActiontype.set_default_plugins_installed:
setDefaultPluginsInstalled(request.data).then(sendResponse);
return true;
default:
break;
}
@@ -211,30 +162,28 @@ export const initRPC = () => {
function handleGetRequests(
request: BackgroundAction,
sendResponse: (data?: any) => void,
): boolean {
) {
const cache = getCacheByTabId(request.data);
const keys = cache.keys() || [];
const data = keys.map((key) => cache.get(key));
sendResponse(data);
return true;
return data;
}
function handleGetProveRequests(
async function handleGetProveRequests(
request: BackgroundAction,
sendResponse: (data?: any) => void,
): boolean {
getNotaryRequests().then(async (reqs) => {
) {
const reqs = await getNotaryRequests();
for (const req of reqs) {
await browser.runtime.sendMessage({
type: BackgroundActiontype.push_action,
data: {
tabId: 'background',
},
action: setRequests(reqs),
action: addRequestHistory(req),
});
sendResponse(reqs);
});
return true;
}
return sendResponse();
}
async function handleFinishProveRequest(
@@ -310,6 +259,7 @@ async function handleRetryProveReqest(
...req,
notaryUrl,
websocketProxyUrl,
loggingFilter: await getLoggingFilter(),
},
});
@@ -325,6 +275,7 @@ async function handleProveRequestStart(
method,
headers,
body,
maxTranscriptSize,
maxSentData,
maxRecvData,
notaryUrl,
@@ -340,6 +291,7 @@ async function handleProveRequestStart(
body,
maxSentData,
maxRecvData,
maxTranscriptSize,
notaryUrl,
websocketProxyUrl,
secretHeaders,
@@ -356,7 +308,7 @@ async function handleProveRequestStart(
action: addRequestHistory(await getNotaryRequest(id)),
});
browser.runtime.sendMessage({
await browser.runtime.sendMessage({
type: BackgroundActiontype.process_prove_request,
data: {
id,
@@ -364,12 +316,14 @@ async function handleProveRequestStart(
method,
headers,
body,
maxTranscriptSize,
maxSentData,
maxRecvData,
notaryUrl,
websocketProxyUrl,
secretHeaders,
secretResps,
loggingFilter: await getLoggingFilter(),
},
});
@@ -381,30 +335,33 @@ async function runPluginProver(request: BackgroundAction, now = Date.now()) {
url,
method,
headers,
body,
secretHeaders,
secretResps,
notaryUrl: _notaryUrl,
websocketProxyUrl: _websocketProxyUrl,
maxSentData: _maxSentData,
maxRecvData: _maxRecvData,
} = request.data;
const resp = await fetch(url, {
method,
headers,
});
const body = await extractBodyFromResponse(resp);
const notaryUrl = _notaryUrl || (await getNotaryApi());
const websocketProxyUrl = _websocketProxyUrl || (await getProxyApi());
const maxSentData = _maxSentData || (await getMaxSent());
const maxRecvData = _maxRecvData || (await getMaxRecv());
const maxTranscriptSize = 16384;
const { id } = await addNotaryRequest(now, {
url,
method,
headers,
body,
maxTranscriptSize,
notaryUrl,
websocketProxyUrl,
maxRecvData,
maxSentData,
secretHeaders,
secretResps,
});
await setNotaryRequestStatus(id, 'pending');
@@ -425,12 +382,12 @@ async function runPluginProver(request: BackgroundAction, now = Date.now()) {
method,
headers,
body,
maxTranscriptSize,
notaryUrl,
websocketProxyUrl,
maxRecvData,
maxSentData,
secretHeaders,
secretResps,
loggingFilter: await getLoggingFilter(),
},
});
}
@@ -445,23 +402,27 @@ export async function handleExecPluginProver(request: BackgroundAction) {
function handleGetCookiesByHostname(
request: BackgroundAction,
sendResponse: (data?: any) => void,
): boolean {
(async () => {
const store = await getCookiesByHost(request.data);
sendResponse(store);
})();
return true;
) {
const cache = getCookieStoreByHost(request.data);
const keys = cache.keys() || [];
const data = keys.reduce((acc: { [k: string]: string }, key) => {
acc[key] = cache.get(key) || '';
return acc;
}, {});
return data;
}
function handleGetHeadersByHostname(
request: BackgroundAction,
sendResponse: (data?: any) => void,
): boolean {
(async () => {
const cache = await getHeadersByHost(request.data);
sendResponse(cache);
})();
return true;
) {
const cache = getHeaderStoreByHost(request.data);
const keys = cache.keys() || [];
const data = keys.reduce((acc: { [k: string]: string }, key) => {
acc[key] = cache.get(key) || '';
return acc;
}, {});
return data;
}
async function handleAddPlugin(
@@ -543,60 +504,43 @@ async function handleGetPluginConfigByHash(
return config;
}
function handleRunPlugin(
async function handleRunPlugin(
request: BackgroundAction,
sendResponse: (data?: any) => void,
) {
(async () => {
const { hash, method, params } = request.data;
const hex = await getPluginByHash(hash);
const arrayBuffer = hexToArrayBuffer(hex!);
const config = await getPluginConfig(arrayBuffer);
const plugin = await makePlugin(arrayBuffer, config);
devlog(`plugin::${method}`, params);
const out = await plugin.call(method, params);
devlog(`plugin response: `, out.string());
sendResponse(JSON.parse(out.string()));
})();
return true;
const { hash, method, params } = request.data;
const hex = await getPluginByHash(hash);
const arrayBuffer = hexToArrayBuffer(hex!);
const config = await getPluginConfig(arrayBuffer);
const plugin = await makePlugin(arrayBuffer, config);
devlog(`plugin::${method}`, params);
const out = await plugin.call(method, params);
devlog(`plugin response: `, out.string());
return JSON.parse(out.string());
}
let cachePopup: browser.Windows.Window | null = null;
async function openPopup(route: string, left?: number, top?: number) {
const tab = await browser.tabs.create({
url: browser.runtime.getURL('popup.html') + '#' + route,
active: false,
});
const popup = await browser.windows.create({
tabId: tab.id,
type: 'popup',
focused: true,
width: 480,
height: 640,
left: Math.round(left || 0),
top: Math.round(top || 0),
});
return { popup, tab };
}
async function handleOpenPopup(request: BackgroundAction) {
if (cachePopup) {
browser.windows.update(cachePopup.id!, {
focused: true,
});
browser.tabs.update(cachePopup.id!, {
url: browser.runtime.getURL('popup.html') + '#' + request.data.route,
});
} else {
const { popup } = await openPopup(
request.data.route,
request.data.position.left,
request.data.position.top,
);
const tab = await browser.tabs.create({
url: browser.runtime.getURL('popup.html') + '#' + request.data.route,
active: false,
});
const popup = await browser.windows.create({
tabId: tab.id,
type: 'popup',
focused: true,
width: 480,
height: 640,
left: request.data.position.left,
top: request.data.position.top,
});
cachePopup = popup;
@@ -610,476 +554,3 @@ async function handleOpenPopup(request: BackgroundAction) {
browser.windows.onRemoved.addListener(onPopUpClose);
}
}
async function handleConnect(request: BackgroundAction) {
const connection = await getConnection(request.data.origin);
const [currentTab] = await browser.tabs.query({
active: true,
currentWindow: true,
});
if (!connection) {
const defer = deferredPromise();
const { popup, tab } = await openPopup(
`connection-approval?origin=${encodeURIComponent(request.data.origin)}&favIconUrl=${encodeURIComponent(currentTab?.favIconUrl || '')}`,
request.data.position.left,
request.data.position.top,
);
const onMessage = async (req: BackgroundAction) => {
if (req.type === BackgroundActiontype.connect_response) {
defer.resolve(req.data);
if (req.data) {
await setConnection(request.data.origin);
} else {
await deleteConnection(request.data.origin);
}
browser.runtime.onMessage.removeListener(onMessage);
browser.tabs.remove(tab.id!);
}
};
const onPopUpClose = (windowId: number) => {
if (windowId === popup.id) {
defer.resolve(false);
browser.windows.onRemoved.removeListener(onPopUpClose);
}
};
browser.runtime.onMessage.addListener(onMessage);
browser.windows.onRemoved.addListener(onPopUpClose);
return defer.promise;
}
return true;
}
async function handleGetHistory(request: BackgroundAction) {
const [currentTab] = await browser.tabs.query({
active: true,
currentWindow: true,
});
const defer = deferredPromise();
const {
origin,
position,
method: filterMethod,
url: filterUrl,
metadata: filterMetadata,
} = request.data;
const { popup, tab } = await openPopup(
`get-history-approval?${filterMetadata ? `metadata=${JSON.stringify(filterMetadata)}&` : ''}method=${filterMethod}&url=${filterUrl}&origin=${encodeURIComponent(origin)}&favIconUrl=${encodeURIComponent(currentTab?.favIconUrl || '')}`,
position.left,
position.top,
);
const onMessage = async (req: BackgroundAction) => {
if (req.type === BackgroundActiontype.get_history_response) {
if (req.data) {
const response = await getNotaryRequests();
const result = response
.map(
({ id, method, url, notaryUrl, websocketProxyUrl, metadata }) => ({
id,
time: new Date(charwise.decode(id)),
method,
url,
notaryUrl,
websocketProxyUrl,
metadata,
}),
)
.filter(({ method, url, metadata }) => {
let matchedMetadata = true;
if (filterMetadata) {
matchedMetadata = Object.entries(
filterMetadata as { [k: string]: string },
).reduce((bool, [k, v]) => {
try {
return bool && minimatch(metadata![k], v);
} catch (e) {
return false;
}
}, matchedMetadata);
}
return (
minimatch(method, filterMethod, { nocase: true }) &&
minimatch(url, filterUrl) &&
matchedMetadata
);
});
defer.resolve(result);
} else {
defer.reject(new Error('user rejected.'));
}
browser.runtime.onMessage.removeListener(onMessage);
browser.tabs.remove(tab.id!);
}
};
const onPopUpClose = (windowId: number) => {
if (windowId === popup.id) {
defer.reject(new Error('user rejected.'));
browser.windows.onRemoved.removeListener(onPopUpClose);
}
};
browser.runtime.onMessage.addListener(onMessage);
browser.windows.onRemoved.addListener(onPopUpClose);
return defer.promise;
}
async function handleGetProof(request: BackgroundAction) {
const [currentTab] = await browser.tabs.query({
active: true,
currentWindow: true,
});
const defer = deferredPromise();
const { origin, position, id } = request.data;
const response = await getNotaryRequest(id);
if (!response) {
defer.reject(new Error('proof id not found.'));
return defer.promise;
}
const { popup, tab } = await openPopup(
`get-proof-approval?id=${id}&origin=${encodeURIComponent(origin)}&favIconUrl=${encodeURIComponent(currentTab?.favIconUrl || '')}`,
position.left,
position.top,
);
const onMessage = async (req: BackgroundAction) => {
if (req.type === BackgroundActiontype.get_proof_response) {
if (req.data) {
defer.resolve(response?.proof || null);
} else {
defer.reject(new Error('user rejected.'));
}
browser.runtime.onMessage.removeListener(onMessage);
browser.tabs.remove(tab.id!);
}
};
const onPopUpClose = (windowId: number) => {
if (windowId === popup.id) {
defer.reject(new Error('user rejected.'));
browser.windows.onRemoved.removeListener(onPopUpClose);
}
};
browser.runtime.onMessage.addListener(onMessage);
browser.windows.onRemoved.addListener(onPopUpClose);
return defer.promise;
}
async function handleNotarizeRequest(request: BackgroundAction) {
const [currentTab] = await browser.tabs.query({
active: true,
currentWindow: true,
});
const defer = deferredPromise();
const {
url,
method = 'GET',
headers,
body,
maxSentData = await getMaxSent(),
maxRecvData = await getMaxRecv(),
notaryUrl = await getNotaryApi(),
websocketProxyUrl = await getProxyApi(),
origin,
position,
metadata,
} = request.data;
const config = JSON.stringify({
url,
method,
headers,
body,
maxSentData,
maxRecvData,
notaryUrl,
websocketProxyUrl,
metadata,
});
const { popup, tab } = await openPopup(
`notarize-approval?config=${encodeURIComponent(config)}&origin=${encodeURIComponent(origin)}&favIconUrl=${encodeURIComponent(currentTab?.favIconUrl || '')}`,
position.left,
position.top,
);
const now = Date.now();
const id = charwise.encode(now).toString('hex');
let isUserClose = true;
const onNotarizationResponse = async (req: any) => {
if (req.type !== OffscreenActionTypes.notarization_response) return;
if (req.data.id !== id) return;
if (req.data.error) defer.reject(req.data.error);
if (req.data.proof) defer.resolve(req.data.proof);
browser.runtime.onMessage.removeListener(onNotarizationResponse);
};
const onMessage = async (req: BackgroundAction) => {
if (req.type === BackgroundActiontype.notarize_response) {
if (req.data) {
try {
const { secretHeaders, secretResps } = req.data;
await addNotaryRequest(now, req.data);
await setNotaryRequestStatus(id, 'pending');
browser.runtime.onMessage.addListener(onNotarizationResponse);
browser.runtime.sendMessage({
type: OffscreenActionTypes.notarization_request,
data: {
id,
url,
method,
headers,
body,
maxSentData,
maxRecvData,
notaryUrl,
websocketProxyUrl,
secretHeaders,
secretResps,
},
});
} catch (e) {
defer.reject(e);
}
} else {
defer.reject(new Error('user rejected.'));
}
browser.runtime.onMessage.removeListener(onMessage);
isUserClose = false;
browser.tabs.remove(tab.id!);
}
};
const onPopUpClose = (windowId: number) => {
if (isUserClose && windowId === popup.id) {
defer.reject(new Error('user rejected.'));
browser.windows.onRemoved.removeListener(onPopUpClose);
}
};
browser.runtime.onMessage.addListener(onMessage);
browser.windows.onRemoved.addListener(onPopUpClose);
return defer.promise;
}
async function handleInstallPluginRequest(request: BackgroundAction) {
const [currentTab] = await browser.tabs.query({
active: true,
currentWindow: true,
});
const defer = deferredPromise();
const { origin, position, url, metadata } = request.data;
let arrayBuffer: ArrayBuffer, config: PluginConfig;
try {
const resp = await fetch(url);
arrayBuffer = await resp.arrayBuffer();
config = await getPluginConfig(arrayBuffer);
} catch (e) {
defer.reject(e);
return defer.promise;
}
const { popup, tab } = await openPopup(
`install-plugin-approval?${metadata ? `metadata=${JSON.stringify(metadata)}&` : ''}url=${url}&origin=${encodeURIComponent(origin)}&favIconUrl=${encodeURIComponent(currentTab?.favIconUrl || '')}`,
position.left,
position.top,
);
const onMessage = async (req: BackgroundAction) => {
if (req.type === BackgroundActiontype.install_plugin_response) {
if (req.data) {
try {
const hex = Buffer.from(arrayBuffer).toString('hex');
const hash = await addPlugin(hex);
await addPluginConfig(hash!, config);
await addPluginMetadata(hash!, {
...metadata,
origin,
filePath: url,
});
defer.resolve(hash);
} catch (e) {
defer.reject(e);
}
} else {
defer.reject(new Error('user rejected.'));
}
browser.runtime.onMessage.removeListener(onMessage);
browser.tabs.remove(tab.id!);
}
};
const onPopUpClose = (windowId: number) => {
if (windowId === popup.id) {
defer.reject(new Error('user rejected.'));
browser.windows.onRemoved.removeListener(onPopUpClose);
}
};
browser.runtime.onMessage.addListener(onMessage);
browser.windows.onRemoved.addListener(onPopUpClose);
return defer.promise;
}
async function handleGetPluginsRequest(request: BackgroundAction) {
const [currentTab] = await browser.tabs.query({
active: true,
currentWindow: true,
});
const defer = deferredPromise();
const {
origin,
position,
origin: filterOrigin,
url: filterUrl,
metadata: filterMetadata,
} = request.data;
const { popup, tab } = await openPopup(
`get-plugins-approval?${filterMetadata ? `metadata=${JSON.stringify(filterMetadata)}&` : ''}&filterOrigin=${filterOrigin}&url=${filterUrl}&origin=${encodeURIComponent(origin)}&favIconUrl=${encodeURIComponent(currentTab?.favIconUrl || '')}`,
position.left,
position.top,
);
const onMessage = async (req: BackgroundAction) => {
if (req.type === BackgroundActiontype.get_plugins_response) {
if (req.data) {
const response = await getPlugins();
const result = response.filter(({ metadata }) => {
let matchedMetadata = true;
if (filterMetadata) {
matchedMetadata = Object.entries(
filterMetadata as { [k: string]: string },
).reduce((bool, [k, v]) => {
try {
return bool && minimatch(metadata![k], v);
} catch (e) {
return false;
}
}, matchedMetadata);
}
return (
minimatch(metadata.filePath, filterUrl) &&
minimatch(metadata.origin, filterOrigin || '**') &&
matchedMetadata
);
});
defer.resolve(result);
} else {
defer.reject(new Error('user rejected.'));
}
browser.runtime.onMessage.removeListener(onMessage);
browser.tabs.remove(tab.id!);
}
};
const onPopUpClose = (windowId: number) => {
if (windowId === popup.id) {
defer.reject(new Error('user rejected.'));
browser.windows.onRemoved.removeListener(onPopUpClose);
}
};
browser.runtime.onMessage.addListener(onMessage);
browser.windows.onRemoved.addListener(onPopUpClose);
return defer.promise;
}
async function handleRunPluginCSRequest(request: BackgroundAction) {
const [currentTab] = await browser.tabs.query({
active: true,
currentWindow: true,
});
const defer = deferredPromise();
const { origin, position, hash } = request.data;
const plugin = await getPluginByHash(hash);
const config = await getPluginConfigByHash(hash);
let isUserClose = true;
if (!plugin || !config) {
defer.reject(new Error('plugin not found.'));
return defer.promise;
}
const { popup, tab } = await openPopup(
`run-plugin-approval?hash=${hash}&origin=${encodeURIComponent(origin)}&favIconUrl=${encodeURIComponent(currentTab?.favIconUrl || '')}`,
position.left,
position.top,
);
const onPluginRequest = async (req: any) => {
console.log(req);
if (req.type !== SidePanelActionTypes.execute_plugin_response) return;
if (req.data.hash !== hash) return;
if (req.data.error) defer.reject(req.data.error);
if (req.data.proof) defer.resolve(req.data.proof);
browser.runtime.onMessage.removeListener(onPluginRequest);
};
const onMessage = async (req: BackgroundAction) => {
if (req.type === BackgroundActiontype.run_plugin_response) {
if (req.data) {
browser.runtime.onMessage.addListener(onPluginRequest);
} else {
defer.reject(new Error('user rejected.'));
}
browser.runtime.onMessage.removeListener(onMessage);
isUserClose = false;
browser.tabs.remove(tab.id!);
}
};
const onPopUpClose = (windowId: number) => {
if (isUserClose && windowId === popup.id) {
defer.reject(new Error('user rejected.'));
browser.windows.onRemoved.removeListener(onPopUpClose);
}
};
browser.runtime.onMessage.addListener(onMessage);
browser.windows.onRemoved.addListener(onPopUpClose);
return defer.promise;
}

View File

@@ -1,118 +0,0 @@
import { ContentScriptTypes, RPCClient } from './rpc';
import { RequestHistory } from '../Background/rpc';
import { PluginConfig, PluginMetadata } from '../../utils/misc';
import { PresentationJSON } from '../../utils/types';
const client = new RPCClient();
class TLSN {
async getHistory(
method: string,
url: string,
metadata?: {
[key: string]: string;
},
): Promise<
(Pick<
RequestHistory,
'id' | 'method' | 'notaryUrl' | 'url' | 'websocketProxyUrl'
> & { time: Date })[]
> {
const resp = await client.call(ContentScriptTypes.get_history, {
method,
url,
metadata,
});
return resp || [];
}
async getProof(id: string): Promise<PresentationJSON | null> {
const resp = await client.call(ContentScriptTypes.get_proof, {
id,
});
return resp || null;
}
async notarize(
url: string,
requestOptions?: {
method?: string;
headers?: { [key: string]: string };
body?: string;
},
proofOptions?: {
notaryUrl?: string;
websocketProxyUrl?: string;
maxSentData?: number;
maxRecvData?: number;
metadata?: {
[k: string]: string;
};
},
): Promise<PresentationJSON> {
const resp = await client.call(ContentScriptTypes.notarize, {
url,
method: requestOptions?.method,
headers: requestOptions?.headers,
body: requestOptions?.body,
maxSentData: proofOptions?.maxSentData,
maxRecvData: proofOptions?.maxRecvData,
notaryUrl: proofOptions?.notaryUrl,
websocketProxyUrl: proofOptions?.websocketProxyUrl,
metadata: proofOptions?.metadata,
});
return resp;
}
async installPlugin(
url: string,
metadata?: { [k: string]: string },
): Promise<string> {
const resp = await client.call(ContentScriptTypes.install_plugin, {
url,
metadata,
});
return resp;
}
async getPlugins(
url: string,
origin?: string,
metadata?: {
[key: string]: string;
},
): Promise<(PluginConfig & { hash: string; metadata: PluginMetadata })[]> {
const resp = await client.call(ContentScriptTypes.get_plugins, {
url,
origin,
metadata,
});
return resp;
}
async runPlugin(hash: string) {
const resp = await client.call(ContentScriptTypes.run_plugin, {
hash,
});
return resp;
}
}
const connect = async () => {
const resp = await client.call(ContentScriptTypes.connect);
if (resp) {
return new TLSN();
}
};
// @ts-ignore
window.tlsn = {
connect,
};

View File

@@ -1,215 +1,9 @@
import browser from 'webextension-polyfill';
import { ContentScriptRequest, ContentScriptTypes, RPCServer } from './rpc';
import { BackgroundActiontype, RequestHistory } from '../Background/rpc';
import { urlify } from '../../utils/misc';
window.onerror = (error) => {
// console.log('error');
// console.log(error);
};
(async () => {
loadScript('content.bundle.js');
const server = new RPCServer();
server.on(ContentScriptTypes.connect, async () => {
const connected = await browser.runtime.sendMessage({
type: BackgroundActiontype.connect_request,
data: {
...getPopupData(),
},
});
if (!connected) throw new Error('user rejected.');
return connected;
});
server.on(
ContentScriptTypes.get_history,
async (
request: ContentScriptRequest<{
method: string;
url: string;
metadata?: { [k: string]: string };
}>,
) => {
const {
method: filterMethod,
url: filterUrl,
metadata,
} = request.params || {};
if (!filterMethod || !filterUrl)
throw new Error('params must include method and url.');
const response: RequestHistory[] = await browser.runtime.sendMessage({
type: BackgroundActiontype.get_history_request,
data: {
...getPopupData(),
method: filterMethod,
url: filterUrl,
metadata,
},
});
return response;
},
);
server.on(
ContentScriptTypes.get_proof,
async (request: ContentScriptRequest<{ id: string }>) => {
const { id } = request.params || {};
if (!id) throw new Error('params must include id.');
const proof = await browser.runtime.sendMessage({
type: BackgroundActiontype.get_proof_request,
data: {
...getPopupData(),
id,
},
});
return proof;
},
);
server.on(
ContentScriptTypes.notarize,
async (
request: ContentScriptRequest<{
url: string;
method?: string;
headers?: { [key: string]: string };
metadata?: { [key: string]: string };
body?: string;
notaryUrl?: string;
websocketProxyUrl?: string;
maxSentData?: number;
maxRecvData?: number;
}>,
) => {
const {
url,
method,
headers,
body,
maxSentData,
maxRecvData,
notaryUrl,
websocketProxyUrl,
metadata,
} = request.params || {};
if (!url || !urlify(url)) throw new Error('invalid url.');
const proof = await browser.runtime.sendMessage({
type: BackgroundActiontype.notarize_request,
data: {
...getPopupData(),
url,
method,
headers,
body,
maxSentData,
maxRecvData,
notaryUrl,
websocketProxyUrl,
metadata,
},
});
return proof;
},
);
server.on(
ContentScriptTypes.install_plugin,
async (
request: ContentScriptRequest<{
url: string;
metadata?: { [k: string]: string };
}>,
) => {
const { url, metadata } = request.params || {};
if (!url) throw new Error('params must include url.');
const response: RequestHistory[] = await browser.runtime.sendMessage({
type: BackgroundActiontype.install_plugin_request,
data: {
...getPopupData(),
url,
metadata,
},
});
return response;
},
);
server.on(
ContentScriptTypes.get_plugins,
async (
request: ContentScriptRequest<{
url: string;
origin?: string;
metadata?: { [k: string]: string };
}>,
) => {
const {
url: filterUrl,
origin: filterOrigin,
metadata,
} = request.params || {};
if (!filterUrl) throw new Error('params must include url.');
const response = await browser.runtime.sendMessage({
type: BackgroundActiontype.get_plugins_request,
data: {
...getPopupData(),
url: filterUrl,
origin: filterOrigin,
metadata,
},
});
return response;
},
);
server.on(
ContentScriptTypes.run_plugin,
async (request: ContentScriptRequest<{ hash: string }>) => {
const { hash } = request.params || {};
if (!hash) throw new Error('params must include hash');
const response = await browser.runtime.sendMessage({
type: BackgroundActiontype.run_plugin_request,
data: {
...getPopupData(),
hash,
},
});
return response;
},
);
console.log('Content script works!');
console.log('Must reload extension for modifications to take effect.');
})();
function loadScript(filename: string) {
const url = browser.runtime.getURL(filename);
const script = document.createElement('script');
script.setAttribute('type', 'text/javascript');
script.setAttribute('src', url);
document.body.appendChild(script);
}
function getPopupData() {
return {
origin: window.origin,
position: {
left: window.screen.width / 2 - 240,
top: window.screen.height / 2 - 300,
},
};
}

View File

@@ -1,118 +0,0 @@
import { deferredPromise, PromiseResolvers } from '../../utils/promise';
export enum ContentScriptTypes {
connect = 'tlsn/cs/connect',
get_history = 'tlsn/cs/get_history',
get_proof = 'tlsn/cs/get_proof',
notarize = 'tlsn/cs/notarize',
install_plugin = 'tlsn/cs/install_plugin',
get_plugins = 'tlsn/cs/get_plugins',
run_plugin = 'tlsn/cs/run_plugin',
}
export type ContentScriptRequest<params> = {
tlsnrpc: string;
} & RPCRequest<ContentScriptTypes, params>;
export type ContentScriptResponse = {
tlsnrpc: string;
} & RPCResponse;
export type RPCRequest<method, params> = {
id: number;
method: method;
params?: params;
};
export 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.tlsnrpc !== '1.0') return;
if (!data.method) return;
const handler = this.#handlers.get(data.method);
if (handler) {
try {
const result = await handler(data);
window.postMessage({
tlsnrpc: '1.0',
id: data.id,
result,
});
} catch (error) {
window.postMessage({
tlsnrpc: '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.tlsnrpc !== '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 = { tlsnrpc: '1.0', id: this.id, method, params };
const defer = deferredPromise();
this.#requests.set(request.id, defer);
window.postMessage(request, '*');
return defer.promise;
}
}

View File

@@ -1,121 +1,85 @@
import React, { useEffect } from 'react';
import * as Comlink from 'comlink';
import { OffscreenActionTypes } from './types';
import {
NotaryServer,
Prover as TProver,
Presentation as TPresentation,
Transcript,
} from 'tlsn-js';
import { verify } from 'tlsn-js-v5';
import { urlify } from '../../utils/misc';
import { BackgroundActiontype } from '../Background/rpc';
import { prove, set_logging_filter, verify } from 'tlsn-js';
import { urlify } from '../../utils/misc';
import browser from 'webextension-polyfill';
import { PresentationJSON } from '../../utils/types';
import { PresentationJSON as PresentationJSONa7 } from 'tlsn-js/build/types';
import { Method } from 'tlsn-js/wasm/pkg';
const { init, Prover, Presentation }: any = Comlink.wrap(
new Worker(new URL('./worker.ts', import.meta.url)),
);
import { getLoggingFilter } from '../../utils/storage';
import { LOGGING_LEVEL_INFO } from '../../utils/constants';
const Offscreen = () => {
useEffect(() => {
(async () => {
const loggingLevel = await browser.runtime.sendMessage({
type: BackgroundActiontype.get_logging_level,
});
await init({ loggingLevel });
// @ts-ignore
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
switch (request.type) {
case OffscreenActionTypes.notarization_request: {
const { id } = request.data;
// @ts-ignore
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
switch (request.type) {
case BackgroundActiontype.process_prove_request: {
const {
url,
method,
headers,
body = '',
maxSentData,
maxRecvData,
maxTranscriptSize,
notaryUrl,
websocketProxyUrl,
id,
secretHeaders,
secretResps,
loggingFilter = LOGGING_LEVEL_INFO,
} = request.data;
(async () => {
try {
const proof = await createProof(request.data);
(async () => {
try {
const token = urlify(url)?.hostname || '';
await set_logging_filter(loggingFilter);
const proof = await prove(url, {
method,
headers,
body,
maxSentData,
maxRecvData,
maxTranscriptSize,
notaryUrl,
websocketProxyUrl: websocketProxyUrl + `?token=${token}`,
secretHeaders,
secretResps,
});
browser.runtime.sendMessage({
type: BackgroundActiontype.finish_prove_request,
data: {
id,
proof,
},
});
browser.runtime.sendMessage({
type: BackgroundActiontype.finish_prove_request,
data: {
id,
proof,
},
});
} catch (error) {
console.log('i caught an error');
console.error(error);
browser.runtime.sendMessage({
type: BackgroundActiontype.finish_prove_request,
data: {
id,
error,
},
});
}
})();
browser.runtime.sendMessage({
type: OffscreenActionTypes.notarization_response,
data: {
id,
proof,
},
});
} catch (error) {
console.error(error);
browser.runtime.sendMessage({
type: BackgroundActiontype.finish_prove_request,
data: {
id,
error,
},
});
break;
}
case BackgroundActiontype.verify_proof: {
(async () => {
const result = await verify(request.data);
sendResponse(result);
})();
browser.runtime.sendMessage({
type: OffscreenActionTypes.notarization_response,
data: {
id,
error,
},
});
}
})();
break;
}
case BackgroundActiontype.process_prove_request: {
const { id } = request.data;
(async () => {
try {
const proof = await createProof(request.data);
browser.runtime.sendMessage({
type: BackgroundActiontype.finish_prove_request,
data: {
id,
proof: proof,
},
});
} catch (error) {
console.error(error);
browser.runtime.sendMessage({
type: BackgroundActiontype.finish_prove_request,
data: {
id,
error,
},
});
}
})();
break;
}
case BackgroundActiontype.verify_proof: {
(async () => {
const result = await verifyProof(request.data);
sendResponse(result);
})();
return true;
}
case BackgroundActiontype.verify_prove_request: {
(async () => {
const proof: PresentationJSON = request.data.proof;
const result: { sent: string; recv: string } =
await verifyProof(proof);
return true;
}
case BackgroundActiontype.verify_prove_request: {
(async () => {
const result = await verify(request.data.proof);
if (result) {
chrome.runtime.sendMessage<any, string>({
type: BackgroundActiontype.finish_prove_request,
data: {
@@ -126,178 +90,17 @@ const Offscreen = () => {
},
},
});
})();
break;
}
default:
break;
}
})();
break;
}
});
})();
default:
break;
}
});
}, []);
return <div className="App" />;
};
export default Offscreen;
function subtractRanges(
ranges: { start: number; end: number },
negatives: { start: number; end: number }[],
): { start: number; end: number }[] {
const returnVal: { start: number; end: number }[] = [ranges];
negatives
.sort((a, b) => (a.start < b.start ? -1 : 1))
.forEach(({ start, end }) => {
const last = returnVal.pop()!;
if (start < last.start || end > last.end) {
console.error('invalid ranges');
return;
}
if (start === last.start && end === last.end) {
return;
}
if (start === last.start && end < last.end) {
returnVal.push({ start: end, end: last.end });
return;
}
if (start > last.start && end < last.end) {
returnVal.push({ start: last.start, end: start });
returnVal.push({ start: end, end: last.end });
return;
}
if (start > last.start && end === last.end) {
returnVal.push({ start: last.start, end: start });
return;
}
});
return returnVal;
}
async function createProof(options: {
url: string;
notaryUrl: string;
websocketProxyUrl: string;
method?: Method;
headers?: {
[name: string]: string;
};
body?: any;
maxSentData?: number;
maxRecvData?: number;
id: string;
secretHeaders: string[];
secretResps: string[];
}): Promise<PresentationJSONa7> {
const {
url,
method = 'GET',
headers = {},
body,
maxSentData,
maxRecvData,
notaryUrl,
websocketProxyUrl,
id,
secretHeaders = [],
secretResps = [],
} = options;
const hostname = urlify(url)?.hostname || '';
const notary = NotaryServer.from(notaryUrl);
const prover: TProver = await new Prover({
id,
serverDns: hostname,
maxSentData,
maxRecvData,
});
await prover.setup(await notary.sessionUrl(maxSentData, maxRecvData));
await prover.sendRequest(websocketProxyUrl + `?token=${hostname}`, {
url,
method,
headers,
body,
});
const transcript = await prover.transcript();
const commit = {
sent: subtractRanges(
transcript.ranges.sent.all,
secretHeaders
.map((secret: string) => {
const index = transcript.sent.indexOf(secret);
return index > -1
? {
start: index,
end: index + secret.length,
}
: null;
})
.filter((data: any) => !!data) as { start: number; end: number }[],
),
recv: subtractRanges(
transcript.ranges.recv.all,
secretResps
.map((secret: string) => {
const index = transcript.recv.indexOf(secret);
return index > -1
? {
start: index,
end: index + secret.length,
}
: null;
})
.filter((data: any) => !!data) as { start: number; end: number }[],
),
};
const notarizationOutputs = await prover.notarize(commit);
const presentation = (await new Presentation({
attestationHex: notarizationOutputs.attestation,
secretsHex: notarizationOutputs.secrets,
notaryUrl: notarizationOutputs.notaryUrl,
websocketProxyUrl: notarizationOutputs.websocketProxyUrl,
reveal: commit,
})) as TPresentation;
const presentationJSON = await presentation.json();
return presentationJSON;
}
async function verifyProof(
proof: PresentationJSON,
): Promise<{ sent: string; recv: string }> {
let result: { sent: string; recv: string };
switch (proof.version) {
case undefined: {
result = await verify(proof);
break;
}
case '0.1.0-alpha.7': {
const presentation: TPresentation = await new Presentation(proof.data);
const verifierOutput = await presentation.verify();
const transcript = new Transcript({
sent: verifierOutput.transcript.sent,
recv: verifierOutput.transcript.recv,
});
result = {
sent: transcript.sent(),
recv: transcript.recv(),
};
break;
}
}
return result;
}

View File

@@ -1,4 +0,0 @@
export enum OffscreenActionTypes {
notarization_request = 'offscreen/notarization_request',
notarization_response = 'offscreen/notarization_response',
}

View File

@@ -1,8 +0,0 @@
import * as Comlink from 'comlink';
import init, { Prover, Presentation } from 'tlsn-js';
Comlink.expose({
init,
Prover,
Presentation,
});

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { createRoot } from 'react-dom/client';
import Options from '../../pages/Options';
import './index.scss';
import './index.css';
const container = document.getElementById('app-container');
const root = createRoot(container!); // createRoot(container!) if you use TypeScript

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect } from 'react';
import { Navigate, Route, Routes, useNavigate } from 'react-router';
import { useDispatch } from 'react-redux';
import {
@@ -20,24 +20,11 @@ import History from '../../pages/History';
import ProofUploader from '../../pages/ProofUploader';
import browser from 'webextension-polyfill';
import store from '../../utils/store';
import PluginUploadInfo from '../../components/PluginInfo';
import ConnectionDetailsModal from '../../components/ConnectionDetailsModal';
import { ConnectionApproval } from '../../pages/ConnectionApproval';
import { GetHistoryApproval } from '../../pages/GetHistoryApproval';
import { GetProofApproval } from '../../pages/GetProofApproval';
import { NotarizeApproval } from '../../pages/NotarizeApproval';
import { InstallPluginApproval } from '../../pages/InstallPluginApproval';
import { GetPluginsApproval } from '../../pages/GetPluginsApproval';
import { RunPluginApproval } from '../../pages/RunPluginApproval';
import Icon from '../../components/Icon';
import classNames from 'classnames';
import { getConnection } from '../Background/db';
import { useIsConnected, setConnection } from '../../reducers/requests';
import { MenuIcon } from '../../components/Menu';
import Plugins from '../../pages/Plugins';
const Popup = () => {
const dispatch = useDispatch();
const activeTab = useActiveTab();
const url = useActiveTabUrl();
const navigate = useNavigate();
useEffect(() => {
@@ -86,7 +73,7 @@ const Popup = () => {
}, []);
return (
<div className="flex flex-col w-full h-full overflow-hidden lg:w-[600px] lg:h-[800px] lg:border lg:m-auto lg:mt-40 lg:bg-white lg:shadow">
<div className="flex flex-col w-full h-full overflow-hidden">
<div className="flex flex-nowrap flex-shrink-0 flex-row items-center relative gap-2 h-9 p-2 cursor-default justify-center bg-slate-300 w-full">
<img
className="absolute left-2 h-5 cursor-pointer"
@@ -94,9 +81,15 @@ const Popup = () => {
alt="logo"
onClick={() => navigate('/')}
/>
<div className="flex flex-row flex-grow items-center justify-end gap-4">
<AppConnectionLogo />
<MenuIcon />
<div className="absolute right-2 flex flex-nowrap flex-row items-center gap-1 justify-center w-fit">
{!!activeTab?.favIconUrl && (
<img
src={activeTab?.favIconUrl}
className="h-5 rounded-full"
alt="logo"
/>
)}
<div className="text-xs">{url?.hostname}</div>
</div>
</div>
<Routes>
@@ -104,23 +97,11 @@ const Popup = () => {
<Route path="/notary/:requestId" element={<Notarize />} />
<Route path="/verify/:requestId/*" element={<ProofViewer />} />
<Route path="/verify" element={<ProofUploader />} />
<Route path="/history" element={<Home tab="history" />} />
<Route path="/requests" element={<Home tab="network" />} />
<Route path="/history" element={<History />} />
<Route path="/requests" element={<Requests />} />
<Route path="/custom/*" element={<RequestBuilder />} />
<Route path="/options" element={<Options />} />
<Route path="/plugins" element={<Plugins />} />
<Route path="/home" element={<Home />} />
<Route path="/plugininfo" element={<PluginUploadInfo />} />
<Route path="/connection-approval" element={<ConnectionApproval />} />
<Route path="/get-history-approval" element={<GetHistoryApproval />} />
<Route path="/get-proof-approval" element={<GetProofApproval />} />
<Route path="/notarize-approval" element={<NotarizeApproval />} />
<Route path="/get-plugins-approval" element={<GetPluginsApproval />} />
<Route path="/run-plugin-approval" element={<RunPluginApproval />} />
<Route
path="/install-plugin-approval"
element={<InstallPluginApproval />}
/>
<Route path="*" element={<Navigate to="/home" />} />
</Routes>
</div>
@@ -128,58 +109,3 @@ const Popup = () => {
};
export default Popup;
function AppConnectionLogo() {
const dispatch = useDispatch();
const activeTab = useActiveTab();
const url = useActiveTabUrl();
const [showConnectionDetails, setShowConnectionDetails] = useState(false);
const connected = useIsConnected();
useEffect(() => {
(async () => {
if (url) {
const isConnected: boolean | null = await getConnection(url?.origin);
dispatch(setConnection(!!isConnected));
}
})();
}, [url]);
return (
<div
className="flex flex-nowrap flex-row items-center gap-1 justify-center w-fit cursor-pointer"
onClick={() => setShowConnectionDetails(true)}
>
<div className="flex flex-row relative bg-black border-[1px] border-black rounded-full">
{!!activeTab?.favIconUrl ? (
<img
src={activeTab?.favIconUrl}
className="h-5 rounded-full"
alt="logo"
/>
) : (
<Icon
fa="fa-solid fa-globe"
className="bg-white text-slate-400 rounded-full"
size={1.25}
/>
)}
<div
className={classNames(
'absolute right-[-2px] bottom-[-2px] rounded-full h-[10px] w-[10px] border-[2px]',
{
'bg-green-500': connected,
'bg-slate-500': !connected,
},
)}
/>
</div>
{showConnectionDetails && (
<ConnectionDetailsModal
showConnectionDetails={showConnectionDetails}
setShowConnectionDetails={setShowConnectionDetails}
/>
)}
</div>
);
}

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>TLSN Extension</title>
<title>Popup</title>
</head>
<body>

View File

@@ -30,10 +30,6 @@ code {
width: 100vw;
height: 100vh;
overflow: hidden;
@media (min-width: 1024px) {
@apply bg-slate-400;
}
}
.button {

View File

@@ -5,6 +5,7 @@ import Popup from './Popup';
import './index.scss';
import { Provider } from 'react-redux';
import store from '../../utils/store';
import { BackgroundActiontype } from '../Background/rpc';
const container = document.getElementById('app-container');
const root = createRoot(container!); // createRoot(container!) if you use TypeScript

View File

@@ -2,13 +2,7 @@ import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import './sidePanel.scss';
import browser from 'webextension-polyfill';
import { fetchPluginConfigByHash, runPlugin } from '../../utils/rpc';
import {
getPluginConfig,
hexToArrayBuffer,
makePlugin,
PluginConfig,
StepConfig,
} from '../../utils/misc';
import { PluginConfig, StepConfig } from '../../utils/misc';
import { PluginList } from '../../components/PluginList';
import DefaultPluginIcon from '../../assets/img/default-plugin-icon.png';
import logo from '../../assets/img/icon-128.png';
@@ -16,10 +10,6 @@ import classNames from 'classnames';
import Icon from '../../components/Icon';
import { useRequestHistory } from '../../reducers/history';
import { BackgroundActiontype } from '../Background/rpc';
import { getPluginByHash, getPluginConfigByHash } from '../Background/db';
import type { Plugin } from '@extism/extism';
import { OffscreenActionTypes } from '../Offscreen/types';
import { SidePanelActionTypes } from './types';
export default function SidePanel(): ReactElement {
const [config, setConfig] = useState<PluginConfig | null>(null);
@@ -29,7 +19,7 @@ export default function SidePanel(): ReactElement {
(async function () {
const result = await browser.storage.local.get('plugin_hash');
const { plugin_hash } = result;
const config = await getPluginConfigByHash(plugin_hash);
const config = await fetchPluginConfigByHash(plugin_hash);
setHash(plugin_hash);
setConfig(config);
// await browser.storage.local.set({ plugin_hash: '' });
@@ -60,41 +50,16 @@ function PluginBody(props: {
const { hash } = props;
const { title, description, icon, steps } = props.config;
const [responses, setResponses] = useState<any[]>([]);
const [notarizationId, setNotarizationId] = useState('');
const notaryRequest = useRequestHistory(notarizationId);
const setResponse = useCallback(
(response: any, i: number) => {
const result = responses.concat();
result[i] = response;
setResponses(result);
if (i === steps!.length - 1 && !!response) {
setNotarizationId(response);
}
},
[hash, responses],
[responses],
);
useEffect(() => {
if (notaryRequest?.status === 'success') {
browser.runtime.sendMessage({
type: SidePanelActionTypes.execute_plugin_response,
data: {
hash,
proof: notaryRequest.proof,
},
});
} else if (notaryRequest?.status === 'error') {
browser.runtime.sendMessage({
type: SidePanelActionTypes.execute_plugin_response,
data: {
hash,
error: notaryRequest.error,
},
});
}
}, [hash, notaryRequest?.status]);
return (
<div className="flex flex-col p-4">
<div className="flex flex-row items-center gap-4">
@@ -137,10 +102,10 @@ function StepContent(
description,
cta,
action,
hash,
setResponse,
lastResponse,
prover,
hash,
} = props;
const [completed, setCompleted] = useState(false);
const [pending, setPending] = useState(false);
@@ -148,37 +113,26 @@ function StepContent(
const [notarizationId, setNotarizationId] = useState('');
const notaryRequest = useRequestHistory(notarizationId);
const getPlugin = useCallback(async () => {
const hex = await getPluginByHash(hash);
const config = await getPluginConfigByHash(hash);
const arrayBuffer = hexToArrayBuffer(hex!);
return makePlugin(arrayBuffer, config!);
}, [hash]);
const processStep = useCallback(async () => {
const plugin = await getPlugin();
if (!plugin) return;
if (index > 0 && !lastResponse) return;
setPending(true);
setError('');
try {
const out = await plugin.call(action, JSON.stringify(lastResponse));
const val = JSON.parse(out.string());
setError('');
const val = await runPlugin(hash, action, JSON.stringify(lastResponse));
if (val && prover) {
setNotarizationId(val);
} else {
setCompleted(!!val);
setResponse(val, index);
}
setResponse(val, index);
} catch (e: any) {
console.error(e);
setError(e?.message || 'Unkonwn error');
} finally {
setPending(false);
}
}, [action, index, lastResponse, prover, getPlugin]);
}, [hash, action, index, lastResponse, prover]);
const onClick = useCallback(() => {
if (
@@ -235,7 +189,7 @@ function StepContent(
)}
onClick={viewProofInPopup}
>
<span className="text-sm">View</span>
<span className="text-sm">View Proof</span>
</button>
);
} else if (notaryRequest?.status === 'pending' || pending || notarizationId) {

View File

@@ -1,4 +0,0 @@
export enum SidePanelActionTypes {
execute_plugin_request = 'sidePanel/execute_plugin_request',
execute_plugin_response = 'sidePanel/execute_plugin_response',
}

View File

@@ -3,8 +3,8 @@
"name": "TLSN Extension",
"description": "A chrome extension for TLSN",
"options_page": "options.html",
"background": {
"service_worker": "background.bundle.js"
"background": { "service_worker": "background.bundle.js",
"persistent": true
},
"action": {
"default_popup": "popup.html",
@@ -28,13 +28,12 @@
],
"web_accessible_resources": [
{
"resources": ["content.styles.css", "icon-128.png", "icon-34.png", "content.bundle.js", "discord_dm.wasm", "twitter_profile.wasm"],
"matches": ["http://*/*", "https://*/*", "<all_urls>"]
"resources": ["content.styles.css", "icon-128.png", "icon-34.png"],
"matches": []
}
],
"host_permissions": ["<all_urls>"],
"permissions": [
"contextMenus".
"offscreen",
"storage",
"webRequest",

View File

@@ -1,44 +0,0 @@
import React, { ReactElement, ReactNode } from 'react';
import logo from '../../assets/img/icon-128.png';
export function BaseApproval({
onSecondaryClick,
onPrimaryClick,
header,
children,
secondaryCTAText = 'Cancel',
primaryCTAText = 'Accept',
}: {
header: ReactNode;
children: ReactNode;
onSecondaryClick: () => void;
onPrimaryClick: () => void;
secondaryCTAText?: string;
primaryCTAText?: string;
}): ReactElement {
return (
<div className="absolute flex flex-col items-center w-screen h-screen bg-white gap-2 cursor-default">
<div className="w-full p-2 border-b border-gray-200 text-gray-500">
<div className="flex flex-row items-end justify-start gap-2">
<img className="h-5" src={logo} alt="logo" />
<span className="font-semibold">{header}</span>
</div>
</div>
<div className="flex flex-col flex-grow gap-2 overflow-y-auto w-full">
{children}
</div>
<div className="flex flex-row w-full gap-2 justify-end border-t p-4">
{!!onSecondaryClick && !!secondaryCTAText && (
<button className="button" onClick={onSecondaryClick}>
{secondaryCTAText}
</button>
)}
{!!onPrimaryClick && !!primaryCTAText && (
<button className="button button--primary" onClick={onPrimaryClick}>
{primaryCTAText}
</button>
)}
</div>
</div>
);
}

View File

@@ -1,62 +0,0 @@
import React, { ReactElement, useCallback } from 'react';
import Icon from '../../components/Icon';
import logo from '../../assets/img/icon-128.png';
import { useSearchParams } from 'react-router-dom';
import { urlify } from '../../utils/misc';
import browser from 'webextension-polyfill';
import { BackgroundActiontype } from '../../entries/Background/rpc';
import { BaseApproval } from '../BaseApproval';
export function ConnectionApproval(): ReactElement {
const [params] = useSearchParams();
const origin = params.get('origin');
const favIconUrl = params.get('favIconUrl');
const hostname = urlify(origin || '')?.hostname;
const onCancel = useCallback(() => {
browser.runtime.sendMessage({
type: BackgroundActiontype.connect_response,
data: false,
});
}, []);
const onAccept = useCallback(() => {
browser.runtime.sendMessage({
type: BackgroundActiontype.connect_response,
data: true,
});
}, []);
return (
<BaseApproval
header={`Connecting to ${hostname}`}
onSecondaryClick={onCancel}
onPrimaryClick={onAccept}
>
<div className="flex flex-col items-center gap-2 py-8">
{!!favIconUrl ? (
<img
src={favIconUrl}
className="h-16 w-16 border border-slate-200 bg-slate-200 rounded-full"
alt="logo"
/>
) : (
<Icon
fa="fa-solid fa-globe"
size={4}
className="h-16 w-16 rounded-full border border-slate-200 text-blue-500"
/>
)}
<div className="text-sm font-semibold">{hostname}</div>
</div>
<div className="text-lg font-bold text-center">Connect to this site?</div>
<div className="text-sm px-8 text-center text-slate-500 flex-grow">
Do you trust this site? By granting this permission, you're allowing
this site to view your installed plugins, suggest requests to notarize,
suggest plugins to install, ask you to share proofs metadata{' '}
<i>(method, url, notary url, and proxy url)</i>, and ask to view a
specific proof.
</div>
</BaseApproval>
);
}

View File

@@ -1,139 +0,0 @@
import React, { ReactElement, useCallback, useEffect } from 'react';
import Icon from '../../components/Icon';
import { useSearchParams } from 'react-router-dom';
import { safeParseJSON, urlify } from '../../utils/misc';
import browser from 'webextension-polyfill';
import { BackgroundActiontype } from '../../entries/Background/rpc';
import { BaseApproval } from '../BaseApproval';
import { minimatch } from 'minimatch';
import { useAllProofHistory } from '../../reducers/history';
import classNames from 'classnames';
export function GetHistoryApproval(): ReactElement {
const [params] = useSearchParams();
const origin = params.get('origin');
const favIconUrl = params.get('favIconUrl');
const method = params.get('method');
const url = params.get('url');
const rawMetadata = params.get('metadata');
const metadata = safeParseJSON(rawMetadata);
const hostname = urlify(origin || '')?.hostname;
const proofs = useAllProofHistory();
useEffect(() => {
browser.runtime.sendMessage({
type: BackgroundActiontype.get_prove_requests,
});
}, []);
const onCancel = useCallback(() => {
browser.runtime.sendMessage({
type: BackgroundActiontype.get_history_response,
data: false,
});
}, []);
const onAccept = useCallback(() => {
browser.runtime.sendMessage({
type: BackgroundActiontype.get_history_response,
data: true,
});
}, []);
const result = proofs.filter((proof) => {
let matchedMetadata = true;
if (metadata) {
matchedMetadata = Object.entries(
metadata as { [k: string]: string },
).reduce((bool, [k, v]) => {
try {
return bool && minimatch(proof.metadata![k], v);
} catch (e) {
return false;
}
}, matchedMetadata);
}
return (
minimatch(proof.method, method!, { nocase: true }) &&
minimatch(proof.url, url!) &&
matchedMetadata
);
});
return (
<BaseApproval
header="Requesting Proof History"
onSecondaryClick={onCancel}
onPrimaryClick={onAccept}
>
<div className="flex flex-col items-center gap-2 py-8">
{!!favIconUrl ? (
<img
src={favIconUrl}
className="h-16 w-16 rounded-full border border-slate-200 bg-slate-200"
alt="logo"
/>
) : (
<Icon
fa="fa-solid fa-globe"
size={4}
className="h-16 w-16 rounded-full border border-slate-200 text-blue-500"
/>
)}
<div className="text-2xl text-center px-8">
Do you want to share proof history with{' '}
<b className="text-blue-500">{hostname}</b>?
</div>
</div>
<div className="flex flex-col items-center gap-4 text-sm px-8 text-center flex-grow">
<div className="text-slate-500">
All proofs matching the following patterns with be shared:
</div>
<table className="border border-collapse table-auto rounded text-xs w-full">
<tbody>
<tr>
<td className="px-2 py-1 border border-slate-300 bg-slate-100 text-slate-500 align-top w-16 text-left">
Method
</td>
<td className="px-2 py-1 border border-slate-300 font-semibold text-black font-mono text-left">
{method?.toUpperCase()}
</td>
</tr>
<tr className="">
<td className="px-2 py-1 border border-slate-300 bg-slate-100 text-slate-500 align-top w-16 text-left">
URL
</td>
<td className="px-2 py-1 border border-slate-300 font-semibold text-black font-mono break-all text-left">
{url}
</td>
</tr>
{rawMetadata && (
<tr className="">
<td className="px-2 py-1 border border-slate-300 bg-slate-100 text-slate-500 align-top w-16 text-left">
Metadata
</td>
<td className="px-2 py-1 border border-slate-300 font-semibold text-black font-mono break-all text-left">
{rawMetadata}
</td>
</tr>
)}
</tbody>
</table>
<div
className={classNames('border rounded font-semibold px-2 py-1', {
'text-green-500 bg-green-200 border-green-300': result.length,
'text-slate-500 bg-slate-200 border-slate-300': !result.length,
})}
>
{result.length} results found
</div>
</div>
<div className="text-xs px-8 pb-2 text-center text-slate-500">
Only certain metadata will be shared with the app, such as <i>id</i>,{' '}
<i>method</i>, <i>url</i>, <i>notary</i>, <i>proxy</i>, and{' '}
<i>timestamp</i>.
</div>
</BaseApproval>
);
}

View File

@@ -1,138 +0,0 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import Icon from '../../components/Icon';
import { useSearchParams } from 'react-router-dom';
import { safeParseJSON, urlify } from '../../utils/misc';
import browser from 'webextension-polyfill';
import { BackgroundActiontype } from '../../entries/Background/rpc';
import { BaseApproval } from '../BaseApproval';
import { getPlugins } from '../../entries/Background/db';
import { minimatch } from 'minimatch';
import classNames from 'classnames';
export function GetPluginsApproval(): ReactElement {
const [params] = useSearchParams();
const origin = params.get('origin');
const favIconUrl = params.get('favIconUrl');
const url = params.get('url');
const filterOrigin = params.get('filterOrigin');
const rawMetadata = params.get('metadata');
const filterMetadata = safeParseJSON(rawMetadata);
const hostname = urlify(origin || '')?.hostname;
const [result, setResult] = useState<any[]>([]);
const onCancel = useCallback(() => {
browser.runtime.sendMessage({
type: BackgroundActiontype.get_plugins_response,
data: false,
});
}, []);
const onAccept = useCallback(() => {
browser.runtime.sendMessage({
type: BackgroundActiontype.get_plugins_response,
data: true,
});
}, []);
useEffect(() => {
(async () => {
const response = await getPlugins();
const res = response.filter(({ metadata }) => {
let matchedMetadata = true;
if (filterMetadata) {
matchedMetadata = Object.entries(
filterMetadata as { [k: string]: string },
).reduce((bool, [k, v]) => {
try {
return bool && minimatch(metadata![k], v);
} catch (e) {
return false;
}
}, matchedMetadata);
}
return (
minimatch(metadata.filePath, url || '**') &&
minimatch(metadata.origin, filterOrigin || '**') &&
matchedMetadata
);
});
setResult(res);
})();
}, [url, filterMetadata]);
return (
<BaseApproval
header="Requesting Plugins"
onSecondaryClick={onCancel}
onPrimaryClick={onAccept}
>
<div className="flex flex-col items-center gap-2 py-8">
{!!favIconUrl ? (
<img
src={favIconUrl}
className="h-16 w-16 rounded-full border border-slate-200 bg-slate-200"
alt="logo"
/>
) : (
<Icon
fa="fa-solid fa-globe"
size={4}
className="h-16 w-16 rounded-full border border-slate-200 text-blue-500"
/>
)}
<div className="text-2xl text-center px-8">
Do you want to share installed plugins with{' '}
<b className="text-blue-500">{hostname}</b>?
</div>
</div>
<div className="flex flex-col items-center gap-4 text-sm px-8 text-center flex-grow">
<div className="text-slate-500">
All plugins matching the following patterns with be shared:
</div>
<table className="border border-collapse table-auto rounded text-xs w-full">
<tbody>
<tr className="">
<td className="px-2 py-1 border border-slate-300 bg-slate-100 text-slate-500 align-top w-16 text-left">
URL
</td>
<td className="px-2 py-1 border border-slate-300 font-semibold text-black font-mono break-all text-left">
{url}
</td>
</tr>
<tr className="">
<td className="px-2 py-1 border border-slate-300 bg-slate-100 text-slate-500 align-top w-16 text-left">
Origin
</td>
<td className="px-2 py-1 border border-slate-300 font-semibold text-black font-mono break-all text-left">
{filterOrigin}
</td>
</tr>
{rawMetadata && (
<tr className="">
<td className="px-2 py-1 border border-slate-300 bg-slate-100 text-slate-500 align-top w-16 text-left">
Metadata
</td>
<td className="px-2 py-1 border border-slate-300 font-semibold text-black font-mono break-all text-left">
{rawMetadata}
</td>
</tr>
)}
</tbody>
</table>
<div
className={classNames('border rounded font-semibold px-2 py-1', {
'text-green-500 bg-green-200 border-green-300': result.length,
'text-slate-500 bg-slate-200 border-slate-300': !result.length,
})}
>
{result.length} results found
</div>
</div>
<div className="text-xs px-8 pb-2 text-center text-slate-500">
Only certain metadata will be shared with the app, such as <i>id</i>,{' '}
<i>method</i>, <i>url</i>, <i>notary</i>, <i>proxy</i>, and{' '}
<i>timestamp</i>.
</div>
</BaseApproval>
);
}

View File

@@ -1,68 +0,0 @@
import React, { ReactElement, useCallback, useEffect } from 'react';
import Icon from '../../components/Icon';
import { useSearchParams } from 'react-router-dom';
import { urlify } from '../../utils/misc';
import browser from 'webextension-polyfill';
import { BackgroundActiontype } from '../../entries/Background/rpc';
import { BaseApproval } from '../BaseApproval';
import { OneRequestHistory } from '../History';
export function GetProofApproval(): ReactElement {
const [params] = useSearchParams();
const origin = params.get('origin');
const favIconUrl = params.get('favIconUrl');
const id = params.get('id');
const hostname = urlify(origin || '')?.hostname;
const onCancel = useCallback(() => {
browser.runtime.sendMessage({
type: BackgroundActiontype.get_proof_response,
data: false,
});
}, []);
const onAccept = useCallback(() => {
browser.runtime.sendMessage({
type: BackgroundActiontype.get_proof_response,
data: true,
});
}, []);
return (
<BaseApproval
header="Requesting Proof History"
onSecondaryClick={onCancel}
onPrimaryClick={onAccept}
>
<div className="flex flex-col items-center gap-2 py-8">
{!!favIconUrl ? (
<img
src={favIconUrl}
className="h-16 w-16 rounded-full border border-slate-200 bg-slate-200"
alt="logo"
/>
) : (
<Icon
fa="fa-solid fa-globe"
size={4}
className="h-16 w-16 rounded-full border border-slate-200 text-blue-500"
/>
)}
<div className="text-2xl text-center px-8">
Do you want to share proof data with{' '}
<b className="text-blue-500">{hostname}</b>?
</div>
</div>
<div className="flex flex-col items-center gap-4 text-sm px-8 text-center flex-grow">
<div className="text-slate-500">
The following proof will be shared:
</div>
<OneRequestHistory
className="w-full !cursor-default hover:bg-white text-xs"
requestId={id!}
hideActions={['share', 'delete', 'retry']}
/>
</div>
</BaseApproval>
);
}

View File

@@ -7,7 +7,13 @@ import {
deleteRequestHistory,
} from '../../reducers/history';
import Icon from '../../components/Icon';
import { getNotaryApi, getProxyApi } from '../../utils/storage';
import {
get,
NOTARY_API_LS_KEY,
PROXY_API_LS_KEY,
getNotaryApi,
getProxyApi,
} from '../../utils/storage';
import { urlify, download, upload } from '../../utils/misc';
import { BackgroundActiontype } from '../../entries/Background/rpc';
import Modal, { ModalContent } from '../../components/Modal/Modal';
@@ -18,28 +24,20 @@ import {
getNotaryRequest,
setNotaryRequestCid,
} from '../../entries/Background/db';
const charwise = require('charwise');
export default function History(): ReactElement {
const history = useHistoryOrder();
return (
<div className="flex flex-col flex-nowrap overflow-y-auto">
{history
.map((id) => {
return <OneRequestHistory key={id} requestId={id} />;
})
.reverse()}
{history.map((id) => {
return <OneRequestHistory key={id} requestId={id} />;
})}
</div>
);
}
export function OneRequestHistory(props: {
requestId: string;
className?: string;
hideActions?: string[];
}): ReactElement {
const { hideActions = [] } = props;
function OneRequestHistory(props: { requestId: string }): ReactElement {
const dispatch = useDispatch();
const request = useRequestHistory(props.requestId);
const [showingError, showError] = useState(false);
@@ -117,12 +115,7 @@ export function OneRequestHistory(props: {
}, [props.requestId, request, cid]);
return (
<div
className={classNames(
'flex flex-row flex-nowrap border rounded-md p-2 gap-1 hover:bg-slate-50 cursor-pointer',
props.className,
)}
>
<div className="flex flex-row flex-nowrap border rounded-md p-2 gap-1 hover:bg-slate-50 cursor-pointer">
<ShareConfirmationModal />
<ErrorModal />
<div className="flex flex-col flex-nowrap flex-grow flex-shrink w-0">
@@ -134,12 +127,6 @@ export function OneRequestHistory(props: {
{requestUrl?.pathname}
</div>
</div>
<div className="flex flex-row">
<div className="font-bold text-slate-400">Time:</div>
<div className="ml-2 text-slate-800">
{new Date(charwise.decode(props.requestId, 'hex')).toISOString()}
</div>
</div>
<div className="flex flex-row">
<div className="font-bold text-slate-400">Host:</div>
<div className="ml-2 text-slate-800">{requestUrl?.host}</div>
@@ -149,7 +136,7 @@ export function OneRequestHistory(props: {
<div className="ml-2 text-slate-800">{request?.notaryUrl}</div>
</div>
<div className="flex flex-row">
<div className="font-bold text-slate-400">TLS Proxy API:</div>
<div className="font-bold text-slate-400">TLS Proxy API: </div>
<div className="ml-2 text-slate-800">
{request?.websocketProxyUrl}
</div>
@@ -162,8 +149,7 @@ export function OneRequestHistory(props: {
className="bg-slate-600 text-slate-200 hover:bg-slate-500 hover:text-slate-100"
onClick={onView}
fa="fa-solid fa-receipt"
ctaText="View"
hidden={hideActions.includes('view')}
ctaText="View Proof"
/>
<ActionButton
className="bg-slate-100 text-slate-300 hover:bg-slate-200 hover:text-slate-500"
@@ -172,42 +158,35 @@ export function OneRequestHistory(props: {
}
fa="fa-solid fa-download"
ctaText="Download"
hidden={hideActions.includes('download')}
/>
<ActionButton
className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-slate-100 text-slate-300 hover:bg-slate-200 hover:text-slate-500 hover:font-bold"
onClick={() => setShowingShareConfirmation(true)}
fa="fa-solid fa-upload"
ctaText="Share"
hidden={hideActions.includes('share')}
/>
</>
)}
{status === 'error' && !!request?.error && (
<ErrorButton hidden={hideActions.includes('error')} />
)}
{(!status || status === 'error') && (
<RetryButton hidden={hideActions.includes('retry')} />
)}
{status === 'error' && !!request?.error && <ErrorButton />}
{(!status || status === 'error') && <RetryButton />}
{status === 'pending' && (
<button className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-slate-100 text-slate-300 font-bold">
<Icon className="animate-spin" fa="fa-solid fa-spinner" size={1} />
<span className="text-xs font-bold">Pending</span>
</button>
)}
<ActionButton
<button
className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-slate-100 text-slate-300 hover:bg-red-100 hover:text-red-500 hover:font-bold"
onClick={onDelete}
fa="fa-solid fa-trash"
ctaText="Delete"
hidden={hideActions.includes('delete')}
/>
>
<Icon className="" fa="fa-solid fa-trash" size={1} />
<span className="text-xs font-bold">Delete</span>
</button>
</div>
</div>
);
function RetryButton(p: { hidden?: boolean }): ReactElement {
if (p.hidden) return <></>;
function RetryButton(): ReactElement {
return (
<button
className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-slate-100 text-slate-300 hover:bg-slate-200 hover:text-slate-500 hover:font-bold"
@@ -219,8 +198,7 @@ export function OneRequestHistory(props: {
);
}
function ErrorButton(p: { hidden?: boolean }): ReactElement {
if (p.hidden) return <></>;
function ErrorButton(): ReactElement {
return (
<button
className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-red-100 text-red-300 hover:bg-red-200 hover:text-red-500 hover:font-bold"
@@ -233,7 +211,6 @@ export function OneRequestHistory(props: {
}
function ErrorModal(): ReactElement {
const msg = typeof request?.error === 'string' && request?.error;
return !showingError ? (
<></>
) : (
@@ -242,7 +219,7 @@ export function OneRequestHistory(props: {
onClose={closeAllModal}
>
<ModalContent className="flex justify-center items-center text-slate-500">
{msg || 'Something went wrong :('}
{request?.error || 'Something went wrong :('}
</ModalContent>
<button
className="m-0 w-24 bg-red-100 text-red-300 hover:bg-red-200 hover:text-red-500"
@@ -332,10 +309,7 @@ function ActionButton(props: {
fa: string;
ctaText: string;
className?: string;
hidden?: boolean;
}): ReactElement {
if (props.hidden) return <></>;
return (
<button
className={classNames(

View File

@@ -1,5 +0,0 @@
#home {
&::-webkit-scrollbar {
display: none;
}
}

View File

@@ -1,285 +1,102 @@
import React, {
ChangeEvent,
MouseEventHandler,
ReactElement,
ReactNode,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import Icon from '../../components/Icon';
import classNames from 'classnames';
import { useNavigate } from 'react-router';
import { useRequests } from '../../reducers/requests';
import { makePlugin, getPluginConfig } from '../../utils/misc';
import { addPlugin } from '../../utils/rpc';
import { PluginList } from '../../components/PluginList';
import { ErrorModal } from '../../components/ErrorModal';
import History from '../History';
import './index.scss';
import Requests from '../Requests';
import PluginUploadInfo from '../../components/PluginInfo';
import {
useOnPluginClick,
usePluginConfig,
usePluginHashes,
} from '../../reducers/plugins';
import { PluginConfig } from '../../utils/misc';
import { getPluginConfigByHash } from '../../entries/Background/db';
import { fetchPluginHashes } from '../../utils/rpc';
import DefaultPluginIcon from '../../assets/img/default-plugin-icon.png';
export default function Home(props: {
tab?: 'history' | 'network';
}): ReactElement {
const [error, showError] = useState('');
const [tab, setTab] = useState<'history' | 'network'>(props.tab || 'history');
const scrollableContent = useRef<HTMLDivElement | null>(null);
const [shouldFix, setFix] = useState(false);
const [actionPanelElement, setActionPanelElement] =
useState<HTMLDivElement | null>(null);
const [scrollTop, setScrollTop] = useState(0);
useEffect(() => {
fetchPluginHashes();
}, []);
useEffect(() => {
const element = scrollableContent.current;
if (!element) return;
if (!actionPanelElement) return;
let timer = Date.now();
const onScroll = () => {
const now = Date.now();
if (now - timer > 20) {
timer = now;
setScrollTop(element.scrollTop);
if (element.scrollTop >= actionPanelElement.clientHeight) {
setFix(true);
} else {
setFix(false);
}
}
};
element.addEventListener('scroll', onScroll);
return () => {
element.removeEventListener('scroll', onScroll);
};
}, [scrollableContent, actionPanelElement]);
return (
<div
id="home"
ref={scrollableContent}
className="flex flex-col flex-grow overflow-y-auto"
>
{error && <ErrorModal onClose={() => showError('')} message={error} />}
<ActionPanel
setActionPanelElement={setActionPanelElement}
scrollTop={scrollTop}
/>
<div
className={classNames('flex flex-row justify-center items-center', {
'fixed top-9 w-full bg-white shadow lg:w-[598px] lg:mt-40': shouldFix,
})}
>
<TabSelector
onClick={() => setTab('network')}
selected={tab === 'network'}
>
Network
</TabSelector>
<TabSelector
onClick={() => setTab('history')}
selected={tab === 'history'}
>
History
</TabSelector>
</div>
<div className="flex-grow">
{tab === 'history' && <History />}
{tab === 'network' && <Requests shouldFix={shouldFix} />}
</div>
</div>
);
}
function ActionPanel({
setActionPanelElement,
scrollTop,
}: {
scrollTop: number;
setActionPanelElement: (el: HTMLDivElement) => void;
}) {
const pluginHashes = usePluginHashes();
export default function Home(): ReactElement {
const requests = useRequests();
const navigate = useNavigate();
const container = useRef<HTMLDivElement | null>(null);
const [isOverflow, setOverflow] = useState(false);
const [expanded, setExpand] = useState(false);
const [error, showError] = useState('');
useEffect(() => {
const element = container.current;
if (!element) return;
setActionPanelElement(element);
const onCheckSize = () => {
if (element.scrollWidth > element.clientWidth) {
setOverflow(true);
} else {
setOverflow(false);
const onAddPlugin = useCallback(
async (evt: ChangeEvent<HTMLInputElement>) => {
if (!evt.target.files) return;
try {
const [file] = evt.target.files;
const arrayBuffer = await file.arrayBuffer();
const plugin = await makePlugin(arrayBuffer);
await getPluginConfig(plugin);
await addPlugin(Buffer.from(arrayBuffer).toString('hex'));
} catch (e: any) {
showError(e?.message || 'Invalid Plugin');
}
};
onCheckSize();
window.addEventListener('resize', onCheckSize);
return () => {
window.removeEventListener('resize', onCheckSize);
};
}, [container, pluginHashes]);
useEffect(() => {
const element = container.current;
if (!element) return;
if (scrollTop >= element.clientHeight) {
setExpand(false);
}
}, [container, scrollTop]);
},
[],
);
return (
<div
ref={container}
className={classNames(
'flex flex-row justify-start items-center gap-4 p-4 border-b relative',
{
'flex-wrap': expanded,
'flex-nowrap': !expanded,
},
)}
>
<NavButton
fa="fa-solid fa-hammer"
onClick={() => navigate('/custom')}
title="Build a custom request"
>
Custom
</NavButton>
<NavButton
fa="fa-solid fa-certificate"
onClick={() => navigate('/verify')}
title="Visualize an attestation"
>
Verify
</NavButton>
{pluginHashes.map((hash) => (
<PluginIcon hash={hash} />
))}
<button
className={
'flex flex-row items-center justify-center self-start rounded relative border-2 border-dashed border-slate-300 hover:border-slate-400 text-slate-300 hover:text-slate-400 h-16 w-16 mx-1'
}
title="Install a plugin"
>
<PluginUploadInfo />
<Icon fa="fa-solid fa-plus" />
</button>
<button
className={classNames(
'absolute right-0 top-0 w-6 h-full bg-slate-100 hover:bg-slate-200 font-semibold',
'flex flex-row items-center justify-center gap-2 text-slate-500 hover:text-slate-700',
{
hidden: !isOverflow || expanded,
},
)}
onClick={() => setExpand(true)}
>
<Icon fa="fa-solid fa-caret-down" size={0.875} />
</button>
<div className="flex flex-col gap-4 py-4 overflow-y-auto">
{error && <ErrorModal onClose={() => showError('')} message={error} />}
<div className="flex flex-col flex-nowrap justify-center gap-2 mx-4">
<NavButton fa="fa-solid fa-table" onClick={() => navigate('/requests')}>
<span>Requests</span>
<span>{`(${requests.length})`}</span>
</NavButton>
<NavButton fa="fa-solid fa-hammer" onClick={() => navigate('/custom')}>
Custom
</NavButton>
<NavButton
fa="fa-solid fa-certificate"
onClick={() => navigate('/verify')}
>
Verify
</NavButton>
<NavButton fa="fa-solid fa-list" onClick={() => navigate('/history')}>
History
</NavButton>
<NavButton className="relative" fa="fa-solid fa-plus">
<input
className="opacity-0 absolute top-0 right-0 h-full w-full"
type="file"
onChange={onAddPlugin}
/>
Add a plugin
</NavButton>
<NavButton fa="fa-solid fa-gear" onClick={() => navigate('/options')}>
Options
</NavButton>
</div>
<PluginList className="mx-4" />
</div>
);
}
function PluginIcon({ hash }: { hash: string }) {
const config = usePluginConfig(hash);
const onPluginClick = useOnPluginClick(hash);
const onClick = useCallback(() => {
if (!config) return;
onPluginClick();
}, [onPluginClick, config]);
return (
<button
className={classNames(
'flex flex-col flex-nowrap items-center justify-center',
'text-white px-2 py-1 gap-1 opacity-90 hover:opacity-100',
)}
onClick={onClick}
>
<Icon
className="rounded-full flex flex-row items-center justify-center flex-grow-0 flex-shrink-0"
url={config?.icon || DefaultPluginIcon}
size={2}
/>
<span className="font-bold text-primary h-10 w-14 overflow-hidden text-ellipsis">
{config?.title}
</span>
</button>
);
}
function TabSelector(props: {
children: string;
className?: string;
selected?: boolean;
onClick: MouseEventHandler;
}): ReactElement {
return (
<button
onClick={props.onClick}
className={classNames(
'flex flex-grow items-center justify-center p-2 font-semibold hover:text-slate-700 border-b-2 ',
{
'font-semibold text-slate-400 border-white': !props.selected,
'font-bold text-primary border-primary': props.selected,
},
props.className,
)}
>
{props.children}
</button>
);
}
function NavButton(props: {
fa: string;
children?: ReactNode;
onClick?: MouseEventHandler;
className?: string;
title?: string;
disabled?: boolean;
}): ReactElement {
return (
<button
className={classNames(
'flex flex-col flex-nowrap items-center justify-center',
'text-white px-2 py-1 gap-1 opacity-90 hover:opacity-100',
'flex flex-row flex-nowrap items-center justify-center',
'text-white rounded px-2 py-1 gap-1',
{
'bg-primary/[.8] hover:bg-primary/[.7] active:bg-primary':
!props.disabled,
'bg-primary/[.5]': props.disabled,
},
props.className,
)}
onClick={props.onClick}
disabled={props.disabled}
title={props.title}
>
<Icon
className="w-8 h-8 rounded-full bg-primary flex flex-row items-center justify-center flex-grow-0 flex-shrink-0"
fa={props.fa}
size={0.875}
/>
<span className="font-bold text-primary h-10 w-14 overflow-hidden text-ellipsis">
<Icon className="flex-grow-0 flex-shrink-0" fa={props.fa} size={1} />
<span className="flex-grow flex-shrink w-0 flex-grow font-bold">
{props.children}
</span>
</button>

View File

@@ -1,108 +0,0 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import Icon from '../../components/Icon';
import { useSearchParams } from 'react-router-dom';
import {
getPluginConfig,
makePlugin,
type PluginConfig,
urlify,
} from '../../utils/misc';
import browser from 'webextension-polyfill';
import { BackgroundActiontype } from '../../entries/Background/rpc';
import { BaseApproval } from '../BaseApproval';
import { PluginPermissions } from '../../components/PluginInfo';
export function InstallPluginApproval(): ReactElement {
const [params] = useSearchParams();
const origin = params.get('origin');
const favIconUrl = params.get('favIconUrl');
const url = params.get('url');
const rawMetadata = params.get('metadata');
const hostname = urlify(origin || '')?.hostname;
const [error, showError] = useState('');
const [pluginBuffer, setPluginBuffer] = useState<ArrayBuffer | any>(null);
const [pluginContent, setPluginContent] = useState<PluginConfig | null>(null);
const onCancel = useCallback(() => {
browser.runtime.sendMessage({
type: BackgroundActiontype.install_plugin_response,
data: false,
});
}, []);
const onAccept = useCallback(() => {
browser.runtime.sendMessage({
type: BackgroundActiontype.install_plugin_response,
data: true,
});
}, []);
useEffect(() => {
(async () => {
try {
const resp = await fetch(url!);
const arrayBuffer = await resp.arrayBuffer();
const plugin = await makePlugin(arrayBuffer);
setPluginContent(await getPluginConfig(plugin));
setPluginBuffer(arrayBuffer);
} catch (e: any) {
showError(e?.message || 'Invalid Plugin');
}
})();
}, [url]);
return (
<BaseApproval
header={`Installing Plugin`}
onSecondaryClick={onCancel}
onPrimaryClick={onAccept}
>
<div className="flex flex-col items-center gap-2 py-8">
{!!favIconUrl ? (
<img
src={favIconUrl}
className="h-16 w-16 rounded-full border border-slate-200 bg-slate-200"
alt="logo"
/>
) : (
<Icon
fa="fa-solid fa-globe"
size={4}
className="h-16 w-16 rounded-full border border-slate-200 text-blue-500"
/>
)}
<div className="text-2xl text-center px-8">
<b className="text-blue-500">{hostname}</b> wants to install a plugin:
</div>
</div>
{!pluginContent && (
<div className="flex flex-col items-center flex-grow gap-4 border border-slate-300 p-8 mx-8 rounded bg-slate-100">
<Icon
className="animate-spin w-fit text-slate-500"
fa="fa-solid fa-spinner"
size={1}
/>
</div>
)}
{pluginContent && (
<div className="flex flex-col flex-grow gap-4 border border-slate-300 p-8 mx-8 rounded bg-slate-100">
<div className="flex flex-col items-center">
<img
className="w-12 h-12 mb-2"
src={pluginContent.icon}
alt="Plugin Icon"
/>
<span className="text-3xl text-blue-600 font-semibold">
{pluginContent.title}
</span>
<div className="text-slate-500 text-lg">
{pluginContent.description}
</div>
</div>
<PluginPermissions className="w-full" pluginContent={pluginContent} />
</div>
)}
</BaseApproval>
);
}

View File

@@ -1,18 +1,21 @@
import classNames from 'classnames';
import React, {
ReactNode,
ReactElement,
useState,
useCallback,
ReactEventHandler,
useEffect,
useRef,
useMemo,
} from 'react';
import { useNavigate, useParams } from 'react-router';
import { useLocation, useNavigate, useParams } from 'react-router';
import { notarizeRequest, useRequest } from '../../reducers/requests';
import Icon from '../../components/Icon';
import { urlify } from '../../utils/misc';
import {
get,
NOTARY_API_LS_KEY,
PROXY_API_LS_KEY,
getNotaryApi,
getProxyApi,
getMaxSent,
@@ -20,6 +23,8 @@ import {
} from '../../utils/storage';
import { useDispatch } from 'react-redux';
const maxTranscriptSize = 16384;
export default function Notarize(): ReactElement {
const params = useParams<{ requestId: string }>();
const req = useRequest(params.requestId);
@@ -47,7 +52,9 @@ export default function Notarize(): ReactElement {
//TODO: for some reason, these needs to be override to work
headers['Accept-Encoding'] = 'identity';
headers['Connection'] = 'close';
if (req.requestBody) {
headers['Content-Length'] = req.requestBody.length.toString();
}
dispatch(
// @ts-ignore
notarizeRequest({
@@ -57,6 +64,7 @@ export default function Notarize(): ReactElement {
body: req.requestBody,
maxSentData,
maxRecvData,
maxTranscriptSize,
notaryUrl,
websocketProxyUrl,
secretHeaders,
@@ -119,7 +127,7 @@ export default function Notarize(): ReactElement {
);
}
export function RevealHeaderStep(props: {
function RevealHeaderStep(props: {
onNext: () => void;
onCancel: () => void;
setSecretHeaders: (secrets: string[]) => void;
@@ -128,8 +136,6 @@ export function RevealHeaderStep(props: {
const req = useRequest(params.requestId);
const [revealed, setRevealed] = useState<{ [key: string]: boolean }>({});
const headers = req?.requestHeaders;
useEffect(() => {
if (!req) return;
@@ -157,21 +163,21 @@ export function RevealHeaderStep(props: {
[revealed, req],
);
if (!headers) return <></>;
if (!req) return <></>;
return (
<div className="flex flex-col flex-nowrap flex-shrink flex-grow h-0">
<div className="border bg-primary/[0.9] text-white border-slate-300 py-1 px-2 font-semibold">
`Step 1 of 2: Select which request headers you want to reveal`
Step 1 of 2: Select which request headers you want to reveal
</div>
<div className="flex-grow flex-shrink h-0 overflow-y-auto">
<table className="border border-slate-300 border-collapse table-fixed">
<tbody className="bg-slate-200">
{headers.map((h) => (
{req.requestHeaders?.map((h) => (
<tr
key={h.name}
className={classNames('border-b border-slate-200 text-xs', {
'bg-slate-50': revealed[h.name],
'bg-slate-50': !!revealed[h.name],
})}
>
<td className="border border-slate-300 py-1 px-2 align-top">
@@ -179,14 +185,14 @@ export function RevealHeaderStep(props: {
type="checkbox"
className="cursor-pointer"
onChange={(e) => changeHeaderKey(h.name, e.target.checked)}
checked={revealed[h.name]}
checked={!!revealed[h.name]}
/>
</td>
<td className="border border-slate-300 font-bold align-top py-1 px-2 whitespace-nowrap">
{h.name}
</td>
<td className="border border-slate-300 break-all align-top py-1 px-2">
{revealed[h.name]
{!!revealed[h.name]
? h.value
: Array(h.value?.length || 0)
.fill('*')
@@ -212,146 +218,34 @@ export function RevealHeaderStep(props: {
);
}
export function RevealHeaderTable(props: {
headers: { name: string; value: string }[];
className?: string;
onChange: (revealed: { [key: string]: boolean }) => void;
}) {
const { headers } = props;
const [revealed, setRevealed] = useState<{ [key: string]: boolean }>({});
const changeHeaderKey = useCallback(
(key: string, shouldReveal: boolean) => {
const result = {
...revealed,
[key]: shouldReveal,
};
setRevealed(result);
props.onChange(result);
},
[revealed],
);
return (
<table
className={classNames(
'border border-slate-300 border-collapse table-fixed',
props.className,
)}
>
<thead className="bg-slate-200">
<th className="border border-slate-300 py-1 px-2 align-middle w-8"></th>
<th className="border border-slate-300 py-1 px-2 align-middle">Name</th>
<th className="border border-slate-300 py-1 px-2 align-middle">
Value
</th>
</thead>
<tbody className="bg-slate-100">
{headers.map((h) => (
<tr
key={h.name}
className={classNames('border-b border-slate-200 text-xs', {
'bg-slate-50': revealed[h.name],
})}
>
<td className="border border-slate-300 py-1 px-2 align-top w-8">
<input
type="checkbox"
className="cursor-pointer"
onChange={(e) => changeHeaderKey(h.name, e.target.checked)}
checked={revealed[h.name]}
/>
</td>
<td className="border border-slate-300 font-bold align-top py-1 px-2 whitespace-nowrap">
{h.name}
</td>
<td className="border border-slate-300 break-all align-top py-1 px-2">
{revealed[h.name]
? h.value
: Array(h.value?.length || 0)
.fill('*')
.join('')}
</td>
</tr>
))}
</tbody>
</table>
);
}
export function HideResponseStep(props: {
function HideResponseStep(props: {
onNext: () => void;
onCancel: () => void;
setSecretResps: (secrets: string[]) => void;
}): React.ReactElement {
}): ReactElement {
const params = useParams<{ requestId: string }>();
const req = useRequest(params.requestId);
const [responseText, setResponseText] = useState('');
const [redactedRanges, setRedactedRanges] = useState<
{ start: number; end: number }[]
>([]);
const [isRedactMode, setIsRedactMode] = useState(true);
const [start, setStart] = useState(0);
const [end, setEnd] = useState(0);
const taRef = useRef<HTMLTextAreaElement | null>(null);
const onSelectionChange: React.MouseEventHandler<HTMLTextAreaElement> =
useCallback(
(e) => {
const ta = e.currentTarget;
if (isRedactMode && ta.selectionEnd > ta.selectionStart) {
const newRange: { start: number; end: number } = {
start: ta.selectionStart,
end: ta.selectionEnd,
};
setRedactedRanges((prevRanges) => {
let updatedRanges = [...prevRanges, newRange].sort(
(a, b) => a.start - b.start,
);
updatedRanges = mergeRanges(updatedRanges);
const secretResps = updatedRanges
.map(({ start, end }) => responseText.substring(start, end))
.filter((d) => !!d);
props.setSecretResps(secretResps);
return updatedRanges;
});
} else if (!isRedactMode) {
const clickPosition = ta.selectionStart;
setRedactedRanges((prevRanges) => {
const updatedRanges = prevRanges.filter(
({ start, end }) => clickPosition < start || clickPosition > end,
);
const secretResps = updatedRanges
.map(({ start, end }) => responseText.substring(start, end))
.filter((d) => !!d);
props.setSecretResps(secretResps);
return updatedRanges;
});
}
},
[responseText, props, isRedactMode],
);
const mergeRanges = (
ranges: { start: number; end: number }[],
): { start: number; end: number }[] => {
if (ranges.length === 0) return [];
const mergedRanges: { start: number; end: number }[] = [ranges[0]];
for (let i = 1; i < ranges.length; i++) {
const lastRange = mergedRanges[mergedRanges.length - 1];
if (ranges[i].start <= lastRange.end) {
lastRange.end = Math.max(lastRange.end, ranges[i].end);
} else {
mergedRanges.push(ranges[i]);
const onSelectionChange: ReactEventHandler<HTMLTextAreaElement> = useCallback(
(e) => {
const ta = e.currentTarget;
if (ta.selectionEnd > ta.selectionStart) {
setStart(ta.selectionStart);
setEnd(ta.selectionEnd);
props.setSecretResps(
[
responseText.substring(0, ta.selectionStart),
responseText.substring(ta.selectionEnd, responseText.length),
].filter((d) => !!d),
);
}
}
return mergedRanges;
};
},
[responseText],
);
useEffect(() => {
if (!req) return;
@@ -382,126 +276,6 @@ export function HideResponseStep(props: {
replay(req.url, options).then((resp) => setResponseText(resp));
}, [req]);
useEffect(() => {
const current = taRef.current;
if (current) {
current.focus();
}
}, [taRef]);
if (!req) return <></>;
const shieldedText = responseText.split('');
redactedRanges.forEach(({ start, end }) => {
for (let i = start; i < end; i++) {
shieldedText[i] = '*';
}
});
return (
<div className="flex flex-col flex-nowrap flex-shrink flex-grow h-0">
<div className="border bg-primary/[0.9] text-white border-slate-300 py-1 px-2 font-semibold">
Step 2 of 2:{' '}
{isRedactMode
? 'Highlight text to redact selected portions'
: 'Click redacted text to unredact'}
</div>
<div className="flex flex-row justify-end p-0.5 gap-2 border-t">
<button
className={`bg-${isRedactMode ? 'red-500' : 'green-500'} text-white font-bold hover:bg-${isRedactMode ? 'red-400' : 'green-400'} px-2 py-0.5 active:bg-${isRedactMode ? 'red-600' : 'green-600'}`}
onClick={() => setIsRedactMode(!isRedactMode)}
>
{isRedactMode ? 'Unredact Text' : 'Redact Text'}
</button>
<button
className="bg-gray-500 text-white font-bold hover:bg-gray-400 px-2 py-0.5 active:bg-gray-600"
onClick={() => setRedactedRanges([])}
>
Unredact All
</button>
</div>
<div className="flex flex-col flex-grow flex-shrink h-0 overflow-y-auto p-2">
<textarea
ref={taRef}
className="flex-grow textarea bg-slate-100 font-mono"
value={shieldedText.join('')}
onMouseUp={onSelectionChange}
/>
</div>
<div className="flex flex-row justify-end p-2 gap-2 border-t">
<button className="button" onClick={props.onCancel}>
Back
</button>
<button
className="bg-primary/[0.9] text-white font-bold hover:bg-primary/[0.8] px-2 py-0.5 active:bg-primary"
onClick={props.onNext}
>
Notarize
</button>
</div>
</div>
);
}
export function RedactBodyTextarea(props: {
className?: string;
onChange: (secretResponse: string[]) => void;
request: {
url: string;
method?: string;
headers?: { [name: string]: string };
formData?: { [k: string]: string[] };
body?: string;
};
}) {
const { className, onChange, request } = props;
const [loading, setLoading] = useState(false);
const [responseText, setResponseText] = useState('');
const [start, setStart] = useState(0);
const [end, setEnd] = useState(0);
const taRef = useRef<HTMLTextAreaElement | null>(null);
const onSelectionChange: ReactEventHandler<HTMLTextAreaElement> = useCallback(
(e) => {
const ta = e.currentTarget;
if (ta.selectionEnd > ta.selectionStart) {
setStart(ta.selectionStart);
setEnd(ta.selectionEnd);
onChange(
[
responseText.substring(0, ta.selectionStart),
responseText.substring(ta.selectionEnd, responseText.length),
].filter((d) => !!d),
);
}
},
[responseText],
);
useEffect(() => {
const options = {
method: request.method,
headers: request.headers,
body: request.body,
};
if (request?.formData) {
const formData = new URLSearchParams();
Object.entries(request.formData).forEach(([key, values]) => {
values.forEach((v) => formData.append(key, v));
});
options.body = formData.toString();
}
setLoading(true);
replay(request.url, options).then((resp) => {
setResponseText(resp);
setLoading(false);
});
}, [request]);
useEffect(() => {
const current = taRef.current;
@@ -511,6 +285,8 @@ export function RedactBodyTextarea(props: {
}
}, [taRef, start, end]);
if (!req) return <></>;
let shieldedText = '';
if (end > start) {
@@ -524,29 +300,31 @@ export function RedactBodyTextarea(props: {
.join(''),
);
}
if (loading) {
return (
<div className="flex flex-col items-center !pt-4 flex-grow textarea bg-slate-100">
<Icon
className="animate-spin w-fit text-slate-500"
fa="fa-solid fa-spinner"
size={1}
return (
<div className="flex flex-col flex-nowrap flex-shrink flex-grow h-0">
<div className="border bg-primary/[0.9] text-white border-slate-300 py-1 px-2 font-semibold">
Step 2 of 2: Highlight text to show only selected text from response
</div>
<div className="flex flex-col flex-grow flex-shrink h-0 overflow-y-auto p-2">
<textarea
ref={taRef}
className="flex-grow textarea bg-slate-100 font-mono"
value={shieldedText || responseText}
onSelect={onSelectionChange}
/>
</div>
);
}
return (
<textarea
ref={taRef}
className={classNames(
'flex-grow textarea bg-slate-100 font-mono',
className,
)}
value={shieldedText || responseText}
onSelect={onSelectionChange}
/>
<div className="flex flex-row justify-end p-2 gap-2 border-t">
<button className="button" onClick={props.onCancel}>
Back
</button>
<button
className="bg-primary/[0.9] text-white font-bold hover:bg-primary/[0.8] px-2 py-0.5 active:bg-primary"
onClick={props.onNext}
>
Notarize
</button>
</div>
</div>
);
}

View File

@@ -1,178 +0,0 @@
import React, { ReactElement, useCallback, useState } from 'react';
import Icon from '../../components/Icon';
import { useSearchParams } from 'react-router-dom';
import { urlify } from '../../utils/misc';
import browser from 'webextension-polyfill';
import { BackgroundActiontype } from '../../entries/Background/rpc';
import { BaseApproval } from '../BaseApproval';
import { RedactBodyTextarea, RevealHeaderTable } from '../Notarize';
export function NotarizeApproval(): ReactElement {
const [params] = useSearchParams();
const origin = params.get('origin');
const favIconUrl = params.get('favIconUrl');
const config = JSON.parse(params.get('config')!);
const hostname = urlify(origin || '')?.hostname;
const [step, setStep] = useState<'overview' | 'headers' | 'response'>(
'overview',
);
const [revealed, setRevealed] = useState<{ [key: string]: boolean }>({});
const [secretResps, setSecretResps] = useState<string[]>([]);
const headerList = Object.entries(config.headers || {}).map(
([name, value]) => ({
name,
value: String(value),
}),
);
const onCancel = useCallback(() => {
if (step === 'headers') return setStep('overview');
if (step === 'response') return setStep('headers');
browser.runtime.sendMessage({
type: BackgroundActiontype.notarize_response,
data: false,
});
}, [step]);
const onAccept = useCallback(() => {
if (step === 'overview') return setStep('headers');
if (step === 'headers') return setStep('response');
const secretHeaders = headerList
.map((h) => {
if (!revealed[h.name]) {
return `${h.name.toLowerCase()}: ${h.value || ''}` || '';
}
return '';
})
.filter((d) => !!d);
browser.runtime.sendMessage({
type: BackgroundActiontype.notarize_response,
data: {
...config,
secretHeaders,
secretResps,
},
});
}, [revealed, step, secretResps, config]);
let body, headerText, primaryCta, secondaryCta;
switch (step) {
case 'overview':
headerText = 'Notarizing Request';
primaryCta = 'Next';
secondaryCta = 'Cancel';
body = (
<>
<div className="flex flex-col items-center gap-2 py-8">
{!!favIconUrl ? (
<img
src={favIconUrl}
className="h-16 w-16 rounded-full border border-slate-200 bg-slate-200"
alt="logo"
/>
) : (
<Icon
fa="fa-solid fa-globe"
size={4}
className="h-16 w-16 rounded-full border border-slate-200 text-blue-500"
/>
)}
<div className="text-2xl text-center px-8">
<b className="text-blue-500">{hostname}</b> wants to notarize the
following request:
</div>
</div>
<div className="flex flex-col items-center gap-4 text-sm px-8 text-center flex-grow break-all">
<table className="border border-collapse table-auto rounded text-xs w-full">
<tbody>
<TableRow label="Method" value={config.method?.toUpperCase()} />
<TableRow label="Request URL" value={config.url} />
<TableRow label="Notary URL" value={config.notaryUrl} />
<TableRow label="Proxy URL" value={config.websocketProxyUrl} />
<TableRow label="Max Sent" value={config.maxSentData} />
<TableRow label="Max Recv" value={config.maxRecvData} />
{config.metadata && (
<TableRow
label="Metadata"
value={JSON.stringify(config.metadata)}
/>
)}
</tbody>
</table>
</div>
<div className="text-xs px-8 pb-2 text-center text-slate-500">
You will be able to review and redact headers and response body.
</div>
</>
);
break;
case 'headers':
headerText = 'Step 1 of 2: Select headers to reveal';
primaryCta = 'Next';
secondaryCta = 'Back';
body = (
<div className="px-2 flex flex-col">
<RevealHeaderTable
className="w-full"
onChange={setRevealed}
headers={headerList}
/>
</div>
);
break;
case 'response':
headerText = 'Step 2 of 2: Highlight response to keep';
primaryCta = 'Notarize';
secondaryCta = 'Back';
body = (
<div className="px-2 flex flex-col flex-grow">
<RedactBodyTextarea
className="w-full "
onChange={setSecretResps}
request={{
url: config.url,
method: config.method,
headers: config.headers,
body: config.body,
formData: config.formData,
}}
/>
</div>
);
break;
}
return (
<BaseApproval
header={headerText}
onSecondaryClick={onCancel}
onPrimaryClick={onAccept}
primaryCTAText={primaryCta}
secondaryCTAText={secondaryCta}
>
{body}
</BaseApproval>
);
}
function TableRow({ label, value }: { label: string; value: string }) {
return (
<tr>
<td className="px-2 py-1 border border-slate-300 bg-slate-100 text-slate-500 align-top text-left w-24">
{label}
</td>
<td className="px-2 py-1 border border-slate-300 font-semibold text-slate-800 text-left">
<input
className="outline-0 flex-grow cursor-default w-full"
type="text"
value={value}
/>
</td>
</tr>
);
}

View File

@@ -24,18 +24,20 @@ import {
NOTARY_PROXY,
MAX_RECV,
MAX_SENT,
LOGGING_LEVEL_INFO,
LOGGING_LEVEL_NONE,
LOGGING_LEVEL_DEBUG,
LOGGING_LEVEL_TRACE,
} from '../../utils/constants';
import Modal, { ModalContent } from '../../components/Modal/Modal';
import browser from 'webextension-polyfill';
import { LoggingLevel } from 'tlsn-js';
import { version } from '../../../package.json';
export default function Options(): ReactElement {
const [notary, setNotary] = useState(NOTARY_API);
const [proxy, setProxy] = useState(NOTARY_PROXY);
const [maxSent, setMaxSent] = useState(MAX_SENT);
const [maxReceived, setMaxReceived] = useState(MAX_RECV);
const [loggingLevel, setLoggingLevel] = useState<LoggingLevel>('Info');
const [loggingLevel, setLoggingLevel] = useState(LOGGING_LEVEL_INFO);
const [dirty, setDirty] = useState(false);
const [shouldReload, setShouldReload] = useState(false);
@@ -48,7 +50,7 @@ export default function Options(): ReactElement {
setProxy((await getProxyApi()) || NOTARY_PROXY);
setMaxReceived((await getMaxRecv()) || MAX_RECV);
setMaxSent((await getMaxSent()) || MAX_SENT);
setLoggingLevel((await getLoggingFilter()) || 'Info');
setLoggingLevel((await getLoggingFilter()) || LOGGING_LEVEL_INFO);
})();
}, [advanced]);
@@ -80,10 +82,6 @@ export default function Options(): ReactElement {
setAdvanced(!advanced);
}, [advanced]);
const openInTab = useCallback((url: string) => {
browser.tabs.create({ url });
}, []);
return (
<div className="flex flex-col flex-nowrap flex-grow">
{showReloadModal && (
@@ -156,22 +154,6 @@ export default function Options(): ReactElement {
Save
</button>
</div>
<div className="flex flex-col w-full items-end gap-2 p-2">
<button
className="button"
onClick={() =>
openInTab('https://github.com/tlsnotary/tlsn-extension/issues/new')
}
>
File an issue
</button>
<button
className="button"
onClick={() => openInTab('https://discord.gg/9XwESXtcN7')}
>
Join our Discord
</button>
</div>
</div>
);
}
@@ -188,7 +170,7 @@ function InputField(props: {
return (
<div className="flex flex-col flex-nowrap py-1 px-2 gap-2">
<div className="font-semibold cursor-default">{label}</div>
<div className="font-semibold">{label}</div>
<input
type={type}
className="input border"
@@ -212,10 +194,6 @@ function NormalOptions(props: {
return (
<div>
<div className="flex flex-col flex-nowrap py-1 px-2 gap-2 cursor-default">
<div className="font-semibold">Version</div>
<div className="input border bg-slate-100">{version}</div>
</div>
<InputField
label="Notary API"
placeholder="https://api.tlsnotary.org"
@@ -236,9 +214,9 @@ function NormalOptions(props: {
setDirty(true);
}}
/>
<div className="flex flex-col flex-nowrap py-1 px-2 gap-2 cursor-default">
<div className="flex flex-col flex-nowrap py-1 px-2 gap-2">
<div className="font-semibold">Explorer URL</div>
<div className="input border bg-slate-100">{EXPLORER_API}</div>
<div className="input border">{EXPLORER_API}</div>
</div>
</div>
);
@@ -247,12 +225,12 @@ function NormalOptions(props: {
function AdvancedOptions(props: {
maxSent: number;
maxReceived: number;
loggingLevel: LoggingLevel;
loggingLevel: string;
setShouldReload: (reload: boolean) => void;
setMaxSent: (value: number) => void;
setMaxReceived: (value: number) => void;
setDirty: (value: boolean) => void;
setLoggingLevel: (level: LoggingLevel) => void;
setLoggingLevel: (level: string) => void;
}) {
const {
maxSent,
@@ -292,17 +270,16 @@ function AdvancedOptions(props: {
<select
className="select !bg-white border !px-2 !py-1"
onChange={(e) => {
setLoggingLevel(e.target.value as LoggingLevel);
setLoggingLevel(e.target.value);
setDirty(true);
setShouldReload(true);
}}
value={loggingLevel}
>
<option value="Error">Error</option>
<option value="Warn">Warn</option>
<option value="Info">Info</option>
<option value="Debug">Debug</option>
<option value="Trace">Trace</option>
<option value={LOGGING_LEVEL_NONE}>None</option>
<option value={LOGGING_LEVEL_INFO}>Info</option>
<option value={LOGGING_LEVEL_DEBUG}>Debug</option>
<option value={LOGGING_LEVEL_TRACE}>Trace</option>
</select>
</div>
<div className="flex flex-row flex-nowrap justify-end gap-2 p-2"></div>

View File

@@ -1,10 +0,0 @@
import React, { ReactElement } from "react";
import { PluginList } from "../../components/PluginList";
export default function Plugins(): ReactElement {
return (
<div className="flex flex-col flex-nowrap flex-grow">
<PluginList className="p-2 overflow-y-auto" />
</div>
)
}

View File

@@ -13,7 +13,6 @@ export default function ProofUploader(): ReactElement {
recv: string;
sent: string;
} | null>(null);
const [uploading, setUploading] = useState(false);
const onFileUpload: ChangeEventHandler<HTMLInputElement> = useCallback(
async (e) => {
@@ -26,21 +25,16 @@ export default function ProofUploader(): ReactElement {
const result = event.target?.result;
if (result) {
const proof = JSON.parse(result as string);
const res = await chrome.runtime
.sendMessage<any, { recv: string; sent: string }>({
type: BackgroundActiontype.verify_proof,
data: proof,
})
.catch(() => null);
if (proof) {
setUploading(false);
setProof(res);
}
const res = await chrome.runtime.sendMessage<
any,
{ recv: string; sent: string }
>({
type: BackgroundActiontype.verify_proof,
data: proof,
});
setProof(res);
}
});
setUploading(true);
reader.readAsText(file);
}
},
@@ -59,23 +53,16 @@ export default function ProofUploader(): ReactElement {
className="absolute w-full h-full top-0 left-0 opacity-0 z-10"
onChange={onFileUpload}
accept=".json"
disabled={uploading}
/>
{uploading ? (
<Icon className="animate-spin" fa="fa-solid fa-spinner" size={2} />
) : (
<>
<Icon className="mb-4" fa="fa-solid fa-upload" size={2} />
<div className="text-lg">Drop your proof here to continue</div>
<div className="text-sm">or</div>
<button
className="button !bg-primary/[.8] !hover:bg-primary/[.7] !active:bg-primary !text-white cursor-pointer"
onClick={() => null}
>
Browse Files
</button>
</>
)}
<Icon className="mb-4" fa="fa-solid fa-upload" size={2} />
<div className="text-lg">Drop your proof here to continue</div>
<div className="text-sm">or</div>
<button
className="button !bg-primary/[.8] !hover:bg-primary/[.7] !active:bg-primary !text-white cursor-pointer"
onClick={() => null}
>
Browse Files
</button>
</div>
</div>
);

View File

@@ -57,14 +57,12 @@ export default function ProofViewer(props?: {
<textarea
className="w-full resize-none bg-slate-100 text-slate-800 border p-2 text-[10px] break-all h-full outline-none font-mono"
value={props?.sent || request?.verification?.sent}
readOnly
></textarea>
)}
{tab === 'recv' && (
<textarea
className="w-full resize-none bg-slate-100 text-slate-800 border p-2 text-[10px] break-all h-full outline-none font-mono"
value={props?.recv || request?.verification?.recv}
readOnly
></textarea>
)}
</div>

View File

@@ -11,20 +11,6 @@ import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import NavigateWithParams from '../../components/NavigateWithParams';
import ResponseDetail from '../../components/ResponseDetail';
import { urlify } from '../../utils/misc';
import { notarizeRequest } from '../../reducers/requests';
import {
getMaxRecv,
getMaxSent,
getNotaryApi,
getProxyApi,
} from '../../utils/storage';
import { useDispatch } from 'react-redux';
import {
formatForRequest,
InputBody,
FormBodyTable,
parseResponse,
} from '../../components/RequestBuilder';
enum TabType {
Params = 'Params',
@@ -39,32 +25,24 @@ export default function RequestBuilder(props?: {
headers?: [string, string, boolean?][];
body?: string;
method?: string;
response?: Response;
}): ReactElement {
const loc = useLocation();
const navigate = useNavigate();
const dispatch = useDispatch();
const subpath = props?.subpath || '/custom';
const [_url, setUrl] = useState(props?.url || '');
const [params, setParams] = useState<[string, string, boolean?][]>(
props?.params || [],
);
const [body, setBody] = useState<string | undefined>(props?.body);
const [formBody, setFormBody] = useState<[string, string, boolean?][]>([
['', '', true],
]);
const [method, setMethod] = useState<string>(props?.method || 'GET');
const [type, setType] = useState<string>('text/plain');
const [headers, setHeaders] = useState<[string, string, boolean?][]>(
props?.headers || [['Content-Type', type, true]],
props?.headers || [],
);
const [body, setBody] = useState<string | undefined>(props?.body);
const [method, setMethod] = useState<string>(props?.method || 'GET');
const [response, setResponse] = useState<Response | null>(
props?.response || null,
);
const [responseData, setResponseData] = useState<{
json: any | null;
text: string | null;
img: string | null;
headers: [string, string][] | null;
} | null>(null);
const url = urlify(_url);
@@ -79,26 +57,6 @@ export default function RequestBuilder(props?: {
setParams(Array.from(url?.searchParams || []));
}, [_url]);
useEffect(() => {
updateContentType(type);
}, [type, method]);
const updateContentType = useCallback(
(type: string) => {
const updateHeaders = headers.filter(
([key]) => key.toLowerCase() !== 'content-type',
);
if (method === 'GET' || method === 'HEAD') {
updateHeaders.push(['Content-Type', type, true]);
} else {
updateHeaders.push(['Content-Type', type, false]);
}
setHeaders(updateHeaders);
},
[method, type, headers],
);
const toggleParam = useCallback(
(i: number) => {
params[i][2] = !params[i][2];
@@ -133,7 +91,7 @@ export default function RequestBuilder(props?: {
const sendRequest = useCallback(async () => {
if (!href) return;
setResponseData(null);
// eslint-disable-next-line no-undef
const opts: RequestInit = {
method,
@@ -144,13 +102,9 @@ export default function RequestBuilder(props?: {
return map;
}, {}),
};
if (method !== 'GET' && method !== 'HEAD') {
if (type === 'application/x-www-form-urlencoded') {
opts.body = formatForRequest(formBody, type);
} else {
opts.body = formatForRequest(body!, type);
}
}
if (body) opts.body = body;
const cookie = headers.find(([key]) => key === 'Cookie');
if (cookie) {
@@ -160,64 +114,15 @@ export default function RequestBuilder(props?: {
const res = await fetch(href, opts);
const contentType =
res.headers.get('content-type') || res.headers.get('Content-Type');
setResponseData(await parseResponse(contentType!, res));
setResponse(res);
navigate(subpath + '/response');
}, [href, method, headers, body, type]);
const onNotarize = useCallback(async () => {
const maxSentData = await getMaxSent();
const maxRecvData = await getMaxRecv();
const notaryUrl = await getNotaryApi();
const websocketProxyUrl = await getProxyApi();
dispatch(
notarizeRequest(
//@ts-ignore
{
url: href || '',
method,
headers: headers.reduce((map: { [key: string]: string }, [k, v]) => {
if (k !== 'Cookie') {
map[k] = v;
}
return map;
}, {}),
body: body ? formatForRequest(body, type) : undefined,
maxSentData,
maxRecvData,
secretHeaders: [],
secretResps: [],
notaryUrl,
websocketProxyUrl,
},
),
);
navigate('/history');
}, [href, method, headers, body, type]);
const onMethod = useCallback(
(e: React.ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value;
if (value === 'GET' || value === 'HEAD') {
setType('');
setMethod(value);
} else {
setMethod(value);
}
},
[method, type],
);
}, [href, method, headers, body]);
return (
<div className="flex flex-col w-full py-2 gap-2 flex-grow">
<div className="flex flex-row px-2">
<select className="select" onChange={(e) => onMethod(e)}>
<select className="select" onChange={(e) => setMethod(e.target.value)}>
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
@@ -229,14 +134,8 @@ export default function RequestBuilder(props?: {
<input
className="input border flex-grow"
type="text"
value={_url}
value={url ? href : _url}
onChange={(e) => setUrl(e.target.value)}
onBlur={() => {
const formattedUrl = urlify(_url);
if (formattedUrl) {
setUrl(formattedUrl.href);
}
}}
/>
<button className="button" disabled={!url} onClick={sendRequest}>
Send
@@ -262,19 +161,13 @@ export default function RequestBuilder(props?: {
>
Body
</TabLabel>
{responseData && (
<div className="flex flex-row justify-between w-full">
<TabLabel
onClick={() => navigate(subpath + '/response')}
active={loc.pathname.includes('response')}
>
Response
</TabLabel>
<button className="button" onClick={onNotarize}>
Notarize
</button>
</div>
{response && (
<TabLabel
onClick={() => navigate(subpath + '/response')}
active={loc.pathname.includes('response')}
>
Response
</TabLabel>
)}
</div>
</div>
@@ -304,38 +197,16 @@ export default function RequestBuilder(props?: {
<Route
path="body"
element={
<div className="h-full">
<select
className={c('select', {
'w-[80px]':
type === 'application/json' ||
type === 'text/plain' ||
type === '',
'w-[200px]': type === 'application/x-www-form-urlencoded',
})}
value={type}
onChange={(e) => setType(e.target.value)}
>
<option value="text/plain">Text</option>
<option value="application/json">JSON</option>
<option value="application/x-www-form-urlencoded">
x-www-form-urlencoded
</option>
</select>
{type === 'application/x-www-form-urlencoded' ? (
<FormBodyTable
formBody={formBody}
setFormBody={setFormBody}
/>
) : (
<InputBody body={body!} setBody={setBody} />
)}
</div>
<textarea
className="textarea h-full w-full resize-none"
value={body}
onChange={(e) => setBody(e.target.value)}
/>
}
/>
<Route
path="response"
element={<ResponseDetail responseData={responseData} />}
element={<ResponseDetail response={response} />}
/>
<Route path="/" element={<NavigateWithParams to="/params" />} />
</Routes>

View File

@@ -2,11 +2,11 @@ import React, { ReactElement } from 'react';
import RequestTable from '../../components/RequestTable';
import { useRequests } from '../../reducers/requests';
export default function Requests(props: { shouldFix?: boolean }): ReactElement {
export default function Requests(): ReactElement {
const requests = useRequests();
return (
<>
<RequestTable shouldFix={props.shouldFix} requests={requests} />
<RequestTable requests={requests} />
</>
);
}

View File

@@ -1,119 +0,0 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import Icon from '../../components/Icon';
import { useSearchParams } from 'react-router-dom';
import { type PluginConfig, PluginMetadata, urlify } from '../../utils/misc';
import browser from 'webextension-polyfill';
import { BackgroundActiontype } from '../../entries/Background/rpc';
import { BaseApproval } from '../BaseApproval';
import { PluginPermissions } from '../../components/PluginInfo';
import {
getPluginConfigByHash,
getPluginMetadataByHash,
} from '../../entries/Background/db';
import { runPlugin } from '../../utils/rpc';
export function RunPluginApproval(): ReactElement {
const [params] = useSearchParams();
const origin = params.get('origin');
const favIconUrl = params.get('favIconUrl');
const hash = params.get('hash');
const hostname = urlify(origin || '')?.hostname;
const [error, showError] = useState('');
const [metadata, setPluginMetadata] = useState<PluginMetadata | null>(null);
const [pluginContent, setPluginContent] = useState<PluginConfig | null>(null);
const onCancel = useCallback(() => {
browser.runtime.sendMessage({
type: BackgroundActiontype.run_plugin_response,
data: false,
});
}, []);
const onAccept = useCallback(async () => {
if (!hash) return;
try {
const tab = await browser.tabs.create({
active: true,
});
await browser.storage.local.set({ plugin_hash: hash });
// @ts-ignore
if (chrome.sidePanel) await chrome.sidePanel.open({ tabId: tab.id });
browser.runtime.sendMessage({
type: BackgroundActiontype.run_plugin_response,
data: true,
});
} catch (e: any) {
showError(e.message);
}
}, [hash]);
useEffect(() => {
(async () => {
if (!hash) return;
try {
const config = await getPluginConfigByHash(hash);
const metadata = await getPluginMetadataByHash(hash);
setPluginContent(config);
setPluginMetadata(metadata);
} catch (e: any) {
showError(e?.message || 'Invalid Plugin');
}
})();
}, [hash]);
return (
<BaseApproval
header={`Execute Plugin`}
onSecondaryClick={onCancel}
onPrimaryClick={onAccept}
>
<div className="flex flex-col items-center gap-2 py-8">
{!!favIconUrl ? (
<img
src={favIconUrl}
className="h-16 w-16 rounded-full border border-slate-200 bg-slate-200"
alt="logo"
/>
) : (
<Icon
fa="fa-solid fa-globe"
size={4}
className="h-16 w-16 rounded-full border border-slate-200 text-blue-500"
/>
)}
<div className="text-2xl text-center px-8">
<b className="text-blue-500">{hostname}</b> wants to execute a plugin:
</div>
</div>
{!pluginContent && (
<div className="flex flex-col items-center flex-grow gap-4 border border-slate-300 p-8 mx-8 rounded bg-slate-100">
<Icon
className="animate-spin w-fit text-slate-500"
fa="fa-solid fa-spinner"
size={1}
/>
</div>
)}
{pluginContent && (
<div className="flex flex-col gap-4 border border-slate-300 p-4 mx-8 rounded bg-slate-100">
<div className="flex flex-col items-center">
<img
className="w-12 h-12 mb-2"
src={pluginContent.icon}
alt="Plugin Icon"
/>
<span className="text-2xl text-blue-600 font-semibold">
{pluginContent.title}
</span>
<div className="text-slate-500 text-base">
{pluginContent.description}
</div>
</div>
</div>
)}
</BaseApproval>
);
}

View File

@@ -8,7 +8,6 @@ import deepEqual from 'fast-deep-equal';
enum ActionType {
'/history/addRequest' = '/history/addRequest',
'/history/setRequests' = '/history/setRequests',
'/history/deleteRequest' = '/history/deleteRequest',
}
@@ -38,13 +37,6 @@ export const addRequestHistory = (request?: RequestHistory | null) => {
};
};
export const setRequests = (requests: RequestHistory[]) => {
return {
type: ActionType['/history/setRequests'],
payload: requests,
};
};
export const deleteRequestHistory = (id: string) => {
chrome.runtime.sendMessage<any, string>({
type: BackgroundActiontype.delete_prove_request,
@@ -80,18 +72,6 @@ export default function history(
order: newOrder,
};
}
case ActionType['/history/setRequests']: {
const payload: RequestHistory[] = action.payload;
return {
...state,
map: payload.reduce((map: { [id: string]: RequestHistory }, req) => {
map[req.id] = req;
return map;
}, {}),
order: payload.map(({ id }) => id),
};
}
case ActionType['/history/deleteRequest']: {
const reqId: string = action.payload;
const newMap = { ...state.map };
@@ -114,12 +94,6 @@ export const useHistoryOrder = (): string[] => {
}, deepEqual);
};
export const useAllProofHistory = (): RequestHistory[] => {
return useSelector((state: AppRootState) => {
return state.history.order.map((id) => state.history.map[id]);
}, deepEqual);
};
export const useRequestHistory = (id?: string): RequestHistory | undefined => {
return useSelector((state: AppRootState) => {
if (!id) return undefined;

View File

@@ -1,11 +1,6 @@
import { useSelector } from 'react-redux';
import { AppRootState } from './index';
import deepEqual from 'fast-deep-equal';
import { useCallback, useEffect, useState } from 'react';
import { getPluginConfigByHash } from '../entries/Background/db';
import { PluginConfig } from '../utils/misc';
import { runPlugin } from '../utils/rpc';
import browser from 'webextension-polyfill';
enum ActionType {
'/plugin/addPlugin' = '/plugin/addPlugin',
@@ -57,31 +52,3 @@ export const usePluginHashes = (): string[] => {
return state.plugins.order;
}, deepEqual);
};
export const usePluginConfig = (hash: string) => {
const [config, setConfig] = useState<PluginConfig | null>(null);
useEffect(() => {
(async function () {
setConfig(await getPluginConfigByHash(hash));
})();
}, [hash]);
return config;
};
export const useOnPluginClick = (hash: string) => {
return useCallback(async () => {
await runPlugin(hash, 'start');
const [tab] = await browser.tabs.query({
active: true,
currentWindow: true,
});
await browser.storage.local.set({ plugin_hash: hash });
// @ts-ignore
if (chrome.sidePanel) await chrome.sidePanel.open({ tabId: tab.id });
window.close();
}, [hash]);
};

View File

@@ -18,7 +18,6 @@ enum ActionType {
'/requests/setRequests' = '/requests/setRequests',
'/requests/addRequest' = '/requests/addRequest',
'/requests/setActiveTab' = '/requests/setActiveTab',
'/requests/isConnected' = '/requests/isConnected',
}
type Action<payload> = {
@@ -33,22 +32,11 @@ type State = {
[requestId: string]: RequestLog;
};
activeTab: chrome.tabs.Tab | null;
isConnected: boolean;
};
const initialState: State = {
map: {},
activeTab: null,
isConnected: false,
};
export const setConnection = (isConnected: boolean): Action<boolean> => ({
type: ActionType['/requests/isConnected'],
payload: isConnected,
});
export const isConnected = (isConnected: boolean) => async () => {
return isConnected;
};
export const setRequests = (requests: RequestLog[]): Action<RequestLog[]> => ({
@@ -69,6 +57,7 @@ export const notarizeRequest = (options: RequestHistory) => async () => {
method: options.method,
headers: options.headers,
body: options.body,
maxTranscriptSize: options.maxTranscriptSize,
maxSentData,
maxRecvData,
secretHeaders: options.secretHeaders,
@@ -124,11 +113,6 @@ export default function requests(
[action.payload.requestId]: action.payload,
},
};
case ActionType['/requests/isConnected']:
return {
...state,
isConnected: action.payload,
};
default:
return state;
}
@@ -158,7 +142,3 @@ export const useActiveTabUrl = (): URL | null => {
return activeTab?.url ? new URL(activeTab.url) : null;
}, deepEqual);
};
export const useIsConnected = (): boolean => {
return useSelector((state: AppRootState) => state.requests.isConnected);
};

View File

@@ -1,5 +1,9 @@
export const EXPLORER_API = 'https://explorer.tlsnotary.org';
export const NOTARY_API = 'https://notary.pse.dev/v0.1.0-alpha.7';
export const NOTARY_API = 'https://notary.pse.dev/v0.1.0-alpha.5';
export const NOTARY_PROXY = 'wss://notary.pse.dev/proxy';
export const MAX_RECV = 16384;
export const MAX_SENT = 4096;
export const LOGGING_LEVEL_NONE = ' ';
export const LOGGING_LEVEL_INFO = 'info,tlsn_extension_rs=info';
export const LOGGING_LEVEL_DEBUG = 'debug,tlsn_extension_rs=debug';
export const LOGGING_LEVEL_TRACE = 'trace,tlsn_extension_rs=trace';

View File

@@ -4,16 +4,14 @@ import {
RequestLog,
} from '../entries/Background/rpc';
import { EXPLORER_API } from './constants';
import createPlugin, {
CallContext,
ExtismPluginOptions,
Plugin,
} from '@extism/extism';
import createPlugin, { CallContext, Plugin } from '@extism/extism';
import browser from 'webextension-polyfill';
import NodeCache from 'node-cache';
import {
getCookieStoreByHost,
getHeaderStoreByHost,
} from '../entries/Background/cache';
import { getNotaryApi, getProxyApi } from './storage';
import { minimatch } from 'minimatch';
import { getCookiesByHost, getHeadersByHost } from '../entries/Background/db';
const charwise = require('charwise');
@@ -153,7 +151,7 @@ export const makePlugin = async (
const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
const injectedConfig = {
tabUrl: tab?.url || 'x://x',
tabUrl: tab?.url,
tabId: tab?.id,
};
@@ -177,8 +175,7 @@ export const makePlugin = async (
if (
!approvedRequests.find(
({ method, url }) =>
method === params.method && minimatch(params.url, url),
({ method, url }) => method === params.method && url === params.url,
)
) {
throw new Error(`Unapproved request - ${params.method}: ${params.url}`);
@@ -198,37 +195,13 @@ export const makePlugin = async (
throw new Error(`Unapproved proxy: ${params.websocketProxyUrl}`);
}
(async () => {
const {
url,
method,
headers,
getSecretResponse,
body: reqBody,
} = params;
let secretResps;
const resp = await fetch(url, {
method,
headers,
body: reqBody,
});
const body = await extractBodyFromResponse(resp);
if (getSecretResponse) {
const out = await plugin.call(getSecretResponse, body);
secretResps = JSON.parse(out.string());
}
handleExecPluginProver({
type: BackgroundActiontype.execute_plugin_prover,
data: {
...params,
body: reqBody,
secretResps,
now,
},
});
})();
handleExecPluginProver({
type: BackgroundActiontype.execute_plugin_prover,
data: {
...params,
now,
},
});
return context.store(`${id}`);
},
@@ -253,8 +226,8 @@ export const makePlugin = async (
if (config?.cookies) {
const cookies: { [hostname: string]: { [key: string]: string } } = {};
for (const host of config.cookies) {
const cache = await getCookiesByHost(host);
cookies[host] = cache;
const cache = getCookieStoreByHost(host);
cookies[host] = cacheToMap(cache);
}
// @ts-ignore
injectedConfig.cookies = JSON.stringify(cookies);
@@ -263,52 +236,45 @@ export const makePlugin = async (
if (config?.headers) {
const headers: { [hostname: string]: { [key: string]: string } } = {};
for (const host of config.headers) {
const cache = await getHeadersByHost(host);
headers[host] = cache;
const cache = getHeaderStoreByHost(host);
headers[host] = cacheToMap(cache);
}
// @ts-ignore
injectedConfig.headers = JSON.stringify(headers);
}
const pluginConfig: ExtismPluginOptions = {
const pluginConfig = {
useWasi: true,
config: injectedConfig,
// allowedHosts: approvedRequests.map((r) => urlify(r.url)?.origin),
functions: {
'extism:host/user': funcs,
},
};
const plugin = await createPlugin(module, pluginConfig);
return plugin;
};
export type StepConfig = {
title: string; // Text for the step's title
description?: string; // Text for the step's description (optional)
cta: string; // Text for the step's call-to-action button
action: string; // The function name that this step will execute
prover?: boolean; // Boolean indicating if this step outputs a notarization (optional)
title: string;
description?: string;
cta: string;
action: string;
prover?: boolean;
};
export type PluginConfig = {
title: string; // The name of the plugin
description: string; // A description of the plugin's purpose
icon?: string; // A base64-encoded image string representing the plugin's icon (optional)
steps?: StepConfig[]; // An array describing the UI steps and behavior (see Step UI below) (optional)
hostFunctions?: string[]; // Host functions that the plugin will have access to
cookies?: string[]; // Cookies the plugin will have access to, cached by the extension from specified hosts (optional)
headers?: string[]; // Headers the plugin will have access to, cached by the extension from specified hosts (optional)
requests: { method: string; url: string }[]; // List of requests that the plugin is allowed to make
notaryUrls?: string[]; // List of notary services that the plugin is allowed to use (optional)
proxyUrls?: string[]; // List of websocket proxies that the plugin is allowed to use (optional)
title: string;
description: string;
icon?: string;
steps?: StepConfig[];
hostFunctions?: string[];
cookies?: string[];
headers?: string[];
requests: { method: string; url: string }[];
notaryUrls?: string[];
proxyUrls?: string[];
};
export type PluginMetadata = {
origin: string;
filePath: string;
} & { [k: string]: string };
export const getPluginConfig = async (
data: Plugin | ArrayBuffer,
): Promise<PluginConfig> => {
@@ -382,11 +348,3 @@ export const cacheToMap = (cache: NodeCache) => {
return acc;
}, {});
};
export function safeParseJSON(data?: string | null) {
try {
return JSON.parse(data!);
} catch (e) {
return null;
}
}

View File

@@ -1,87 +0,0 @@
import { PluginConfig } from './misc';
import React, { ReactElement, ReactNode } from 'react';
import Icon from '../components/Icon';
export const HostFunctionsDescriptions: {
[key: string]: (pluginContent: PluginConfig) => ReactElement;
} = {
redirect: () => {
return (
<PermissionDescription fa="fa-solid fa-diamond-turn-right">
<span>Redirect your current tab to any URL</span>
</PermissionDescription>
);
},
notarize: ({ notaryUrls, proxyUrls }) => {
const notaries = ['default notary'].concat(notaryUrls || []);
const proxies = ['default proxy'].concat(proxyUrls || []);
return (
<>
<PermissionDescription fa="fa-solid fa-route">
<span className="cursor-default">
<span className="mr-1">Proxy notarization requests thru</span>
<MultipleParts parts={proxies} />
</span>
</PermissionDescription>
<PermissionDescription fa="fa-solid fa-stamp">
<span className="cursor-default">
<span className="mr-1">Submit notarization requests to</span>
<MultipleParts parts={notaries} />
</span>
</PermissionDescription>
</>
);
},
};
export function PermissionDescription({
fa,
children,
}: {
fa: string;
children?: ReactNode;
}): ReactElement {
return (
<div className="flex flex-row gap-4 items-start cursor-default">
<Icon className="" size={1.6125} fa={fa} />
<div className="text-sm mt-[0.125rem]">{children}</div>
</div>
);
}
export function MultipleParts({ parts }: { parts: string[] }): ReactElement {
const content = [];
if (parts.length > 1) {
for (let i = 0; i < parts.length; i++) {
content.push(
<span key={i} className="text-blue-600">
{parts[i]}
</span>,
);
if (parts.length - i === 2) {
content.push(
<span key={i + 'separator'} className="inline-block mx-1">
and
</span>,
);
} else if (parts.length - i > 1) {
content.push(
<span key={i + 'separator'} className="inline-block mr-1">
,
</span>,
);
}
}
} else {
content.push(
<span key={0} className="text-blue-600">
{parts[0]}
</span>,
);
}
return <>{content}</>;
}

View File

@@ -1,17 +0,0 @@
export const deferredPromise = (): {
promise: Promise<never>;
resolve: (data?: any) => void;
reject: (reason?: any) => void;
} => {
let resolve: (data?: any) => void, reject: (reason?: any) => void;
const promise = new Promise((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
});
// @ts-ignore
return { promise, resolve, reject };
};
export type PromiseResolvers = ReturnType<typeof deferredPromise>;

View File

@@ -2,6 +2,20 @@ import browser from 'webextension-polyfill';
import { BackgroundActiontype } from '../entries/Background/rpc';
import { PluginConfig } from './misc';
export async function getCookiesByHost(hostname: string) {
return browser.runtime.sendMessage({
type: BackgroundActiontype.get_cookies_by_hostname,
data: hostname,
});
}
export async function getHeadersByHost(hostname: string) {
return browser.runtime.sendMessage({
type: BackgroundActiontype.get_headers_by_hostname,
data: hostname,
});
}
export async function addPlugin(hex: string) {
return browser.runtime.sendMessage({
type: BackgroundActiontype.add_plugin,

View File

@@ -1,11 +1,10 @@
import { LoggingLevel } from 'tlsn-js';
import { MAX_RECV, MAX_SENT, NOTARY_API, NOTARY_PROXY } from './constants';
import { LOGGING_LEVEL_INFO } from './constants';
export const NOTARY_API_LS_KEY = 'notary-api';
export const PROXY_API_LS_KEY = 'proxy-api';
export const MAX_SENT_LS_KEY = 'max-sent';
export const MAX_RECEIVED_LS_KEY = 'max-received';
export const LOGGING_FILTER_KEY = 'logging-filter-2';
export const LOGGING_FILTER_KEY = 'logging-filter';
export async function set(key: string, value: string) {
return chrome.storage.sync.set({ [key]: value });
@@ -15,25 +14,25 @@ export async function get(key: string, defaultValue?: string) {
return chrome.storage.sync
.get(key)
.then((json: any) => json[key] || defaultValue)
.catch(() => defaultValue);
.catch(() => '');
}
export async function getMaxSent() {
return parseInt(await get(MAX_SENT_LS_KEY, MAX_SENT.toString()));
return parseInt(await get(MAX_SENT_LS_KEY, '4096'));
}
export async function getMaxRecv() {
return parseInt(await get(MAX_RECEIVED_LS_KEY, MAX_RECV.toString()));
return parseInt(await get(MAX_RECEIVED_LS_KEY, '16384'));
}
export async function getNotaryApi() {
return await get(NOTARY_API_LS_KEY, NOTARY_API);
return await get(NOTARY_API_LS_KEY, 'https://notary.pse.dev/v0.1.0-alpha.5');
}
export async function getProxyApi() {
return await get(PROXY_API_LS_KEY, NOTARY_PROXY);
return await get(PROXY_API_LS_KEY, 'wss://notary.pse.dev/proxy');
}
export async function getLoggingFilter(): Promise<LoggingLevel> {
return await get(LOGGING_FILTER_KEY, 'Info');
export async function getLoggingFilter() {
return await get(LOGGING_FILTER_KEY, LOGGING_LEVEL_INFO);
}

View File

@@ -1,10 +0,0 @@
import { PresentationJSON as PresentationJSONa7 } from 'tlsn-js/build/types';
export type PresentationJSON = PresentationJSONa5 | PresentationJSONa7;
export type PresentationJSONa5 = {
version?: undefined;
session: any;
substrings: any;
notaryUrl: string;
};

View File

@@ -40,8 +40,7 @@ var options = {
mode: process.env.NODE_ENV || "development",
ignoreWarnings: [
/Circular dependency between chunks with runtime/,
/ResizeObserver loop completed with undelivered notifications/,
/Should not import the named export/,
/ResizeObserver loop completed with undelivered notifications/
],
entry: {
@@ -49,7 +48,6 @@ var options = {
popup: path.join(__dirname, "src", "entries", "Popup", "index.tsx"),
background: path.join(__dirname, "src", "entries", "Background", "index.ts"),
contentScript: path.join(__dirname, "src", "entries", "Content", "index.ts"),
content: path.join(__dirname, "src", "entries", "Content", "content.ts"),
offscreen: path.join(__dirname, "src", "entries", "Offscreen", "index.tsx"),
sidePanel: path.join(__dirname, "src", "entries", "SidePanel", "index.tsx"),
},
@@ -83,9 +81,6 @@ var options = {
loader: "sass-loader",
options: {
sourceMap: true,
sassOptions: {
silenceDeprecations: ["legacy-js-api"],
}
},
},
],
@@ -203,21 +198,31 @@ var options = {
}),
new CopyWebpackPlugin({
patterns: [
// {
// from: "node_modules/tlsn-js/build/7.js",
// to: path.join(__dirname, "build"),
// force: true,
// },
// {
// from: "node_modules/tlsn-js/build/250.js",
// to: path.join(__dirname, "build"),
// force: true,
// },
// {
// from: "node_modules/tlsn-js/build/278.js",
// to: path.join(__dirname, "build"),
// force: true,
// },
// {
// from: "node_modules/tlsn-js/build/349.js",
// to: path.join(__dirname, "build"),
// force: true,
// },
{
from: "node_modules/tlsn-js/build",
to: path.join(__dirname, "build"),
force: true,
},
{
from: "src/assets/plugins/discord_dm.wasm",
to: path.join(__dirname, "build"),
force: true,
},
{
from: "src/assets/plugins/twitter_profile.wasm",
to: path.join(__dirname, "build"),
force: true,
},
],
}),
new HtmlWebpackPlugin({

7413
yarn.lock Normal file

File diff suppressed because it is too large Load Diff