mirror of
https://github.com/tlsnotary/tlsn-extension.git
synced 2026-01-14 01:28:23 -05:00
Compare commits
19 Commits
hackathon2
...
requests-f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdaf6ca815 | ||
|
|
511272a451 | ||
|
|
047eb673f3 | ||
|
|
b8d2ba06d7 | ||
|
|
ca1ea2b34e | ||
|
|
53ba6f69b8 | ||
|
|
217824f2bf | ||
|
|
869fa5eeaa | ||
|
|
92f9d65c23 | ||
|
|
c481ee6bbf | ||
|
|
810e7bf415 | ||
|
|
8348756f0a | ||
|
|
9890604391 | ||
|
|
71cef56356 | ||
|
|
25689017b0 | ||
|
|
c68e2e1548 | ||
|
|
8bb76ad969 | ||
|
|
d55279501e | ||
|
|
2331074c9c |
22
README.md
22
README.md
@@ -1,9 +1,27 @@
|
||||
![MIT licensed][mit-badge]
|
||||
![Apache licensed][apache-badge]
|
||||
[![Build Status][actions-badge]][actions-url]
|
||||
|
||||
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
[apache-badge]: https://img.shields.io/github/license/saltstack/salt
|
||||
[actions-badge]: https://github.com/tlsnotary/tlsn-extension/actions/workflows/build.yaml/badge.svg
|
||||
[actions-url]: https://github.com/tlsnotary/tlsn-extension/actions?query=workflow%3Abuild+branch%3Amain++
|
||||
|
||||
<img src="src/assets/img/icon-128.png" width="64"/>
|
||||
|
||||
# Chrome Extension (MV3) for TLSNotary
|
||||
|
||||
### ⚠️ Notice
|
||||
- When running the extension against a [notary server](https://github.com/tlsnotary/tlsn/tree/dev/notary-server), please ensure that the server's version is the same as the version of this extension
|
||||
> [!IMPORTANT]
|
||||
> ⚠️ When running the extension against a [notary server](https://github.com/tlsnotary/tlsn/tree/dev/notary-server), please ensure that the server's version is the same as the version of this extension
|
||||
|
||||
## License
|
||||
This repository is licensed under either of
|
||||
|
||||
- [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
||||
- [MIT license](http://opensource.org/licenses/MIT)
|
||||
|
||||
at your option.
|
||||
|
||||
|
||||
## Installing and Running
|
||||
|
||||
|
||||
4554
package-lock.json
generated
4554
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,7 @@
|
||||
"lint:fix": "eslint . --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@extism/extism": "^1.0.2",
|
||||
"@fortawesome/fontawesome-free": "^6.4.2",
|
||||
"async-mutex": "^0.4.0",
|
||||
"buffer": "^6.0.3",
|
||||
@@ -36,7 +37,7 @@
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-thunk": "^2.4.2",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"tlsn-js": "0.1.0-alpha.5.1"
|
||||
"tlsn-js": "0.1.0-alpha.5.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.12",
|
||||
|
||||
48
plugins/README.md
Normal file
48
plugins/README.md
Normal 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
4
plugins/hello/hello.d.ts
vendored
Normal 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
6
plugins/hello/hello.js
Normal 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
BIN
plugins/hello/hello.wasm
Normal file
Binary file not shown.
14
plugins/reddit/index.d.ts
vendored
Normal file
14
plugins/reddit/index.d.ts
vendored
Normal 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
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
14
plugins/twitter_profile/index.d.ts
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
103
plugins/twitter_profile/index.js
Normal file
103
plugins/twitter_profile/index.js
Normal file
File diff suppressed because one or more lines are too long
BIN
plugins/twitter_profile/index.wasm
Normal file
BIN
plugins/twitter_profile/index.wasm
Normal file
Binary file not shown.
2488
pnpm-lock.yaml
generated
2488
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
BIN
src/assets/img/default-plugin-icon.png
Normal file
BIN
src/assets/img/default-plugin-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
26
src/components/ErrorModal/index.tsx
Normal file
26
src/components/ErrorModal/index.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React, { ReactElement } from 'react';
|
||||
import Modal, { ModalContent } from '../Modal/Modal';
|
||||
|
||||
export function ErrorModal(props: {
|
||||
onClose: () => void;
|
||||
message: string;
|
||||
}): ReactElement {
|
||||
const { onClose, message } = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="flex flex-col gap-4 items-center text-base cursor-default justify-center !w-auto mx-4 my-[50%] min-h-24 p-4 border border-red-500 !bg-red-100"
|
||||
onClose={onClose}
|
||||
>
|
||||
<ModalContent className="flex justify-center items-center text-red-500">
|
||||
{message || 'Something went wrong :('}
|
||||
</ModalContent>
|
||||
<button
|
||||
className="m-0 w-24 bg-red-200 text-red-400 hover:bg-red-200 hover:text-red-500"
|
||||
onClick={onClose}
|
||||
>
|
||||
OK
|
||||
</button>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
23
src/components/PluginList/index.scss
Normal file
23
src/components/PluginList/index.scss
Normal file
@@ -0,0 +1,23 @@
|
||||
.plugin-box {
|
||||
&__remove-icon {
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
width: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
transition: 200ms opacity;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.plugin-box__remove-icon {
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
padding: .5rem;
|
||||
opacity: .5;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
113
src/components/PluginList/index.tsx
Normal file
113
src/components/PluginList/index.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
MouseEventHandler,
|
||||
ReactElement,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
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';
|
||||
import classNames from 'classnames';
|
||||
import Icon from '../Icon';
|
||||
import './index.scss';
|
||||
import browser from 'webextension-polyfill';
|
||||
import { ErrorModal } from '../ErrorModal';
|
||||
|
||||
export function PluginList(props: { className?: string }): ReactElement {
|
||||
const hashes = usePluginHashes();
|
||||
|
||||
useEffect(() => {
|
||||
fetchPluginHashes();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
{hashes.map((hash) => (
|
||||
<Plugin key={hash} hash={hash} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Plugin(props: {
|
||||
hash: string;
|
||||
onClick?: () => void;
|
||||
}): ReactElement {
|
||||
const [error, showError] = useState('');
|
||||
const [config, setConfig] = useState<PluginConfig | null>(null);
|
||||
|
||||
const onClick = useCallback(async () => {
|
||||
if (!config) return;
|
||||
|
||||
try {
|
||||
await runPlugin(props.hash, 'start');
|
||||
|
||||
const [tab] = await browser.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
});
|
||||
|
||||
await browser.storage.local.set({ plugin_hash: props.hash });
|
||||
|
||||
// @ts-ignore
|
||||
if (chrome.sidePanel) await chrome.sidePanel.open({ tabId: tab.id });
|
||||
|
||||
window.close();
|
||||
} catch (e: any) {
|
||||
showError(e.message);
|
||||
}
|
||||
}, [props.hash, config]);
|
||||
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
setConfig(await fetchPluginConfigByHash(props.hash));
|
||||
})();
|
||||
}, [props.hash]);
|
||||
|
||||
const onRemove: MouseEventHandler = useCallback(
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
removePlugin(props.hash);
|
||||
},
|
||||
[props.hash],
|
||||
);
|
||||
|
||||
if (!config) return <></>;
|
||||
|
||||
return (
|
||||
<button
|
||||
className={classNames(
|
||||
'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} />}
|
||||
<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>
|
||||
<div>{config.description}</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
@@ -17,8 +16,14 @@ import {
|
||||
} from 'react-router';
|
||||
import Icon from '../Icon';
|
||||
import NavigateWithParams from '../NavigateWithParams';
|
||||
import { get, NOTARY_API_LS_KEY, PROXY_API_LS_KEY } from '../../utils/storage';
|
||||
import { urlify } from '../../utils/misc';
|
||||
import {
|
||||
set,
|
||||
MAX_SENT_LS_KEY,
|
||||
MAX_RECEIVED_LS_KEY,
|
||||
getMaxRecv,
|
||||
getMaxSent,
|
||||
} from '../../utils/storage';
|
||||
import { MAX_RECV, MAX_SENT } from '../../utils/constants';
|
||||
|
||||
type Props = {
|
||||
requestId: string;
|
||||
@@ -31,7 +36,6 @@ export default function RequestDetail(props: Props): ReactElement {
|
||||
const notarize = useCallback(async () => {
|
||||
if (!request) return;
|
||||
|
||||
console.log('/notary/' + props.requestId);
|
||||
navigate('/notary/' + request.requestId);
|
||||
}, [request, props.requestId]);
|
||||
|
||||
@@ -54,6 +58,9 @@ export default function RequestDetail(props: Props): ReactElement {
|
||||
<RequestDetailsHeaderTab path="/response">
|
||||
Response
|
||||
</RequestDetailsHeaderTab>
|
||||
<RequestDetailsHeaderTab path="/advanced">
|
||||
Advanced
|
||||
</RequestDetailsHeaderTab>
|
||||
<button
|
||||
className="absolute right-2 bg-primary/[0.9] text-white font-bold px-2 py-0.5 hover:bg-primary/[0.8] active:bg-primary"
|
||||
onClick={notarize}
|
||||
@@ -74,6 +81,7 @@ export default function RequestDetail(props: Props): ReactElement {
|
||||
path="response"
|
||||
element={<WebResponse requestId={props.requestId} />}
|
||||
/>
|
||||
<Route path="advanced" element={<AdvancedOptions />} />
|
||||
<Route path="/" element={<NavigateWithParams to="/headers" />} />
|
||||
</Routes>
|
||||
</>
|
||||
@@ -101,6 +109,62 @@ function RequestDetailsHeaderTab(props: {
|
||||
);
|
||||
}
|
||||
|
||||
function AdvancedOptions(): ReactElement {
|
||||
const [maxSent, setMaxSent] = useState(MAX_SENT);
|
||||
const [maxRecv, setMaxRecv] = useState(MAX_RECV);
|
||||
|
||||
const [dirty, setDirty] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setMaxRecv((await getMaxRecv()) || MAX_RECV);
|
||||
setMaxSent((await getMaxSent()) || MAX_SENT);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const onSave = useCallback(async () => {
|
||||
await set(MAX_RECEIVED_LS_KEY, maxRecv.toString());
|
||||
await set(MAX_SENT_LS_KEY, maxSent.toString());
|
||||
setDirty(false);
|
||||
}, [maxSent, maxRecv]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-nowrap py-1 px-2 gap-2">
|
||||
<div className="font-semibold">Max Sent Data</div>
|
||||
<input
|
||||
type="number"
|
||||
className="input border"
|
||||
value={maxSent}
|
||||
min={0}
|
||||
onChange={(e) => {
|
||||
setMaxSent(parseInt(e.target.value));
|
||||
setDirty(true);
|
||||
}}
|
||||
/>
|
||||
<div className="font-semibold">Max Received Data</div>
|
||||
<input
|
||||
type="number"
|
||||
className="input border"
|
||||
value={maxRecv}
|
||||
min={0}
|
||||
onChange={(e) => {
|
||||
setMaxRecv(parseInt(e.target.value));
|
||||
setDirty(true);
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-row flex-nowrap justify-end gap-2 p-2">
|
||||
<button
|
||||
className="button !bg-primary/[0.9] hover:bg-primary/[0.8] active:bg-primary !text-white"
|
||||
disabled={!dirty}
|
||||
onClick={onSave}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RequestPayload(props: Props): ReactElement {
|
||||
const data = useRequest(props.requestId);
|
||||
const [url, setUrl] = useState<URL | null>();
|
||||
|
||||
@@ -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,6 +27,50 @@ export const getCacheByTabId = (tabId: number): NodeCache => {
|
||||
return RequestsLogs[tabId];
|
||||
};
|
||||
|
||||
export const clearCache = () => {
|
||||
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();
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Level } from 'level';
|
||||
import type { RequestHistory } from './rpc';
|
||||
import { PluginConfig, sha256 } from '../../utils/misc';
|
||||
const charwise = require('charwise');
|
||||
|
||||
const db = new Level('./ext-db', {
|
||||
@@ -8,6 +9,12 @@ const db = new Level('./ext-db', {
|
||||
const historyDb = db.sublevel<string, RequestHistory>('history', {
|
||||
valueEncoding: 'json',
|
||||
});
|
||||
const pluginDb = db.sublevel<string, string>('plugin', {
|
||||
valueEncoding: 'hex',
|
||||
});
|
||||
const pluginConfigDb = db.sublevel<string, PluginConfig>('pluginConfig', {
|
||||
valueEncoding: 'json',
|
||||
});
|
||||
|
||||
export async function addNotaryRequest(
|
||||
now = Date.now(),
|
||||
@@ -122,3 +129,96 @@ export async function getNotaryRequest(
|
||||
): Promise<RequestHistory | 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;
|
||||
}
|
||||
|
||||
export async function getPluginByHash(hash: string): Promise<string | null> {
|
||||
try {
|
||||
const plugin = await pluginDb.get(hash);
|
||||
return plugin;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function addPlugin(hex: string): Promise<string | null> {
|
||||
const hash = await sha256(hex);
|
||||
|
||||
if (await getPluginByHash(hash)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await pluginDb.put(hash, hex);
|
||||
return hash;
|
||||
}
|
||||
|
||||
export async function removePlugin(hash: string): Promise<string | null> {
|
||||
const existing = await pluginDb.get(hash);
|
||||
|
||||
if (!existing) return null;
|
||||
|
||||
await pluginDb.del(hash);
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
export async function getPluginConfigByHash(
|
||||
hash: string,
|
||||
): Promise<PluginConfig | null> {
|
||||
try {
|
||||
const config = await pluginConfigDb.get(hash);
|
||||
return config;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function addPluginConfig(
|
||||
hash: string,
|
||||
config: PluginConfig,
|
||||
): Promise<PluginConfig | null> {
|
||||
if (await getPluginConfigByHash(hash)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await pluginConfigDb.put(hash, config);
|
||||
return config;
|
||||
}
|
||||
|
||||
export async function removePluginConfig(
|
||||
hash: string,
|
||||
): Promise<PluginConfig | null> {
|
||||
const existing = await pluginConfigDb.get(hash);
|
||||
|
||||
if (!existing) return null;
|
||||
|
||||
await pluginConfigDb.del(hash);
|
||||
|
||||
return existing;
|
||||
}
|
||||
|
||||
export async function setNotaryRequestCid(
|
||||
id: string,
|
||||
cid: string,
|
||||
): Promise<RequestHistory | null> {
|
||||
const existing = await historyDb.get(id);
|
||||
|
||||
if (!existing) return null;
|
||||
|
||||
const newReq = {
|
||||
...existing,
|
||||
cid,
|
||||
};
|
||||
|
||||
await historyDb.put(id, newReq);
|
||||
|
||||
return newReq;
|
||||
}
|
||||
|
||||
@@ -1,8 +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';
|
||||
|
||||
export const onSendHeaders = (
|
||||
details: browser.WebRequest.OnSendHeadersDetailsType,
|
||||
@@ -13,6 +18,26 @@ export const onSendHeaders = (
|
||||
if (method !== 'OPTIONS') {
|
||||
const cache = getCacheByTabId(tabId);
|
||||
const existing = cache.get<RequestLog>(requestId);
|
||||
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) => {
|
||||
cookieStore.set(cookie[0].trim(), cookie[1]);
|
||||
});
|
||||
} else {
|
||||
headerStore.set(name, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
cache.set(requestId, {
|
||||
...existing,
|
||||
method: details.method as 'GET' | 'POST',
|
||||
@@ -34,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);
|
||||
@@ -69,7 +93,6 @@ export const onResponseStarted = (
|
||||
if (method === 'OPTIONS') return;
|
||||
|
||||
const cache = getCacheByTabId(tabId);
|
||||
|
||||
const existing = cache.get<RequestLog>(requestId);
|
||||
const newLog: RequestLog = {
|
||||
requestHeaders: [],
|
||||
|
||||
@@ -1,21 +1,51 @@
|
||||
import browser from 'webextension-polyfill';
|
||||
import { clearCache, getCacheByTabId } from './cache';
|
||||
import {
|
||||
clearCache,
|
||||
getCacheByTabId,
|
||||
getCookieStoreByHost,
|
||||
getHeaderStoreByHost,
|
||||
} from './cache';
|
||||
import { addRequestHistory } from '../../reducers/history';
|
||||
import {
|
||||
getNotaryRequests,
|
||||
addNotaryRequest,
|
||||
addNotaryRequestProofs,
|
||||
getNotaryRequest,
|
||||
setNotaryRequestStatus,
|
||||
setNotaryRequestError,
|
||||
setNotaryRequestVerification,
|
||||
getNotaryRequests,
|
||||
removeNotaryRequest,
|
||||
setNotaryRequestError,
|
||||
setNotaryRequestStatus,
|
||||
setNotaryRequestVerification,
|
||||
addPlugin,
|
||||
getPluginHashes,
|
||||
getPluginByHash,
|
||||
removePlugin,
|
||||
addPluginConfig,
|
||||
getPluginConfigByHash,
|
||||
removePluginConfig,
|
||||
} from './db';
|
||||
import { addOnePlugin, removeOnePlugin } from '../../reducers/plugins';
|
||||
import {
|
||||
devlog,
|
||||
extractBodyFromResponse,
|
||||
getPluginConfig,
|
||||
hexToArrayBuffer,
|
||||
makePlugin,
|
||||
} from '../../utils/misc';
|
||||
import {
|
||||
getLoggingFilter,
|
||||
getMaxRecv,
|
||||
getMaxSent,
|
||||
getNotaryApi,
|
||||
getProxyApi,
|
||||
} from '../../utils/storage';
|
||||
|
||||
const charwise = require('charwise');
|
||||
|
||||
export enum BackgroundActiontype {
|
||||
get_requests = 'get_requests',
|
||||
clear_requests = 'clear_requests',
|
||||
push_action = 'push_action',
|
||||
execute_plugin_prover = 'execute_plugin_prover',
|
||||
get_prove_requests = 'get_prove_requests',
|
||||
prove_request_start = 'prove_request_start',
|
||||
process_prove_request = 'process_prove_request',
|
||||
@@ -24,6 +54,16 @@ export enum BackgroundActiontype {
|
||||
verify_proof = 'verify_proof',
|
||||
delete_prove_request = 'delete_prove_request',
|
||||
retry_prove_request = 'retry_prove_request',
|
||||
get_cookies_by_hostname = 'get_cookies_by_hostname',
|
||||
get_headers_by_hostname = 'get_headers_by_hostname',
|
||||
add_plugin = 'add_plugin',
|
||||
remove_plugin = 'remove_plugin',
|
||||
get_plugin_by_hash = 'get_plugin_by_hash',
|
||||
get_plugin_config_by_hash = 'get_plugin_config_by_hash',
|
||||
run_plugin = 'run_plugin',
|
||||
get_plugin_hashes = 'get_plugin_hashes',
|
||||
open_popup = 'open_popup',
|
||||
change_route = 'change_route',
|
||||
}
|
||||
|
||||
export type BackgroundAction = {
|
||||
@@ -55,6 +95,8 @@ export type RequestHistory = {
|
||||
headers: { [key: string]: string };
|
||||
body?: string;
|
||||
maxTranscriptSize: number;
|
||||
maxSentData: number;
|
||||
maxRecvData: number;
|
||||
notaryUrl: string;
|
||||
websocketProxyUrl: string;
|
||||
status: '' | 'pending' | 'success' | 'error';
|
||||
@@ -67,6 +109,7 @@ export type RequestHistory = {
|
||||
};
|
||||
secretHeaders?: string[];
|
||||
secretResps?: string[];
|
||||
cid?: string;
|
||||
};
|
||||
|
||||
export const initRPC = () => {
|
||||
@@ -89,6 +132,26 @@ export const initRPC = () => {
|
||||
return handleRetryProveReqest(request, sendResponse);
|
||||
case BackgroundActiontype.prove_request_start:
|
||||
return handleProveRequestStart(request, sendResponse);
|
||||
case BackgroundActiontype.get_cookies_by_hostname:
|
||||
return handleGetCookiesByHostname(request, sendResponse);
|
||||
case BackgroundActiontype.get_headers_by_hostname:
|
||||
return handleGetHeadersByHostname(request, sendResponse);
|
||||
case BackgroundActiontype.add_plugin:
|
||||
return handleAddPlugin(request, sendResponse);
|
||||
case BackgroundActiontype.remove_plugin:
|
||||
return handleRemovePlugin(request, sendResponse);
|
||||
case BackgroundActiontype.get_plugin_hashes:
|
||||
return handleGetPluginHashes(request, sendResponse);
|
||||
case BackgroundActiontype.get_plugin_by_hash:
|
||||
return handleGetPluginByHash(request, sendResponse);
|
||||
case BackgroundActiontype.get_plugin_config_by_hash:
|
||||
return handleGetPluginConfigByHash(request, sendResponse);
|
||||
case BackgroundActiontype.run_plugin:
|
||||
return handleRunPlugin(request, sendResponse);
|
||||
case BackgroundActiontype.execute_plugin_prover:
|
||||
return handleExecPluginProver(request);
|
||||
case BackgroundActiontype.open_popup:
|
||||
return handleOpenPopup(request);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -196,6 +259,7 @@ async function handleRetryProveReqest(
|
||||
...req,
|
||||
notaryUrl,
|
||||
websocketProxyUrl,
|
||||
loggingFilter: await getLoggingFilter(),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -212,6 +276,8 @@ async function handleProveRequestStart(
|
||||
headers,
|
||||
body,
|
||||
maxTranscriptSize,
|
||||
maxSentData,
|
||||
maxRecvData,
|
||||
notaryUrl,
|
||||
websocketProxyUrl,
|
||||
secretHeaders,
|
||||
@@ -223,6 +289,8 @@ async function handleProveRequestStart(
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
maxSentData,
|
||||
maxRecvData,
|
||||
maxTranscriptSize,
|
||||
notaryUrl,
|
||||
websocketProxyUrl,
|
||||
@@ -249,12 +317,240 @@ async function handleProveRequestStart(
|
||||
headers,
|
||||
body,
|
||||
maxTranscriptSize,
|
||||
maxSentData,
|
||||
maxRecvData,
|
||||
notaryUrl,
|
||||
websocketProxyUrl,
|
||||
secretHeaders,
|
||||
secretResps,
|
||||
loggingFilter: await getLoggingFilter(),
|
||||
},
|
||||
});
|
||||
|
||||
return sendResponse();
|
||||
}
|
||||
|
||||
async function runPluginProver(request: BackgroundAction, now = Date.now()) {
|
||||
const {
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
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,
|
||||
});
|
||||
|
||||
await setNotaryRequestStatus(id, 'pending');
|
||||
|
||||
await browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.push_action,
|
||||
data: {
|
||||
tabId: 'background',
|
||||
},
|
||||
action: addRequestHistory(await getNotaryRequest(id)),
|
||||
});
|
||||
|
||||
await browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.process_prove_request,
|
||||
data: {
|
||||
id,
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
maxTranscriptSize,
|
||||
notaryUrl,
|
||||
websocketProxyUrl,
|
||||
maxRecvData,
|
||||
maxSentData,
|
||||
loggingFilter: await getLoggingFilter(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function handleExecPluginProver(request: BackgroundAction) {
|
||||
const now = request.data.now;
|
||||
const id = charwise.encode(now).toString('hex');
|
||||
runPluginProver(request, now);
|
||||
return id;
|
||||
}
|
||||
|
||||
function handleGetCookiesByHostname(
|
||||
request: BackgroundAction,
|
||||
sendResponse: (data?: any) => void,
|
||||
) {
|
||||
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,
|
||||
) {
|
||||
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(
|
||||
request: BackgroundAction,
|
||||
sendResponse: (data?: any) => void,
|
||||
) {
|
||||
try {
|
||||
const config = await getPluginConfig(hexToArrayBuffer(request.data));
|
||||
|
||||
if (config) {
|
||||
const hash = await addPlugin(request.data);
|
||||
|
||||
if (hash) {
|
||||
await addPluginConfig(hash, config);
|
||||
|
||||
await browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.push_action,
|
||||
data: {
|
||||
tabId: 'background',
|
||||
},
|
||||
action: addOnePlugin(hash),
|
||||
});
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
return sendResponse();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRemovePlugin(
|
||||
request: BackgroundAction,
|
||||
sendResponse: (data?: any) => void,
|
||||
) {
|
||||
await removePlugin(request.data);
|
||||
await removePluginConfig(request.data);
|
||||
await browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.push_action,
|
||||
data: {
|
||||
tabId: 'background',
|
||||
},
|
||||
action: removeOnePlugin(request.data),
|
||||
});
|
||||
|
||||
return sendResponse();
|
||||
}
|
||||
|
||||
async function handleGetPluginHashes(
|
||||
request: BackgroundAction,
|
||||
sendResponse: (data?: any) => void,
|
||||
) {
|
||||
const hashes = await getPluginHashes();
|
||||
for (const hash of hashes) {
|
||||
await browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.push_action,
|
||||
data: {
|
||||
tabId: 'background',
|
||||
},
|
||||
action: addOnePlugin(hash),
|
||||
});
|
||||
}
|
||||
return sendResponse();
|
||||
}
|
||||
|
||||
async function handleGetPluginByHash(
|
||||
request: BackgroundAction,
|
||||
sendResponse: (data?: any) => void,
|
||||
) {
|
||||
const hash = request.data;
|
||||
const hex = await getPluginByHash(hash);
|
||||
return hex;
|
||||
}
|
||||
|
||||
async function handleGetPluginConfigByHash(
|
||||
request: BackgroundAction,
|
||||
sendResponse: (data?: any) => void,
|
||||
) {
|
||||
const hash = request.data;
|
||||
const config = await getPluginConfigByHash(hash);
|
||||
return config;
|
||||
}
|
||||
|
||||
async function handleRunPlugin(
|
||||
request: BackgroundAction,
|
||||
sendResponse: (data?: any) => void,
|
||||
) {
|
||||
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 handleOpenPopup(request: BackgroundAction) {
|
||||
if (cachePopup) {
|
||||
browser.windows.update(cachePopup.id!, {
|
||||
focused: true,
|
||||
});
|
||||
} else {
|
||||
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;
|
||||
|
||||
const onPopUpClose = (windowId: number) => {
|
||||
if (windowId === popup.id) {
|
||||
cachePopup = null;
|
||||
browser.windows.onRemoved.removeListener(onPopUpClose);
|
||||
}
|
||||
};
|
||||
|
||||
browser.windows.onRemoved.addListener(onPopUpClose);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { BackgroundActiontype } from '../Background/rpc';
|
||||
import { prove, verify } from 'tlsn-js';
|
||||
import { prove, set_logging_filter, verify } from 'tlsn-js';
|
||||
import { urlify } from '../../utils/misc';
|
||||
import browser from 'webextension-polyfill';
|
||||
import { getLoggingFilter } from '../../utils/storage';
|
||||
import { LOGGING_LEVEL_INFO } from '../../utils/constants';
|
||||
|
||||
const Offscreen = () => {
|
||||
useEffect(() => {
|
||||
@@ -15,21 +17,27 @@ const Offscreen = () => {
|
||||
method,
|
||||
headers,
|
||||
body = '',
|
||||
maxSentData,
|
||||
maxRecvData,
|
||||
maxTranscriptSize,
|
||||
notaryUrl,
|
||||
websocketProxyUrl,
|
||||
id,
|
||||
secretHeaders,
|
||||
secretResps,
|
||||
loggingFilter = LOGGING_LEVEL_INFO,
|
||||
} = 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}`,
|
||||
|
||||
@@ -19,6 +19,7 @@ import ProofViewer from '../../pages/ProofViewer';
|
||||
import History from '../../pages/History';
|
||||
import ProofUploader from '../../pages/ProofUploader';
|
||||
import browser from 'webextension-polyfill';
|
||||
import store from '../../utils/store';
|
||||
|
||||
const Popup = () => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -49,6 +50,28 @@ const Popup = () => {
|
||||
})();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
chrome.runtime.onMessage.addListener((request) => {
|
||||
switch (request.type) {
|
||||
case BackgroundActiontype.push_action: {
|
||||
if (
|
||||
request.data.tabId === store.getState().requests.activeTab?.id ||
|
||||
request.data.tabId === 'background'
|
||||
) {
|
||||
store.dispatch(request.action);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case BackgroundActiontype.change_route: {
|
||||
if (request.data.tabId === 'background') {
|
||||
navigate(request.route);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<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">
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { HashRouter } from 'react-router-dom';
|
||||
|
||||
import Popup from './Popup';
|
||||
import './index.scss';
|
||||
import { Provider } from 'react-redux';
|
||||
@@ -11,20 +10,6 @@ import { BackgroundActiontype } from '../Background/rpc';
|
||||
const container = document.getElementById('app-container');
|
||||
const root = createRoot(container!); // createRoot(container!) if you use TypeScript
|
||||
|
||||
chrome.runtime.onMessage.addListener((request) => {
|
||||
switch (request.type) {
|
||||
case BackgroundActiontype.push_action: {
|
||||
if (
|
||||
request.data.tabId === store.getState().requests.activeTab?.id ||
|
||||
request.data.tabId === 'background'
|
||||
) {
|
||||
store.dispatch(request.action);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
root.render(
|
||||
<Provider store={store}>
|
||||
<HashRouter>
|
||||
|
||||
235
src/entries/SidePanel/SidePanel.tsx
Normal file
235
src/entries/SidePanel/SidePanel.tsx
Normal file
@@ -0,0 +1,235 @@
|
||||
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
|
||||
import './sidePanel.scss';
|
||||
import browser from 'webextension-polyfill';
|
||||
import { fetchPluginConfigByHash, runPlugin } from '../../utils/rpc';
|
||||
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';
|
||||
import classNames from 'classnames';
|
||||
import Icon from '../../components/Icon';
|
||||
import { useRequestHistory } from '../../reducers/history';
|
||||
import { BackgroundActiontype } from '../Background/rpc';
|
||||
|
||||
export default function SidePanel(): ReactElement {
|
||||
const [config, setConfig] = useState<PluginConfig | null>(null);
|
||||
const [hash, setHash] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
const result = await browser.storage.local.get('plugin_hash');
|
||||
const { plugin_hash } = result;
|
||||
const config = await fetchPluginConfigByHash(plugin_hash);
|
||||
setHash(plugin_hash);
|
||||
setConfig(config);
|
||||
// await browser.storage.local.set({ plugin_hash: '' });
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col bg-slate-100 w-screen h-screen">
|
||||
<div className="relative 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="h-5" src={logo} alt="logo" />
|
||||
<button
|
||||
className="button absolute right-2"
|
||||
onClick={() => window.close()}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
{!config && <PluginList />}
|
||||
{config && <PluginBody hash={hash} config={config} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PluginBody(props: {
|
||||
config: PluginConfig;
|
||||
hash: string;
|
||||
}): ReactElement {
|
||||
const { hash } = props;
|
||||
const { title, description, icon, steps } = props.config;
|
||||
const [responses, setResponses] = useState<any[]>([]);
|
||||
|
||||
const setResponse = useCallback(
|
||||
(response: any, i: number) => {
|
||||
const result = responses.concat();
|
||||
result[i] = response;
|
||||
setResponses(result);
|
||||
},
|
||||
[responses],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col p-4">
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
<img className="w-12 h-12 self-start" src={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 text-base">
|
||||
{title}
|
||||
</div>
|
||||
<div className="text-slate-500 text-sm">{description}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-start gap-8 mt-8">
|
||||
{steps?.map((step, i) => (
|
||||
<StepContent
|
||||
hash={hash}
|
||||
index={i}
|
||||
setResponse={setResponse}
|
||||
lastResponse={i > 0 ? responses[i - 1] : undefined}
|
||||
responses={responses}
|
||||
{...step}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StepContent(
|
||||
props: StepConfig & {
|
||||
hash: string;
|
||||
index: number;
|
||||
setResponse: (resp: any, i: number) => void;
|
||||
responses: any[];
|
||||
lastResponse?: any;
|
||||
},
|
||||
): ReactElement {
|
||||
const {
|
||||
index,
|
||||
title,
|
||||
description,
|
||||
cta,
|
||||
action,
|
||||
hash,
|
||||
setResponse,
|
||||
lastResponse,
|
||||
prover,
|
||||
} = props;
|
||||
const [completed, setCompleted] = useState(false);
|
||||
const [pending, setPending] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [notarizationId, setNotarizationId] = useState('');
|
||||
const notaryRequest = useRequestHistory(notarizationId);
|
||||
|
||||
const processStep = useCallback(async () => {
|
||||
if (index > 0 && !lastResponse) return;
|
||||
|
||||
setPending(true);
|
||||
try {
|
||||
setError('');
|
||||
const val = await runPlugin(hash, action, JSON.stringify(lastResponse));
|
||||
if (val && prover) {
|
||||
setNotarizationId(val);
|
||||
} else {
|
||||
setCompleted(!!val);
|
||||
setResponse(val, index);
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
setError(e?.message || 'Unkonwn error');
|
||||
} finally {
|
||||
setPending(false);
|
||||
}
|
||||
}, [hash, action, index, lastResponse, prover]);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
if (
|
||||
pending ||
|
||||
completed ||
|
||||
notaryRequest?.status === 'pending' ||
|
||||
notaryRequest?.status === 'success'
|
||||
)
|
||||
return;
|
||||
processStep();
|
||||
}, [processStep, pending, completed, notaryRequest]);
|
||||
|
||||
const viewProofInPopup = useCallback(async () => {
|
||||
if (!notaryRequest) return;
|
||||
chrome.runtime.sendMessage<any, string>({
|
||||
type: BackgroundActiontype.verify_prove_request,
|
||||
data: notaryRequest,
|
||||
});
|
||||
await browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.open_popup,
|
||||
data: {
|
||||
position: {
|
||||
left: window.screen.width / 2 - 240,
|
||||
top: window.screen.height / 2 - 300,
|
||||
},
|
||||
route: `/verify/${notaryRequest.id}`,
|
||||
},
|
||||
});
|
||||
}, [notaryRequest, notarizationId]);
|
||||
|
||||
useEffect(() => {
|
||||
processStep();
|
||||
}, [processStep]);
|
||||
|
||||
let btnContent = null;
|
||||
|
||||
if (completed) {
|
||||
btnContent = (
|
||||
<button
|
||||
className={classNames(
|
||||
'button mt-2 w-fit flex flex-row flex-nowrap items-center gap-2',
|
||||
'!bg-green-200 !text-black cursor-default border border-green-500 rounded',
|
||||
)}
|
||||
>
|
||||
<Icon className="text-green-600" fa="fa-solid fa-check" />
|
||||
<span className="text-sm">DONE</span>
|
||||
</button>
|
||||
);
|
||||
} else if (notaryRequest?.status === 'success') {
|
||||
btnContent = (
|
||||
<button
|
||||
className={classNames(
|
||||
'button button--primary mt-2 w-fit flex flex-row flex-nowrap items-center gap-2',
|
||||
)}
|
||||
onClick={viewProofInPopup}
|
||||
>
|
||||
<span className="text-sm">View Proof</span>
|
||||
</button>
|
||||
);
|
||||
} else if (notaryRequest?.status === 'pending' || pending || notarizationId) {
|
||||
btnContent = (
|
||||
<button className="button mt-2 w-fit flex flex-row flex-nowrap items-center gap-2 cursor-default">
|
||||
<Icon className="animate-spin" fa="fa-solid fa-spinner" size={1} />
|
||||
<span className="text-sm">{cta}</span>
|
||||
</button>
|
||||
);
|
||||
} else {
|
||||
btnContent = (
|
||||
<button
|
||||
className={classNames(
|
||||
'button mt-2 w-fit flex flex-row flex-nowrap items-center gap-2',
|
||||
)}
|
||||
disabled={index > 0 && typeof lastResponse === 'undefined'}
|
||||
onClick={onClick}
|
||||
>
|
||||
<span className="text-sm">{cta}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-row gap-4 text-base w-full">
|
||||
<div className="text-slate-500 self-start">{index + 1}.</div>
|
||||
<div className="flex flex-col flex-grow flex-shrink w-0">
|
||||
<div
|
||||
className={classNames('font-semibold', {
|
||||
'line-through text-slate-500': completed,
|
||||
})}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
{!!description && (
|
||||
<div className="text-slate-500 text-sm">{description}</div>
|
||||
)}
|
||||
{!!error && <div className="text-red-500 text-sm">{error}</div>}
|
||||
{btnContent}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
13
src/entries/SidePanel/index.html
Normal file
13
src/entries/SidePanel/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" width="480px">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Side Panel</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app-container"></div>
|
||||
<div id="modal-root"></div>
|
||||
</body>
|
||||
</html>
|
||||
29
src/entries/SidePanel/index.tsx
Normal file
29
src/entries/SidePanel/index.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import SidePanel from './SidePanel';
|
||||
import store from '../../utils/store';
|
||||
import { Provider } from 'react-redux';
|
||||
import { BackgroundActiontype } from '../Background/rpc';
|
||||
|
||||
const container = document.getElementById('app-container');
|
||||
const root = createRoot(container!); // createRoot(container!) if you use TypeScript
|
||||
|
||||
chrome.runtime.onMessage.addListener((request) => {
|
||||
switch (request.type) {
|
||||
case BackgroundActiontype.push_action: {
|
||||
if (
|
||||
request.data.tabId === store.getState().requests.activeTab?.id ||
|
||||
request.data.tabId === 'background'
|
||||
) {
|
||||
store.dispatch(request.action);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
root.render(
|
||||
<Provider store={store}>
|
||||
<SidePanel />
|
||||
</Provider>,
|
||||
);
|
||||
28
src/entries/SidePanel/sidePanel.scss
Normal file
28
src/entries/SidePanel/sidePanel.scss
Normal file
@@ -0,0 +1,28 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
$fa-font-path: "~@fortawesome/fontawesome-free/webfonts";
|
||||
|
||||
@import "../../../node_modules/@fortawesome/fontawesome-free/scss/fontawesome";
|
||||
@import "../../../node_modules/@fortawesome/fontawesome-free/scss/brands";
|
||||
@import "../../../node_modules/@fortawesome/fontawesome-free/scss/solid";
|
||||
@import "../../../node_modules/@fortawesome/fontawesome-free/scss/regular";
|
||||
@import "../Popup/index.scss";
|
||||
|
||||
html {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
#app-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
@@ -3,11 +3,16 @@
|
||||
"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",
|
||||
"default_icon": "icon-34.png"
|
||||
},
|
||||
"side_panel": {
|
||||
"default_path": "sidePanel.html"
|
||||
},
|
||||
"icons": {
|
||||
"128": "icon-128.png"
|
||||
},
|
||||
@@ -32,6 +37,7 @@
|
||||
"offscreen",
|
||||
"storage",
|
||||
"webRequest",
|
||||
"activeTab"
|
||||
"activeTab",
|
||||
"sidePanel"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { ReactElement, useState, useCallback } from 'react';
|
||||
import React, { ReactElement, useState, useCallback, useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useNavigate } from 'react-router';
|
||||
import {
|
||||
@@ -7,13 +7,23 @@ import {
|
||||
deleteRequestHistory,
|
||||
} from '../../reducers/history';
|
||||
import Icon from '../../components/Icon';
|
||||
import { get, NOTARY_API_LS_KEY, PROXY_API_LS_KEY } 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';
|
||||
import classNames from 'classnames';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { EXPLORER_API } from '../../utils/constants';
|
||||
import {
|
||||
getNotaryRequest,
|
||||
setNotaryRequestCid,
|
||||
} from '../../entries/Background/db';
|
||||
|
||||
export default function History(): ReactElement {
|
||||
const history = useHistoryOrder();
|
||||
@@ -34,15 +44,29 @@ function OneRequestHistory(props: { requestId: string }): ReactElement {
|
||||
const [uploadError, setUploadError] = useState('');
|
||||
const [showingShareConfirmation, setShowingShareConfirmation] =
|
||||
useState(false);
|
||||
const [cid, setCid] = useState('');
|
||||
const [cid, setCid] = useState<{ [key: string]: string }>({});
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const { status } = request || {};
|
||||
const requestUrl = urlify(request?.url || '');
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const request = await getNotaryRequest(props.requestId);
|
||||
if (request && request.cid) {
|
||||
setCid({ [props.requestId]: request.cid });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error fetching data', e);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const onRetry = useCallback(async () => {
|
||||
const notaryUrl = await get(NOTARY_API_LS_KEY);
|
||||
const websocketProxyUrl = await get(PROXY_API_LS_KEY);
|
||||
const notaryUrl = await getNotaryApi();
|
||||
const websocketProxyUrl = await getProxyApi();
|
||||
chrome.runtime.sendMessage<any, string>({
|
||||
type: BackgroundActiontype.retry_prove_request,
|
||||
data: {
|
||||
@@ -81,13 +105,14 @@ function OneRequestHistory(props: { requestId: string }): ReactElement {
|
||||
`${request?.id}.json`,
|
||||
JSON.stringify(request?.proof),
|
||||
);
|
||||
setCid(data);
|
||||
setCid((prevCid) => ({ ...prevCid, [props.requestId]: data }));
|
||||
await setNotaryRequestCid(props.requestId, data);
|
||||
} catch (e: any) {
|
||||
setUploadError(e.message);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [props.requestId, request, cid]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-row flex-nowrap border rounded-md p-2 gap-1 hover:bg-slate-50 cursor-pointer">
|
||||
@@ -215,7 +240,7 @@ function OneRequestHistory(props: { requestId: string }): ReactElement {
|
||||
onClose={closeAllModal}
|
||||
>
|
||||
<ModalContent className="flex flex-col w-full gap-4 items-center text-base justify-center">
|
||||
{!cid ? (
|
||||
{!cid[props.requestId] ? (
|
||||
<p className="text-slate-500 text-center">
|
||||
{uploadError ||
|
||||
'This will make your proof publicly accessible by anyone with the CID'}
|
||||
@@ -224,13 +249,13 @@ function OneRequestHistory(props: { requestId: string }): ReactElement {
|
||||
<input
|
||||
className="input w-full bg-slate-100 border border-slate-200"
|
||||
readOnly
|
||||
value={`${EXPLORER_API}/ipfs/${cid}`}
|
||||
value={`${EXPLORER_API}/ipfs/${cid[props.requestId]}`}
|
||||
onFocus={(e) => e.target.select()}
|
||||
/>
|
||||
)}
|
||||
</ModalContent>
|
||||
<div className="flex flex-row gap-2 justify-center">
|
||||
{!cid ? (
|
||||
{!cid[props.requestId] ? (
|
||||
<>
|
||||
{!uploadError && (
|
||||
<button
|
||||
@@ -258,7 +283,9 @@ function OneRequestHistory(props: { requestId: string }): ReactElement {
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={() => copy(`${EXPLORER_API}/ipfs/${cid}`)}
|
||||
onClick={() =>
|
||||
copy(`${EXPLORER_API}/ipfs/${cid[props.requestId]}`)
|
||||
}
|
||||
className="m-0 w-24 bg-slate-600 text-slate-200 hover:bg-slate-500 hover:text-slate-100 font-bold"
|
||||
>
|
||||
Copy
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
MouseEventHandler,
|
||||
ReactElement,
|
||||
ReactNode,
|
||||
@@ -8,38 +9,46 @@ import React, {
|
||||
import Icon from '../../components/Icon';
|
||||
import classNames from 'classnames';
|
||||
import { useNavigate } from 'react-router';
|
||||
import {
|
||||
notarizeRequest,
|
||||
useActiveTabUrl,
|
||||
useRequests,
|
||||
} from '../../reducers/requests';
|
||||
import { Link } from 'react-router-dom';
|
||||
import bookmarks from '../../../utils/bookmark/bookmarks.json';
|
||||
import { replayRequest, urlify } from '../../utils/misc';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { get, NOTARY_API_LS_KEY, PROXY_API_LS_KEY } from '../../utils/storage';
|
||||
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';
|
||||
|
||||
export default function Home(): ReactElement {
|
||||
const requests = useRequests();
|
||||
const url = useActiveTabUrl();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const [error, showError] = useState('');
|
||||
|
||||
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');
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<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-magnifying-glass"
|
||||
onClick={() => navigate('/custom')}
|
||||
>
|
||||
<NavButton fa="fa-solid fa-hammer" onClick={() => navigate('/custom')}>
|
||||
Custom
|
||||
</NavButton>
|
||||
<NavButton
|
||||
fa="fa-solid fa-magnifying-glass"
|
||||
fa="fa-solid fa-certificate"
|
||||
onClick={() => navigate('/verify')}
|
||||
>
|
||||
Verify
|
||||
@@ -47,131 +56,19 @@ export default function Home(): ReactElement {
|
||||
<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>
|
||||
{!bookmarks.length && (
|
||||
<div className="flex flex-col flex-nowrap">
|
||||
<div className="flex flex-col items-center justify-center text-slate-300 cursor-default select-none">
|
||||
<div>No available notarization for {url?.hostname}</div>
|
||||
<div>
|
||||
Browse <Link to="/requests">Requests</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col px-4 gap-4">
|
||||
{bookmarks.map((bm, i) => {
|
||||
try {
|
||||
const reqs = requests.filter((req) => {
|
||||
return req?.url?.includes(bm.url);
|
||||
});
|
||||
|
||||
const bmHost = urlify(bm.targetUrl)?.host;
|
||||
const isReady = !!reqs.length;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="flex flex-col flex-nowrap border rounded-md p-2 gap-1 hover:bg-slate-50 cursor-pointer"
|
||||
>
|
||||
<div className="flex flex-row items-center text-xs">
|
||||
<div className="bg-slate-200 text-slate-400 px-1 py-0.5 rounded-sm">
|
||||
{bm.method}
|
||||
</div>
|
||||
<div className="text-slate-400 px-2 py-1 rounded-md">
|
||||
{bm.type}
|
||||
</div>
|
||||
</div>
|
||||
<div className="font-bold">{bm.title}</div>
|
||||
<div className="italic">{bm.description}</div>
|
||||
{isReady && (
|
||||
<button
|
||||
className="button button--primary w-fit self-end mt-2"
|
||||
onClick={async () => {
|
||||
if (!isReady) return;
|
||||
|
||||
const req = reqs[0];
|
||||
const res = await replayRequest(req);
|
||||
const secretHeaders = req.requestHeaders
|
||||
.map((h) => {
|
||||
return (
|
||||
`${h.name.toLowerCase()}: ${h.value || ''}` || ''
|
||||
);
|
||||
})
|
||||
.filter((d) => !!d);
|
||||
const selectedValue = res.match(
|
||||
new RegExp(bm.responseSelector, 'g'),
|
||||
);
|
||||
|
||||
if (selectedValue) {
|
||||
const revealed = bm.valueTransform.replace(
|
||||
'%s',
|
||||
selectedValue[0],
|
||||
);
|
||||
const selectionStart = res.indexOf(revealed);
|
||||
const selectionEnd =
|
||||
selectionStart + revealed.length - 1;
|
||||
const secretResps = [
|
||||
res.substring(0, selectionStart),
|
||||
res.substring(selectionEnd, res.length),
|
||||
].filter((d) => !!d);
|
||||
|
||||
const hostname = urlify(req.url)?.hostname;
|
||||
const notaryUrl = await get(NOTARY_API_LS_KEY);
|
||||
const websocketProxyUrl = await get(PROXY_API_LS_KEY);
|
||||
|
||||
const headers: { [k: string]: string } =
|
||||
req.requestHeaders.reduce(
|
||||
(acc: any, h) => {
|
||||
acc[h.name] = h.value;
|
||||
return acc;
|
||||
},
|
||||
{ Host: hostname },
|
||||
);
|
||||
|
||||
//TODO: for some reason, these needs to be override to work
|
||||
headers['Accept-Encoding'] = 'identity';
|
||||
headers['Connection'] = 'close';
|
||||
|
||||
dispatch(
|
||||
// @ts-ignore
|
||||
notarizeRequest({
|
||||
url: req.url,
|
||||
method: req.method,
|
||||
headers: headers,
|
||||
body: req.requestBody,
|
||||
maxTranscriptSize: 16384,
|
||||
notaryUrl,
|
||||
websocketProxyUrl,
|
||||
secretHeaders,
|
||||
secretResps,
|
||||
}),
|
||||
);
|
||||
|
||||
navigate(`/history`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Notarize
|
||||
</button>
|
||||
)}
|
||||
{!isReady && (
|
||||
<button
|
||||
className="button w-fit self-end mt-2"
|
||||
onClick={() => chrome.tabs.update({ url: bm.targetUrl })}
|
||||
>
|
||||
{`Go to ${bmHost}`}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
<PluginList className="mx-4" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,15 @@ 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 } from '../../utils/storage';
|
||||
import {
|
||||
get,
|
||||
NOTARY_API_LS_KEY,
|
||||
PROXY_API_LS_KEY,
|
||||
getNotaryApi,
|
||||
getProxyApi,
|
||||
getMaxSent,
|
||||
getMaxRecv,
|
||||
} from '../../utils/storage';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
const maxTranscriptSize = 16384;
|
||||
@@ -29,9 +37,10 @@ export default function Notarize(): ReactElement {
|
||||
const notarize = useCallback(async () => {
|
||||
if (!req) return;
|
||||
const hostname = urlify(req.url)?.hostname;
|
||||
const notaryUrl = await get(NOTARY_API_LS_KEY);
|
||||
const websocketProxyUrl = await get(PROXY_API_LS_KEY);
|
||||
|
||||
const notaryUrl = await getNotaryApi();
|
||||
const websocketProxyUrl = await getProxyApi();
|
||||
const maxSentData = await getMaxSent();
|
||||
const maxRecvData = await getMaxRecv();
|
||||
const headers: { [k: string]: string } = req.requestHeaders.reduce(
|
||||
(acc: any, h) => {
|
||||
acc[h.name] = h.value;
|
||||
@@ -43,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({
|
||||
@@ -51,6 +62,8 @@ export default function Notarize(): ReactElement {
|
||||
method: req.method,
|
||||
headers,
|
||||
body: req.requestBody,
|
||||
maxSentData,
|
||||
maxRecvData,
|
||||
maxTranscriptSize,
|
||||
notaryUrl,
|
||||
websocketProxyUrl,
|
||||
@@ -129,7 +142,6 @@ function RevealHeaderStep(props: {
|
||||
props.setSecretHeaders(
|
||||
req.requestHeaders
|
||||
.map((h) => {
|
||||
console.log(h.name, !revealed[h.name]);
|
||||
if (!revealed[h.name]) {
|
||||
return `${h.name.toLowerCase()}: ${h.value || ''}` || '';
|
||||
}
|
||||
|
||||
@@ -1,60 +1,150 @@
|
||||
import React, { ReactElement, useState, useEffect, useCallback } from 'react';
|
||||
import React, {
|
||||
ReactElement,
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
MouseEvent,
|
||||
} from 'react';
|
||||
import {
|
||||
set,
|
||||
get,
|
||||
NOTARY_API_LS_KEY,
|
||||
PROXY_API_LS_KEY,
|
||||
MAX_SENT_LS_KEY,
|
||||
MAX_RECEIVED_LS_KEY,
|
||||
getMaxSent,
|
||||
getMaxRecv,
|
||||
getNotaryApi,
|
||||
getProxyApi,
|
||||
getLoggingFilter,
|
||||
LOGGING_FILTER_KEY,
|
||||
} from '../../utils/storage';
|
||||
import {
|
||||
EXPLORER_API,
|
||||
NOTARY_API,
|
||||
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';
|
||||
|
||||
export default function Options(): ReactElement {
|
||||
const [notary, setNotary] = useState('https://notary.pse.dev/v0.1.0-alpha.5');
|
||||
const [proxy, setProxy] = useState('wss://notary.pse.dev/proxy');
|
||||
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(LOGGING_LEVEL_INFO);
|
||||
|
||||
const [dirty, setDirty] = useState(false);
|
||||
const [shouldReload, setShouldReload] = useState(false);
|
||||
const [advanced, setAdvanced] = useState(false);
|
||||
const [showReloadModal, setShowReloadModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setNotary(await get(NOTARY_API_LS_KEY));
|
||||
setProxy(await get(PROXY_API_LS_KEY));
|
||||
setNotary((await getNotaryApi()) || NOTARY_API);
|
||||
setProxy((await getProxyApi()) || NOTARY_PROXY);
|
||||
setMaxReceived((await getMaxRecv()) || MAX_RECV);
|
||||
setMaxSent((await getMaxSent()) || MAX_SENT);
|
||||
setLoggingLevel((await getLoggingFilter()) || LOGGING_LEVEL_INFO);
|
||||
})();
|
||||
}, []);
|
||||
}, [advanced]);
|
||||
|
||||
const onSave = useCallback(async () => {
|
||||
await set(NOTARY_API_LS_KEY, notary);
|
||||
await set(PROXY_API_LS_KEY, proxy);
|
||||
setDirty(false);
|
||||
}, [notary, proxy]);
|
||||
const onSave = useCallback(
|
||||
async (e: MouseEvent<HTMLButtonElement>, skipCheck = false) => {
|
||||
if (!skipCheck && shouldReload) {
|
||||
setShowReloadModal(true);
|
||||
return;
|
||||
}
|
||||
await set(NOTARY_API_LS_KEY, notary);
|
||||
await set(PROXY_API_LS_KEY, proxy);
|
||||
await set(MAX_SENT_LS_KEY, maxSent.toString());
|
||||
await set(MAX_RECEIVED_LS_KEY, maxReceived.toString());
|
||||
await set(LOGGING_FILTER_KEY, loggingLevel);
|
||||
setDirty(false);
|
||||
},
|
||||
[notary, proxy, maxSent, maxReceived, loggingLevel, shouldReload],
|
||||
);
|
||||
|
||||
const onSaveAndReload = useCallback(
|
||||
async (e: MouseEvent<HTMLButtonElement>) => {
|
||||
await onSave(e, true);
|
||||
browser.runtime.reload();
|
||||
},
|
||||
[onSave],
|
||||
);
|
||||
|
||||
const onAdvanced = useCallback(() => {
|
||||
setAdvanced(!advanced);
|
||||
}, [advanced]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-nowrap flex-grow">
|
||||
<div className="flex flex-row flex-nowrap py-1 px-2 gap-2 font-bold text-base">
|
||||
Settings
|
||||
{showReloadModal && (
|
||||
<Modal
|
||||
className="flex flex-col items-center text-base cursor-default justify-center !w-auto mx-4 my-[50%] p-4 gap-4"
|
||||
onClose={() => setShowReloadModal(false)}
|
||||
>
|
||||
<ModalContent className="flex flex-col w-full gap-4 items-center text-base justify-center">
|
||||
Modifying your logging your will require your extension to reload.
|
||||
Do you want to proceed?
|
||||
</ModalContent>
|
||||
<div className="flex flex-row justify-end items-center gap-2 w-full">
|
||||
<button
|
||||
className="button"
|
||||
onClick={() => setShowReloadModal(false)}
|
||||
>
|
||||
No
|
||||
</button>
|
||||
<button
|
||||
className="button button--primary"
|
||||
onClick={onSaveAndReload}
|
||||
>
|
||||
Yes
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
<div className="flex flex-row flex-nowrap justify-between items-between py-1 px-2 gap-2">
|
||||
<p className="font-bold text-base">Settings</p>
|
||||
</div>
|
||||
<div className="flex flex-col flex-nowrap py-1 px-2 gap-2">
|
||||
<div className="font-semibold">Notary API</div>
|
||||
<input
|
||||
type="text"
|
||||
className="input border"
|
||||
placeholder="http://localhost:7047"
|
||||
onChange={(e) => {
|
||||
setNotary(e.target.value);
|
||||
setDirty(true);
|
||||
}}
|
||||
value={notary}
|
||||
<NormalOptions
|
||||
notary={notary}
|
||||
setNotary={setNotary}
|
||||
proxy={proxy}
|
||||
setProxy={setProxy}
|
||||
setDirty={setDirty}
|
||||
/>
|
||||
<div className="justify-left px-2 pt-3 gap-2">
|
||||
<button className="font-bold" onClick={onAdvanced}>
|
||||
<i
|
||||
className={
|
||||
advanced
|
||||
? 'fa-solid fa-caret-down pr-1'
|
||||
: 'fa-solid fa-caret-right pr-1'
|
||||
}
|
||||
></i>
|
||||
Advanced
|
||||
</button>
|
||||
</div>
|
||||
{!advanced ? (
|
||||
<></>
|
||||
) : (
|
||||
<AdvancedOptions
|
||||
maxSent={maxSent}
|
||||
setMaxSent={setMaxSent}
|
||||
maxReceived={maxReceived}
|
||||
setMaxReceived={setMaxReceived}
|
||||
setDirty={setDirty}
|
||||
loggingLevel={loggingLevel}
|
||||
setLoggingLevel={setLoggingLevel}
|
||||
setShouldReload={setShouldReload}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col flex-nowrap py-1 px-2 gap-2">
|
||||
<div className="font-semibold">Proxy API</div>
|
||||
<input
|
||||
type="text"
|
||||
className="input border"
|
||||
placeholder="ws://127.0.0.1:55688"
|
||||
onChange={(e) => {
|
||||
setProxy(e.target.value);
|
||||
setDirty(true);
|
||||
}}
|
||||
value={proxy}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-row flex-nowrap justify-end gap-2 p-2">
|
||||
<button
|
||||
className="button !bg-primary/[0.9] hover:bg-primary/[0.8] active:bg-primary !text-white"
|
||||
@@ -67,3 +157,132 @@ export default function Options(): ReactElement {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InputField(props: {
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
type?: string;
|
||||
min?: number;
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
}) {
|
||||
const { label, placeholder, value, type, min, onChange } = props;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-nowrap py-1 px-2 gap-2">
|
||||
<div className="font-semibold">{label}</div>
|
||||
<input
|
||||
type={type}
|
||||
className="input border"
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
min={min}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NormalOptions(props: {
|
||||
notary: string;
|
||||
setNotary: (value: string) => void;
|
||||
proxy: string;
|
||||
setProxy: (value: string) => void;
|
||||
setDirty: (value: boolean) => void;
|
||||
}) {
|
||||
const { notary, setNotary, proxy, setProxy, setDirty } = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<InputField
|
||||
label="Notary API"
|
||||
placeholder="https://api.tlsnotary.org"
|
||||
value={notary}
|
||||
type="text"
|
||||
onChange={(e) => {
|
||||
setNotary(e.target.value);
|
||||
setDirty(true);
|
||||
}}
|
||||
/>
|
||||
<InputField
|
||||
label="Proxy API"
|
||||
placeholder="https://proxy.tlsnotary.org"
|
||||
value={proxy}
|
||||
type="text"
|
||||
onChange={(e) => {
|
||||
setProxy(e.target.value);
|
||||
setDirty(true);
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col flex-nowrap py-1 px-2 gap-2">
|
||||
<div className="font-semibold">Explorer URL</div>
|
||||
<div className="input border">{EXPLORER_API}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AdvancedOptions(props: {
|
||||
maxSent: number;
|
||||
maxReceived: number;
|
||||
loggingLevel: string;
|
||||
setShouldReload: (reload: boolean) => void;
|
||||
setMaxSent: (value: number) => void;
|
||||
setMaxReceived: (value: number) => void;
|
||||
setDirty: (value: boolean) => void;
|
||||
setLoggingLevel: (level: string) => void;
|
||||
}) {
|
||||
const {
|
||||
maxSent,
|
||||
setMaxSent,
|
||||
maxReceived,
|
||||
setMaxReceived,
|
||||
setDirty,
|
||||
setLoggingLevel,
|
||||
loggingLevel,
|
||||
setShouldReload,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<InputField
|
||||
label="Set Max Received Data"
|
||||
value={maxReceived.toString()}
|
||||
type="number"
|
||||
min={0}
|
||||
onChange={(e) => {
|
||||
setMaxReceived(parseInt(e.target.value));
|
||||
setDirty(true);
|
||||
}}
|
||||
/>
|
||||
<InputField
|
||||
label="Set Max Sent Data"
|
||||
value={maxSent.toString()}
|
||||
type="number"
|
||||
min={0}
|
||||
onChange={(e) => {
|
||||
setMaxSent(parseInt(e.target.value));
|
||||
setDirty(true);
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-col flex-nowrap py-1 px-2 gap-2">
|
||||
<div className="font-semibold">Logging Level</div>
|
||||
<select
|
||||
className="select !bg-white border !px-2 !py-1"
|
||||
onChange={(e) => {
|
||||
setLoggingLevel(e.target.value);
|
||||
setDirty(true);
|
||||
setShouldReload(true);
|
||||
}}
|
||||
value={loggingLevel}
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { combineReducers } from 'redux';
|
||||
import requests from './requests';
|
||||
import history from './history';
|
||||
import plugins from './plugins';
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
requests,
|
||||
history,
|
||||
plugins,
|
||||
});
|
||||
|
||||
export type AppRootState = ReturnType<typeof rootReducer>;
|
||||
|
||||
54
src/reducers/plugins.tsx
Normal file
54
src/reducers/plugins.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppRootState } from './index';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
enum ActionType {
|
||||
'/plugin/addPlugin' = '/plugin/addPlugin',
|
||||
'/plugin/removePlugin' = '/plugin/removePlugin',
|
||||
}
|
||||
|
||||
type Action<payload> = {
|
||||
type: ActionType;
|
||||
payload?: payload;
|
||||
error?: boolean;
|
||||
meta?: any;
|
||||
};
|
||||
|
||||
type State = {
|
||||
order: string[];
|
||||
};
|
||||
|
||||
const initState: State = {
|
||||
order: [],
|
||||
};
|
||||
|
||||
export const addOnePlugin = (hash: string): Action<string> => ({
|
||||
type: ActionType['/plugin/addPlugin'],
|
||||
payload: hash,
|
||||
});
|
||||
|
||||
export const removeOnePlugin = (hash: string): Action<string> => ({
|
||||
type: ActionType['/plugin/removePlugin'],
|
||||
payload: hash,
|
||||
});
|
||||
|
||||
export default function plugins(state = initState, action: Action<any>): State {
|
||||
switch (action.type) {
|
||||
case ActionType['/plugin/addPlugin']:
|
||||
return {
|
||||
order: [...new Set(state.order.concat(action.payload))],
|
||||
};
|
||||
case ActionType['/plugin/removePlugin']:
|
||||
return {
|
||||
order: state.order.filter((h) => h !== action.payload),
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export const usePluginHashes = (): string[] => {
|
||||
return useSelector((state: AppRootState) => {
|
||||
return state.plugins.order;
|
||||
}, deepEqual);
|
||||
};
|
||||
@@ -5,7 +5,12 @@ import {
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppRootState } from './index';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { get, NOTARY_API_LS_KEY, PROXY_API_LS_KEY } from '../utils/storage';
|
||||
import {
|
||||
getNotaryApi,
|
||||
getProxyApi,
|
||||
getMaxSent,
|
||||
getMaxRecv,
|
||||
} from '../utils/storage';
|
||||
import { BackgroundActiontype } from '../entries/Background/rpc';
|
||||
import browser from 'webextension-polyfill';
|
||||
|
||||
@@ -40,14 +45,10 @@ export const setRequests = (requests: RequestLog[]): Action<RequestLog[]> => ({
|
||||
});
|
||||
|
||||
export const notarizeRequest = (options: RequestHistory) => async () => {
|
||||
const notaryUrl = await get(
|
||||
NOTARY_API_LS_KEY,
|
||||
'https://notary.pse.dev/v0.1.0-alpha.5',
|
||||
);
|
||||
const websocketProxyUrl = await get(
|
||||
PROXY_API_LS_KEY,
|
||||
'wss://notary.pse.dev/proxy',
|
||||
);
|
||||
const notaryUrl = await getNotaryApi();
|
||||
const websocketProxyUrl = await getProxyApi();
|
||||
const maxSentData = await getMaxSent();
|
||||
const maxRecvData = await getMaxRecv();
|
||||
|
||||
chrome.runtime.sendMessage<any, string>({
|
||||
type: BackgroundActiontype.prove_request_start,
|
||||
@@ -57,6 +58,8 @@ export const notarizeRequest = (options: RequestHistory) => async () => {
|
||||
headers: options.headers,
|
||||
body: options.body,
|
||||
maxTranscriptSize: options.maxTranscriptSize,
|
||||
maxSentData,
|
||||
maxRecvData,
|
||||
secretHeaders: options.secretHeaders,
|
||||
secretResps: options.secretResps,
|
||||
notaryUrl,
|
||||
|
||||
@@ -1 +1,9 @@
|
||||
export const EXPLORER_API = 'http://localhost:3000';
|
||||
export const EXPLORER_API = 'https://explorer.tlsnotary.org';
|
||||
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';
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
import { RequestLog } from '../entries/Background/rpc';
|
||||
import {
|
||||
BackgroundActiontype,
|
||||
handleExecPluginProver,
|
||||
RequestLog,
|
||||
} from '../entries/Background/rpc';
|
||||
import { EXPLORER_API } from './constants';
|
||||
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';
|
||||
|
||||
const charwise = require('charwise');
|
||||
|
||||
export function urlify(
|
||||
text: string,
|
||||
@@ -44,7 +58,6 @@ export function download(filename: string, content: string) {
|
||||
|
||||
export async function upload(filename: string, content: string) {
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append(
|
||||
'file',
|
||||
new Blob([content], { type: 'application/json' }),
|
||||
@@ -95,8 +108,14 @@ export async function replayRequest(req: RequestLog): Promise<string> {
|
||||
|
||||
// @ts-ignore
|
||||
const resp = await fetch(req.url, options);
|
||||
return extractBodyFromResponse(resp);
|
||||
}
|
||||
|
||||
export const extractBodyFromResponse = async (
|
||||
resp: Response,
|
||||
): Promise<string> => {
|
||||
const contentType =
|
||||
resp?.headers.get('content-type') || resp?.headers.get('Content-Type');
|
||||
resp.headers.get('content-type') || resp.headers.get('Content-Type');
|
||||
|
||||
if (contentType?.includes('application/json')) {
|
||||
return resp.text();
|
||||
@@ -107,4 +126,225 @@ export async function replayRequest(req: RequestLog): Promise<string> {
|
||||
} else {
|
||||
return resp.blob().then((blob) => blob.text());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const sha256 = async (data: string) => {
|
||||
const encoder = new TextEncoder().encode(data);
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', encoder);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
const hashHex = hashArray
|
||||
.map((bytes) => bytes.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
return hashHex;
|
||||
};
|
||||
|
||||
const VALID_HOST_FUNCS: { [name: string]: string } = {
|
||||
redirect: 'redirect',
|
||||
notarize: 'notarize',
|
||||
};
|
||||
|
||||
export const makePlugin = async (
|
||||
arrayBuffer: ArrayBuffer,
|
||||
config?: PluginConfig,
|
||||
) => {
|
||||
const module = await WebAssembly.compile(arrayBuffer);
|
||||
const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
|
||||
|
||||
const injectedConfig = {
|
||||
tabUrl: tab?.url,
|
||||
tabId: tab?.id,
|
||||
};
|
||||
|
||||
const approvedRequests = config?.requests || [];
|
||||
const approvedNotary = [await getNotaryApi()].concat(config?.notaryUrls);
|
||||
const approvedProxy = [await getProxyApi()].concat(config?.proxyUrls);
|
||||
|
||||
const HostFunctions: {
|
||||
[key: string]: (callContext: CallContext, ...args: any[]) => any;
|
||||
} = {
|
||||
redirect: function (context: CallContext, off: bigint) {
|
||||
const r = context.read(off);
|
||||
const url = r.text();
|
||||
browser.tabs.update(tab.id, { url });
|
||||
},
|
||||
notarize: function (context: CallContext, off: bigint) {
|
||||
const r = context.read(off);
|
||||
const params = JSON.parse(r.text());
|
||||
const now = Date.now();
|
||||
const id = charwise.encode(now).toString('hex');
|
||||
|
||||
if (
|
||||
!approvedRequests.find(
|
||||
({ method, url }) => method === params.method && url === params.url,
|
||||
)
|
||||
) {
|
||||
throw new Error(`Unapproved request - ${params.method}: ${params.url}`);
|
||||
}
|
||||
|
||||
if (
|
||||
params.notaryUrl &&
|
||||
!approvedNotary.find((n) => n === params.notaryUrl)
|
||||
) {
|
||||
throw new Error(`Unapproved notary: ${params.notaryUrl}`);
|
||||
}
|
||||
|
||||
if (
|
||||
params.websocketProxyUrl &&
|
||||
!approvedProxy.find((w) => w === params.websocketProxyUrl)
|
||||
) {
|
||||
throw new Error(`Unapproved proxy: ${params.websocketProxyUrl}`);
|
||||
}
|
||||
|
||||
handleExecPluginProver({
|
||||
type: BackgroundActiontype.execute_plugin_prover,
|
||||
data: {
|
||||
...params,
|
||||
now,
|
||||
},
|
||||
});
|
||||
|
||||
return context.store(`${id}`);
|
||||
},
|
||||
};
|
||||
|
||||
const funcs: {
|
||||
[key: string]: (callContext: CallContext, ...args: any[]) => any;
|
||||
} = {};
|
||||
|
||||
for (const fn of Object.keys(VALID_HOST_FUNCS)) {
|
||||
funcs[fn] = function (context: CallContext) {
|
||||
throw new Error(`no permission for ${fn}`);
|
||||
};
|
||||
}
|
||||
|
||||
if (config?.hostFunctions) {
|
||||
for (const fn of config.hostFunctions) {
|
||||
funcs[fn] = HostFunctions[fn];
|
||||
}
|
||||
}
|
||||
|
||||
if (config?.cookies) {
|
||||
const cookies: { [hostname: string]: { [key: string]: string } } = {};
|
||||
for (const host of config.cookies) {
|
||||
const cache = getCookieStoreByHost(host);
|
||||
cookies[host] = cacheToMap(cache);
|
||||
}
|
||||
// @ts-ignore
|
||||
injectedConfig.cookies = JSON.stringify(cookies);
|
||||
}
|
||||
|
||||
if (config?.headers) {
|
||||
const headers: { [hostname: string]: { [key: string]: string } } = {};
|
||||
for (const host of config.headers) {
|
||||
const cache = getHeaderStoreByHost(host);
|
||||
headers[host] = cacheToMap(cache);
|
||||
}
|
||||
// @ts-ignore
|
||||
injectedConfig.headers = JSON.stringify(headers);
|
||||
}
|
||||
|
||||
const pluginConfig = {
|
||||
useWasi: true,
|
||||
config: injectedConfig,
|
||||
functions: {
|
||||
'extism:host/user': funcs,
|
||||
},
|
||||
};
|
||||
const plugin = await createPlugin(module, pluginConfig);
|
||||
return plugin;
|
||||
};
|
||||
|
||||
export type StepConfig = {
|
||||
title: string;
|
||||
description?: string;
|
||||
cta: string;
|
||||
action: string;
|
||||
prover?: boolean;
|
||||
};
|
||||
|
||||
export type PluginConfig = {
|
||||
title: string;
|
||||
description: string;
|
||||
icon?: string;
|
||||
steps?: StepConfig[];
|
||||
hostFunctions?: string[];
|
||||
cookies?: string[];
|
||||
headers?: string[];
|
||||
requests: { method: string; url: string }[];
|
||||
notaryUrls?: string[];
|
||||
proxyUrls?: string[];
|
||||
};
|
||||
|
||||
export const getPluginConfig = async (
|
||||
data: Plugin | ArrayBuffer,
|
||||
): Promise<PluginConfig> => {
|
||||
const plugin = data instanceof ArrayBuffer ? await makePlugin(data) : data;
|
||||
const out = await plugin.call('config');
|
||||
const config: PluginConfig = JSON.parse(out.string());
|
||||
|
||||
assert(typeof config.title === 'string' && config.title.length);
|
||||
assert(typeof config.description === 'string' && config.description.length);
|
||||
assert(!config.icon || typeof config.icon === 'string');
|
||||
|
||||
for (const req of config.requests) {
|
||||
assert(typeof req.method === 'string' && req.method);
|
||||
assert(typeof req.url === 'string' && req.url);
|
||||
}
|
||||
|
||||
if (config.hostFunctions) {
|
||||
for (const func of config.hostFunctions) {
|
||||
assert(typeof func === 'string' && !!VALID_HOST_FUNCS[func]);
|
||||
}
|
||||
}
|
||||
|
||||
if (config.notaryUrls) {
|
||||
for (const notaryUrl of config.notaryUrls) {
|
||||
assert(typeof notaryUrl === 'string' && notaryUrl);
|
||||
}
|
||||
}
|
||||
|
||||
if (config.proxyUrls) {
|
||||
for (const proxyUrl of config.proxyUrls) {
|
||||
assert(typeof proxyUrl === 'string' && proxyUrl);
|
||||
}
|
||||
}
|
||||
|
||||
if (config.cookies) {
|
||||
for (const name of config.cookies) {
|
||||
assert(typeof name === 'string' && name.length);
|
||||
}
|
||||
}
|
||||
|
||||
if (config.headers) {
|
||||
for (const name of config.headers) {
|
||||
assert(typeof name === 'string' && name.length);
|
||||
}
|
||||
}
|
||||
|
||||
if (config.steps) {
|
||||
for (const step of config.steps) {
|
||||
assert(typeof step.title === 'string' && step.title.length);
|
||||
assert(!step.description || typeof step.description);
|
||||
assert(typeof step.cta === 'string' && step.cta.length);
|
||||
assert(typeof step.action === 'string' && step.action.length);
|
||||
assert(!step.prover || typeof step.prover === 'boolean');
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
export const assert = (expr: any, msg = 'unknown error') => {
|
||||
if (!expr) throw new Error(msg);
|
||||
};
|
||||
|
||||
export const hexToArrayBuffer = (hex: string) =>
|
||||
new Uint8Array(Buffer.from(hex, 'hex')).buffer;
|
||||
|
||||
export const cacheToMap = (cache: NodeCache) => {
|
||||
const keys = cache.keys();
|
||||
return keys.reduce((acc: { [k: string]: string }, key) => {
|
||||
acc[key] = cache.get(key) || '';
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
64
src/utils/rpc.ts
Normal file
64
src/utils/rpc.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
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,
|
||||
data: hex,
|
||||
});
|
||||
}
|
||||
|
||||
export async function removePlugin(hash: string) {
|
||||
return browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.remove_plugin,
|
||||
data: hash,
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchPluginHashes() {
|
||||
return browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.get_plugin_hashes,
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchPluginByHash(hash: string) {
|
||||
return browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.get_plugin_by_hash,
|
||||
data: hash,
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchPluginConfigByHash(
|
||||
hash: string,
|
||||
): Promise<PluginConfig | null> {
|
||||
return browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.get_plugin_config_by_hash,
|
||||
data: hash,
|
||||
});
|
||||
}
|
||||
|
||||
export async function runPlugin(hash: string, method: string, params?: string) {
|
||||
return browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.run_plugin,
|
||||
data: {
|
||||
hash,
|
||||
method,
|
||||
params,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
import { LOGGING_LEVEL_INFO } from './constants';
|
||||
|
||||
export const NOTARY_API_LS_KEY = 'notary-api';
|
||||
export const PROXY_API_LS_KEY = 'proxy-api';
|
||||
export const HISTORY_LS_KEY = 'history';
|
||||
export const MAX_SENT_LS_KEY = 'max-sent';
|
||||
export const MAX_RECEIVED_LS_KEY = 'max-received';
|
||||
export const LOGGING_FILTER_KEY = 'logging-filter';
|
||||
|
||||
export async function set(key: string, value: string) {
|
||||
return chrome.storage.sync.set({ [key]: value });
|
||||
@@ -12,3 +16,23 @@ export async function get(key: string, defaultValue?: string) {
|
||||
.then((json: any) => json[key] || defaultValue)
|
||||
.catch(() => '');
|
||||
}
|
||||
|
||||
export async function getMaxSent() {
|
||||
return parseInt(await get(MAX_SENT_LS_KEY, '4096'));
|
||||
}
|
||||
|
||||
export async function getMaxRecv() {
|
||||
return parseInt(await get(MAX_RECEIVED_LS_KEY, '16384'));
|
||||
}
|
||||
|
||||
export async function getNotaryApi() {
|
||||
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, 'wss://notary.pse.dev/proxy');
|
||||
}
|
||||
|
||||
export async function getLoggingFilter() {
|
||||
return await get(LOGGING_FILTER_KEY, LOGGING_LEVEL_INFO);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"target": "esnext",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
[
|
||||
{
|
||||
"url": "https://api.twitter.com/1.1/account/settings.json",
|
||||
"targetUrl": "https://www.twitter.com",
|
||||
"method": "GET",
|
||||
"type": "xmlhttprequest",
|
||||
"title": "Twitter Profile",
|
||||
"description": "Notarize ownership of a twitter profile. To start, go to your own profile",
|
||||
"responseSelector": "(?<=\"screen_name\":)\"(.*?)\"",
|
||||
"valueTransform": "\"screen_name\":%s"
|
||||
},
|
||||
{
|
||||
"url": "https://gateway.reddit.com/desktopapi/v1/prefs",
|
||||
"targetUrl": "https://www.reddit.com/settings",
|
||||
"method": "GET",
|
||||
"type": "xmlhttprequest",
|
||||
"title": "Reddit Profile",
|
||||
"description": "Notarize ownership of a reddit profile. To start, go to reddit.com/settings",
|
||||
"responseSelector": "(?<=\"displayText\": )\"(.*?)\"",
|
||||
"valueTransform": "\"displayText\": %s"
|
||||
}
|
||||
]
|
||||
@@ -1,34 +0,0 @@
|
||||
import bookmarks from './bookmarks.json';
|
||||
import { RequestLog } from '../../src/entries/Background/rpc';
|
||||
|
||||
type Bookmark = {
|
||||
url: string;
|
||||
title: string;
|
||||
description: string;
|
||||
method: string;
|
||||
type: string;
|
||||
};
|
||||
|
||||
export function findBookmarksByURL(url: URL | null): Bookmark[] {
|
||||
if (!url) return [];
|
||||
|
||||
return bookmarks.filter((m) => {
|
||||
const _url = new URL(m.url);
|
||||
return url.host === _url.host;
|
||||
});
|
||||
}
|
||||
|
||||
export function filterByBookmarks(requests: RequestLog[]): Bookmark[] {
|
||||
const hosts = requests
|
||||
.map((r) => r.url && new URL(r.url).host)
|
||||
.reduce((acc: { [host: string]: string }, host) => {
|
||||
acc[host] = host;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return bookmarks.filter((bm) => {
|
||||
if (hosts[new URL(bm.url).host]) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -49,6 +49,7 @@ var options = {
|
||||
background: path.join(__dirname, "src", "entries", "Background", "index.ts"),
|
||||
contentScript: path.join(__dirname, "src", "entries", "Content", "index.ts"),
|
||||
offscreen: path.join(__dirname, "src", "entries", "Offscreen", "index.tsx"),
|
||||
sidePanel: path.join(__dirname, "src", "entries", "SidePanel", "index.tsx"),
|
||||
},
|
||||
// chromeExtensionBoilerplate: {
|
||||
// notHotReload: ["background", "contentScript", "devtools"],
|
||||
@@ -146,9 +147,9 @@ var options = {
|
||||
new webpack.ProgressPlugin(),
|
||||
// expose and write the allowed env vars on the compiled bundle
|
||||
new webpack.EnvironmentPlugin(["NODE_ENV"]),
|
||||
new ExtReloader({
|
||||
manifest: path.resolve(__dirname, "src/manifest.json")
|
||||
}),
|
||||
// new ExtReloader({
|
||||
// manifest: path.resolve(__dirname, "src/manifest.json")
|
||||
// }),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{
|
||||
@@ -242,6 +243,12 @@ var options = {
|
||||
chunks: ["offscreen"],
|
||||
cache: false,
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: path.join(__dirname, "src", "entries", "SidePanel", "index.html"),
|
||||
filename: "sidePanel.html",
|
||||
chunks: ["sidePanel"],
|
||||
cache: false,
|
||||
}),
|
||||
new webpack.ProvidePlugin({
|
||||
Buffer: ['buffer', 'Buffer'],
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user