Compare commits

...

34 Commits

Author SHA1 Message Date
tsukino
303448a3c1 chore: add gitignore 2025-04-30 15:03:01 +08:00
tsukino
46674491c6 chore: update to alpha.9 2025-04-30 15:02:35 +08:00
Hendrik Eeckhaut
0ba4a71bba Added link to chrome web store (#173) 2025-04-16 17:22:26 +02:00
Tanner
ade6d7e575 Added Errors to Notarization Progress (#147)
* feat: added Error for RequestProgress

* feat: added logic to handle errors depending on progress

* chore: linting errors

* fix: linting build error

* Avoid magic numbers

* lint

* feat: alpha.8

* fix: package.json path

* chore: update lockfiles

* fix: notary url

* feat: notarization timeouts

* fix: notarization timeouts

* feat: fixed comments + errors are properly thrown and status changes

* fix: notarization timeouts work properly now

* fix: timeout error message fix

* fix: lint

* fix: lint

* fix: errors should keep state after being thrown now

* chore: lint

* fix: fixing metadata with new progress status

---------

Co-authored-by: Hendrik Eeckhaut <hendrik@eeckhaut.org>
Co-authored-by: tsukino <0xtsukino@gmail.com>
2025-04-15 10:00:57 -07:00
Hendrik Eeckhaut
3f37c1aee8 Update extism to v1.0.3 (#166) 2025-04-10 10:02:48 +02:00
tsukino
5545d7abed refactor: remove unused versions (#167) 2025-03-31 16:57:41 +02:00
Hendrik Eeckhaut
b674f5c579 Correct package version (#165) 2025-03-28 13:52:26 +01:00
Hendrik Eeckhaut
49a94e1bbc build: Update to alpha.9 (#162) 2025-03-28 13:44:22 +01:00
Hendrik Eeckhaut
64d572d663 ci: Merge GH workflows into one ci flow + add release job 2025-03-28 11:32:24 +01:00
Hendrik Eeckhaut
2f35278235 ci: use npm to build (instead of pnpm) (#160) 2025-03-27 16:02:36 +01:00
tsukino
5af783e48c fix: notaryUrl and websoccketProxyUrl types in proof viewer (#159) 2025-03-14 04:47:56 -04:00
tsukino
1ea69d0574 feat: alpha.8 (#156)
* feat: alpha.8

* fix: package.json path

* chore: update lockfiles

* fix: notary url
2025-03-14 04:05:11 -04:00
Hendrik Eeckhaut
8bfcf8c8d5 fix: correct github workflow name (#144) 2025-03-14 03:42:10 -04:00
dylan1951
fe825d7ebb Add support for passing params to plugin (#152)
* Add support for passing params to plugin start() method.

* Remove GUI parameter inputs for now

---------

Co-authored-by: dylan <dylanbradshaw107@hotmail.com>
2025-03-14 03:41:19 -04:00
tsukino
ca4986c3bb fix: parse raw buffers from transcript (#154) 2025-03-12 10:43:16 -04:00
Tanner
da756721d4 UI changes (#142)
* feat: adding metadata to proofviewer

* feat: X button conditionally displays on how the proof viewer is opened

* feat: added confirmation modal to deleting history

* chore: clean up

* feat: added verifierKey and notaryKey to metadata in proofviewer

* feat: UI fixes and adding RemoveHistory modal to ProofViewer
2025-03-05 18:59:59 -08:00
Tanner
38852620d2 feat: sidepanel now refreshes on subsequent plugin runs if still open (#153) 2025-03-05 18:52:45 -08:00
Hendrik Eeckhaut
1c9f340add Use GitHub ci to release extension to the Chrome webstore (#133) 2025-02-07 03:30:35 -05:00
tsukino
423be796f6 chore: version 0.1.0.704 (#143) 2025-02-07 01:12:51 -05:00
dylan1951
7631baf939 fix: cookie parsing logic (#141) 2025-02-05 14:28:38 +01:00
Tanner
8733b26e12 feat: fix Install Plugin button in extension menu
#131
2025-01-30 16:04:41 +01:00
tsukino
846bc1ef29 feat: add progress for notarization request and resurface error message (#119)
#119
2025-01-28 10:29:21 +01:00
tsukino
efb4386d1b chore: update default twitter plugin and bump version (#132) 2025-01-21 20:00:11 -05:00
mac
06dc4fac83 chore: add domains to websockify_config (#121)
chore: Remove stale websockify_config

Co-authored-by: Hendrik Eeckhaut <hendrik@eeckhaut.org>
2025-01-21 11:12:27 +01:00
tsukino
3b98fc3b2f fix: allow querying with both url and hostname when caching headers and cookies (#123) 2025-01-21 05:00:19 -05:00
tsukino
e3166d62a0 refactor: offscreen and ws event handlers (#115)
* refactor: offscreen rpc code

* refactor: ws message sender
2024-12-11 04:50:56 -05:00
Tanner
ca382f3532 fix: unintended sendResponse removed (#120) 2024-12-04 22:46:25 -05:00
Tanner
b8d068b828 feat: Add plugin context to get browser storage (#101) 2024-12-03 07:08:26 -05:00
tsukino
4c908d0611 chore: version pump (#117) 2024-11-08 02:56:57 -05:00
tsukino
ed3e797bad fix: send correct message on plugin execution using content script (#116)
* fix: send correct message on plugin execution using content script

* fix: get plugins filter logic and run plugin message timing on content script
2024-11-08 02:55:31 -05:00
tsukino
398950598e feat: implement p2p prover and verifier (#114) 2024-11-07 04:12:41 -05:00
tsukino
f3b8cc1066 fix: use notarized transcript instead of pre-fetched response for redaction (#113)
* wip

* fix: use transcript for redaction

* refactor: add map secret to ranges utility

* fix: body selector

* fix
2024-11-05 07:32:02 -05:00
tsukino
bd8e58c042 feat: dispatch event when cs is loaded (#112) 2024-10-29 05:39:59 -04:00
tsukino
fb4c81b851 fix: throw error when installilng duplicate pluign (#111) 2024-10-29 05:08:55 -04:00
55 changed files with 7453 additions and 21478 deletions

View File

@@ -1,46 +0,0 @@
name: build
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 16
- uses: pnpm/action-setup@v2
name: Install pnpm
with:
version: 8
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install
- name: Build
run: npm run build

70
.github/workflows/ci.yaml vendored Normal file
View File

@@ -0,0 +1,70 @@
name: ci
on:
pull_request:
release:
types: [published]
jobs:
build-lint-test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 18
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Lint
run: npm run lint
- name: Test Webpack Build
run: npm run build:webpack
- name: Save extension zip file for releases
if: github.event_name == 'release'
uses: actions/upload-artifact@v4
with:
name: tlsn-extension-${{ github.ref_name }}.zip
path: ./zip/tlsn-extension-${{ github.ref_name }}.zip
if-no-files-found: error
release:
if: github.event_name == 'release'
runs-on: ubuntu-latest
needs: build-lint-test
steps:
- name: Download extension from build-lint-test job
uses: actions/download-artifact@v4
with:
name: tlsn-extension-${{ github.ref_name }}.zip
path: ./tlsn-extension-${{ github.ref_name }}.zip
- name: 📦 Add extension zip file to release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release upload "${{ github.event.release.tag_name }}" \
./tlsn-extension-${{ github.ref_name }}.zip \
--clobber
# Get tokens as documented on
# * https://developer.chrome.com/docs/webstore/using-api#beforeyoubegin
# * https://github.com/fregante/chrome-webstore-upload-keys?tab=readme-ov-file
- name: 💨 Publish to chrome store
uses: browser-actions/release-chrome-extension@latest # https://github.com/browser-actions/release-chrome-extension/tree/latest/
with:
extension-id: "gcfkkledipjbgdbimfpijgbkhajiaaph"
extension-path: tlsn-extension-${{ github.ref_name }}.zip
oauth-client-id: ${{ secrets.OAUTH_CLIENT_ID }}
oauth-client-secret: ${{ secrets.OAUTH_CLIENT_SECRET }}
oauth-refresh-token: ${{ secrets.OAUTH_REFRESH_TOKEN }}

View File

@@ -1,46 +0,0 @@
name: lint
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 16
- uses: pnpm/action-setup@v2
name: Install pnpm
with:
version: 8
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install
- name: Lint
run: npm run lint

1
.gitignore vendored
View File

@@ -8,3 +8,4 @@ bin/
build
tlsn/
zip
.vscode

View File

@@ -25,7 +25,11 @@ at your option.
## Installing and Running
### Procedures:
The easiest way to install the TLSN browser extension is to use the [Chrome Web Store](https://chromewebstore.google.com/detail/tlsn-extension/gcfkkledipjbgdbimfpijgbkhajiaaph).
You can also build and run it locally as explained in the following steps.
### Procedure:
1. Check if your [Node.js](https://nodejs.org/) version is >= **18**.
2. Clone this repository.

15350
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "tlsn-extension",
"version": "0.1.0.700",
"version": "0.1.0.1000",
"license": "MIT",
"repository": {
"type": "git",
@@ -16,7 +16,7 @@
"lint:fix": "eslint . --fix"
},
"dependencies": {
"@extism/extism": "^1.0.2",
"@extism/extism": "^1.0.3",
"@fortawesome/fontawesome-free": "^6.4.2",
"async-mutex": "^0.4.0",
"buffer": "^6.0.3",
@@ -24,8 +24,10 @@
"classnames": "^2.3.2",
"comlink": "^4.4.1",
"copy-to-clipboard": "^3.3.3",
"dayjs": "^1.11.13",
"fast-deep-equal": "^3.1.3",
"fuse.js": "^6.6.2",
"http-parser-js": "^0.5.9",
"level": "^8.0.0",
"minimatch": "^9.0.4",
"node-cache": "^5.1.2",
@@ -38,8 +40,7 @@
"redux-logger": "^3.0.6",
"redux-thunk": "^2.4.2",
"tailwindcss": "^3.3.3",
"tlsn-js": "0.1.0-alpha.7.1",
"tlsn-js-v5": "npm:tlsn-js@0.1.0-alpha.5.4"
"tlsn-js": "0.1.0-alpha.10.0"
},
"devDependencies": {
"@babel/core": "^7.20.12",
@@ -93,4 +94,4 @@
"webpack-ext-reloader": "^1.1.12",
"zip-webpack-plugin": "^4.0.1"
}
}
}

8645
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" ?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 32 32" enable-background="new 0 0 32 32" id="Glyph" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M16,13c-1.654,0-3,1.346-3,3s1.346,3,3,3s3-1.346,3-3S17.654,13,16,13z" id="XMLID_287_"/><path d="M6,13c-1.654,0-3,1.346-3,3s1.346,3,3,3s3-1.346,3-3S7.654,13,6,13z" id="XMLID_289_"/><path d="M26,13c-1.654,0-3,1.346-3,3s1.346,3,3,3s3-1.346,3-3S27.654,13,26,13z" id="XMLID_291_"/></svg>

After

Width:  |  Height:  |  Size: 621 B

BIN
src/assets/img/notarize.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -60,7 +60,7 @@ export default function Menu(props: {
props.setOpen(false);
}}
>
<PluginUploadInfo />
<PluginUploadInfo onPluginInstalled={() => props.setOpen(false)} />
<span>Install Plugin</span>
</MenuRow>
<MenuRow

View File

@@ -26,7 +26,11 @@ import { ErrorModal } from '../ErrorModal';
import classNames from 'classnames';
import DefaultPluginIcon from '../../assets/img/default-plugin-icon.png';
export default function PluginUploadInfo(): ReactElement {
export default function PluginUploadInfo({
onPluginInstalled,
}: {
onPluginInstalled?: () => void;
}): ReactElement {
const [error, showError] = useState('');
const [pluginBuffer, setPluginBuffer] = useState<ArrayBuffer | any>(null);
const [pluginContent, setPluginContent] = useState<PluginConfig | null>(null);
@@ -36,6 +40,7 @@ export default function PluginUploadInfo(): ReactElement {
try {
await addPlugin(Buffer.from(pluginBuffer).toString('hex'));
setPluginContent(null);
onPluginInstalled?.();
} catch (e: any) {
showError(e?.message || 'Invalid Plugin');
}
@@ -72,6 +77,9 @@ export default function PluginUploadInfo(): ReactElement {
className="opacity-0 absolute top-0 right-0 h-full w-full cursor-pointer"
type="file"
onChange={onPluginInfo}
onClick={(e) => {
e.stopPropagation();
}}
/>
{error && <ErrorModal onClose={() => showError('')} message={error} />}
{pluginContent && (
@@ -193,6 +201,22 @@ export function PluginPermissions({
</span>
</PermissionDescription>
)}
{pluginContent.localStorage && (
<PermissionDescription fa="fa-solid fa-database">
<span className="cursor-default">
<span className="mr-1">Access local storage storage from</span>
<MultipleParts parts={pluginContent.localStorage} />
</span>
</PermissionDescription>
)}
{pluginContent.sessionStorage && (
<PermissionDescription fa="fa-solid fa-database">
<span className="cursor-default">
<span className="mr-1">Access session storage from</span>
<MultipleParts parts={pluginContent.sessionStorage} />
</span>
</PermissionDescription>
)}
{pluginContent.requests && (
<PermissionDescription fa="fa-solid fa-globe">
<span className="cursor-default">

View File

@@ -6,7 +6,7 @@
padding: 0;
overflow: hidden;
transition: 200ms opacity;
transition-delay: 200ms;
}
&:hover {

View File

@@ -7,7 +7,11 @@ import React, {
} from 'react';
import { fetchPluginHashes, removePlugin, runPlugin } from '../../utils/rpc';
import { usePluginHashes } from '../../reducers/plugins';
import { PluginConfig } from '../../utils/misc';
import {
getPluginConfig,
hexToArrayBuffer,
PluginConfig,
} from '../../utils/misc';
import DefaultPluginIcon from '../../assets/img/default-plugin-icon.png';
import classNames from 'classnames';
import Icon from '../Icon';
@@ -20,8 +24,18 @@ import {
PluginInfoModalHeader,
} from '../PluginInfo';
import { getPluginConfigByHash } from '../../entries/Background/db';
import { SidePanelActionTypes } from '../../entries/SidePanel/types';
import { openSidePanel } from '../../entries/utils';
export function PluginList(props: { className?: string }): ReactElement {
export function PluginList({
className,
unremovable,
onClick,
}: {
className?: string;
unremovable?: boolean;
onClick?: (hash: string) => void;
}): ReactElement {
const hashes = usePluginHashes();
useEffect(() => {
@@ -29,65 +43,85 @@ export function PluginList(props: { className?: string }): ReactElement {
}, []);
return (
<div
className={classNames('flex flex-col flex-nowrap gap-1', props.className)}
>
<div className={classNames('flex flex-col flex-nowrap gap-1', 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} />
<Plugin
key={hash}
hash={hash}
unremovable={unremovable}
onClick={onClick}
/>
))}
</div>
);
}
export function Plugin(props: {
export function Plugin({
hash,
hex,
unremovable,
onClick,
className,
}: {
hash: string;
onClick?: () => void;
hex?: string;
className?: string;
onClick?: (hash: string) => void;
unremovable?: boolean;
}): ReactElement {
const [error, showError] = useState('');
const [config, setConfig] = useState<PluginConfig | null>(null);
const [pluginInfo, showPluginInfo] = useState(false);
const [remove, showRemove] = useState(false);
const onClick = useCallback(async () => {
const onRunPlugin = useCallback(async () => {
if (!config || remove) return;
try {
await runPlugin(props.hash, 'start');
if (onClick) {
onClick(hash);
return;
}
const [tab] = await browser.tabs.query({
active: true,
currentWindow: true,
try {
await openSidePanel();
browser.runtime.sendMessage({
type: SidePanelActionTypes.execute_plugin_request,
data: {
pluginHash: hash,
},
});
await browser.storage.local.set({ plugin_hash: props.hash });
// @ts-ignore
if (chrome.sidePanel) await chrome.sidePanel.open({ tabId: tab.id });
await runPlugin(hash, 'start');
window.close();
} catch (e: any) {
showError(e.message);
}
}, [props.hash, config, remove]);
}, [hash, config, remove, onClick]);
useEffect(() => {
(async function () {
setConfig(await getPluginConfigByHash(props.hash));
if (hex) {
setConfig(await getPluginConfig(hexToArrayBuffer(hex)));
} else {
setConfig(await getPluginConfigByHash(hash));
}
})();
}, [props.hash]);
}, [hash, hex]);
const onRemove: MouseEventHandler = useCallback(
(e) => {
e.stopPropagation();
removePlugin(props.hash);
removePlugin(hash);
showRemove(false);
},
[props.hash, remove],
[hash, remove],
);
const onConfirmRemove: MouseEventHandler = useCallback(
@@ -95,7 +129,7 @@ export function Plugin(props: {
e.stopPropagation();
showRemove(true);
},
[props.hash, remove],
[hash, remove],
);
const onPluginInfo: MouseEventHandler = useCallback(
@@ -103,7 +137,7 @@ export function Plugin(props: {
e.stopPropagation();
showPluginInfo(true);
},
[props.hash, pluginInfo],
[hash, pluginInfo],
);
if (!config) return <></>;
@@ -113,8 +147,9 @@ export function Plugin(props: {
className={classNames(
'flex flex-row justify-center 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',
className,
)}
onClick={onClick}
onClick={onRunPlugin}
>
{!!error && <ErrorModal onClose={() => showError('')} message={error} />}
{!remove ? (
@@ -129,11 +164,13 @@ export function Plugin(props: {
className="flex flex-row items-center justify-center cursor-pointer plugin-box__remove-icon"
onClick={onPluginInfo}
/>
<Icon
fa="fa-solid fa-xmark"
className="flex flex-row items-center justify-center cursor-pointer text-red-500 bg-red-200 rounded-full plugin-box__remove-icon"
onClick={onConfirmRemove}
/>
{!unremovable && (
<Icon
fa="fa-solid fa-xmark"
className="flex flex-row items-center justify-center cursor-pointer text-red-500 bg-red-200 rounded-full plugin-box__remove-icon"
onClick={onConfirmRemove}
/>
)}
</div>
</div>
<div>{config.description}</div>

View File

@@ -1,7 +1,8 @@
import { Level } from 'level';
import type { RequestHistory } from './rpc';
import { PluginConfig, PluginMetadata, sha256 } from '../../utils/misc';
import { PluginConfig, PluginMetadata, sha256, urlify } from '../../utils/misc';
import { RequestHistory, RequestProgress } from './rpc';
import mutex from './mutex';
import { minimatch } from 'minimatch';
const charwise = require('charwise');
export const db = new Level('./ext-db', {
@@ -28,6 +29,12 @@ const cookiesDb = db.sublevel<string, boolean>('cookies', {
const headersDb = db.sublevel<string, boolean>('headers', {
valueEncoding: 'json',
});
const localStorageDb = db.sublevel<string, any>('sessionStorage', {
valueEncoding: 'json',
});
const sessionStorageDb = db.sublevel<string, any>('localStorage', {
valueEncoding: 'json',
});
const appDb = db.sublevel<string, any>('app', {
valueEncoding: 'json',
});
@@ -105,9 +112,33 @@ export async function setNotaryRequestError(
return newReq;
}
export async function setNotaryRequestProgress(
id: string,
progress: RequestProgress,
errorMessage?: string,
): Promise<RequestHistory | null> {
const existing = await historyDb.get(id);
if (!existing) return null;
const newReq: RequestHistory = {
...existing,
progress,
errorMessage,
};
await historyDb.put(id, newReq);
return newReq;
}
export async function setNotaryRequestVerification(
id: string,
verification: { sent: string; recv: string },
verification: {
sent: string;
recv: string;
verifierKey: string;
notaryKey?: string;
},
): Promise<RequestHistory | null> {
const existing = await historyDb.get(id);
@@ -234,10 +265,16 @@ export async function getPlugins(): Promise<
ret.push({
...config,
hash,
metadata: metadata || {
filePath: '',
origin: '',
},
metadata: metadata
? {
...metadata,
hash,
}
: {
filePath: '',
origin: '',
hash,
},
});
}
}
@@ -313,18 +350,32 @@ export async function clearCookies(host: string) {
});
}
export async function getCookies(host: string, name: string) {
export async function getCookies(link: string, name: string) {
try {
const existing = await cookiesDb.sublevel(host).get(name);
const existing = await cookiesDb.sublevel(link).get(name);
return existing;
} catch (e) {
return null;
}
}
export async function getCookiesByHost(host: string) {
export async function getCookiesByHost(link: string) {
const ret: { [key: string]: string } = {};
for await (const [key, value] of cookiesDb.sublevel(host).iterator()) {
const links: { [k: string]: boolean } = {};
const url = urlify(link);
for await (const sublevel of cookiesDb.keys({ keyEncoding: 'utf8' })) {
const l = sublevel.split('!')[1];
links[l] = true;
}
const cookieLink = url
? Object.keys(links).filter((l) => minimatch(l, link))[0]
: Object.keys(links).filter((l) => urlify(l)?.host === link)[0];
if (!cookieLink) return ret;
for await (const [key, value] of cookiesDb.sublevel(cookieLink).iterator()) {
ret[key] = value;
}
return ret;
@@ -347,10 +398,10 @@ export async function getConnection(origin: string) {
}
}
export async function setHeaders(host: string, name: string, value?: string) {
export async function setHeaders(link: string, name: string, value?: string) {
if (!value) return null;
return mutex.runExclusive(async () => {
await headersDb.sublevel(host).put(name, value);
await headersDb.sublevel(link).put(name, value);
return true;
});
}
@@ -370,20 +421,87 @@ export async function getHeaders(host: string, name: string) {
return null;
}
}
export async function getHeadersByHost(host: string) {
export async function getHeadersByHost(link: string) {
const ret: { [key: string]: string } = {};
for await (const [key, value] of headersDb.sublevel(host).iterator()) {
const url = urlify(link);
const links: { [k: string]: boolean } = {};
for await (const sublevel of headersDb.keys({ keyEncoding: 'utf8' })) {
const l = sublevel.split('!')[1];
links[l] = true;
}
const headerLink = url
? Object.keys(links).filter((l) => minimatch(l, link))[0]
: Object.keys(links).filter((l) => urlify(l)?.host === link)[0];
if (!headerLink) return ret;
for await (const [key, value] of headersDb.sublevel(headerLink).iterator()) {
ret[key] = value;
}
return ret;
}
async function getDefaultPluginsInstalled(): Promise<boolean> {
export async function setLocalStorage(
host: string,
name: string,
value: string,
) {
return mutex.runExclusive(async () => {
await localStorageDb.sublevel(host).put(name, value);
return true;
});
}
export async function setSessionStorage(
host: string,
name: string,
value: string,
) {
return mutex.runExclusive(async () => {
await sessionStorageDb.sublevel(host).put(name, value);
return true;
});
}
export async function clearLocalStorage(host: string) {
return mutex.runExclusive(async () => {
await localStorageDb.sublevel(host).clear();
return true;
});
}
export async function clearSessionStorage(host: string) {
return mutex.runExclusive(async () => {
await sessionStorageDb.sublevel(host).clear();
return true;
});
}
export async function getLocalStorageByHost(host: string) {
const ret: { [key: string]: string } = {};
for await (const [key, value] of localStorageDb.sublevel(host).iterator()) {
ret[key] = value;
}
return ret;
}
export async function getSessionStorageByHost(host: string) {
const ret: { [key: string]: string } = {};
for await (const [key, value] of sessionStorageDb.sublevel(host).iterator()) {
ret[key] = value;
}
return ret;
}
async function getDefaultPluginsInstalled(): Promise<string | boolean> {
return appDb.get(AppDatabaseKey.DefaultPluginsInstalled).catch(() => false);
}
export async function setDefaultPluginsInstalled(installed = false) {
export async function setDefaultPluginsInstalled(
installed: string | boolean = false,
) {
return mutex.runExclusive(async () => {
await appDb.put(AppDatabaseKey.DefaultPluginsInstalled, installed);
});

View File

@@ -4,8 +4,7 @@ import mutex from './mutex';
import browser from 'webextension-polyfill';
import { addRequest } from '../../reducers/requests';
import { urlify } from '../../utils/misc';
import { setCookies, setHeaders } from './db';
import { getHeadersByHost, setCookies, setHeaders } from './db';
export const onSendHeaders = (
details: browser.WebRequest.OnSendHeadersDetailsType,
) => {
@@ -15,20 +14,24 @@ export const onSendHeaders = (
if (method !== 'OPTIONS') {
const cache = getCacheByTabId(tabId);
const existing = cache.get<RequestLog>(requestId);
const { hostname } = urlify(details.url) || {};
const { origin, pathname } = urlify(details.url) || {};
if (hostname && details.requestHeaders) {
const link = [origin, pathname].join('');
if (link && details.requestHeaders) {
details.requestHeaders.forEach((header) => {
const { name, value } = header;
if (/^cookie$/i.test(name) && value) {
value
.split(';')
.map((v) => v.split('='))
.forEach((cookie) => {
setCookies(hostname, cookie[0].trim(), cookie[1]);
});
value.split(';').forEach((cookieStr) => {
const index = cookieStr.indexOf('=');
if (index !== -1) {
const cookieName = cookieStr.slice(0, index).trim();
const cookieValue = cookieStr.slice(index + 1);
setCookies(link, cookieName, cookieValue);
}
});
} else {
setHeaders(hostname, name, value);
setHeaders(link, name, value);
}
});
}

View File

@@ -1,7 +1,7 @@
import { onBeforeRequest, onResponseStarted, onSendHeaders } from './handlers';
import { deleteCacheByTabId } from './cache';
import browser from 'webextension-polyfill';
import { getAppState, setDefaultPluginsInstalled } from './db';
import { getAppState, removePlugin, setDefaultPluginsInstalled } from './db';
import { installPlugin } from './plugins/utils';
(async () => {
@@ -35,15 +35,36 @@ import { installPlugin } from './plugins/utils';
const { defaultPluginsInstalled } = await getAppState();
if (!defaultPluginsInstalled) {
try {
const twitterProfileUrl = browser.runtime.getURL('twitter_profile.wasm');
const discordDmUrl = browser.runtime.getURL('discord_dm.wasm');
await installPlugin(twitterProfileUrl);
await installPlugin(discordDmUrl);
} finally {
await setDefaultPluginsInstalled(true);
switch (defaultPluginsInstalled) {
case false: {
try {
const twitterProfileUrl = browser.runtime.getURL(
'twitter_profile.wasm',
);
const discordDmUrl = browser.runtime.getURL('discord_dm.wasm');
await installPlugin(twitterProfileUrl);
await installPlugin(discordDmUrl);
} finally {
await setDefaultPluginsInstalled('0.1.0.703');
}
break;
}
case true: {
try {
await removePlugin(
'6931d2ad63340d3a1fb1a5c1e3f4454c5a518164d6de5ad272e744832355ee02',
);
const twitterProfileUrl = browser.runtime.getURL(
'twitter_profile.wasm',
);
await installPlugin(twitterProfileUrl);
} finally {
await setDefaultPluginsInstalled('0.1.0.703');
}
break;
}
case '0.1.0.703':
break;
}
const { initRPC } = await import('./rpc');

View File

@@ -26,4 +26,18 @@ export async function installPlugin(
filePath,
});
return hash;
}
export function mapSecretsToRange(secrets: string[], text: string) {
return secrets
.map((secret: string) => {
const index = text.indexOf(secret);
return index > -1
? {
start: index,
end: index + secret.length,
}
: null;
})
.filter((data: any) => !!data) as { start: number; end: number }[]
}

View File

@@ -26,6 +26,9 @@ import {
getHeadersByHost,
getAppState,
setDefaultPluginsInstalled,
setLocalStorage,
setSessionStorage,
setNotaryRequestProgress,
} from './db';
import { addOnePlugin, removeOnePlugin } from '../../reducers/plugins';
import {
@@ -41,11 +44,26 @@ import {
getMaxSent,
getNotaryApi,
getProxyApi,
getRendezvousApi,
} from '../../utils/storage';
import { deferredPromise } from '../../utils/promise';
import { minimatch } from 'minimatch';
import { OffscreenActionTypes } from '../Offscreen/types';
import { SidePanelActionTypes } from '../SidePanel/types';
import { pushToRedux } from '../utils';
import {
connectSession,
disconnectSession,
getP2PState,
requestProof,
endProofRequest,
onProverInstantiated,
sendMessage,
sendPairedMessage,
} from './ws';
import { parseHttpMessage } from '../../utils/parser';
import { mapStringToRange, subtractRanges } from 'tlsn-js';
import { PresentationJSON } from 'tlsn-js/build/types';
const charwise = require('charwise');
@@ -54,22 +72,27 @@ export enum BackgroundActiontype {
clear_requests = 'clear_requests',
push_action = 'push_action',
execute_plugin_prover = 'execute_plugin_prover',
execute_p2p_plugin_prover = 'execute_p2p_plugin_prover',
get_prove_requests = 'get_prove_requests',
prove_request_start = 'prove_request_start',
process_prove_request = 'process_prove_request',
finish_prove_request = 'finish_prove_request',
update_request_progress = 'update_request_progress',
verify_prove_request = 'verify_prove_request',
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',
// Plugins
add_plugin = 'add_plugin',
remove_plugin = 'remove_plugin',
get_plugin_by_hash = 'get_plugin_by_hash',
read_plugin_config = 'read_plugin_config',
get_plugin_config_by_hash = 'get_plugin_config_by_hash',
run_plugin = 'run_plugin',
get_plugin_hashes = 'get_plugin_hashes',
// Content Script
open_popup = 'open_popup',
change_route = 'change_route',
connect_request = 'connect_request',
@@ -86,9 +109,33 @@ export enum BackgroundActiontype {
get_plugins_response = 'get_plugins_response',
run_plugin_request = 'run_plugin_request',
run_plugin_response = 'run_plugin_response',
get_secrets_from_transcript = 'get_secrets_from_transcript',
// App State
get_logging_level = 'get_logging_level',
get_app_state = 'get_app_state',
set_default_plugins_installed = 'set_default_plugins_installed',
set_local_storage = 'set_local_storage',
get_local_storage = 'get_local_storage',
set_session_storage = 'set_session_storage',
get_session_storage = 'get_session_storage',
connect_rendezvous = 'connect_rendezvous',
disconnect_rendezvous = 'disconnect_rendezvous',
send_pair_request = 'send_pair_request',
cancel_pair_request = 'cancel_pair_request',
accept_pair_request = 'accept_pair_request',
reject_pair_request = 'reject_pair_request',
cancel_proof_request = 'cancel_proof_request',
accept_proof_request = 'accept_proof_request',
reject_proof_request = 'reject_proof_request',
start_proof_request = 'start_proof_request',
proof_request_end = 'proof_request_end',
verifier_started = 'verifier_started',
prover_instantiated = 'prover_instantiated',
prover_setup = 'prover_setup',
prover_started = 'prover_started',
get_p2p_state = 'get_p2p_state',
request_p2p_proof = 'request_p2p_proof',
request_p2p_proof_by_hash = 'request_p2p_proof_by_hash',
}
export type BackgroundAction = {
@@ -113,6 +160,38 @@ export type RequestLog = {
responseHeaders?: browser.WebRequest.HttpHeaders;
};
export enum RequestProgress {
CreatingProver,
GettingSession,
SettingUpProver,
SendingRequest,
ReadingTranscript,
FinalizingOutputs,
Error,
}
export function progressText(
progress: RequestProgress,
errorMessage?: string,
): string {
switch (progress) {
case RequestProgress.CreatingProver:
return 'Creating prover...';
case RequestProgress.GettingSession:
return 'Getting session url from notary...';
case RequestProgress.SettingUpProver:
return 'Setting up prover mpc backend...';
case RequestProgress.SendingRequest:
return 'Sending request...';
case RequestProgress.ReadingTranscript:
return 'Reading request transcript...';
case RequestProgress.FinalizingOutputs:
return 'Finalizing notarization outputs...';
case RequestProgress.Error:
return errorMessage ? errorMessage : 'Error: Notarization Failed';
}
}
export type RequestHistory = {
id: string;
url: string;
@@ -124,16 +203,20 @@ export type RequestHistory = {
notaryUrl: string;
websocketProxyUrl: string;
status: '' | 'pending' | 'success' | 'error';
progress?: RequestProgress;
error?: any;
proof?: { session: any; substrings: any };
proof?: { session: any; substrings: any } | PresentationJSON;
requestBody?: any;
verification?: {
sent: string;
recv: string;
verifierKey: string;
notaryKey?: string;
};
secretHeaders?: string[];
secretResps?: string[];
cid?: string;
errorMessage?: string;
metadata?: {
[k: string]: string;
};
@@ -152,6 +235,8 @@ export const initRPC = () => {
return handleGetProveRequests(request, sendResponse);
case BackgroundActiontype.finish_prove_request:
return handleFinishProveRequest(request, sendResponse);
case BackgroundActiontype.update_request_progress:
return handleUpdateRequestProgress(request, sendResponse);
case BackgroundActiontype.delete_prove_request:
return removeNotaryRequest(request.data);
case BackgroundActiontype.retry_prove_request:
@@ -170,12 +255,19 @@ export const initRPC = () => {
return handleGetPluginHashes(request, sendResponse);
case BackgroundActiontype.get_plugin_by_hash:
return handleGetPluginByHash(request, sendResponse);
case BackgroundActiontype.read_plugin_config:
getPluginConfig(request.data).then(sendResponse);
return true;
case BackgroundActiontype.get_plugin_config_by_hash:
return handleGetPluginConfigByHash(request, sendResponse);
case BackgroundActiontype.run_plugin:
return handleRunPlugin(request, sendResponse);
case BackgroundActiontype.get_secrets_from_transcript:
return handleGetSecretsFromTranscript(request, sendResponse);
case BackgroundActiontype.execute_plugin_prover:
return handleExecPluginProver(request);
case BackgroundActiontype.execute_p2p_plugin_prover:
return handleExecP2PPluginProver(request);
case BackgroundActiontype.open_popup:
return handleOpenPopup(request);
case BackgroundActiontype.connect_request:
@@ -201,6 +293,80 @@ export const initRPC = () => {
case BackgroundActiontype.set_default_plugins_installed:
setDefaultPluginsInstalled(request.data).then(sendResponse);
return true;
case BackgroundActiontype.set_local_storage:
return handleSetLocalStorage(request, sender, sendResponse);
case BackgroundActiontype.set_session_storage:
return handleSetSessionStorage(request, sender, sendResponse);
case BackgroundActiontype.connect_rendezvous:
connectSession().then(sendResponse);
return;
case BackgroundActiontype.disconnect_rendezvous:
disconnectSession().then(sendResponse);
return;
case BackgroundActiontype.send_pair_request:
sendMessage(request.data, 'pair_request').then(sendResponse);
return;
case BackgroundActiontype.cancel_pair_request:
sendMessage(request.data, 'pair_request_cancel').then(sendResponse);
return;
case BackgroundActiontype.accept_pair_request:
sendMessage(request.data, 'pair_request_accept').then(sendResponse);
return;
case BackgroundActiontype.reject_pair_request:
sendMessage(request.data, 'pair_request_reject').then(sendResponse);
return;
case BackgroundActiontype.cancel_proof_request:
sendPairedMessage('proof_request_cancel', {
pluginHash: request.data,
}).then(sendResponse);
return;
case BackgroundActiontype.accept_proof_request:
sendPairedMessage('proof_request_accept', {
plugfinHash: request.data,
}).then(sendResponse);
return;
case BackgroundActiontype.reject_proof_request:
sendPairedMessage('proof_request_reject', {
pluginHash: request.data,
}).then(sendResponse);
return;
case BackgroundActiontype.start_proof_request:
sendPairedMessage('proof_request_start', {
pluginHash: request.data.pluginHash,
}).then(sendResponse);
return;
case BackgroundActiontype.proof_request_end:
endProofRequest(request.data).then(sendResponse);
return;
case BackgroundActiontype.verifier_started:
sendPairedMessage('verifier_started', {
pluginHash: request.data.pluginHash,
}).then(sendResponse);
return;
case BackgroundActiontype.prover_started:
sendPairedMessage('prover_started', {
pluginHash: request.data.pluginHash,
}).then(sendResponse);
return;
case BackgroundActiontype.prover_instantiated:
onProverInstantiated();
return;
case BackgroundActiontype.prover_setup:
sendPairedMessage('prover_setup', {
pluginHash: request.data.pluginHash,
}).then(sendResponse);
return;
case BackgroundActiontype.request_p2p_proof:
requestProof(request.data).then(sendResponse);
return;
case BackgroundActiontype.request_p2p_proof_by_hash:
sendPairedMessage('request_proof_by_hash', {
pluginHash: request.data,
}).then(sendResponse);
return;
case BackgroundActiontype.get_p2p_state:
getP2PState();
return;
default:
break;
}
@@ -247,44 +413,39 @@ async function handleFinishProveRequest(
const newReq = await addNotaryRequestProofs(id, proof);
if (!newReq) return;
await browser.runtime.sendMessage({
type: BackgroundActiontype.push_action,
data: {
tabId: 'background',
},
action: addRequestHistory(await getNotaryRequest(id)),
});
await pushToRedux(addRequestHistory(await getNotaryRequest(id)));
}
if (error) {
const newReq = await setNotaryRequestError(id, error);
if (!newReq) return;
await browser.runtime.sendMessage({
type: BackgroundActiontype.push_action,
data: {
tabId: 'background',
},
action: addRequestHistory(await getNotaryRequest(id)),
});
await pushToRedux(addRequestHistory(await getNotaryRequest(id)));
}
if (verification) {
const newReq = await setNotaryRequestVerification(id, verification);
if (!newReq) return;
await browser.runtime.sendMessage({
type: BackgroundActiontype.push_action,
data: {
tabId: 'background',
},
action: addRequestHistory(await getNotaryRequest(id)),
});
await pushToRedux(addRequestHistory(await getNotaryRequest(id)));
}
return sendResponse();
}
async function handleUpdateRequestProgress(
request: BackgroundAction,
sendResponse: (data?: any) => void,
) {
const { id, progress, errorMessage } = request.data;
const newReq = await setNotaryRequestProgress(id, progress, errorMessage);
if (!newReq) return;
await pushToRedux(addRequestHistory(await getNotaryRequest(id)));
return sendResponse();
}
async function handleRetryProveReqest(
request: BackgroundAction,
sendResponse: (data?: any) => void,
@@ -296,13 +457,7 @@ async function handleRetryProveReqest(
const req = await getNotaryRequest(id);
await browser.runtime.sendMessage({
type: BackgroundActiontype.push_action,
data: {
tabId: 'background',
},
action: addRequestHistory(req),
});
await pushToRedux(addRequestHistory(req));
await browser.runtime.sendMessage({
type: BackgroundActiontype.process_prove_request,
@@ -348,13 +503,7 @@ async function handleProveRequestStart(
await setNotaryRequestStatus(id, 'pending');
await browser.runtime.sendMessage({
type: BackgroundActiontype.push_action,
data: {
tabId: 'background',
},
action: addRequestHistory(await getNotaryRequest(id)),
});
await pushToRedux(addRequestHistory(await getNotaryRequest(id)));
browser.runtime.sendMessage({
type: BackgroundActiontype.process_prove_request,
@@ -382,8 +531,9 @@ async function runPluginProver(request: BackgroundAction, now = Date.now()) {
method,
headers,
body,
secretHeaders,
secretResps,
secretHeaders = [],
getSecretResponse,
getSecretResponseFn,
notaryUrl: _notaryUrl,
websocketProxyUrl: _websocketProxyUrl,
maxSentData: _maxSentData,
@@ -394,6 +544,8 @@ async function runPluginProver(request: BackgroundAction, now = Date.now()) {
const maxSentData = _maxSentData || (await getMaxSent());
const maxRecvData = _maxRecvData || (await getMaxRecv());
let secretResps: string[] = [];
const { id } = await addNotaryRequest(now, {
url,
method,
@@ -408,17 +560,85 @@ async function runPluginProver(request: BackgroundAction, now = Date.now()) {
});
await setNotaryRequestStatus(id, 'pending');
await pushToRedux(addRequestHistory(await getNotaryRequest(id)));
await browser.runtime.sendMessage({
type: BackgroundActiontype.push_action,
data: {
tabId: 'background',
},
action: addRequestHistory(await getNotaryRequest(id)),
let listenerActive = true;
let responseListener: (request: any) => void;
const proverPromise = new Promise<void>((resolve, reject) => {
responseListener = async (request: any) => {
if (!listenerActive) return;
const { data, type } = request;
if (type !== OffscreenActionTypes.create_prover_response) {
return;
}
if (data.id !== id) {
return;
}
try {
if (data.error) {
throw new Error(data.error);
}
const transcript: { recv: number[]; sent: number[] } = data.transcript;
const { body: recvBody } = parseHttpMessage(
Buffer.from(transcript.recv),
'response',
);
if (getSecretResponse) {
secretResps = await getSecretResponseFn(
...recvBody.map((body) => body.toString('utf-8')),
);
}
const commit = {
sent: subtractRanges(
{ start: 0, end: transcript.sent.length },
mapStringToRange(
secretHeaders,
Buffer.from(transcript.sent).toString('utf-8'),
),
),
recv: subtractRanges(
{ start: 0, end: transcript.recv.length },
mapStringToRange(
secretResps,
Buffer.from(transcript.recv).toString('utf-8'),
),
),
};
browser.runtime.sendMessage({
type: OffscreenActionTypes.create_presentation_request,
data: {
id,
commit,
notaryUrl,
websocketProxyUrl,
},
});
resolve();
} catch (error) {
console.error('Prover response error:', error);
reject(error);
} finally {
listenerActive = false;
browser.runtime.onMessage.removeListener(responseListener);
}
};
browser.runtime.onMessage.addListener(responseListener);
});
await browser.runtime.sendMessage({
type: BackgroundActiontype.process_prove_request,
browser.runtime.sendMessage({
type: OffscreenActionTypes.create_prover_request,
data: {
id,
url,
@@ -429,12 +649,107 @@ async function runPluginProver(request: BackgroundAction, now = Date.now()) {
websocketProxyUrl,
maxRecvData,
maxSentData,
secretHeaders,
},
});
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
if (listenerActive) {
listenerActive = false;
browser.runtime.onMessage.removeListener(responseListener);
reject(new Error('Notarization Timed Out'));
}
// 3 minute timeout
}, 180000);
});
try {
await Promise.race([proverPromise, timeoutPromise]);
} catch (error: any) {
await setNotaryRequestStatus(id, 'error');
await setNotaryRequestError(id, error.message);
browser.runtime.sendMessage({
type: BackgroundActiontype.update_request_progress,
data: {
id,
progress: RequestProgress.Error,
error: error.message,
},
});
await pushToRedux(addRequestHistory(await getNotaryRequest(id)));
throw error;
}
}
async function handleGetSecretsFromTranscript(
request: BackgroundAction,
sendResponse: (data?: any) => void,
) {
const { pluginHash, pluginHex, p2p, transcript, method } = request.data;
const hex = (await getPluginByHash(pluginHash)) || pluginHex;
const arrayBuffer = hexToArrayBuffer(hex!);
const config = await getPluginConfig(arrayBuffer);
const plugin = await makePlugin(arrayBuffer, config, p2p);
const { body: recvBody } = parseHttpMessage(
Buffer.from(transcript.recv),
'response',
);
const out = await plugin.call(
method,
...recvBody.map((body) => body.toString('utf-8')),
);
const secretResps = JSON.parse(out.string());
await browser.runtime.sendMessage({
type: OffscreenActionTypes.get_secrets_from_transcript_success,
data: {
secretResps,
},
});
}
async function runP2PPluginProver(request: BackgroundAction, now = Date.now()) {
const {
pluginHash,
pluginHex,
url,
method,
headers,
body,
secretHeaders,
getSecretResponse,
websocketProxyUrl: _websocketProxyUrl,
maxSentData: _maxSentData,
maxRecvData: _maxRecvData,
clientId,
} = request.data;
const rendezvousApi = await getRendezvousApi();
const proverUrl = `${rendezvousApi}?clientId=${clientId}:proof`;
const websocketProxyUrl = _websocketProxyUrl || (await getProxyApi());
const maxSentData = _maxSentData || (await getMaxSent());
const maxRecvData = _maxRecvData || (await getMaxRecv());
await browser.runtime.sendMessage({
type: OffscreenActionTypes.start_p2p_prover,
data: {
pluginHash,
pluginHex,
url,
method,
headers,
body,
proverUrl,
websocketProxyUrl,
maxRecvData,
maxSentData,
secretHeaders,
getSecretResponse,
},
});
}
export async function handleExecPluginProver(request: BackgroundAction) {
const now = request.data.now;
const id = charwise.encode(now).toString('hex');
@@ -442,6 +757,13 @@ export async function handleExecPluginProver(request: BackgroundAction) {
return id;
}
export async function handleExecP2PPluginProver(request: BackgroundAction) {
const now = request.data.now;
const id = charwise.encode(now).toString('hex');
runP2PPluginProver(request, now);
return id;
}
function handleGetCookiesByHostname(
request: BackgroundAction,
sendResponse: (data?: any) => void,
@@ -464,6 +786,39 @@ function handleGetHeadersByHostname(
return true;
}
async function handleSetLocalStorage(
request: BackgroundAction,
sender: browser.Runtime.MessageSender,
sendResponse: (data?: any) => void,
) {
if (sender.tab?.url) {
const url = new URL(sender.tab.url);
const hostname = url.hostname;
const { data } = request;
for (const [key, value] of Object.entries(data)) {
await setLocalStorage(hostname, key, value as string);
}
}
}
async function handleSetSessionStorage(
request: BackgroundAction,
sender: browser.Runtime.MessageSender,
sendResponse: (data?: any) => void,
) {
if (
request.type === BackgroundActiontype.set_session_storage &&
sender.tab?.url
) {
const url = new URL(sender.tab.url);
const hostname = url.hostname;
const { data } = request;
for (const [key, value] of Object.entries(data)) {
await setSessionStorage(hostname, key, value as string);
}
}
}
async function handleAddPlugin(
request: BackgroundAction,
sendResponse: (data?: any) => void,
@@ -477,13 +832,7 @@ async function handleAddPlugin(
if (hash) {
await addPluginConfig(hash, config);
await browser.runtime.sendMessage({
type: BackgroundActiontype.push_action,
data: {
tabId: 'background',
},
action: addOnePlugin(hash),
});
await pushToRedux(addOnePlugin(hash));
}
}
} finally {
@@ -497,13 +846,7 @@ async function handleRemovePlugin(
) {
await removePlugin(request.data);
await removePluginConfig(request.data);
await browser.runtime.sendMessage({
type: BackgroundActiontype.push_action,
data: {
tabId: 'background',
},
action: removeOnePlugin(request.data),
});
await pushToRedux(removeOnePlugin(request.data));
return sendResponse();
}
@@ -514,13 +857,7 @@ async function handleGetPluginHashes(
) {
const hashes = await getPluginHashes();
for (const hash of hashes) {
await browser.runtime.sendMessage({
type: BackgroundActiontype.push_action,
data: {
tabId: 'background',
},
action: addOnePlugin(hash),
});
await pushToRedux(addOnePlugin(hash));
}
return sendResponse();
}
@@ -548,11 +885,11 @@ function handleRunPlugin(
sendResponse: (data?: any) => void,
) {
(async () => {
const { hash, method, params } = request.data;
const { hash, method, params, meta } = request.data;
const hex = await getPluginByHash(hash);
const arrayBuffer = hexToArrayBuffer(hex!);
const config = await getPluginConfig(arrayBuffer);
const plugin = await makePlugin(arrayBuffer, config);
const plugin = await makePlugin(arrayBuffer, config, meta?.p2p);
devlog(`plugin::${method}`, params);
const out = await plugin.call(method, params);
devlog(`plugin response: `, out.string());
@@ -921,6 +1258,11 @@ async function handleInstallPluginRequest(request: BackgroundAction) {
try {
const hex = Buffer.from(arrayBuffer).toString('hex');
const hash = await addPlugin(hex);
if (!hash) {
throw new Error('Plugin already exist.');
}
await addPluginConfig(hash!, config);
await addPluginMetadata(hash!, {
...metadata,
@@ -1029,7 +1371,7 @@ async function handleRunPluginCSRequest(request: BackgroundAction) {
});
const defer = deferredPromise();
const { origin, position, hash } = request.data;
const { origin, position, hash, params } = request.data;
const plugin = await getPluginByHash(hash);
const config = await getPluginConfigByHash(hash);
@@ -1041,13 +1383,12 @@ async function handleRunPluginCSRequest(request: BackgroundAction) {
}
const { popup, tab } = await openPopup(
`run-plugin-approval?hash=${hash}&origin=${encodeURIComponent(origin)}&favIconUrl=${encodeURIComponent(currentTab?.favIconUrl || '')}`,
`run-plugin-approval?hash=${hash}&origin=${encodeURIComponent(origin)}&favIconUrl=${encodeURIComponent(currentTab?.favIconUrl || '')}&params=${encodeURIComponent(JSON.stringify(params) || '')}`,
position.left,
position.top,
);
const onPluginRequest = async (req: any) => {
console.log(req);
if (req.type !== SidePanelActionTypes.execute_plugin_response) return;
if (req.data.hash !== hash) return;

View File

@@ -0,0 +1,463 @@
import { devlog, safeParseJSON, sha256 } from '../../utils/misc';
import {
appendIncomingPairingRequests,
appendIncomingProofRequests,
appendOutgoingPairingRequests,
appendOutgoingProofRequest,
setClientId,
setConnected,
setIncomingPairingRequest,
setIncomingProofRequest,
setIsProving,
setIsVerifying,
setOutgoingPairingRequest,
setOutgoingProofRequest,
setP2PError,
setP2PPresentation,
setPairing,
} from '../../reducers/p2p';
import { pushToRedux } from '../utils';
import { getPluginByHash } from './db';
import browser from 'webextension-polyfill';
import { OffscreenActionTypes } from '../Offscreen/types';
import { getMaxRecv, getMaxSent, getRendezvousApi } from '../../utils/storage';
import { SidePanelActionTypes } from '../SidePanel/types';
import { Transcript, VerifierOutput } from 'tlsn-js';
const state: {
clientId: string;
pairing: string;
socket: WebSocket | null;
connected: boolean;
reqId: number;
incomingPairingRequests: string[];
outgoingPairingRequests: string[];
incomingProofRequests: string[];
outgoingProofRequests: string[];
isProving: boolean;
isVerifying: boolean;
presentation: null | { sent: string; recv: string };
} = {
clientId: '',
pairing: '',
socket: null,
connected: false,
reqId: 0,
incomingPairingRequests: [],
outgoingPairingRequests: [],
incomingProofRequests: [],
outgoingProofRequests: [],
isProving: false,
isVerifying: false,
presentation: null,
};
export const getP2PState = async () => {
pushToRedux(setPairing(state.pairing));
pushToRedux(setConnected(state.connected));
pushToRedux(setClientId(state.clientId));
pushToRedux(setIncomingPairingRequest(state.incomingPairingRequests));
pushToRedux(setOutgoingPairingRequest(state.outgoingPairingRequests));
pushToRedux(setIncomingProofRequest(state.incomingProofRequests));
pushToRedux(setOutgoingProofRequest(state.outgoingProofRequests));
pushToRedux(setIsProving(state.isProving));
pushToRedux(setIsVerifying(state.isVerifying));
pushToRedux(setP2PPresentation(state.presentation));
};
export const connectSession = async () => {
if (state.socket) return;
const rendezvousAPI = await getRendezvousApi();
const socket = new WebSocket(rendezvousAPI);
socket.onopen = () => {
devlog('Connected to websocket');
state.connected = true;
state.socket = socket;
pushToRedux(setConnected(true));
const heartbeatInterval = setInterval(() => {
if (socket.readyState === 1) {
// Check if connection is open
socket.send(bufferify({ method: 'ping' }));
} else {
disconnectSession();
clearInterval(heartbeatInterval); // Stop heartbeat if connection is closed
}
}, 55000);
};
socket.onmessage = async (event) => {
const message: any = safeParseJSON(await event.data.text());
if (message.error) {
pushToRedux(setP2PError(message.error.message));
return;
}
switch (message.method) {
case 'client_connect': {
const { clientId } = message.params;
state.clientId = clientId;
pushToRedux(setClientId(clientId));
break;
}
case 'pair_request': {
const { from } = message.params;
state.incomingPairingRequests = [
...new Set(state.incomingPairingRequests.concat(from)),
];
pushToRedux(appendIncomingPairingRequests(from));
sendMessage(from, 'pair_request_sent', { pairId: state.clientId });
break;
}
case 'pair_request_sent': {
const { pairId } = message.params;
state.outgoingPairingRequests = [
...new Set(state.outgoingPairingRequests.concat(pairId)),
];
pushToRedux(appendOutgoingPairingRequests(pairId));
break;
}
case 'pair_request_cancel': {
const { from } = message.params;
state.incomingPairingRequests = state.incomingPairingRequests.filter(
(id) => id !== from,
);
pushToRedux(setIncomingPairingRequest(state.incomingPairingRequests));
sendMessage(from, 'pair_request_cancelled', { pairId: state.clientId });
break;
}
case 'pair_request_cancelled': {
const { pairId } = message.params;
state.outgoingPairingRequests = state.outgoingPairingRequests.filter(
(id) => id !== pairId,
);
pushToRedux(setOutgoingPairingRequest(state.outgoingPairingRequests));
break;
}
case 'pair_request_reject': {
const { from } = message.params;
state.outgoingPairingRequests = state.outgoingPairingRequests.filter(
(id) => id !== from,
);
pushToRedux(setOutgoingPairingRequest(state.outgoingPairingRequests));
sendMessage(from, 'pair_request_rejected', { pairId: state.clientId });
break;
}
case 'pair_request_accept': {
const { from } = message.params;
state.pairing = from;
state.outgoingPairingRequests = state.outgoingPairingRequests.filter(
(id) => id !== from,
);
pushToRedux(setOutgoingPairingRequest(state.outgoingPairingRequests));
pushToRedux(setPairing(from));
sendMessage(from, 'pair_request_success', { pairId: state.clientId });
break;
}
case 'pair_request_success': {
const { pairId } = message.params;
state.pairing = pairId;
pushToRedux(setPairing(pairId));
state.incomingPairingRequests = state.incomingPairingRequests.filter(
(id) => id !== pairId,
);
pushToRedux(setIncomingPairingRequest(state.incomingPairingRequests));
break;
}
case 'pair_request_rejected': {
const { pairId } = message.params;
state.incomingPairingRequests = state.incomingPairingRequests.filter(
(id) => id !== pairId,
);
pushToRedux(setIncomingPairingRequest(state.incomingPairingRequests));
break;
}
case 'request_proof': {
const { plugin, pluginHash, from } = message.params;
state.incomingProofRequests = [
...new Set(state.incomingProofRequests.concat(plugin)),
];
pushToRedux(appendIncomingProofRequests(plugin));
sendMessage(from, 'proof_request_received', { pluginHash });
break;
}
case 'request_proof_by_hash': {
const { pluginHash, from } = message.params;
const plugin = await getPluginByHash(pluginHash);
if (plugin) {
state.incomingProofRequests = [
...new Set(state.incomingProofRequests.concat(plugin)),
];
pushToRedux(appendIncomingProofRequests(plugin));
sendMessage(from, 'proof_request_received', { pluginHash });
} else {
sendMessage(from, 'request_proof_by_hash_failed', { pluginHash });
}
break;
}
case 'request_proof_by_hash_failed': {
const { pluginHash } = message.params;
requestProof(pluginHash);
break;
}
case 'proof_request_received': {
const { pluginHash } = message.params;
state.outgoingProofRequests = [
...new Set(state.outgoingProofRequests.concat(pluginHash)),
];
pushToRedux(appendOutgoingProofRequest(pluginHash));
break;
}
case 'proof_request_cancelled':
await handleRemoveOutgoingProofRequest(message);
break;
case 'proof_request_reject': {
const { pluginHash, from } = message.params;
await handleRemoveOutgoingProofRequest(message);
sendMessage(from, 'proof_request_rejected', { pluginHash });
break;
}
case 'proof_request_cancel': {
const { pluginHash, from } = message.params;
await handleRemoveIncomingProofRequest(message);
sendMessage(from, 'proof_request_cancelled', { pluginHash });
break;
}
case 'proof_request_rejected':
await handleRemoveIncomingProofRequest(message);
break;
case 'proof_request_accept': {
const { pluginHash, from } = message.params;
const maxSentData = await getMaxSent();
const maxRecvData = await getMaxRecv();
const rendezvousApi = await getRendezvousApi();
browser.runtime.sendMessage({
type: OffscreenActionTypes.start_p2p_verifier,
data: {
pluginHash,
maxSentData,
maxRecvData,
verifierUrl:
rendezvousApi + '?clientId=' + state.clientId + ':proof',
peerId: state.pairing,
},
});
state.isVerifying = true;
pushToRedux(setIsVerifying(true));
break;
}
case 'verifier_started': {
const { pluginHash } = message.params;
browser.runtime.sendMessage({
type: SidePanelActionTypes.start_p2p_plugin,
data: {
pluginHash: pluginHash,
},
});
break;
}
case 'prover_setup': {
const { pluginHash } = message.params;
browser.runtime.sendMessage({
type: OffscreenActionTypes.prover_setup,
data: {
pluginHash: pluginHash,
},
});
break;
}
case 'prover_started': {
const { pluginHash } = message.params;
browser.runtime.sendMessage({
type: OffscreenActionTypes.prover_started,
data: {
pluginHash: pluginHash,
},
});
break;
}
case 'proof_request_start': {
const { pluginHash, from } = message.params;
browser.runtime.sendMessage({
type: OffscreenActionTypes.start_p2p_proof_request,
data: {
pluginHash: pluginHash,
},
});
break;
}
case 'proof_request_end': {
const { pluginHash, proof } = message.params;
const transcript = new Transcript({
sent: proof.transcript.sent,
recv: proof.transcript.recv,
});
state.presentation = {
sent: transcript.sent(),
recv: transcript.recv(),
};
pushToRedux(setP2PPresentation(state.presentation));
browser.runtime.sendMessage({
type: OffscreenActionTypes.end_p2p_proof_request,
data: {
pluginHash: pluginHash,
proof: proof,
},
});
break;
}
default:
console.warn(`Unknown message type "${message.method}"`);
break;
}
};
socket.onerror = () => {
console.error('Error connecting to websocket');
pushToRedux(setConnected(false));
};
};
async function handleRemoveOutgoingProofRequest(message: {
params: { pluginHash: string };
}) {
const { pluginHash } = message.params;
state.outgoingProofRequests = state.outgoingProofRequests.filter(
(hash) => hash !== pluginHash,
);
pushToRedux(setOutgoingProofRequest(state.outgoingProofRequests));
}
async function handleRemoveIncomingProofRequest(message: {
params: { pluginHash: string };
}) {
const { pluginHash } = message.params;
const plugin = await getPluginByHash(pluginHash);
const incomingProofRequest = [];
for (const hex of state.incomingProofRequests) {
if (plugin) {
if (plugin !== hex) incomingProofRequest.push(hex);
} else {
if ((await sha256(hex)) !== pluginHash) incomingProofRequest.push(hex);
}
}
state.incomingProofRequests = incomingProofRequest;
pushToRedux(setIncomingProofRequest(state.incomingProofRequests));
}
export const disconnectSession = async () => {
if (!state.socket) return;
const socket = state.socket;
state.socket = null;
state.clientId = '';
state.pairing = '';
state.connected = false;
state.incomingPairingRequests = [];
state.outgoingPairingRequests = [];
state.incomingProofRequests = [];
state.outgoingProofRequests = [];
state.isProving = false;
state.isVerifying = false;
state.presentation = null;
pushToRedux(setPairing(''));
pushToRedux(setConnected(false));
pushToRedux(setClientId(''));
pushToRedux(setIncomingPairingRequest([]));
pushToRedux(setOutgoingPairingRequest([]));
pushToRedux(setIncomingProofRequest([]));
pushToRedux(setOutgoingProofRequest([]));
pushToRedux(setIsProving(false));
pushToRedux(setIsVerifying(false));
pushToRedux(setP2PPresentation(null));
await socket.close();
};
export async function sendMessage(
target: string,
method: string,
params?: any,
) {
const { socket, clientId } = state;
if (clientId === target) {
console.error('client cannot send message to itself.');
return;
}
if (!socket) {
console.error('socket connection not found.');
return;
}
if (!clientId) {
console.error('clientId not found.');
return;
}
socket.send(
bufferify({
method,
params: {
from: clientId,
to: target,
id: state.reqId++,
...params,
},
}),
);
}
export async function sendPairedMessage(method: string, params?: any) {
const { pairing } = state;
if (!pairing) {
console.error('not paired to a peer.');
return;
}
sendMessage(pairing, method, params);
}
export const requestProof = async (pluginHash: string) => {
const pluginHex = await getPluginByHash(pluginHash);
sendPairedMessage('request_proof', {
plugin: pluginHex,
pluginHash,
});
};
export const endProofRequest = async (data: {
pluginHash: string;
proof: VerifierOutput;
}) => {
const transcript = new Transcript({
sent: data.proof.transcript.sent,
recv: data.proof.transcript.recv,
});
state.presentation = {
sent: transcript.sent(),
recv: transcript.recv(),
};
pushToRedux(setP2PPresentation(state.presentation));
sendPairedMessage('proof_request_end', {
pluginHash: data.pluginHash,
proof: data.proof,
});
};
export const onProverInstantiated = async () => {
state.isProving = true;
pushToRedux(setIsProving(true));
};
function bufferify(data: any): Buffer {
return Buffer.from(JSON.stringify(data));
}

View File

@@ -95,9 +95,10 @@ class TLSN {
return resp;
}
async runPlugin(hash: string) {
async runPlugin(hash: string, params?: Record<string, string>) {
const resp = await client.call(ContentScriptTypes.run_plugin, {
hash,
params,
});
return resp;
@@ -116,3 +117,5 @@ const connect = async () => {
window.tlsn = {
connect,
};
window.dispatchEvent(new CustomEvent('tlsn_loaded'));

View File

@@ -1,4 +1,4 @@
import browser from 'webextension-polyfill';
import browser, { browserAction } from 'webextension-polyfill';
import { ContentScriptRequest, ContentScriptTypes, RPCServer } from './rpc';
import { BackgroundActiontype, RequestHistory } from '../Background/rpc';
import { urlify } from '../../utils/misc';
@@ -7,6 +7,24 @@ import { urlify } from '../../utils/misc';
loadScript('content.bundle.js');
const server = new RPCServer();
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === BackgroundActiontype.get_local_storage) {
chrome.runtime.sendMessage({
type: BackgroundActiontype.set_local_storage,
data: { ...localStorage },
});
}
});
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === BackgroundActiontype.get_session_storage) {
chrome.runtime.sendMessage({
type: BackgroundActiontype.set_session_storage,
data: { ...sessionStorage },
});
}
});
server.on(ContentScriptTypes.connect, async () => {
const connected = await browser.runtime.sendMessage({
type: BackgroundActiontype.connect_request,
@@ -178,8 +196,13 @@ import { urlify } from '../../utils/misc';
server.on(
ContentScriptTypes.run_plugin,
async (request: ContentScriptRequest<{ hash: string }>) => {
const { hash } = request.params || {};
async (
request: ContentScriptRequest<{
hash: string;
params?: Record<string, string>;
}>,
) => {
const { hash, params } = request.params || {};
if (!hash) throw new Error('params must include hash');
@@ -188,6 +211,7 @@ import { urlify } from '../../utils/misc';
data: {
...getPopupData(),
hash,
params,
},
});

View File

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

View File

@@ -0,0 +1,623 @@
import browser from 'webextension-polyfill';
import {
BackgroundActiontype,
progressText,
RequestProgress,
} from '../Background/rpc';
import {
mapStringToRange,
NotaryServer,
Method,
Presentation as TPresentation,
Prover as TProver,
subtractRanges,
Transcript,
Verifier as TVerifier,
} from 'tlsn-js';
import { convertNotaryWsToHttp, devlog, urlify } from '../../utils/misc';
import * as Comlink from 'comlink';
import { PresentationJSON as PresentationJSONa7 } from 'tlsn-js/build/types';
import { OffscreenActionTypes } from './types';
import { PresentationJSON } from '../../utils/types';
import { waitForEvent } from '../utils';
import {
setNotaryRequestError,
setNotaryRequestStatus,
} from '../Background/db';
const { init, Prover, Presentation, Verifier }: any = Comlink.wrap(
new Worker(new URL('./worker.ts', import.meta.url)),
);
const provers: { [id: string]: TProver } = {};
export const initThreads = async () => {
const loggingLevel = await browser.runtime.sendMessage({
type: BackgroundActiontype.get_logging_level,
hardwareConcurrency: navigator.hardwareConcurrency,
});
await init({
loggingLevel,
hardwareConcurrency: navigator.hardwareConcurrency,
});
};
export const onNotarizationRequest = async (request: any) => {
const { id } = request.data;
try {
const proof = await createProof(request.data);
browser.runtime.sendMessage({
type: BackgroundActiontype.finish_prove_request,
data: {
id,
proof,
},
});
browser.runtime.sendMessage({
type: OffscreenActionTypes.notarization_response,
data: {
id,
proof,
},
});
} catch (error: any) {
console.error(error);
browser.runtime.sendMessage({
type: BackgroundActiontype.finish_prove_request,
data: {
id,
error: error?.message || 'Unknown error',
},
});
browser.runtime.sendMessage({
type: OffscreenActionTypes.notarization_response,
data: {
id,
error: error?.message || 'Unknown error',
},
});
}
};
export const onCreateProverRequest = async (request: any) => {
const { id } = request.data;
try {
const prover = await createProver(request.data);
provers[id] = prover;
updateRequestProgress(id, RequestProgress.ReadingTranscript);
browser.runtime.sendMessage({
type: OffscreenActionTypes.create_prover_response,
data: {
id,
transcript: await prover.transcript(),
},
});
} catch (error: any) {
console.error(error);
browser.runtime.sendMessage({
type: OffscreenActionTypes.create_prover_response,
data: {
id,
error: error?.message || 'Unknown error',
},
});
}
};
export const onCreatePresentationRequest = async (request: any) => {
const { id, commit, notaryUrl, websocketProxyUrl } = request.data;
const prover = provers[id];
try {
if (!prover) throw new Error(`Cannot find prover ${id}.`);
updateRequestProgress(id, RequestProgress.FinalizingOutputs);
const notarizationOutputs = await prover.notarize(commit);
const presentation = (await new Presentation({
attestationHex: notarizationOutputs.attestation,
secretsHex: notarizationOutputs.secrets,
notaryUrl: notarizationOutputs.notaryUrl,
websocketProxyUrl: notarizationOutputs.websocketProxyUrl,
reveal: commit,
})) as TPresentation;
const json = await presentation.json();
browser.runtime.sendMessage({
type: BackgroundActiontype.finish_prove_request,
data: {
id,
proof: {
...json,
meta: {
...json.meta,
notaryUrl,
websocketProxyUrl,
},
},
},
});
delete provers[id];
} catch (error: any) {
console.error(error);
browser.runtime.sendMessage({
type: BackgroundActiontype.finish_prove_request,
data: {
id,
error: error?.message || 'Unknown error',
},
});
}
};
export const onProcessProveRequest = async (request: any) => {
const { id } = request.data;
try {
const proof = await createProof(request.data);
browser.runtime.sendMessage({
type: BackgroundActiontype.finish_prove_request,
data: {
id,
proof: proof,
},
});
} catch (error: any) {
console.error(error);
browser.runtime.sendMessage({
type: BackgroundActiontype.finish_prove_request,
data: {
id,
error: error?.message || 'Unknown error',
},
});
}
};
export const onVerifyProof = async (request: any, sendResponse: any) => {
const result = await verifyProof(request.data);
sendResponse(result);
};
export const onVerifyProofRequest = async (request: any) => {
const proof: PresentationJSON = request.data.proof;
const result: {
sent: string;
recv: string;
verifierKey?: string;
notaryKey?: string;
} = await verifyProof(proof);
chrome.runtime.sendMessage<any, string>({
type: BackgroundActiontype.finish_prove_request,
data: {
id: request.data.id,
verification: {
sent: result.sent,
recv: result.recv,
verifierKey: result.verifierKey,
notaryKey: result.notaryKey,
},
},
});
};
export const startP2PVerifier = async (request: any) => {
const { pluginHash, maxSentData, maxRecvData, verifierUrl } = request.data;
const verifier: TVerifier = await new Verifier({
id: pluginHash,
maxSentData: maxSentData,
maxRecvData: maxRecvData,
});
await verifier.connect(verifierUrl);
const proverStarted = waitForEvent(OffscreenActionTypes.prover_started);
browser.runtime.sendMessage({
type: BackgroundActiontype.verifier_started,
data: {
pluginHash,
},
});
await waitForEvent(OffscreenActionTypes.prover_setup);
verifier.verify().then((res) => {
browser.runtime.sendMessage({
type: BackgroundActiontype.proof_request_end,
data: {
pluginHash,
proof: res,
},
});
});
await proverStarted;
browser.runtime.sendMessage({
type: BackgroundActiontype.start_proof_request,
data: {
pluginHash,
},
});
};
export const startP2PProver = async (request: any) => {
const {
pluginHash,
pluginHex,
url,
method,
headers,
body,
proverUrl,
websocketProxyUrl,
maxRecvData,
maxSentData,
secretHeaders,
getSecretResponse,
} = request.data;
const hostname = urlify(url)?.hostname || '';
const prover: TProver = await new Prover({
id: pluginHash,
serverDns: hostname,
maxSentData,
maxRecvData,
});
browser.runtime.sendMessage({
type: BackgroundActiontype.prover_instantiated,
data: {
pluginHash,
},
});
const proofRequestStart = waitForEvent(
OffscreenActionTypes.start_p2p_proof_request,
);
const proverSetup = prover.setup(proverUrl);
await new Promise((r) => setTimeout(r, 5000));
browser.runtime.sendMessage({
type: BackgroundActiontype.prover_setup,
data: {
pluginHash,
},
});
await proverSetup;
browser.runtime.sendMessage({
type: BackgroundActiontype.prover_started,
data: {
pluginHash,
},
});
await proofRequestStart;
await prover.sendRequest(websocketProxyUrl + `?token=${hostname}`, {
url,
method,
headers,
body,
});
const transcript = await prover.transcript();
let secretResps: string[] = [];
if (getSecretResponse) {
browser.runtime.sendMessage({
type: BackgroundActiontype.get_secrets_from_transcript,
data: {
pluginHash,
pluginHex,
method: getSecretResponse,
transcript,
p2p: true,
},
});
const msg: any = await waitForEvent(
OffscreenActionTypes.get_secrets_from_transcript_success,
);
secretResps = msg.data.secretResps;
}
const commit = {
sent: subtractRanges(
{ start: 0, end: transcript.sent.length },
mapStringToRange(
secretHeaders,
Buffer.from(transcript.sent).toString('utf-8'),
),
),
recv: subtractRanges(
{ start: 0, end: transcript.recv.length },
mapStringToRange(
secretResps,
Buffer.from(transcript.recv).toString('utf-8'),
),
),
};
const endRequest = waitForEvent(OffscreenActionTypes.end_p2p_proof_request);
await prover.reveal(commit);
await endRequest;
};
async function createProof(options: {
url: string;
notaryUrl: string;
websocketProxyUrl: string;
method?: Method;
headers?: {
[name: string]: string;
};
body?: any;
maxSentData?: number;
maxRecvData?: number;
id: string;
secretHeaders: string[];
secretResps: string[];
}): Promise<PresentationJSONa7> {
const {
url,
method = 'GET',
headers = {},
body,
maxSentData,
maxRecvData,
notaryUrl,
websocketProxyUrl,
id,
secretHeaders = [],
secretResps = [],
} = options;
const hostname = urlify(url)?.hostname || '';
const notary = NotaryServer.from(notaryUrl);
updateRequestProgress(id, RequestProgress.CreatingProver);
const prover: TProver = await new Prover({
id,
serverDns: hostname,
maxSentData,
maxRecvData,
});
updateRequestProgress(id, RequestProgress.GettingSession);
const sessionUrl = await notary.sessionUrl(maxSentData, maxRecvData);
updateRequestProgress(id, RequestProgress.SettingUpProver);
await prover.setup(sessionUrl);
updateRequestProgress(id, RequestProgress.SendingRequest);
await prover.sendRequest(websocketProxyUrl + `?token=${hostname}`, {
url,
method,
headers,
body,
});
updateRequestProgress(id, RequestProgress.ReadingTranscript);
const transcript = await prover.transcript();
const commit = {
sent: subtractRanges(
{ start: 0, end: transcript.sent.length },
mapStringToRange(
secretHeaders,
Buffer.from(transcript.sent).toString('utf-8'),
),
),
recv: subtractRanges(
{ start: 0, end: transcript.recv.length },
mapStringToRange(
secretResps,
Buffer.from(transcript.recv).toString('utf-8'),
),
),
};
updateRequestProgress(id, RequestProgress.FinalizingOutputs);
const notarizationOutputs = await prover.notarize(commit);
const presentation = (await new Presentation({
attestationHex: notarizationOutputs.attestation,
secretsHex: notarizationOutputs.secrets,
notaryUrl: notarizationOutputs.notaryUrl,
websocketProxyUrl: notarizationOutputs.websocketProxyUrl,
reveal: commit,
})) as TPresentation;
const json = await presentation.json();
return {
...json,
meta: {
...json,
notaryUrl: notaryUrl,
websocketProxyUrl: websocketProxyUrl,
},
};
}
async function createProver(options: {
url: string;
notaryUrl: string;
websocketProxyUrl: string;
method?: Method;
headers?: {
[name: string]: string;
};
body?: any;
maxSentData?: number;
maxRecvData?: number;
id: string;
}): Promise<TProver> {
const {
url,
method = 'GET',
headers = {},
body,
maxSentData,
maxRecvData,
notaryUrl,
websocketProxyUrl,
id,
} = options;
const hostname = urlify(url)?.hostname || '';
const notary = NotaryServer.from(notaryUrl);
try {
const prover: TProver = await handleProgress(
id,
RequestProgress.CreatingProver,
() =>
new Prover({
id,
serverDns: hostname,
maxSentData,
maxRecvData,
}),
'Error creating prover',
);
const sessionUrl = await handleProgress(
id,
RequestProgress.GettingSession,
() => notary.sessionUrl(maxSentData, maxRecvData),
'Error getting session from Notary',
);
await handleProgress(
id,
RequestProgress.SettingUpProver,
() => prover.setup(sessionUrl),
'Error setting up prover',
);
await handleProgress(
id,
RequestProgress.SendingRequest,
() =>
prover.sendRequest(websocketProxyUrl + `?token=${hostname}`, {
url,
method,
headers,
body,
}),
'Error sending request',
);
return prover;
} catch (error) {
throw error;
}
}
async function verifyProof(proof: PresentationJSON): Promise<{
sent: string;
recv: string;
verifierKey?: string;
notaryKey?: string;
}> {
let result: {
sent: string;
recv: string;
verifierKey?: string;
notaryKey?: string;
};
switch (proof.version) {
case undefined:
case '0.1.0-alpha.7':
case '0.1.0-alpha.8':
case '0.1.0-alpha.9':
result = {
sent: 'version not supported',
recv: 'version not supported',
};
break;
case '0.1.0-alpha.10':
result = await verify(proof);
break;
}
return result!;
}
async function verify(proof: PresentationJSON) {
if (proof.version !== '0.1.0-alpha.10') {
throw new Error('wrong version');
}
const presentation: TPresentation = await new Presentation(proof.data);
const verifierOutput = await presentation.verify();
const transcript = new Transcript({
sent: verifierOutput.transcript.sent,
recv: verifierOutput.transcript.recv,
});
const vk = await presentation.verifyingKey();
const verifyingKey = Buffer.from(vk.data).toString('hex');
const notaryUrl = proof.meta.notaryUrl
? convertNotaryWsToHttp(proof.meta.notaryUrl)
: '';
const publicKey = await new NotaryServer(notaryUrl)
.publicKey()
.catch(() => '');
return {
sent: transcript.sent(),
recv: transcript.recv(),
verifierKey: verifyingKey,
notaryKey: publicKey,
};
}
function updateRequestProgress(
id: string,
progress: RequestProgress,
errorMessage?: string,
) {
const progressMessage =
progress === RequestProgress.Error
? `${errorMessage || 'Notarization Failed'}`
: progressText(progress);
devlog(`Request ${id}: ${progressMessage}`);
browser.runtime.sendMessage({
type: BackgroundActiontype.update_request_progress,
data: {
id,
progress,
errorMessage,
},
});
}
async function handleProgress<T>(
id: string,
progress: RequestProgress,
action: () => Promise<T>,
errorMessage: string,
): Promise<T> {
try {
updateRequestProgress(id, progress);
return await action();
} catch (error: any) {
updateRequestProgress(id, RequestProgress.Error, errorMessage);
await setNotaryRequestStatus(id, 'error');
await setNotaryRequestError(
id,
errorMessage || error.message || 'Unknown error',
);
throw error;
}
}

View File

@@ -1,4 +1,15 @@
export enum OffscreenActionTypes {
notarization_request = 'offscreen/notarization_request',
notarization_response = 'offscreen/notarization_response',
create_prover_request = 'offscreen/create_prover_request',
create_prover_response = 'offscreen/create_prover_response',
create_presentation_request = 'offscreen/create_presentation_request',
create_presentation_response = 'offscreen/create_presentation_response',
get_secrets_from_transcript_success = 'offscreen/get_secrets_from_transcript_success',
start_p2p_verifier = 'offscreen/start_p2p_verifier',
start_p2p_prover = 'offscreen/start_p2p_prover',
prover_started = 'offscreen/prover_started',
prover_setup = 'offscreen/prover_setup',
start_p2p_proof_request = 'offscreen/start_p2p_proof_request',
end_p2p_proof_request = 'offscreen/end_p2p_proof_request',
}

View File

@@ -0,0 +1,39 @@
export function subtractRanges(
ranges: { start: number; end: number },
negatives: { start: number; end: number }[],
): { start: number; end: number }[] {
const returnVal: { start: number; end: number }[] = [ranges];
negatives
.sort((a, b) => (a.start < b.start ? -1 : 1))
.forEach(({ start, end }) => {
const last = returnVal.pop()!;
if (start < last.start || end > last.end) {
console.error('invalid ranges');
return;
}
if (start === last.start && end === last.end) {
return;
}
if (start === last.start && end < last.end) {
returnVal.push({ start: end, end: last.end });
return;
}
if (start > last.start && end < last.end) {
returnVal.push({ start: last.start, end: start });
returnVal.push({ start: end, end: last.end });
return;
}
if (start > last.start && end === last.end) {
returnVal.push({ start: last.start, end: start });
return;
}
});
return returnVal;
}

View File

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

View File

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

View File

@@ -20,6 +20,7 @@ import History from '../../pages/History';
import ProofUploader from '../../pages/ProofUploader';
import browser from 'webextension-polyfill';
import store from '../../utils/store';
import { isPopupWindow } from '../../utils/misc';
import PluginUploadInfo from '../../components/PluginInfo';
import ConnectionDetailsModal from '../../components/ConnectionDetailsModal';
import { ConnectionApproval } from '../../pages/ConnectionApproval';
@@ -35,10 +36,16 @@ import { getConnection } from '../Background/db';
import { useIsConnected, setConnection } from '../../reducers/requests';
import { MenuIcon } from '../../components/Menu';
import Plugins from '../../pages/Plugins';
import { P2PHome } from '../../pages/PeerToPeer';
import { fetchP2PState } from '../../reducers/p2p';
const Popup = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const [isPopup, setIsPopup] = useState(isPopupWindow());
useEffect(() => {
fetchP2PState();
}, []);
useEffect(() => {
(async () => {
@@ -95,8 +102,12 @@ const Popup = () => {
onClick={() => navigate('/')}
/>
<div className="flex flex-row flex-grow items-center justify-end gap-4">
<AppConnectionLogo />
<MenuIcon />
{!isPopup && (
<>
<AppConnectionLogo />
<MenuIcon />
</>
)}
</div>
</div>
<Routes>
@@ -117,6 +128,7 @@ const Popup = () => {
<Route path="/notarize-approval" element={<NotarizeApproval />} />
<Route path="/get-plugins-approval" element={<GetPluginsApproval />} />
<Route path="/run-plugin-approval" element={<RunPluginApproval />} />
<Route path="/p2p" element={<P2PHome />} />
<Route
path="/install-plugin-approval"
element={<InstallPluginApproval />}

View File

@@ -4,6 +4,11 @@ import { HashRouter } from 'react-router-dom';
import Popup from './Popup';
import './index.scss';
import { Provider } from 'react-redux';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import localizedFormat from 'dayjs/plugin/localizedFormat';
dayjs.extend(relativeTime);
dayjs.extend(localizedFormat);
import store from '../../utils/store';
const container = document.getElementById('app-container');

View File

@@ -1,7 +1,6 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import './sidePanel.scss';
import browser from 'webextension-polyfill';
import { fetchPluginConfigByHash, runPlugin } from '../../utils/rpc';
import {
getPluginConfig,
hexToArrayBuffer,
@@ -9,36 +8,81 @@ 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';
import {
BackgroundActiontype,
progressText,
RequestProgress,
} from '../Background/rpc';
import { getPluginByHash, getPluginConfigByHash } from '../Background/db';
import type { Plugin } from '@extism/extism';
import { OffscreenActionTypes } from '../Offscreen/types';
import { SidePanelActionTypes } from './types';
import { fetchP2PState, useClientId } from '../../reducers/p2p';
export default function SidePanel(): ReactElement {
const [config, setConfig] = useState<PluginConfig | null>(null);
const [hash, setHash] = useState('');
const [hex, setHex] = useState('');
const [p2p, setP2P] = useState(false);
const [params, setParams] = useState<Record<string, string> | undefined>();
const [started, setStarted] = useState(false);
const clientId = useClientId();
useEffect(() => {
(async function () {
const result = await browser.storage.local.get('plugin_hash');
const { plugin_hash } = result;
const config = await getPluginConfigByHash(plugin_hash);
setHash(plugin_hash);
setConfig(config);
// await browser.storage.local.set({ plugin_hash: '' });
})();
fetchP2PState();
browser.runtime.sendMessage({
type: SidePanelActionTypes.panel_opened,
});
}, []);
useEffect(() => {
browser.runtime.onMessage.addListener(async (request) => {
const { type, data } = request;
switch (type) {
case SidePanelActionTypes.execute_plugin_request: {
setConfig(await getPluginConfigByHash(data.pluginHash));
setHash(data.pluginHash);
setParams(data.pluginParams);
setStarted(true);
break;
}
case SidePanelActionTypes.run_p2p_plugin_request: {
const { pluginHash, plugin } = data;
const config =
(await getPluginConfigByHash(pluginHash)) ||
(await getPluginConfig(hexToArrayBuffer(plugin)));
setHash(pluginHash);
setHex(plugin);
setP2P(true);
setConfig(config);
break;
}
case SidePanelActionTypes.start_p2p_plugin: {
setStarted(true);
break;
}
case SidePanelActionTypes.is_panel_open: {
return { isOpen: true };
}
case SidePanelActionTypes.reset_panel: {
setConfig(null);
setHash('');
setHex('');
setStarted(false);
break;
}
}
});
}, []);
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">
<div className="relative flex flex-nowrap flex-shrink-0 flex-row items-center 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"
@@ -47,8 +91,17 @@ export default function SidePanel(): ReactElement {
Close
</button>
</div>
{!config && <PluginList />}
{config && <PluginBody hash={hash} config={config} />}
{/*{!config && <PluginList />}*/}
{started && config && (
<PluginBody
hash={hash}
hex={hex}
config={config}
p2p={p2p}
clientId={clientId}
presetParameterValues={params}
/>
)}
</div>
);
}
@@ -56,9 +109,13 @@ export default function SidePanel(): ReactElement {
function PluginBody(props: {
config: PluginConfig;
hash: string;
hex?: string;
clientId?: string;
p2p?: boolean;
presetParameterValues?: Record<string, string>;
}): ReactElement {
const { hash } = props;
const { title, description, icon, steps } = props.config;
const { hash, hex, config, p2p, clientId, presetParameterValues } = props;
const { title, description, icon, steps } = config;
const [responses, setResponses] = useState<any[]>([]);
const [notarizationId, setNotarizationId] = useState('');
const notaryRequest = useRequestHistory(notarizationId);
@@ -109,11 +166,17 @@ function PluginBody(props: {
<div className="flex flex-col items-start gap-8 mt-8">
{steps?.map((step, i) => (
<StepContent
key={i}
hash={hash}
config={config}
hex={hex}
index={i}
setResponse={setResponse}
lastResponse={i > 0 ? responses[i - 1] : undefined}
responses={responses}
p2p={p2p}
clientId={clientId}
parameterValues={presetParameterValues}
{...step}
/>
))}
@@ -125,10 +188,15 @@ function PluginBody(props: {
function StepContent(
props: StepConfig & {
hash: string;
hex?: string;
clientId?: string;
index: number;
setResponse: (resp: any, i: number) => void;
responses: any[];
lastResponse?: any;
config: PluginConfig;
p2p?: boolean;
parameterValues?: Record<string, string>;
},
): ReactElement {
const {
@@ -141,6 +209,11 @@ function StepContent(
lastResponse,
prover,
hash,
hex: _hex,
config,
p2p = false,
clientId = '',
parameterValues,
} = props;
const [completed, setCompleted] = useState(false);
const [pending, setPending] = useState(false);
@@ -149,11 +222,10 @@ function StepContent(
const notaryRequest = useRequestHistory(notarizationId);
const getPlugin = useCallback(async () => {
const hex = await getPluginByHash(hash);
const config = await getPluginConfigByHash(hash);
const hex = (await getPluginByHash(hash)) || _hex;
const arrayBuffer = hexToArrayBuffer(hex!);
return makePlugin(arrayBuffer, config!);
}, [hash]);
return makePlugin(arrayBuffer, config, { p2p, clientId });
}, [hash, _hex, config, p2p, clientId]);
const processStep = useCallback(async () => {
const plugin = await getPlugin();
@@ -164,7 +236,13 @@ function StepContent(
setError('');
try {
const out = await plugin.call(action, JSON.stringify(lastResponse));
const out = await plugin.call(
action,
index > 0
? JSON.stringify(lastResponse)
: JSON.stringify(parameterValues),
);
console.log(out);
const val = JSON.parse(out.string());
if (val && prover) {
setNotarizationId(val);
@@ -209,13 +287,37 @@ function StepContent(
});
}, [notaryRequest, notarizationId]);
const viewP2P = useCallback(async () => {
await browser.runtime.sendMessage({
type: BackgroundActiontype.open_popup,
data: {
position: {
left: window.screen.width / 2 - 240,
top: window.screen.height / 2 - 300,
},
route: `/p2p`,
},
});
}, []);
useEffect(() => {
processStep();
}, [processStep]);
let btnContent = null;
if (completed) {
if (prover && p2p) {
btnContent = (
<button
className={classNames(
'button button--primary mt-2 w-fit flex flex-row flex-nowrap items-center gap-2',
)}
onClick={viewP2P}
>
<span className="text-sm">View in P2P</span>
</button>
);
} else if (completed) {
btnContent = (
<button
className={classNames(
@@ -240,10 +342,27 @@ function StepContent(
);
} 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>
<div className="flex flex-col gap-2">
{notaryRequest?.progress === RequestProgress.Error && (
<div className="flex flex-row items-center gap-2 text-red-600">
<Icon fa="fa-solid fa-triangle-exclamation" size={1} />
<span className="text-sm">
{notaryRequest?.errorMessage ||
progressText(notaryRequest.progress)}
</span>
</div>
)}
{notaryRequest?.progress !== RequestProgress.Error && (
<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">
{notaryRequest?.progress !== undefined
? `(${(((notaryRequest.progress + 1) / 6.06) * 100).toFixed()}%) ${progressText(notaryRequest.progress)}`
: 'Pending...'}
</span>
</button>
)}
</div>
);
} else {
btnContent = (

View File

@@ -1,4 +1,10 @@
export enum SidePanelActionTypes {
panel_opened = 'sidePanel/panel_opened',
execute_plugin_request = 'sidePanel/execute_plugin_request',
execute_plugin_response = 'sidePanel/execute_plugin_response',
run_p2p_plugin_request = 'sidePanel/run_p2p_plugin_request',
run_p2p_plugin_response = 'sidePanel/run_p2p_plugin_response',
start_p2p_plugin = 'sidePanel/start_p2p_plugin',
is_panel_open = 'sidePanel/is_panel_open',
reset_panel = 'sidePanel/reset_panel',
}

74
src/entries/utils.ts Normal file
View File

@@ -0,0 +1,74 @@
import browser from 'webextension-polyfill';
import { BackgroundActiontype } from './Background/rpc';
import { SidePanelActionTypes } from './SidePanel/types';
import { deferredPromise } from '../utils/promise';
import { devlog } from '../utils/misc';
export const pushToRedux = async (action: {
type: string;
payload?: any;
error?: boolean;
meta?: any;
}) => {
return browser.runtime.sendMessage({
type: BackgroundActiontype.push_action,
data: {
tabId: 'background',
},
action,
});
};
export const openSidePanel = async () => {
const { promise, resolve, reject } = deferredPromise();
try {
const response = await browser.runtime.sendMessage({
type: SidePanelActionTypes.is_panel_open,
});
if (response?.isOpen) {
await browser.runtime.sendMessage({
type: SidePanelActionTypes.reset_panel,
});
resolve();
return promise;
}
const [tab] = await browser.tabs.query({
active: true,
currentWindow: true,
});
const listener = async (request: any) => {
if (request.type === SidePanelActionTypes.panel_opened) {
browser.runtime.onMessage.removeListener(listener);
resolve();
}
};
browser.runtime.onMessage.addListener(listener);
// @ts-ignore
if (chrome.sidePanel) await chrome.sidePanel.open({ tabId: tab.id });
} catch (e) {
reject(e);
}
return promise;
};
export const waitForEvent = async (event: string) => {
const { promise, resolve } = deferredPromise();
const listener = async (request: any) => {
if (request.type === event) {
devlog('received event:', event);
browser.runtime.onMessage.removeListener(listener);
resolve(request);
}
};
browser.runtime.onMessage.addListener(listener);
return promise;
};

View File

@@ -58,7 +58,7 @@ export function GetPluginsApproval(): ReactElement {
});
setResult(res);
})();
}, [url, filterMetadata]);
}, [url, JSON.stringify(filterMetadata)]);
return (
<BaseApproval

View File

@@ -1,30 +1,27 @@
import React, { ReactElement, useState, useCallback, useEffect } from 'react';
import React, { ReactElement, useState, useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { useNavigate } from 'react-router';
import {
useHistoryOrder,
useRequestHistory,
deleteRequestHistory,
} from '../../reducers/history';
import { useHistoryOrder, useRequestHistory } from '../../reducers/history';
import Icon from '../../components/Icon';
import { getNotaryApi, getProxyApi } from '../../utils/storage';
import { urlify, download, upload } from '../../utils/misc';
import { BackgroundActiontype } from '../../entries/Background/rpc';
import NotarizeIcon from '../../assets/img/notarize.png';
import { urlify } from '../../utils/misc';
import {
BackgroundActiontype,
progressText,
RequestProgress,
} 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';
import dayjs from 'dayjs';
import RequestMenu from './request-menu';
const charwise = require('charwise');
export default function History(): ReactElement {
const history = useHistoryOrder();
return (
<div className="flex flex-col flex-nowrap overflow-y-auto">
<div className="flex flex-col flex-nowrap overflow-y-auto pb-36">
{history
.map((id) => {
return <OneRequestHistory key={id} requestId={id} />;
@@ -43,42 +40,11 @@ export function OneRequestHistory(props: {
const dispatch = useDispatch();
const request = useRequestHistory(props.requestId);
const [showingError, showError] = useState(false);
const [uploadError, setUploadError] = useState('');
const [showingShareConfirmation, setShowingShareConfirmation] =
useState(false);
const [cid, setCid] = useState<{ [key: string]: string }>({});
const [uploading, setUploading] = useState(false);
const [showingMenu, showMenu] = 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 getNotaryApi();
const websocketProxyUrl = await getProxyApi();
chrome.runtime.sendMessage<any, string>({
type: BackgroundActiontype.retry_prove_request,
data: {
id: props.requestId,
notaryUrl,
websocketProxyUrl,
},
});
}, [props.requestId]);
const onView = useCallback(() => {
chrome.runtime.sendMessage<any, string>({
type: BackgroundActiontype.verify_prove_request,
@@ -87,151 +53,97 @@ export function OneRequestHistory(props: {
navigate('/verify/' + request?.id);
}, [request]);
const onDelete = useCallback(async () => {
dispatch(deleteRequestHistory(props.requestId));
}, [props.requestId]);
const onShowError = useCallback(async () => {
showError(true);
}, [request?.error, showError]);
const closeAllModal = useCallback(() => {
setShowingShareConfirmation(false);
showError(false);
}, [setShowingShareConfirmation, showError]);
}, [showError]);
const handleUpload = useCallback(async () => {
setUploading(true);
try {
const data = await upload(
`${request?.id}.json`,
JSON.stringify(request?.proof),
);
setCid((prevCid) => ({ ...prevCid, [props.requestId]: data }));
await setNotaryRequestCid(props.requestId, data);
} catch (e: any) {
setUploadError(e.message);
} finally {
setUploading(false);
}
}, [props.requestId, request, cid]);
const day = dayjs(charwise.decode(props.requestId, 'hex'));
return (
<div
className={classNames(
'flex flex-row flex-nowrap border rounded-md p-2 gap-1 hover:bg-slate-50 cursor-pointer',
'flex flex-row items-center flex-nowrap border rounded-md px-2.5 py-3 gap-0.5 hover:bg-slate-50 cursor-pointer relative',
{
'!cursor-default !bg-slate-200': status === 'pending',
},
props.className,
)}
onClick={() => {
if (status === 'success') onView();
if (status === 'error') onShowError();
}}
>
<ShareConfirmationModal />
<ErrorModal />
<div className="flex flex-col flex-nowrap flex-grow flex-shrink w-0">
<div className="flex flex-row items-center text-xs">
<div className="bg-slate-200 text-slate-400 px-1 py-0.5 rounded-sm">
{request?.method}
</div>
<div className="text-black font-bold px-2 py-1 rounded-md overflow-hidden text-ellipsis">
{requestUrl?.pathname}
</div>
<div className="w-12 h-12 rounded-full flex flex-row items-center justify-center bg-slate-300">
<img
className="relative w-7 h-7 top-[-1px] opacity-60"
src={NotarizeIcon}
/>
</div>
<div className="flex flex-col flex-nowrap flex-grow flex-shrink w-0 gap-1">
<div className="flex flex-row text-black text-sm font-semibold px-2 rounded-md overflow-hidden text-ellipsis gap-1">
<span>Notarize request</span>
<span className="font-normal border-b border-dashed border-slate-400 text-slate-500">
{requestUrl?.hostname}
</span>
</div>
<div className="flex flex-row">
<div className="font-bold text-slate-400">Time:</div>
<div className="ml-2 text-slate-800">
{new Date(charwise.decode(props.requestId, 'hex')).toISOString()}
</div>
</div>
<div className="flex flex-row">
<div className="font-bold text-slate-400">Host:</div>
<div className="ml-2 text-slate-800">{requestUrl?.host}</div>
</div>
<div className="flex flex-row">
<div className="font-bold text-slate-400">Notary API:</div>
<div className="ml-2 text-slate-800">{request?.notaryUrl}</div>
</div>
<div className="flex flex-row">
<div className="font-bold text-slate-400">TLS Proxy API:</div>
<div className="ml-2 text-slate-800">
{request?.websocketProxyUrl}
</div>
<div
className={classNames('font-semibold px-2 rounded-sm w-fit', {
'text-green-600': status === 'success',
'text-red-600': status === 'error',
})}
>
{status === 'success' && 'Success'}
{status === 'error' && 'Error'}
{status === 'pending' && (
<div className="text-center flex flex-row flex-grow-0 gap-2 self-end items-center justify-center text-slate-600">
<Icon
className="animate-spin"
fa="fa-solid fa-spinner"
size={1}
/>
<span className="">
{request?.progress === RequestProgress.Error
? `${progressText(request.progress, request.errorMessage)}`
: request?.progress
? `(${(
((request.progress + 1) / 6.06) *
100
).toFixed()}%) ${progressText(request.progress)}`
: 'Pending...'}
</span>
</div>
)}
</div>
</div>
<div className="flex flex-col gap-1">
{status === 'success' && (
<>
<ActionButton
className="bg-slate-600 text-slate-200 hover:bg-slate-500 hover:text-slate-100"
onClick={onView}
fa="fa-solid fa-receipt"
ctaText="View"
hidden={hideActions.includes('view')}
/>
<ActionButton
className="bg-slate-100 text-slate-300 hover:bg-slate-200 hover:text-slate-500"
onClick={() =>
download(`${request?.id}.json`, JSON.stringify(request?.proof))
}
fa="fa-solid fa-download"
ctaText="Download"
hidden={hideActions.includes('download')}
/>
<ActionButton
className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-slate-100 text-slate-300 hover:bg-slate-200 hover:text-slate-500 hover:font-bold"
onClick={() => setShowingShareConfirmation(true)}
fa="fa-solid fa-upload"
ctaText="Share"
hidden={hideActions.includes('share')}
/>
</>
)}
{status === 'error' && !!request?.error && (
<ErrorButton hidden={hideActions.includes('error')} />
)}
{(!status || status === 'error') && (
<RetryButton hidden={hideActions.includes('retry')} />
)}
{status === 'pending' && (
<button className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-slate-100 text-slate-300 font-bold">
<Icon className="animate-spin" fa="fa-solid fa-spinner" size={1} />
<span className="text-xs font-bold">Pending</span>
</button>
)}
<ActionButton
className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-slate-100 text-slate-300 hover:bg-red-100 hover:text-red-500 hover:font-bold"
onClick={onDelete}
fa="fa-solid fa-trash"
ctaText="Delete"
hidden={hideActions.includes('delete')}
/>
<div className="flex flex-col items-end gap-1">
<div className="h-4">
{!hideActions.length && (
<Icon
className="text-slate-500 hover:text-slate-600 relative"
fa="fa-solid fa-ellipsis"
onClick={(e) => {
e.stopPropagation();
showMenu(true);
}}
>
{showingMenu && (
<RequestMenu requestId={props.requestId} showMenu={showMenu} />
)}
</Icon>
)}
</div>
<div className="text-slate-500" title={day.format('LLLL')}>
{day.fromNow()}
</div>
</div>
</div>
);
function RetryButton(p: { hidden?: boolean }): ReactElement {
if (p.hidden) return <></>;
return (
<button
className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-slate-100 text-slate-300 hover:bg-slate-200 hover:text-slate-500 hover:font-bold"
onClick={onRetry}
>
<Icon fa="fa-solid fa-arrows-rotate" size={1} />
<span className="text-xs font-bold">Retry</span>
</button>
);
}
function ErrorButton(p: { hidden?: boolean }): ReactElement {
if (p.hidden) return <></>;
return (
<button
className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-red-100 text-red-300 hover:bg-red-200 hover:text-red-500 hover:font-bold"
onClick={onShowError}
>
<Icon fa="fa-solid fa-circle-exclamation" size={1} />
<span className="text-xs font-bold">Error</span>
</button>
);
}
function ErrorModal(): ReactElement {
const msg = typeof request?.error === 'string' && request?.error;
return !showingError ? (
@@ -242,7 +154,7 @@ export function OneRequestHistory(props: {
onClose={closeAllModal}
>
<ModalContent className="flex justify-center items-center text-slate-500">
{msg || 'Something went wrong :('}
{msg || request?.errorMessage}
</ModalContent>
<button
className="m-0 w-24 bg-red-100 text-red-300 hover:bg-red-200 hover:text-red-500"
@@ -253,99 +165,4 @@ export function OneRequestHistory(props: {
</Modal>
);
}
function ShareConfirmationModal(): ReactElement {
return !showingShareConfirmation ? (
<></>
) : (
<Modal
className="flex flex-col items-center text-base cursor-default justify-center !w-auto mx-4 my-[50%] p-4 gap-4"
onClose={closeAllModal}
>
<ModalContent className="flex flex-col w-full gap-4 items-center text-base justify-center">
{!cid[props.requestId] ? (
<p className="text-slate-500 text-center">
{uploadError ||
'This will make your proof publicly accessible by anyone with the CID'}
</p>
) : (
<input
className="input w-full bg-slate-100 border border-slate-200"
readOnly
value={`${EXPLORER_API}/ipfs/${cid[props.requestId]}`}
onFocus={(e) => e.target.select()}
/>
)}
</ModalContent>
<div className="flex flex-row gap-2 justify-center">
{!cid[props.requestId] ? (
<>
{!uploadError && (
<button
onClick={handleUpload}
className="button button--primary flex flex-row items-center justify-center gap-2 m-0"
disabled={uploading}
>
{uploading && (
<Icon
className="animate-spin"
fa="fa-solid fa-spinner"
size={1}
/>
)}
I understand
</button>
)}
<button
className="m-0 w-24 bg-slate-100 text-slate-400 hover:bg-slate-200 hover:text-slate-600 font-bold"
onClick={closeAllModal}
>
Close
</button>
</>
) : (
<>
<button
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
</button>
<button
className="m-0 w-24 bg-slate-100 text-slate-400 hover:bg-slate-200 hover:text-slate-600 font-bold"
onClick={closeAllModal}
>
Close
</button>
</>
)}
</div>
</Modal>
);
}
}
function ActionButton(props: {
onClick: () => void;
fa: string;
ctaText: string;
className?: string;
hidden?: boolean;
}): ReactElement {
if (props.hidden) return <></>;
return (
<button
className={classNames(
'flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 hover:font-bold',
props.className,
)}
onClick={props.onClick}
>
<Icon className="" fa={props.fa} size={1} />
<span className="text-xs font-bold">{props.ctaText}</span>
</button>
);
}

View File

@@ -0,0 +1,307 @@
import React, {
MouseEventHandler,
ReactElement,
ReactNode,
useCallback,
useEffect,
useState,
} from 'react';
import classNames from 'classnames';
import Icon from '../../components/Icon';
import {
addRequestCid,
deleteRequestHistory,
useRequestHistory,
} from '../../reducers/history';
import { download, upload } from '../../utils/misc';
import Modal, { ModalContent } from '../../components/Modal/Modal';
import { EXPLORER_API } from '../../utils/constants';
import copy from 'copy-to-clipboard';
import { setNotaryRequestCid } from '../../entries/Background/db';
import { useDispatch } from 'react-redux';
import { getNotaryApi, getProxyApi } from '../../utils/storage';
import { BackgroundActiontype } from '../../entries/Background/rpc';
export default function RequestMenu({
requestId,
showMenu,
}: {
showMenu: (opened: boolean) => void;
requestId: string;
}): ReactElement {
const dispatch = useDispatch();
const request = useRequestHistory(requestId);
const [showingShareConfirmation, setShowingShareConfirmation] =
useState(false);
const [showRemoveModal, setShowRemoveModal] = useState(false);
const onRetry = useCallback(async () => {
const notaryUrl = await getNotaryApi();
const websocketProxyUrl = await getProxyApi();
chrome.runtime.sendMessage<any, string>({
type: BackgroundActiontype.retry_prove_request,
data: {
id: requestId,
notaryUrl,
websocketProxyUrl,
},
});
}, [requestId]);
const onDelete = useCallback(async () => {
dispatch(deleteRequestHistory(requestId));
}, [requestId]);
if (!request) return <></>;
const { status } = request;
return (
<>
{showingShareConfirmation && (
<ShareConfirmationModal
requestId={requestId}
setShowingShareConfirmation={setShowingShareConfirmation}
showMenu={showMenu}
/>
)}
<RemoveHistory
onRemove={onDelete}
showRemovalModal={showRemoveModal}
setShowRemoveModal={setShowRemoveModal}
onCancel={() => setShowRemoveModal(false)}
/>
<div
className="fixed top-0 left-0 w-screen h-screen z-10 cursor-default"
onClick={(e) => {
e.stopPropagation();
showMenu(false);
}}
/>
<div className="absolute top-[100%] right-0 rounded-md z-20">
<div className="flex flex-col bg-slate-200 w-40 shadow rounded-md py">
{status === 'success' && (
<>
<RequestMenuRow
fa="fa-solid fa-download"
className="border-b border-slate-300"
onClick={(e) => {
e.stopPropagation();
showMenu(false);
download(`${request.id}.json`, JSON.stringify(request.proof));
}}
>
Download
</RequestMenuRow>
<RequestMenuRow
fa="fa-solid fa-upload"
className="border-b border-slate-300"
onClick={(e) => {
e.stopPropagation();
setShowingShareConfirmation(true);
}}
>
Share
</RequestMenuRow>
</>
)}
{status === 'error' && (
<RequestMenuRow
fa="fa-solid fa-arrows-rotate"
className="border-b border-slate-300"
onClick={(e) => {
e.stopPropagation();
onRetry();
showMenu(false);
}}
>
Retry
</RequestMenuRow>
)}
<RequestMenuRow
fa="fa-solid fa-trash"
className="border-b border-slate-300 !text-red-500"
onClick={(e) => {
e.stopPropagation();
setShowRemoveModal(true);
}}
>
Delete
</RequestMenuRow>
</div>
</div>
</>
);
}
function RequestMenuRow(props: {
fa: string;
children?: ReactNode;
onClick?: MouseEventHandler;
className?: string;
}): ReactElement {
return (
<div
className={classNames(
'flex flex-row items-center py-3 px-4 gap-2 hover:bg-slate-300 cursor-pointer text-slate-800 hover:text-slate-900 font-semibold',
props.className,
)}
onClick={props.onClick}
>
<Icon size={0.875} fa={props.fa} />
{props.children}
</div>
);
}
function ShareConfirmationModal({
setShowingShareConfirmation,
requestId,
showMenu,
}: {
showMenu: (opened: boolean) => void;
setShowingShareConfirmation: (showing: boolean) => void;
requestId: string;
}): ReactElement {
const dispatch = useDispatch();
const request = useRequestHistory(requestId);
const [uploadError, setUploadError] = useState('');
const [uploading, setUploading] = useState(false);
const handleUpload = useCallback(async () => {
setUploading(true);
try {
const data = await upload(
`${request?.id}.json`,
JSON.stringify(request?.proof),
);
await setNotaryRequestCid(requestId, data);
dispatch(addRequestCid(requestId, data));
} catch (e: any) {
setUploadError(e.message);
} finally {
setUploading(false);
}
}, [requestId, request, request?.cid]);
const onClose = useCallback(() => {
setShowingShareConfirmation(false);
showMenu(false);
}, [showMenu]);
return !request ? (
<></>
) : (
<Modal
className="flex flex-col items-center text-base cursor-default justify-center !w-auto mx-4 my-[50%] p-4 gap-4"
onClose={(e) => {
e.stopPropagation();
onClose();
}}
>
<ModalContent className="flex flex-col w-full gap-4 items-center text-base justify-center">
{!request.cid ? (
<p className="text-slate-500 text-center">
{uploadError ||
'This will make your proof publicly accessible by anyone with the CID'}
</p>
) : (
<input
className="input w-full bg-slate-100 border border-slate-200"
readOnly
value={`${EXPLORER_API}/ipfs/${request.cid}`}
onFocus={(e) => e.target.select()}
/>
)}
</ModalContent>
<div className="flex flex-row gap-2 justify-center">
{!request.cid ? (
<>
{!uploadError && (
<button
onClick={handleUpload}
className="button button--primary flex flex-row items-center justify-center gap-2 m-0"
disabled={uploading}
>
{uploading && (
<Icon
className="animate-spin"
fa="fa-solid fa-spinner"
size={1}
/>
)}
I understand
</button>
)}
<button
className="m-0 w-24 bg-slate-100 text-slate-400 hover:bg-slate-200 hover:text-slate-600 font-bold"
onClick={onClose}
>
Close
</button>
</>
) : (
<>
<button
onClick={() => copy(`${EXPLORER_API}/ipfs/${request.cid}`)}
className="m-0 w-24 bg-slate-600 text-slate-200 hover:bg-slate-500 hover:text-slate-100 font-bold"
>
Copy
</button>
<button
className="m-0 w-24 bg-slate-100 text-slate-400 hover:bg-slate-200 hover:text-slate-600 font-bold"
onClick={onClose}
>
Close
</button>
</>
)}
</div>
</Modal>
);
}
export function RemoveHistory(props: {
onRemove: () => void;
showRemovalModal: boolean;
setShowRemoveModal: (show: boolean) => void;
onCancel: () => void;
}): ReactElement {
const { onRemove, setShowRemoveModal, showRemovalModal } = props;
const onCancel = useCallback(() => {
setShowRemoveModal(false);
}, [showRemovalModal]);
return !showRemovalModal ? (
<></>
) : (
<Modal
onClose={onCancel}
className="flex flex-col items-center text-base cursor-default justify-center !w-auto mx-4 my-[50%] p-4 gap-4"
>
<ModalContent className="flex flex-col w-full gap-4 items-center text-base justify-center">
<div className="text-base">
Are you sure you want to delete this attestation?
</div>
<div className="mb-1">
<span className="text-red-500 font-bold">Warning:</span> this cannot
be undone.
</div>
<div className="flex flex-row gap-2 justify-end">
<button
className="m-0 w-24 bg-slate-100 text-slate-300 hover:bg-slate-200 hover:text-slate-500"
onClick={onCancel}
>
Cancel
</button>
<button
className="m-0 w-24 bg-red-100 text-red-300 hover:bg-red-200 hover:text-red-500"
onClick={onRemove}
>
Delete
</button>
</div>
</ModalContent>
</Modal>
);
}

View File

@@ -20,10 +20,9 @@ import {
usePluginConfig,
usePluginHashes,
} from '../../reducers/plugins';
import { PluginConfig } from '../../utils/misc';
import { getPluginConfigByHash } from '../../entries/Background/db';
import { fetchPluginHashes } from '../../utils/rpc';
import DefaultPluginIcon from '../../assets/img/default-plugin-icon.png';
import { useClientId } from '../../reducers/p2p';
export default function Home(props: {
tab?: 'history' | 'network';
@@ -78,9 +77,13 @@ export default function Home(props: {
scrollTop={scrollTop}
/>
<div
className={classNames('flex flex-row justify-center items-center', {
'fixed top-9 w-full bg-white shadow lg:w-[598px] lg:mt-40': shouldFix,
})}
className={classNames(
'flex flex-row justify-center items-center z-10',
{
'fixed top-9 w-full bg-white shadow lg:w-[598px] lg:mt-40':
shouldFix,
},
)}
>
<TabSelector
onClick={() => setTab('network')}
@@ -112,25 +115,26 @@ function ActionPanel({
}) {
const pluginHashes = usePluginHashes();
const navigate = useNavigate();
const clientId = useClientId();
const container = useRef<HTMLDivElement | null>(null);
const [isOverflow, setOverflow] = useState(false);
const [expanded, setExpand] = useState(false);
useEffect(() => {
const onCheckSize = useCallback(() => {
const element = container.current;
if (!element) return;
setActionPanelElement(element);
const onCheckSize = () => {
if (element.scrollWidth > element.clientWidth) {
setOverflow(true);
} else {
setOverflow(false);
}
};
if (element.scrollWidth > element.clientWidth) {
setOverflow(true);
} else {
setOverflow(false);
}
}, [container]);
useEffect(() => {
onCheckSize();
window.addEventListener('resize', onCheckSize);
@@ -138,7 +142,7 @@ function ActionPanel({
return () => {
window.removeEventListener('resize', onCheckSize);
};
}, [container, pluginHashes]);
}, [onCheckSize, pluginHashes]);
useEffect(() => {
const element = container.current;
@@ -175,12 +179,23 @@ function ActionPanel({
>
Verify
</NavButton>
<NavButton
className={'relative'}
fa="fa-solid fa-network-wired"
iconSize={0.5}
iconClassName={classNames({
'!text-green-500': clientId,
})}
onClick={() => navigate('/p2p')}
>
P2P
</NavButton>
{pluginHashes.map((hash) => (
<PluginIcon hash={hash} />
<PluginIcon hash={hash} onCheckSize={onCheckSize} />
))}
<button
className={
'flex flex-row items-center justify-center self-start rounded relative border-2 border-dashed border-slate-300 hover:border-slate-400 text-slate-300 hover:text-slate-400 h-16 w-16 mx-1'
'flex flex-row shrink-0 items-center justify-center self-start rounded relative border-2 border-dashed border-slate-300 hover:border-slate-400 text-slate-300 hover:text-slate-400 h-16 w-16 mx-1'
}
title="Install a plugin"
>
@@ -203,7 +218,13 @@ function ActionPanel({
);
}
function PluginIcon({ hash }: { hash: string }) {
function PluginIcon({
hash,
onCheckSize,
}: {
hash: string;
onCheckSize: () => void;
}) {
const config = usePluginConfig(hash);
const onPluginClick = useOnPluginClick(hash);
@@ -212,11 +233,16 @@ function PluginIcon({ hash }: { hash: string }) {
onPluginClick();
}, [onPluginClick, config]);
if (!config) return null;
return (
<button
ref={() => {
onCheckSize();
}}
className={classNames(
'flex flex-col flex-nowrap items-center justify-center',
'text-white px-2 py-1 gap-1 opacity-90 hover:opacity-100',
'text-white px-2 py-1 gap-1 opacity-90 hover:opacity-100 w-18',
)}
onClick={onClick}
>
@@ -261,13 +287,15 @@ function NavButton(props: {
onClick?: MouseEventHandler;
className?: string;
title?: string;
iconClassName?: string;
disabled?: boolean;
iconSize?: number;
}): ReactElement {
return (
<button
className={classNames(
'flex flex-col flex-nowrap items-center justify-center',
'text-white px-2 py-1 gap-1 opacity-90 hover:opacity-100',
'text-white px-2 py-1 gap-1 opacity-90 hover:opacity-100 w-18',
props.className,
)}
onClick={props.onClick}
@@ -275,7 +303,10 @@ function NavButton(props: {
title={props.title}
>
<Icon
className="w-8 h-8 rounded-full bg-primary flex flex-row items-center justify-center flex-grow-0 flex-shrink-0"
className={classNames(
'w-8 h-8 rounded-full bg-primary flex flex-row items-center justify-center flex-grow-0 flex-shrink-0',
props.iconClassName,
)}
fa={props.fa}
size={0.875}
/>

View File

@@ -17,6 +17,8 @@ import {
getProxyApi,
getLoggingFilter,
LOGGING_FILTER_KEY,
getRendezvousApi,
RENDEZVOUS_API_LS_KEY,
} from '../../utils/storage';
import {
EXPLORER_API,
@@ -24,6 +26,7 @@ import {
NOTARY_PROXY,
MAX_RECV,
MAX_SENT,
RENDEZVOUS_API,
} from '../../utils/constants';
import Modal, { ModalContent } from '../../components/Modal/Modal';
import browser from 'webextension-polyfill';
@@ -36,6 +39,7 @@ export default function Options(): ReactElement {
const [maxSent, setMaxSent] = useState(MAX_SENT);
const [maxReceived, setMaxReceived] = useState(MAX_RECV);
const [loggingLevel, setLoggingLevel] = useState<LoggingLevel>('Info');
const [rendezvous, setRendezvous] = useState(RENDEZVOUS_API);
const [dirty, setDirty] = useState(false);
const [shouldReload, setShouldReload] = useState(false);
@@ -44,11 +48,17 @@ export default function Options(): ReactElement {
useEffect(() => {
(async () => {
setNotary((await getNotaryApi()) || NOTARY_API);
setProxy((await getProxyApi()) || NOTARY_PROXY);
setNotary(await getNotaryApi());
setProxy(await getProxyApi());
})();
}, []);
useEffect(() => {
(async () => {
setMaxReceived((await getMaxRecv()) || MAX_RECV);
setMaxSent((await getMaxSent()) || MAX_SENT);
setLoggingLevel((await getLoggingFilter()) || 'Info');
setRendezvous((await getRendezvousApi()) || RENDEZVOUS_API);
})();
}, [advanced]);
@@ -63,9 +73,18 @@ export default function Options(): ReactElement {
await set(MAX_SENT_LS_KEY, maxSent.toString());
await set(MAX_RECEIVED_LS_KEY, maxReceived.toString());
await set(LOGGING_FILTER_KEY, loggingLevel);
await set(RENDEZVOUS_API_LS_KEY, rendezvous);
setDirty(false);
},
[notary, proxy, maxSent, maxReceived, loggingLevel, shouldReload],
[
notary,
proxy,
maxSent,
maxReceived,
loggingLevel,
rendezvous,
shouldReload,
],
);
const onSaveAndReload = useCallback(
@@ -85,7 +104,7 @@ export default function Options(): ReactElement {
}, []);
return (
<div className="flex flex-col flex-nowrap flex-grow">
<div className="flex flex-col flex-nowrap flex-grow overflow-y-auto">
{showReloadModal && (
<Modal
className="flex flex-col items-center text-base cursor-default justify-center !w-auto mx-4 my-[50%] p-4 gap-4"
@@ -145,6 +164,8 @@ export default function Options(): ReactElement {
loggingLevel={loggingLevel}
setLoggingLevel={setLoggingLevel}
setShouldReload={setShouldReload}
rendezvous={rendezvous}
setRendezvous={setRendezvous}
/>
)}
<div className="flex flex-row flex-nowrap justify-end gap-2 p-2">
@@ -248,11 +269,13 @@ function AdvancedOptions(props: {
maxSent: number;
maxReceived: number;
loggingLevel: LoggingLevel;
rendezvous: string;
setShouldReload: (reload: boolean) => void;
setMaxSent: (value: number) => void;
setMaxReceived: (value: number) => void;
setDirty: (value: boolean) => void;
setLoggingLevel: (level: LoggingLevel) => void;
setRendezvous: (api: string) => void;
}) {
const {
maxSent,
@@ -263,6 +286,8 @@ function AdvancedOptions(props: {
setLoggingLevel,
loggingLevel,
setShouldReload,
rendezvous,
setRendezvous,
} = props;
return (
@@ -287,6 +312,15 @@ function AdvancedOptions(props: {
setDirty(true);
}}
/>
<InputField
label="Rendezvous API (for P2P)"
value={rendezvous}
type="text"
onChange={(e) => {
setRendezvous(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

View File

@@ -0,0 +1,498 @@
import React, {
ChangeEvent,
ReactElement,
useCallback,
useEffect,
useState,
} from 'react';
import Icon from '../../components/Icon';
import classNames from 'classnames';
import {
connectRendezvous,
disconnectRendezvous,
fetchP2PState,
sendPairRequest,
useClientId,
useIncomingPairingRequests,
useOutgoingPairingRequests,
cancelPairRequest,
useP2PError,
setP2PError,
acceptPairRequest,
rejectPairRequest,
usePairId,
useIncomingProofRequests,
requestProofByHash,
useOutgoingProofRequests,
acceptProofRequest,
rejectProofRequest,
cancelProofRequest,
useP2PProving,
useP2PVerifying,
useP2PPresentation,
} from '../../reducers/p2p';
import { useDispatch } from 'react-redux';
import Modal, { ModalHeader } from '../../components/Modal/Modal';
import { Plugin, PluginList } from '../../components/PluginList';
import browser from 'webextension-polyfill';
import { sha256 } from '../../utils/misc';
import { openSidePanel } from '../../entries/utils';
import { SidePanelActionTypes } from '../../entries/SidePanel/types';
import { verify } from 'tlsn-js-v5';
import ProofViewer from '../ProofViewer';
export function P2PHome(): ReactElement {
const clientId = useClientId();
useEffect(() => {
fetchP2PState();
}, []);
const toggleConnection = useCallback(async () => {
if (!clientId) {
connectRendezvous();
} else {
disconnectRendezvous();
}
}, [clientId]);
return (
<div className="flex flex-col h-full cursor-default gap-2 my-2">
<div className="flex flex-row border border-slate-300 rounded mx-2">
<div className="bg-slate-200 px-2 py-1 flex-grow-0 border-r border-slate-300">
Client ID
</div>
<input
className={classNames(
'flex-grow outline-0 px-2 py-1 cursor-default font-semibold',
{
'text-slate-400 bg-slate-100': !clientId,
'text-green-500 cursor-pointer': clientId,
},
)}
onClick={(e) => {
// @ts-ignore
if (e.target.select && clientId) e.target.select();
}}
value={clientId ? clientId : '--'}
readOnly
/>
<button
className="flex-grow-0 px-2 py-1 button border-l border-slate-300"
onClick={toggleConnection}
>
{clientId ? 'Stop' : 'Start'}
</button>
</div>
<ClientStatus />
<div className="flex flex-row mx-2 flex-grow flex-shrink h-0 p-2">
<div className="text-slate-400 text-center w-full font-semibold">
No proofs history
</div>
</div>
</div>
);
}
function ClientStatus() {
const clientId = useClientId();
const error = useP2PError();
const pairId = usePairId();
const [incomingPairingRequest] = useIncomingPairingRequests();
const [outgoingPairingRequest] = useOutgoingPairingRequests();
let body = null;
if (!clientId) {
body = <ClientNotStarted />;
} else if (pairId) {
body = <Paired />;
} else if (!incomingPairingRequest && !outgoingPairingRequest) {
body = <PendingConnection />;
} else if (incomingPairingRequest) {
body = <IncomingRequest />;
} else if (outgoingPairingRequest) {
body = <OutgoingRequest />;
}
return (
<div
className={classNames(
'flex flex-col items-center justify-center border border-slate-300',
'flex-grow-0 flex-shrink rounded mx-2 bg-slate-100 py-4 gap-4',
)}
>
{body}
{error && <span className="text-xs text-red-500">{error}</span>}
</div>
);
}
function Paired() {
const pairId = usePairId();
const clientId = useClientId();
const [incomingProofRequest] = useIncomingProofRequests();
const [outgoingPluginHash] = useOutgoingProofRequests();
const [incomingPluginHash, setIncomingPluginHash] = useState('');
const [showingModal, showModal] = useState(false);
const isProving = useP2PProving();
const isVerifying = useP2PVerifying();
const presentation = useP2PPresentation();
useEffect(() => {
(async () => {
if (!incomingProofRequest) {
setIncomingPluginHash('');
return;
}
const hash = await sha256(incomingProofRequest);
setIncomingPluginHash(hash);
})();
}, [incomingProofRequest]);
useEffect(() => {
showModal(false);
}, [outgoingPluginHash]);
const accept = useCallback(async () => {
if (incomingPluginHash) {
await openSidePanel();
browser.runtime.sendMessage({
type: SidePanelActionTypes.run_p2p_plugin_request,
data: {
pluginHash: incomingPluginHash,
plugin: incomingProofRequest,
},
});
acceptProofRequest(incomingPluginHash);
window.close();
}
}, [incomingPluginHash, incomingProofRequest, clientId]);
const reject = useCallback(() => {
if (incomingPluginHash) rejectProofRequest(incomingPluginHash);
}, [incomingPluginHash]);
const cancel = useCallback(() => {
if (outgoingPluginHash) cancelProofRequest(outgoingPluginHash);
}, [outgoingPluginHash]);
let body;
if (incomingPluginHash) {
body = (
<IncomingProof
incomingProofRequest={incomingProofRequest}
incomingPluginHash={incomingPluginHash}
accept={accept}
reject={reject}
isProving={isProving}
/>
);
} else if (outgoingPluginHash) {
body = (
<OutgoingProof
outgoingPluginHash={outgoingPluginHash}
cancel={cancel}
isVerifying={isVerifying}
/>
);
} else {
body = (
<button
className="button button--primary"
onClick={() => showModal(true)}
>
Request Proof
</button>
);
}
return (
<div className="flex flex-col items-center gap-2 px-4 w-full">
{showingModal && <PluginListModal onClose={() => showModal(false)} />}
<div>
<span>Paired with </span>
<span className="font-semibold text-blue-500">{pairId}</span>
</div>
{body}
</div>
);
}
function IncomingProof({
incomingPluginHash,
incomingProofRequest,
reject,
accept,
isProving,
}: {
incomingPluginHash: string;
incomingProofRequest: string;
reject: () => void;
accept: () => void;
isProving: boolean;
}) {
const presentation = useP2PPresentation();
const [showingTranscript, showTranscript] = useState(false);
if (isProving) {
return (
<>
{presentation && showingTranscript && (
<Modal
className="h-full m-0 rounded-none"
onClose={() => showTranscript(false)}
>
<ProofViewer
className="h-full"
sent={presentation.sent}
recv={presentation.recv}
/>
</Modal>
)}
<div className="font-semibold text-orange-500">
{presentation ? 'Proving Completed' : 'Proving to your peer...'}
</div>
<Plugin
className="w-full bg-white !cursor-default hover:!bg-white active:!bg-white hover:!border-slate-300"
hash={incomingPluginHash}
hex={incomingProofRequest}
onClick={() => null}
unremovable
/>
<div className="flex flex-row gap-2">
<button
className="button button--primary"
onClick={() => showTranscript(true)}
disabled={!presentation}
>
View
</button>
</div>
</>
);
}
return (
<>
<div className="font-semibold text-orange-500">
Your peer is requesting the following proof:
</div>
<Plugin
className="w-full bg-white !cursor-default hover:!bg-white active:!bg-white hover:!border-slate-300"
hash={incomingPluginHash}
hex={incomingProofRequest}
onClick={() => null}
unremovable
/>
<div className="flex flex-row gap-2">
<button className="button" onClick={reject}>
Decline
</button>
<button className="button button--primary" onClick={accept}>
Accept
</button>
</div>
</>
);
}
function OutgoingProof({
outgoingPluginHash,
cancel,
isVerifying,
}: {
isVerifying: boolean;
outgoingPluginHash: string;
cancel: () => void;
}) {
const presentation = useP2PPresentation();
const [showingTranscript, showTranscript] = useState(false);
if (isVerifying) {
return (
<>
{presentation && showingTranscript && (
<Modal
className="h-full m-0 rounded-none"
onClose={() => showTranscript(false)}
>
<ProofViewer
className="h-full"
sent={presentation.sent}
recv={presentation.recv}
/>
</Modal>
)}
<div className="font-semibold text-orange-500">
{presentation
? 'Verification Completed'
: 'Verifying with your peer...'}
</div>
<Plugin
className="w-full bg-white !cursor-default hover:!bg-white active:!bg-white hover:!border-slate-300"
hash={outgoingPluginHash}
onClick={() => null}
unremovable
/>
<div className="flex flex-row gap-2">
<button
className="button button--primary"
onClick={() => showTranscript(true)}
disabled={!presentation}
>
View
</button>
</div>
</>
);
}
return (
<>
<div className="font-semibold text-orange-500">
Sent request for following proof:
</div>
<Plugin
className="w-full bg-white !cursor-default hover:!bg-white active:!bg-white hover:!border-slate-300"
hash={outgoingPluginHash}
onClick={() => null}
unremovable
/>
<button className="button" onClick={cancel}>
Cancel
</button>
</>
);
}
function PluginListModal({ onClose }: { onClose: () => void }) {
const onRequestProof = useCallback(async (hash: string) => {
requestProofByHash(hash);
}, []);
return (
<Modal className="mx-4" onClose={onClose}>
<ModalHeader onClose={onClose}>Choose a plugin to continue</ModalHeader>
<PluginList className="m-2" onClick={onRequestProof} unremovable />
</Modal>
);
}
function IncomingRequest() {
const [incomingRequest] = useIncomingPairingRequests();
const accept = useCallback(() => {
if (incomingRequest) acceptPairRequest(incomingRequest);
}, [incomingRequest]);
const reject = useCallback(() => {
if (incomingRequest) rejectPairRequest(incomingRequest);
}, [incomingRequest]);
return (
<div className="flex flex-col items-center gap-2">
<div>
<span className="font-semibold text-blue-500">{incomingRequest}</span>
<span> wants to pair with you.</span>
</div>
<div className="flex flex-row gap-2">
<button className="button" onClick={reject}>
Decline
</button>
<button className="button button--primary" onClick={accept}>
Accept
</button>
</div>
</div>
);
}
function OutgoingRequest() {
const [outgoingRequest] = useOutgoingPairingRequests();
const cancel = useCallback(() => {
if (outgoingRequest) {
cancelPairRequest(outgoingRequest);
}
}, [outgoingRequest]);
return (
<div className="flex flex-col items-center gap-2">
<span className="flex flex-row items-center gap-2 mx-2">
<Icon
className="animate-spin w-fit text-slate-500"
fa="fa-solid fa-spinner"
size={1}
/>
<span>
<span>Awaiting response from </span>
<span className="font-semibold text-blue-500">{outgoingRequest}</span>
<span>...</span>
</span>
</span>
<button className="button" onClick={cancel}>
Cancel
</button>
</div>
);
}
function ClientNotStarted() {
return (
<div className="flex flex-col text-slate-500 font-semibold gap-2">
Client has not started
<button className="button button--primary" onClick={connectRendezvous}>
Start Client
</button>
</div>
);
}
function PendingConnection() {
const dispatch = useDispatch();
const [target, setTarget] = useState('');
const onSend = useCallback(() => {
dispatch(setP2PError(''));
sendPairRequest(target);
}, [target]);
const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
dispatch(setP2PError(''));
setTarget(e.target.value);
}, []);
return (
<div className="flex flex-col w-full items-center gap-2">
<div className="flex flex-row justify-center gap-2">
<Icon
className="animate-spin w-fit text-slate-500"
fa="fa-solid fa-spinner"
size={1}
/>
<div className="text-slate-500 font-semibold">
Waiting for pairing request...
</div>
</div>
<div className="text-slate-500">or</div>
<div className="w-full flex flex-row px-2 items-center">
<input
className="flex-grow flex-shrink w-0 outline-0 px-2 py-1 cursor-default"
placeholder="Enter Peer ID to send pairing request"
onChange={onChange}
value={target}
/>
<button
className="button button--primary w-fit h-full"
onClick={onSend}
>
Send Pairing Request
</button>
</div>
</div>
);
}

View File

@@ -7,14 +7,17 @@ import React, {
import Icon from '../../components/Icon';
import { BackgroundActiontype } from '../../entries/Background/rpc';
import ProofViewer from '../ProofViewer';
import { convertNotaryWsToHttp } from '../../utils/misc';
export default function ProofUploader(): ReactElement {
const [proof, setProof] = useState<{
recv: string;
sent: string;
verifierKey?: string;
notaryKey?: string;
} | null>(null);
const [uploading, setUploading] = useState(false);
const [metadata, setMetaData] = useState<any>({ meta: '', version: '' });
const onFileUpload: ChangeEventHandler<HTMLInputElement> = useCallback(
async (e) => {
// @ts-ignore
@@ -26,8 +29,19 @@ export default function ProofUploader(): ReactElement {
const result = event.target?.result;
if (result) {
const proof = JSON.parse(result as string);
const notaryUrl = convertNotaryWsToHttp(proof.meta.notaryUrl);
proof.meta.notaryUrl = notaryUrl;
setMetaData({ meta: proof.meta, version: proof.version });
const res = await chrome.runtime
.sendMessage<any, { recv: string; sent: string }>({
.sendMessage<
any,
{
recv: string;
sent: string;
verifierKey?: string;
notaryKey?: string;
}
>({
type: BackgroundActiontype.verify_proof,
data: proof,
})
@@ -48,7 +62,15 @@ export default function ProofUploader(): ReactElement {
);
if (proof) {
return <ProofViewer recv={proof.recv} sent={proof.sent} />;
return (
<ProofViewer
recv={proof.recv}
sent={proof.sent}
verifierKey={proof.verifierKey}
notaryKey={proof.notaryKey}
info={metadata}
/>
);
}
return (

View File

@@ -2,41 +2,99 @@ import React, {
ReactNode,
ReactElement,
useState,
useEffect,
MouseEventHandler,
useCallback,
} from 'react';
import { useParams, useNavigate } from 'react-router';
import c from 'classnames';
import { useRequestHistory } from '../../reducers/history';
import {
deleteRequestHistory,
useRequestHistory,
} from '../../reducers/history';
import Icon from '../../components/Icon';
import { download } from '../../utils/misc';
import {
convertNotaryWsToHttp,
download,
isPopupWindow,
} from '../../utils/misc';
import classNames from 'classnames';
import { useDispatch } from 'react-redux';
import { RemoveHistory } from '../History/request-menu';
import { PresentationJSON } from 'tlsn-js/build/types';
import { RequestHistory } from '../../entries/Background/rpc';
export default function ProofViewer(props?: {
className?: string;
recv?: string;
sent?: string;
verifierKey?: string;
notaryKey?: string;
info?: {
meta: { notaryUrl: string; websocketProxyUrl: string };
version: string;
};
}): ReactElement {
const dispatch = useDispatch();
const { requestId } = useParams<{ requestId: string }>();
const request = useRequestHistory(requestId);
const navigate = useNavigate();
const [tab, setTab] = useState('sent');
const [isPopup, setIsPopup] = useState(isPopupWindow());
const [showRemoveModal, setShowRemoveModal] = useState(false);
const onDelete = useCallback(async () => {
if (requestId) {
dispatch(deleteRequestHistory(requestId));
if (isPopup) window.close();
navigate(-1);
}
}, [requestId]);
const notaryUrl = extractFromProps('notaryUrl', props, request);
const websocketProxyUrl = extractFromProps(
'websocketProxyUrl',
props,
request,
);
return (
<div className="flex flex-col w-full py-2 gap-2 flex-grow">
<div
className={classNames(
'flex flex-col w-full py-2 gap-2 flex-grow',
props?.className,
)}
>
<RemoveHistory
onRemove={onDelete}
showRemovalModal={showRemoveModal}
setShowRemoveModal={setShowRemoveModal}
onCancel={() => setShowRemoveModal(false)}
/>
<div className="flex flex-col px-2">
<div className="flex flex-row gap-2 items-center">
<Icon
className={c(
'px-1 select-none cursor-pointer',
'text-slate-400 border-b-2 border-transparent hover:text-slate-500 active:text-slate-800',
)}
onClick={() => navigate(-1)}
fa="fa-solid fa-xmark"
/>
{!isPopup && (
<Icon
className={c(
'px-1 select-none cursor-pointer',
'text-slate-400 border-b-2 border-transparent hover:text-slate-500 active:text-slate-800',
)}
onClick={() => navigate(-1)}
fa="fa-solid fa-xmark"
/>
)}
<TabLabel onClick={() => setTab('sent')} active={tab === 'sent'}>
Sent
</TabLabel>
<TabLabel onClick={() => setTab('recv')} active={tab === 'recv'}>
Recv
</TabLabel>
<TabLabel
onClick={() => setTab('metadata')}
active={tab === 'metadata'}
>
Metadata
</TabLabel>
<div className="flex flex-row flex-grow items-center justify-end">
{!props?.recv && (
<button
@@ -49,6 +107,12 @@ export default function ProofViewer(props?: {
Download
</button>
)}
<button
className="button !text-red-500"
onClick={() => setShowRemoveModal(true)}
>
Delete
</button>
</div>
</div>
</div>
@@ -67,11 +131,61 @@ export default function ProofViewer(props?: {
readOnly
></textarea>
)}
{tab === 'metadata' && (
<div className="w-full resize-none bg-slate-100 text-slate-800 border p-2 text-[10px] break-all h-full outline-none font-mono">
<MetadataRow
label="Version"
//@ts-ignore
value={props?.info?.version || request?.proof?.version}
/>
<MetadataRow label="Notary URL" value={notaryUrl} />
<MetadataRow
label="Websocket Proxy URL"
value={websocketProxyUrl}
/>
<MetadataRow
label="Verifying Key"
value={props?.verifierKey || request?.verification?.verifierKey}
/>
<MetadataRow
label="Notary Key"
value={props?.notaryKey || request?.verification?.notaryKey}
/>
</div>
)}
</div>
</div>
);
}
function extractFromProps(
key: 'notaryUrl' | 'websocketProxyUrl',
props?: {
className?: string;
recv?: string;
sent?: string;
verifierKey?: string;
notaryKey?: string;
info?: {
meta: { notaryUrl: string; websocketProxyUrl: string };
version: string;
};
},
request?: RequestHistory,
) {
let value;
if (props?.info?.meta) {
value = props.info.meta[key];
} else if (request && (request?.proof as PresentationJSON)?.meta) {
value = (request.proof as PresentationJSON).meta[key];
} else {
value = '';
}
return value;
}
function TabLabel(props: {
children: ReactNode;
onClick: MouseEventHandler;
@@ -90,3 +204,20 @@ function TabLabel(props: {
</button>
);
}
function MetadataRow({
label,
value,
}: {
label: string;
value: string | undefined;
}) {
return (
<div>
<div>{label}:</div>
<div className="text-sm font-semibold whitespace-pre-wrap">
{value || 'N/A'}
</div>
</div>
);
}

View File

@@ -11,12 +11,15 @@ import {
getPluginMetadataByHash,
} from '../../entries/Background/db';
import { runPlugin } from '../../utils/rpc';
import { SidePanelActionTypes } from '../../entries/SidePanel/types';
import { deferredPromise } from '../../utils/promise';
export function RunPluginApproval(): ReactElement {
const [params] = useSearchParams();
const origin = params.get('origin');
const favIconUrl = params.get('favIconUrl');
const hash = params.get('hash');
const pluginParams = params.get('params');
const hostname = urlify(origin || '')?.hostname;
const [error, showError] = useState('');
const [metadata, setPluginMetadata] = useState<PluginMetadata | null>(null);
@@ -38,9 +41,30 @@ export function RunPluginApproval(): ReactElement {
await browser.storage.local.set({ plugin_hash: hash });
const { promise, resolve } = deferredPromise();
const listener = async (request: any) => {
if (request.type === SidePanelActionTypes.panel_opened) {
browser.runtime.onMessage.removeListener(listener);
resolve();
}
};
browser.runtime.onMessage.addListener(listener);
// @ts-ignore
if (chrome.sidePanel) await chrome.sidePanel.open({ tabId: tab.id });
await promise;
browser.runtime.sendMessage({
type: SidePanelActionTypes.execute_plugin_request,
data: {
pluginHash: hash,
pluginParams: pluginParams ? JSON.parse(pluginParams) : undefined,
},
});
browser.runtime.sendMessage({
type: BackgroundActiontype.run_plugin_response,
data: true,

View File

@@ -1,6 +1,7 @@
import {
BackgroundActiontype,
RequestHistory,
RequestProgress,
} from '../entries/Background/rpc';
import { useSelector } from 'react-redux';
import { AppRootState } from './index';
@@ -10,6 +11,7 @@ enum ActionType {
'/history/addRequest' = '/history/addRequest',
'/history/setRequests' = '/history/setRequests',
'/history/deleteRequest' = '/history/deleteRequest',
'/history/addRequestCid' = '/history/addRequestCid',
}
type Action<payload> = {
@@ -45,6 +47,13 @@ export const setRequests = (requests: RequestHistory[]) => {
};
};
export const addRequestCid = (requestId: string, cid: string) => {
return {
type: ActionType['/history/addRequestCid'],
payload: { requestId, cid },
};
};
export const deleteRequestHistory = (id: string) => {
chrome.runtime.sendMessage<any, string>({
type: BackgroundActiontype.delete_prove_request,
@@ -68,6 +77,9 @@ export default function history(
if (!payload) return state;
const existing = state.map[payload.id];
if (existing?.progress === RequestProgress.Error) {
return state;
}
const newMap = {
...state.map,
[payload.id]: payload,
@@ -82,13 +94,20 @@ export default function history(
}
case ActionType['/history/setRequests']: {
const payload: RequestHistory[] = action.payload;
const newMap = payload.reduce(
(map: { [id: string]: RequestHistory }, req) => {
if (state.map[req.id]?.progress === RequestProgress.Error) {
map[req.id] = state.map[req.id];
} else {
map[req.id] = req;
}
return map;
},
{},
);
return {
...state,
map: payload.reduce((map: { [id: string]: RequestHistory }, req) => {
map[req.id] = req;
return map;
}, {}),
map: newMap,
order: payload.map(({ id }) => id),
};
}
@@ -103,6 +122,20 @@ export default function history(
order: newOrder,
};
}
case ActionType['/history/addRequestCid']: {
const { requestId, cid } = action.payload;
if (!state.map[requestId]) return state;
return {
...state,
map: {
...state.map,
[requestId]: {
...state.map[requestId],
cid,
},
},
};
}
default:
return state;
}

View File

@@ -2,11 +2,13 @@ import { combineReducers } from 'redux';
import requests from './requests';
import history from './history';
import plugins from './plugins';
import p2p from './p2p';
const rootReducer = combineReducers({
requests,
history,
plugins,
p2p,
});
export type AppRootState = ReturnType<typeof rootReducer>;

375
src/reducers/p2p.ts Normal file
View File

@@ -0,0 +1,375 @@
import { useSelector } from 'react-redux';
import { AppRootState } from './index';
import deepEqual from 'fast-deep-equal';
import browser from 'webextension-polyfill';
import { BackgroundActiontype } from '../entries/Background/rpc';
enum ActionType {
'/p2p/setConnected' = '/p2p/setConnected',
'/p2p/setClientId' = '/p2p/setClientId',
'/p2p/setPairing' = '/p2p/setPairing',
'/p2p/setError' = '/p2p/setError',
'/p2p/appendIncomingPairingRequest' = '/p2p/appendIncomingPairingRequest',
'/p2p/appendOutgoingPairingRequest' = '/p2p/appendOutgoingPairingRequest',
'/p2p/setIncomingPairingRequest' = '/p2p/setIncomingPairingRequest',
'/p2p/setOutgoingPairingRequest' = '/p2p/setOutgoingPairingRequest',
'/p2p/appendIncomingProofRequest' = '/p2p/appendIncomingProofRequest',
'/p2p/appendOutgoingProofRequest' = '/p2p/appendOutgoingProofRequest',
'/p2p/setIncomingProofRequest' = '/p2p/setIncomingProofRequest',
'/p2p/setOutgoingProofRequest' = '/p2p/setOutgoingProofRequest',
'/p2p/setIsProving' = '/p2p/setIsProving',
'/p2p/setIsVerifying' = '/p2p/setIsVerifying',
'/p2p/setPresentation' = '/p2p/setPresentation',
}
type Action<payload> = {
type: ActionType;
payload?: payload;
error?: boolean;
meta?: any;
};
type State = {
clientId: string;
pairing: string;
connected: boolean;
error: string;
incomingPairingRequests: string[];
outgoingPairingRequests: string[];
incomingProofRequests: string[];
outgoingProofRequests: string[];
isProving: boolean;
isVerifying: boolean;
presentation: null | {
sent: string;
recv: string;
};
};
export type RequestProofMessage = {
to: string;
from: string;
id: number;
text?: undefined;
};
const initialState: State = {
clientId: '',
pairing: '',
error: '',
connected: false,
incomingPairingRequests: [],
outgoingPairingRequests: [],
incomingProofRequests: [],
outgoingProofRequests: [],
isProving: false,
isVerifying: false,
presentation: null,
};
export const fetchP2PState = async () => {
return browser.runtime.sendMessage({
type: BackgroundActiontype.get_p2p_state,
});
};
export const connectRendezvous = () => {
return browser.runtime.sendMessage({
type: BackgroundActiontype.connect_rendezvous,
});
};
export const disconnectRendezvous = () => {
return browser.runtime.sendMessage({
type: BackgroundActiontype.disconnect_rendezvous,
});
};
export const setConnected = (connected = false) => ({
type: ActionType['/p2p/setConnected'],
payload: connected,
});
export const setClientId = (clientId: string) => ({
type: ActionType['/p2p/setClientId'],
payload: clientId,
});
export const setPairing = (clientId: string) => ({
type: ActionType['/p2p/setPairing'],
payload: clientId,
});
export const appendIncomingPairingRequests = (peerId: string) => ({
type: ActionType['/p2p/appendIncomingPairingRequest'],
payload: peerId,
});
export const appendIncomingProofRequests = (peerId: string) => ({
type: ActionType['/p2p/appendIncomingProofRequest'],
payload: peerId,
});
export const appendOutgoingPairingRequests = (peerId: string) => ({
type: ActionType['/p2p/appendOutgoingPairingRequest'],
payload: peerId,
});
export const appendOutgoingProofRequest = (peerId: string) => ({
type: ActionType['/p2p/appendOutgoingProofRequest'],
payload: peerId,
});
export const setIncomingPairingRequest = (peerIds: string[]) => ({
type: ActionType['/p2p/setIncomingPairingRequest'],
payload: peerIds,
});
export const setOutgoingPairingRequest = (peerIds: string[]) => ({
type: ActionType['/p2p/setOutgoingPairingRequest'],
payload: peerIds,
});
export const setIncomingProofRequest = (peerIds: string[]) => ({
type: ActionType['/p2p/setIncomingProofRequest'],
payload: peerIds,
});
export const setOutgoingProofRequest = (peerIds: string[]) => ({
type: ActionType['/p2p/setOutgoingProofRequest'],
payload: peerIds,
});
export const setP2PError = (error: string) => ({
type: ActionType['/p2p/setError'],
payload: error,
});
export const setIsProving = (proving: boolean) => ({
type: ActionType['/p2p/setIsProving'],
payload: proving,
});
export const setIsVerifying = (verifying: boolean) => ({
type: ActionType['/p2p/setIsVerifying'],
payload: verifying,
});
export const setP2PPresentation = (
presentation: null | { sent: string; recv: string },
) => ({
type: ActionType['/p2p/setPresentation'],
payload: presentation,
});
export const requestProofByHash = (pluginHash: string) => {
return browser.runtime.sendMessage({
type: BackgroundActiontype.request_p2p_proof_by_hash,
data: pluginHash,
});
};
export const sendPairRequest = async (targetId: string) => {
return browser.runtime.sendMessage({
type: BackgroundActiontype.send_pair_request,
data: targetId,
});
};
export const cancelPairRequest = async (targetId: string) => {
return browser.runtime.sendMessage({
type: BackgroundActiontype.cancel_pair_request,
data: targetId,
});
};
export const acceptPairRequest = async (targetId: string) => {
return browser.runtime.sendMessage({
type: BackgroundActiontype.accept_pair_request,
data: targetId,
});
};
export const rejectPairRequest = async (targetId: string) => {
return browser.runtime.sendMessage({
type: BackgroundActiontype.reject_pair_request,
data: targetId,
});
};
export const cancelProofRequest = async (plughinHash: string) => {
return browser.runtime.sendMessage({
type: BackgroundActiontype.cancel_proof_request,
data: plughinHash,
});
};
export const acceptProofRequest = async (plughinHash: string) => {
return browser.runtime.sendMessage({
type: BackgroundActiontype.accept_proof_request,
data: plughinHash,
});
};
export const rejectProofRequest = async (plughinHash: string) => {
return browser.runtime.sendMessage({
type: BackgroundActiontype.reject_proof_request,
data: plughinHash,
});
};
export default function p2p(state = initialState, action: Action<any>): State {
switch (action.type) {
case ActionType['/p2p/setConnected']:
return {
...state,
connected: action.payload,
};
case ActionType['/p2p/setClientId']:
return {
...state,
clientId: action.payload,
};
case ActionType['/p2p/setPairing']:
return {
...state,
pairing: action.payload,
};
case ActionType['/p2p/appendIncomingPairingRequest']:
return {
...state,
incomingPairingRequests: [
...new Set(state.incomingPairingRequests.concat(action.payload)),
],
};
case ActionType['/p2p/appendOutgoingPairingRequest']:
return {
...state,
outgoingPairingRequests: [
...new Set(state.outgoingPairingRequests.concat(action.payload)),
],
};
case ActionType['/p2p/setIncomingPairingRequest']:
return {
...state,
incomingPairingRequests: action.payload,
};
case ActionType['/p2p/setOutgoingPairingRequest']:
return {
...state,
outgoingPairingRequests: action.payload,
};
case ActionType['/p2p/appendIncomingProofRequest']:
return {
...state,
incomingProofRequests: [
...new Set(state.incomingProofRequests.concat(action.payload)),
],
};
case ActionType['/p2p/appendOutgoingProofRequest']:
return {
...state,
outgoingProofRequests: [
...new Set(state.outgoingProofRequests.concat(action.payload)),
],
};
case ActionType['/p2p/setIncomingProofRequest']:
return {
...state,
incomingProofRequests: action.payload,
};
case ActionType['/p2p/setOutgoingProofRequest']:
return {
...state,
outgoingProofRequests: action.payload,
};
case ActionType['/p2p/setError']:
return {
...state,
error: action.payload,
};
case ActionType['/p2p/setIsProving']:
return {
...state,
isProving: action.payload,
};
case ActionType['/p2p/setIsVerifying']:
return {
...state,
isVerifying: action.payload,
};
case ActionType['/p2p/setPresentation']:
return {
...state,
presentation: action.payload,
};
default:
return state;
}
}
export function useClientId() {
return useSelector((state: AppRootState) => {
return state.p2p.clientId;
}, deepEqual);
}
export function useConnected() {
return useSelector((state: AppRootState) => {
return state.p2p.connected;
}, deepEqual);
}
export function usePairId(): string {
return useSelector((state: AppRootState) => {
return state.p2p.pairing;
}, deepEqual);
}
export function useIncomingPairingRequests(): string[] {
return useSelector((state: AppRootState) => {
return state.p2p.incomingPairingRequests;
}, deepEqual);
}
export function useOutgoingPairingRequests(): string[] {
return useSelector((state: AppRootState) => {
return state.p2p.outgoingPairingRequests;
}, deepEqual);
}
export function useIncomingProofRequests(): string[] {
return useSelector((state: AppRootState) => {
return state.p2p.incomingProofRequests;
}, deepEqual);
}
export function useOutgoingProofRequests(): string[] {
return useSelector((state: AppRootState) => {
return state.p2p.outgoingProofRequests;
}, deepEqual);
}
export function useP2PError(): string {
return useSelector((state: AppRootState) => {
return state.p2p.error;
}, deepEqual);
}
export function useP2PVerifying(): boolean {
return useSelector((state: AppRootState) => {
return state.p2p.isVerifying;
}, deepEqual);
}
export function useP2PProving(): boolean {
return useSelector((state: AppRootState) => {
return state.p2p.isProving;
}, deepEqual);
}
export function useP2PPresentation(): null | { sent: string; recv: string } {
return useSelector((state: AppRootState) => {
return state.p2p.presentation;
}, deepEqual);
}

View File

@@ -6,6 +6,8 @@ import { getPluginConfigByHash } from '../entries/Background/db';
import { PluginConfig } from '../utils/misc';
import { runPlugin } from '../utils/rpc';
import browser from 'webextension-polyfill';
import { openSidePanel } from '../entries/utils';
import { SidePanelActionTypes } from '../entries/SidePanel/types';
enum ActionType {
'/plugin/addPlugin' = '/plugin/addPlugin',
@@ -70,17 +72,18 @@ export const usePluginConfig = (hash: string) => {
export const useOnPluginClick = (hash: string) => {
return useCallback(async () => {
await runPlugin(hash, 'start');
const [tab] = await browser.tabs.query({
active: true,
currentWindow: true,
});
await browser.storage.local.set({ plugin_hash: hash });
// @ts-ignore
if (chrome.sidePanel) await chrome.sidePanel.open({ tabId: tab.id });
await openSidePanel();
browser.runtime.sendMessage({
type: SidePanelActionTypes.execute_plugin_request,
data: {
pluginHash: hash,
},
});
await runPlugin(hash, 'start');
window.close();
}, [hash]);

View File

@@ -1,5 +1,6 @@
export const EXPLORER_API = 'https://explorer.tlsnotary.org';
export const NOTARY_API = 'https://notary.pse.dev/v0.1.0-alpha.7';
export const NOTARY_API = 'https://notary.pse.dev/v0.1.0-alpha.10';
export const RENDEZVOUS_API = 'wss://explorer.tlsnotary.org';
export const NOTARY_PROXY = 'wss://notary.pse.dev/proxy';
export const MAX_RECV = 16384;
export const MAX_SENT = 4096;

View File

@@ -1,5 +1,6 @@
import {
BackgroundActiontype,
handleExecP2PPluginProver,
handleExecPluginProver,
RequestLog,
} from '../entries/Background/rpc';
@@ -13,8 +14,12 @@ import browser from 'webextension-polyfill';
import NodeCache from 'node-cache';
import { getNotaryApi, getProxyApi } from './storage';
import { minimatch } from 'minimatch';
import { getCookiesByHost, getHeadersByHost } from '../entries/Background/db';
import {
getCookiesByHost,
getHeadersByHost,
getLocalStorageByHost,
getSessionStorageByHost,
} from '../entries/Background/db';
const charwise = require('charwise');
export function urlify(
@@ -84,6 +89,19 @@ export const copyText = async (text: string): Promise<void> => {
}
};
export function convertNotaryWsToHttp(notaryWs: string) {
const { protocol, pathname, hostname, port } = new URL(notaryWs);
if (protocol === 'https:' || protocol === 'http:') {
return notaryWs;
}
const p = protocol === 'wss:' ? 'https:' : 'http:';
const pt = port ? `:${port}` : '';
const path = pathname === '/' ? '' : pathname.replace('/notarize', '');
const h = hostname === 'localhost' ? '127.0.0.1' : hostname;
return p + '//' + h + pt + path;
}
export async function replayRequest(req: RequestLog): Promise<string> {
const options = {
method: req.method,
@@ -148,6 +166,10 @@ const VALID_HOST_FUNCS: { [name: string]: string } = {
export const makePlugin = async (
arrayBuffer: ArrayBuffer,
config?: PluginConfig,
meta?: {
p2p: boolean;
clientId: string;
},
) => {
const module = await WebAssembly.compile(arrayBuffer);
const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
@@ -199,41 +221,44 @@ export const makePlugin = async (
}
(async () => {
const {
url,
method,
headers,
getSecretResponse,
body: reqBody,
} = params;
let secretResps;
const resp = await fetch(url, {
method,
headers,
body: reqBody,
});
const body = await extractBodyFromResponse(resp);
const { getSecretResponse, body: reqBody } = params;
if (getSecretResponse) {
const out = await plugin.call(getSecretResponse, body);
secretResps = JSON.parse(out.string());
if (meta?.p2p) {
const pluginHex = Buffer.from(arrayBuffer).toString('hex');
handleExecP2PPluginProver({
type: BackgroundActiontype.execute_p2p_plugin_prover,
data: {
...params,
pluginHash: await sha256(pluginHex),
pluginHex,
body: reqBody,
now,
clientId: meta.clientId,
},
});
} else {
handleExecPluginProver({
type: BackgroundActiontype.execute_plugin_prover,
data: {
...params,
body: reqBody,
getSecretResponseFn: async (body: string) => {
return new Promise((resolve) => {
setTimeout(async () => {
const out = await plugin.call(getSecretResponse, body);
resolve(JSON.parse(out.string()));
}, 0);
});
},
now,
},
});
}
handleExecPluginProver({
type: BackgroundActiontype.execute_plugin_prover,
data: {
...params,
body: reqBody,
secretResps,
now,
},
});
})();
return context.store(`${id}`);
},
};
const funcs: {
[key: string]: (callContext: CallContext, ...args: any[]) => any;
} = {};
@@ -250,21 +275,61 @@ export const makePlugin = async (
}
}
if (config?.localStorage) {
const localStorage: { [hostname: string]: { [key: string]: string } } = {};
const [tab] = await chrome.tabs.query({
active: true,
lastFocusedWindow: true,
});
await chrome.tabs.sendMessage(tab.id as number, {
type: BackgroundActiontype.get_local_storage,
});
//@ts-ignore
for (const host of config.localStorage) {
const cache = await getLocalStorageByHost(host);
localStorage[host] = cache;
}
//@ts-ignore
injectedConfig.localStorage = JSON.stringify(localStorage);
}
if (config?.sessionStorage) {
const sessionStorage: { [hostname: string]: { [key: string]: string } } =
{};
const [tab] = await chrome.tabs.query({
active: true,
lastFocusedWindow: true,
});
await chrome.tabs.sendMessage(tab.id as number, {
type: BackgroundActiontype.get_session_storage,
});
//@ts-ignore
for (const host of config.sessionStorage) {
const cache = await getSessionStorageByHost(host);
sessionStorage[host] = cache;
}
//@ts-ignore
injectedConfig.sessionStorage = JSON.stringify(sessionStorage);
}
if (config?.cookies) {
const cookies: { [hostname: string]: { [key: string]: string } } = {};
for (const host of config.cookies) {
const cache = await getCookiesByHost(host);
cookies[host] = cache;
const cookies: { [link: string]: { [key: string]: string } } = {};
for (const link of config.cookies) {
const cache = await getCookiesByHost(link);
cookies[link] = 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 = await getHeadersByHost(host);
headers[host] = cache;
const headers: { [link: string]: { [key: string]: string } } = {};
for (const link of config.headers) {
const cache = await getHeadersByHost(link);
headers[link] = cache;
}
// @ts-ignore
injectedConfig.headers = JSON.stringify(headers);
@@ -293,12 +358,14 @@ export type StepConfig = {
export type PluginConfig = {
title: string; // The name of the plugin
description: string; // A description of the plugin's purpose
description: string; // A description of the plugin purpose
icon?: string; // A base64-encoded image string representing the plugin's icon (optional)
steps?: StepConfig[]; // An array describing the UI steps and behavior (see Step UI below) (optional)
hostFunctions?: string[]; // Host functions that the plugin will have access to
cookies?: string[]; // Cookies the plugin will have access to, cached by the extension from specified hosts (optional)
headers?: string[]; // Headers the plugin will have access to, cached by the extension from specified hosts (optional)
localStorage?: string[]; // LocalStorage the plugin will have access to, cached by the extension from specified hosts (optional)
sessionStorage?: string[]; // SessionStorage the plugin will have access to, cached by the extension from specified hosts (optional)
requests: { method: string; url: string }[]; // List of requests that the plugin is allowed to make
notaryUrls?: string[]; // List of notary services that the plugin is allowed to use (optional)
proxyUrls?: string[]; // List of websocket proxies that the plugin is allowed to use (optional)
@@ -348,7 +415,16 @@ export const getPluginConfig = async (
assert(typeof name === 'string' && name.length);
}
}
if (config.localStorage) {
for (const name of config.localStorage) {
assert(typeof name === 'string' && name.length);
}
}
if (config.sessionStorage) {
for (const name of config.sessionStorage) {
assert(typeof name === 'string' && name.length);
}
}
if (config.headers) {
for (const name of config.headers) {
assert(typeof name === 'string' && name.length);
@@ -390,3 +466,9 @@ export function safeParseJSON(data?: string | null) {
return null;
}
}
export function isPopupWindow(): boolean {
return (
!!window.opener || window.matchMedia('(display-mode: standalone)').matches
);
}

33
src/utils/parser.ts Normal file
View File

@@ -0,0 +1,33 @@
import { HTTPParser } from 'http-parser-js';
export function parseHttpMessage(buffer: Buffer, type: 'request' | 'response') {
const parser = new HTTPParser(
type === 'request' ? HTTPParser.REQUEST : HTTPParser.RESPONSE,
);
const body: Buffer[] = [];
let complete = false;
let headers: string[] = [];
parser.onBody = (t) => {
body.push(t);
};
parser.onHeadersComplete = (res) => {
headers = res.headers;
};
parser.onMessageComplete = () => {
complete = true;
};
parser.execute(buffer);
parser.finish();
if (!complete) throw new Error(`Could not parse ${type.toUpperCase()}`);
return {
info: buffer.toString('utf-8').split('\r\n')[0],
headers,
body,
};
}

View File

@@ -13,7 +13,7 @@ export const HostFunctionsDescriptions: {
);
},
notarize: ({ notaryUrls, proxyUrls }) => {
const notaries = ['default notary'].concat(notaryUrls || []);
const notaries = ['default notary', 'your peer'].concat(notaryUrls || []);
const proxies = ['default proxy'].concat(proxyUrls || []);
return (

View File

@@ -38,7 +38,12 @@ export async function fetchPluginConfigByHash(
});
}
export async function runPlugin(hash: string, method: string, params?: string) {
export async function runPlugin(
hash: string,
method: string,
params?: string,
meta?: any,
) {
return browser.runtime.sendMessage({
type: BackgroundActiontype.run_plugin,
data: {
@@ -46,5 +51,6 @@ export async function runPlugin(hash: string, method: string, params?: string) {
method,
params,
},
meta,
});
}

View File

@@ -1,11 +1,13 @@
import { LoggingLevel } from 'tlsn-js';
import { MAX_RECV, MAX_SENT, NOTARY_API, NOTARY_PROXY } from './constants';
import { RENDEZVOUS_API } from './constants';
export const NOTARY_API_LS_KEY = 'notary-api';
export const PROXY_API_LS_KEY = 'proxy-api';
export const MAX_SENT_LS_KEY = 'max-sent';
export const MAX_RECEIVED_LS_KEY = 'max-received';
export const LOGGING_FILTER_KEY = 'logging-filter-2';
export const RENDEZVOUS_API_LS_KEY = 'rendezvous-api';
export async function set(key: string, value: string) {
return chrome.storage.sync.set({ [key]: value });
@@ -37,3 +39,7 @@ export async function getProxyApi() {
export async function getLoggingFilter(): Promise<LoggingLevel> {
return await get(LOGGING_FILTER_KEY, 'Info');
}
export async function getRendezvousApi(): Promise<string> {
return await get(RENDEZVOUS_API_LS_KEY, RENDEZVOUS_API);
}

View File

@@ -7,7 +7,6 @@ var webpack = require("webpack"),
TerserPlugin = require("terser-webpack-plugin");
var { CleanWebpackPlugin } = require("clean-webpack-plugin");
var ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");
var ReactRefreshTypeScript = require("react-refresh-typescript");
var ExtReloader = require('webpack-ext-reloader');
const ASSET_PATH = process.env.ASSET_PATH || "/";
@@ -42,6 +41,9 @@ var options = {
/Circular dependency between chunks with runtime/,
/ResizeObserver loop completed with undelivered notifications/,
/Should not import the named export/,
/Sass @import rules are deprecated and will be removed in Dart Sass 3.0.0/,
/Global built-in functions are deprecated and will be removed in Dart Sass 3.0.0./,
/repetitive deprecation warnings omitted/,
],
entry: {
@@ -111,11 +113,6 @@ var options = {
{
loader: require.resolve("ts-loader"),
options: {
getCustomTransformers: () => ({
before: [isDevelopment && ReactRefreshTypeScript()].filter(
Boolean
),
}),
transpileOnly: isDevelopment,
},
},

View File

@@ -1,6 +0,0 @@
api.twitter.com: api.twitter.com:443
twitter.com: twitter.com:443
api.reddit.com: api.reddit.com:443
reddit.com: reddit.com:443
gateway.reddit.com: gateway.reddit.com:443
swapi.dev: swapi.dev:443