Compare commits

..

3 Commits

Author SHA1 Message Date
jayssj11
336f30b56c auto capture of url and tab name, need to refactor code again (#3) 2024-10-29 08:04:16 +01:00
jayssj11
31a531fd07 Chat component (#2)
* request capture when server responds with Sample Requests and Responses but do not capture response yet

* break down request and response in separate entities, still need to capture response properly

* response capture works

* code refactor
2024-10-22 10:34:39 +02:00
jayssj11
4ad394419b chat window added to the extension (#1) 2024-10-02 16:47:38 +02:00
71 changed files with 23968 additions and 8597 deletions

46
.github/workflows/build.yaml vendored Normal file
View File

@@ -0,0 +1,46 @@
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

View File

@@ -1,70 +0,0 @@
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 }}

46
.github/workflows/lint.yaml vendored Normal file
View File

@@ -0,0 +1,46 @@
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,4 +8,3 @@ bin/
build
tlsn/
zip
.vscode

View File

@@ -25,11 +25,7 @@ at your option.
## Installing and Running
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:
### Procedures:
1. Check if your [Node.js](https://nodejs.org/) version is >= **18**.
2. Clone this repository.

16719
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.1000",
"version": "0.1.0.6",
"license": "MIT",
"repository": {
"type": "git",
@@ -16,18 +16,18 @@
"lint:fix": "eslint . --fix"
},
"dependencies": {
"@extism/extism": "^2.0.0-rc11",
"@extism/extism": "^1.0.2",
"@fortawesome/fontawesome-free": "^6.4.2",
"@types/axios": "^0.14.0",
"async-mutex": "^0.4.0",
"axios": "^1.7.7",
"buffer": "^6.0.3",
"charwise": "^3.0.1",
"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",
@@ -40,7 +40,8 @@
"redux-logger": "^3.0.6",
"redux-thunk": "^2.4.2",
"tailwindcss": "^3.3.3",
"tlsn-js": "0.1.0-alpha.10.0"
"tlsn-js": "0.1.0-alpha.6.2",
"tlsn-js-v5": "npm:tlsn-js@0.1.0-alpha.5.4"
},
"devDependencies": {
"@babel/core": "^7.20.12",
@@ -94,4 +95,4 @@
"webpack-ext-reloader": "^1.1.12",
"zip-webpack-plugin": "^4.0.1"
}
}
}

8639
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 621 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

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

View File

@@ -1,4 +1,14 @@
import React, { Children, MouseEventHandler, ReactNode } from 'react';
import React, {
ChangeEvent,
Children,
MouseEventHandler,
ReactElement,
ReactNode,
useCallback,
useState,
} from 'react';
import { makePlugin, getPluginConfig } from '../../utils/misc';
import { addPlugin } from '../../utils/rpc';
import Modal, {
ModalHeader,
ModalContent,
@@ -12,9 +22,69 @@ import {
MultipleParts,
PermissionDescription,
} from '../../utils/plugins';
import { ErrorModal } from '../ErrorModal';
import classNames from 'classnames';
import DefaultPluginIcon from '../../assets/img/default-plugin-icon.png';
export default function PluginUploadInfo(): ReactElement {
const [error, showError] = useState('');
const [pluginBuffer, setPluginBuffer] = useState<ArrayBuffer | any>(null);
const [pluginContent, setPluginContent] = useState<PluginConfig | null>(null);
const onAddPlugin = useCallback(
async (evt: React.MouseEvent<HTMLButtonElement>) => {
try {
await addPlugin(Buffer.from(pluginBuffer).toString('hex'));
setPluginContent(null);
} catch (e: any) {
showError(e?.message || 'Invalid Plugin');
}
},
[pluginContent, pluginBuffer],
);
const onPluginInfo = useCallback(
async (evt: ChangeEvent<HTMLInputElement>) => {
if (!evt.target.files) return;
try {
const [file] = evt.target.files;
const arrayBuffer = await file.arrayBuffer();
const plugin = await makePlugin(arrayBuffer);
setPluginContent(await getPluginConfig(plugin));
setPluginBuffer(arrayBuffer);
} catch (e: any) {
showError(e?.message || 'Invalid Plugin');
} finally {
evt.target.value = '';
}
},
[setPluginContent, setPluginBuffer],
);
const onClose = useCallback(() => {
setPluginContent(null);
setPluginBuffer(null);
}, []);
return (
<>
<input
className="opacity-0 absolute top-0 right-0 h-full w-full cursor-pointer"
type="file"
onChange={onPluginInfo}
/>
{error && <ErrorModal onClose={() => showError('')} message={error} />}
{pluginContent && (
<PluginInfoModal
pluginContent={pluginContent}
onClose={onClose}
onAddPlugin={onAddPlugin}
/>
)}
</>
);
}
export function PluginInfoModalHeader(props: {
className?: string;
children: ReactNode | ReactNode[];
@@ -123,22 +193,6 @@ 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

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

@@ -26,6 +26,7 @@ import {
getMaxSent,
} from '../../utils/storage';
import { MAX_RECV, MAX_SENT } from '../../utils/constants';
import { urlify } from '../../utils/misc';
type Props = {
requestId: string;

View File

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

View File

@@ -1,8 +1,7 @@
import { Level } from 'level';
import { PluginConfig, PluginMetadata, sha256, urlify } from '../../utils/misc';
import { RequestHistory, RequestProgress } from './rpc';
import type { RequestHistory } from './rpc';
import { PluginConfig, PluginMetadata, sha256 } from '../../utils/misc';
import mutex from './mutex';
import { minimatch } from 'minimatch';
const charwise = require('charwise');
export const db = new Level('./ext-db', {
@@ -29,12 +28,6 @@ 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',
});
@@ -112,33 +105,9 @@ 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;
verifierKey: string;
notaryKey?: string;
},
verification: { sent: string; recv: string },
): Promise<RequestHistory | null> {
const existing = await historyDb.get(id);
@@ -188,44 +157,41 @@ export async function getPluginHashes(): Promise<string[]> {
return retVal;
}
export async function getPluginByUrl(url: string): Promise<string | null> {
export async function getPluginByHash(hash: string): Promise<string | null> {
try {
const plugin = await pluginDb.get(url);
const plugin = await pluginDb.get(hash);
return plugin;
} catch (e) {
return null;
}
}
export async function addPlugin(
hex: string,
url: string,
): Promise<string | null> {
export async function addPlugin(hex: string): Promise<string | null> {
const hash = await sha256(hex);
if (await getPluginByUrl(url)) {
return url;
if (await getPluginByHash(hash)) {
return null;
}
await pluginDb.put(url, hex);
await pluginDb.put(hash, hex);
return hash;
}
export async function removePlugin(url: string): Promise<string | null> {
const existing = await pluginDb.get(url);
export async function removePlugin(hash: string): Promise<string | null> {
const existing = await pluginDb.get(hash);
if (!existing) return null;
await pluginDb.del(url);
await pluginDb.del(hash);
return url;
return hash;
}
export async function getPluginConfigByUrl(
url: string,
export async function getPluginConfigByHash(
hash: string,
): Promise<PluginConfig | null> {
try {
const config = await pluginConfigDb.get(url);
const config = await pluginConfigDb.get(hash);
return config;
} catch (e) {
return null;
@@ -233,25 +199,25 @@ export async function getPluginConfigByUrl(
}
export async function addPluginConfig(
url: string,
hash: string,
config: PluginConfig,
): Promise<PluginConfig | null> {
if (await getPluginConfigByUrl(url)) {
if (await getPluginConfigByHash(hash)) {
return null;
}
await pluginConfigDb.put(url, config);
await pluginConfigDb.put(hash, config);
return config;
}
export async function removePluginConfig(
url: string,
hash: string,
): Promise<PluginConfig | null> {
const existing = await pluginConfigDb.get(url);
const existing = await pluginConfigDb.get(hash);
if (!existing) return null;
await pluginConfigDb.del(url);
await pluginConfigDb.del(hash);
return existing;
}
@@ -262,33 +228,27 @@ export async function getPlugins(): Promise<
const hashes = await getPluginHashes();
const ret: (PluginConfig & { hash: string; metadata: PluginMetadata })[] = [];
for (const hash of hashes) {
const config = await getPluginConfigByUrl(hash);
const metadata = await getPluginMetadataByUrl(hash);
const config = await getPluginConfigByHash(hash);
const metadata = await getPluginMetadataByHash(hash);
if (config) {
ret.push({
...config,
hash,
metadata: metadata
? {
...metadata,
hash,
}
: {
filePath: '',
origin: '',
hash,
},
metadata: metadata || {
filePath: '',
origin: '',
},
});
}
}
return ret;
}
export async function getPluginMetadataByUrl(
url: string,
export async function getPluginMetadataByHash(
hash: string,
): Promise<PluginMetadata | null> {
try {
const metadata = await pluginMetadataDb.get(url);
const metadata = await pluginMetadataDb.get(hash);
return metadata;
} catch (e) {
return null;
@@ -296,21 +256,21 @@ export async function getPluginMetadataByUrl(
}
export async function addPluginMetadata(
url: string,
hash: string,
metadata: PluginMetadata,
): Promise<PluginMetadata | null> {
await pluginMetadataDb.put(url, metadata);
await pluginMetadataDb.put(hash, metadata);
return metadata;
}
export async function removePluginMetadata(
url: string,
hash: string,
): Promise<PluginMetadata | null> {
const existing = await pluginMetadataDb.get(url);
const existing = await pluginMetadataDb.get(hash);
if (!existing) return null;
await pluginMetadataDb.del(url);
await pluginMetadataDb.del(hash);
return existing;
}
@@ -353,32 +313,18 @@ export async function clearCookies(host: string) {
});
}
export async function getCookies(link: string, name: string) {
export async function getCookies(host: string, name: string) {
try {
const existing = await cookiesDb.sublevel(link).get(name);
const existing = await cookiesDb.sublevel(host).get(name);
return existing;
} catch (e) {
return null;
}
}
export async function getCookiesByHost(link: string) {
export async function getCookiesByHost(host: string) {
const ret: { [key: string]: string } = {};
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()) {
for await (const [key, value] of cookiesDb.sublevel(host).iterator()) {
ret[key] = value;
}
return ret;
@@ -401,10 +347,10 @@ export async function getConnection(origin: string) {
}
}
export async function setHeaders(link: string, name: string, value?: string) {
export async function setHeaders(host: string, name: string, value?: string) {
if (!value) return null;
return mutex.runExclusive(async () => {
await headersDb.sublevel(link).put(name, value);
await headersDb.sublevel(host).put(name, value);
return true;
});
}
@@ -424,87 +370,20 @@ export async function getHeaders(host: string, name: string) {
return null;
}
}
export async function getHeadersByHost(link: string) {
export async function getHeadersByHost(host: string) {
const ret: { [key: string]: string } = {};
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()) {
for await (const [key, value] of headersDb.sublevel(host).iterator()) {
ret[key] = value;
}
return ret;
}
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> {
async function getDefaultPluginsInstalled(): Promise<boolean> {
return appDb.get(AppDatabaseKey.DefaultPluginsInstalled).catch(() => false);
}
export async function setDefaultPluginsInstalled(
installed: string | boolean = false,
) {
export async function setDefaultPluginsInstalled(installed = false) {
return mutex.runExclusive(async () => {
await appDb.put(AppDatabaseKey.DefaultPluginsInstalled, installed);
});

View File

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

@@ -2,37 +2,28 @@ import { addPlugin, addPluginConfig, addPluginMetadata } from '../db';
import { getPluginConfig } from '../../../utils/misc';
export async function installPlugin(
url: string,
urlOrBuffer: ArrayBuffer | string,
origin = '',
filePath = '',
metadata: {[key: string]: string} = {},
) {
const resp = await fetch(url);
const arrayBuffer = await resp.arrayBuffer();
let arrayBuffer;
if (typeof urlOrBuffer === 'string') {
const resp = await fetch(urlOrBuffer);
arrayBuffer = await resp.arrayBuffer();
} else {
arrayBuffer = urlOrBuffer;
}
const config = await getPluginConfig(arrayBuffer);
const hex = Buffer.from(arrayBuffer).toString('hex');
const hash = await addPlugin(hex, url);
await addPluginConfig(url, config);
await addPluginMetadata(url, {
const hash = await addPlugin(hex);
await addPluginConfig(hash!, config);
await addPluginMetadata(hash!, {
...metadata,
origin,
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 }[]
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,463 +0,0 @@
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 { getPluginByUrl } 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 getPluginByUrl(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 getPluginByUrl(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 getPluginByUrl(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

@@ -1,9 +1,40 @@
import { ContentScriptTypes, RPCClient } from './rpc';
import { PresentationJSON } from '../../utils/types';
import { RequestHistory } from '../Background/rpc';
import { PluginConfig, PluginMetadata } from '../../utils/misc';
import { Proof } from '../../utils/types';
const client = new RPCClient();
class TLSN {
async getHistory(
method: string,
url: string,
metadata?: {
[key: string]: string;
},
): Promise<
(Pick<
RequestHistory,
'id' | 'method' | 'notaryUrl' | 'url' | 'websocketProxyUrl'
> & { time: Date })[]
> {
const resp = await client.call(ContentScriptTypes.get_history, {
method,
url,
metadata,
});
return resp || [];
}
async getProof(id: string): Promise<Proof | null> {
const resp = await client.call(ContentScriptTypes.get_proof, {
id,
});
return resp || null;
}
async notarize(
url: string,
requestOptions?: {
@@ -16,11 +47,12 @@ class TLSN {
websocketProxyUrl?: string;
maxSentData?: number;
maxRecvData?: number;
maxTranscriptSize?: number;
metadata?: {
[k: string]: string;
};
},
): Promise<PresentationJSON> {
): Promise<Proof> {
const resp = await client.call(ContentScriptTypes.notarize, {
url,
method: requestOptions?.method,
@@ -28,6 +60,7 @@ class TLSN {
body: requestOptions?.body,
maxSentData: proofOptions?.maxSentData,
maxRecvData: proofOptions?.maxRecvData,
maxTranscriptSize: proofOptions?.maxTranscriptSize,
notaryUrl: proofOptions?.notaryUrl,
websocketProxyUrl: proofOptions?.websocketProxyUrl,
metadata: proofOptions?.metadata,
@@ -36,10 +69,37 @@ class TLSN {
return resp;
}
async runPlugin(url: string, params?: Record<string, string>) {
const resp = await client.call(ContentScriptTypes.run_plugin_by_url, {
async installPlugin(
url: string,
metadata?: { [k: string]: string },
): Promise<string> {
const resp = await client.call(ContentScriptTypes.install_plugin, {
url,
params,
metadata,
});
return resp;
}
async getPlugins(
url: string,
origin?: string,
metadata?: {
[key: string]: string;
},
): Promise<(PluginConfig & { hash: string; metadata: PluginMetadata })[]> {
const resp = await client.call(ContentScriptTypes.get_plugins, {
url,
origin,
metadata,
});
return resp;
}
async runPlugin(hash: string) {
const resp = await client.call(ContentScriptTypes.run_plugin, {
hash,
});
return resp;
@@ -47,12 +107,14 @@ class TLSN {
}
const connect = async () => {
return new TLSN();
const resp = await client.call(ContentScriptTypes.connect);
if (resp) {
return new TLSN();
}
};
// @ts-ignore
window.tlsn = {
connect,
};
window.dispatchEvent(new CustomEvent('tlsn_loaded'));

View File

@@ -1,4 +1,4 @@
import browser, { browserAction } from 'webextension-polyfill';
import browser from 'webextension-polyfill';
import { ContentScriptRequest, ContentScriptTypes, RPCServer } from './rpc';
import { BackgroundActiontype, RequestHistory } from '../Background/rpc';
import { urlify } from '../../utils/misc';
@@ -7,23 +7,69 @@ 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 },
});
}
server.on(ContentScriptTypes.connect, async () => {
const connected = await browser.runtime.sendMessage({
type: BackgroundActiontype.connect_request,
data: {
...getPopupData(),
},
});
if (!connected) throw new Error('user rejected.');
return connected;
});
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.get_history,
async (
request: ContentScriptRequest<{
method: string;
url: string;
metadata?: { [k: string]: string };
}>,
) => {
const {
method: filterMethod,
url: filterUrl,
metadata,
} = request.params || {};
if (!filterMethod || !filterUrl)
throw new Error('params must include method and url.');
const response: RequestHistory[] = await browser.runtime.sendMessage({
type: BackgroundActiontype.get_history_request,
data: {
...getPopupData(),
method: filterMethod,
url: filterUrl,
metadata,
},
});
}
});
return response;
},
);
server.on(
ContentScriptTypes.get_proof,
async (request: ContentScriptRequest<{ id: string }>) => {
const { id } = request.params || {};
if (!id) throw new Error('params must include id.');
const proof = await browser.runtime.sendMessage({
type: BackgroundActiontype.get_proof_request,
data: {
...getPopupData(),
id,
},
});
return proof;
},
);
server.on(
ContentScriptTypes.notarize,
@@ -38,6 +84,7 @@ import { urlify } from '../../utils/misc';
websocketProxyUrl?: string;
maxSentData?: number;
maxRecvData?: number;
maxTranscriptSize?: number;
}>,
) => {
const {
@@ -47,6 +94,7 @@ import { urlify } from '../../utils/misc';
body,
maxSentData,
maxRecvData,
maxTranscriptSize,
notaryUrl,
websocketProxyUrl,
metadata,
@@ -64,6 +112,7 @@ import { urlify } from '../../utils/misc';
body,
maxSentData,
maxRecvData,
maxTranscriptSize,
notaryUrl,
websocketProxyUrl,
metadata,
@@ -75,23 +124,73 @@ import { urlify } from '../../utils/misc';
);
server.on(
ContentScriptTypes.run_plugin_by_url,
ContentScriptTypes.install_plugin,
async (
request: ContentScriptRequest<{
url: string;
params?: Record<string, string>;
metadata?: { [k: string]: string };
}>,
) => {
const { url, params } = request.params || {};
const { url, metadata } = request.params || {};
if (!url) throw new Error('params must include url');
if (!url) throw new Error('params must include url.');
const response = await browser.runtime.sendMessage({
type: BackgroundActiontype.run_plugin_by_url_request,
const response: RequestHistory[] = await browser.runtime.sendMessage({
type: BackgroundActiontype.install_plugin_request,
data: {
...getPopupData(),
url,
params,
metadata,
},
});
return response;
},
);
server.on(
ContentScriptTypes.get_plugins,
async (
request: ContentScriptRequest<{
url: string;
origin?: string;
metadata?: { [k: string]: string };
}>,
) => {
const {
url: filterUrl,
origin: filterOrigin,
metadata,
} = request.params || {};
if (!filterUrl) throw new Error('params must include url.');
const response = await browser.runtime.sendMessage({
type: BackgroundActiontype.get_plugins_request,
data: {
...getPopupData(),
url: filterUrl,
origin: filterOrigin,
metadata,
},
});
return response;
},
);
server.on(
ContentScriptTypes.run_plugin,
async (request: ContentScriptRequest<{ hash: string }>) => {
const { hash } = request.params || {};
if (!hash) throw new Error('params must include hash');
const response = await browser.runtime.sendMessage({
type: BackgroundActiontype.run_plugin_request,
data: {
...getPopupData(),
hash,
},
});

View File

@@ -1,8 +1,13 @@
import { deferredPromise, PromiseResolvers } from '../../utils/promise';
export enum ContentScriptTypes {
connect = 'tlsn/cs/connect',
get_history = 'tlsn/cs/get_history',
get_proof = 'tlsn/cs/get_proof',
notarize = 'tlsn/cs/notarize',
run_plugin_by_url = 'tlsn/cs/run_plugin_by_url',
install_plugin = 'tlsn/cs/install_plugin',
get_plugins = 'tlsn/cs/get_plugins',
run_plugin = 'tlsn/cs/run_plugin',
}
export type ContentScriptRequest<params> = {

View File

@@ -1,56 +1,131 @@
import React, { useEffect } from 'react';
import * as Comlink from 'comlink';
import { OffscreenActionTypes } from './types';
import { BackgroundActiontype } from '../Background/rpc';
import {
initThreads,
onCreatePresentationRequest,
onCreateProverRequest,
onNotarizationRequest,
onProcessProveRequest,
onVerifyProof,
onVerifyProofRequest,
startP2PProver,
startP2PVerifier,
} from './rpc';
NotaryServer,
Prover as _Prover,
NotarizedSession as _NotarizedSession,
TlsProof as _TlsProof,
} 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 { Proof, ProofV1 } from '../../utils/types';
import { Method } from 'tlsn-js/wasm/pkg';
const { init, Prover, NotarizedSession, TlsProof }: any = Comlink.wrap(
new Worker(new URL('./worker.ts', import.meta.url)),
);
const Offscreen = () => {
useEffect(() => {
(async () => {
await initThreads();
const loggingLevel = await browser.runtime.sendMessage({
type: BackgroundActiontype.get_logging_level,
});
await init({ loggingLevel });
// @ts-ignore
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
switch (request.type) {
case OffscreenActionTypes.notarization_request: {
onNotarizationRequest(request);
break;
}
case OffscreenActionTypes.create_prover_request: {
onCreateProverRequest(request);
break;
}
case OffscreenActionTypes.create_presentation_request: {
onCreatePresentationRequest(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,
},
});
}
})();
break;
}
case BackgroundActiontype.process_prove_request: {
onProcessProveRequest(request);
const { id } = request.data;
(async () => {
try {
const proof = await createProof(request.data);
browser.runtime.sendMessage({
type: BackgroundActiontype.finish_prove_request,
data: {
id,
proof: proof,
},
});
} catch (error) {
console.error(error);
browser.runtime.sendMessage({
type: BackgroundActiontype.finish_prove_request,
data: {
id,
error,
},
});
}
})();
break;
}
case BackgroundActiontype.verify_proof: {
onVerifyProof(request, sendResponse);
(async () => {
const result = await verifyProof(request.data);
sendResponse(result);
})();
return true;
}
case BackgroundActiontype.verify_prove_request: {
onVerifyProofRequest(request);
break;
}
case OffscreenActionTypes.start_p2p_verifier: {
startP2PVerifier(request);
break;
}
case OffscreenActionTypes.start_p2p_prover: {
startP2PProver(request);
(async () => {
const proof: Proof = 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,
},
},
});
})();
break;
}
default:
@@ -64,3 +139,162 @@ 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<ProofV1> {
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: _Prover = 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 session: _NotarizedSession = await new NotarizedSession(
await prover.notarize(commit),
);
const proofHex = await session.proof(commit);
const proof: ProofV1 = {
version: '1.0',
meta: {
notaryUrl,
websocketProxyUrl,
},
data: proofHex,
};
return proof;
}
async function verifyProof(
proof: Proof,
): Promise<{ sent: string; recv: string }> {
let result: { sent: string; recv: string };
switch (proof.version) {
case undefined: {
result = await verify(proof);
break;
}
case '1.0': {
const tlsProof: _TlsProof = await new TlsProof(proof.data);
result = await tlsProof.verify({
typ: 'P256',
key: await NotaryServer.from(proof.meta.notaryUrl).publicKey(),
});
break;
}
}
return result;
}

View File

@@ -1,623 +0,0 @@
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 {
pluginUrl,
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: pluginUrl,
serverDns: hostname,
maxSentData,
maxRecvData,
});
browser.runtime.sendMessage({
type: BackgroundActiontype.prover_instantiated,
data: {
pluginUrl,
},
});
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: {
pluginUrl,
},
});
await proverSetup;
browser.runtime.sendMessage({
type: BackgroundActiontype.prover_started,
data: {
pluginUrl,
},
});
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: {
pluginUrl,
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,15 +1,4 @@
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

@@ -1,39 +0,0 @@
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,9 +1,9 @@
import * as Comlink from 'comlink';
import init, { Prover, Presentation, Verifier } from 'tlsn-js';
import init, { Prover, NotarizedSession, TlsProof } from 'tlsn-js';
Comlink.expose({
init,
Prover,
Presentation,
Verifier,
NotarizedSession,
TlsProof,
});

View File

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

View File

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

View File

@@ -4,11 +4,6 @@ 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,6 +1,7 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import './sidePanel.scss';
import browser from 'webextension-polyfill';
import { fetchPluginConfigByHash, runPlugin } from '../../utils/rpc';
import {
getPluginConfig,
hexToArrayBuffer,
@@ -8,81 +9,36 @@ 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,
progressText,
RequestProgress,
} from '../Background/rpc';
import { getPluginByUrl, getPluginConfigByUrl } from '../Background/db';
import { BackgroundActiontype } from '../Background/rpc';
import { getPluginByHash, getPluginConfigByHash } from '../Background/db';
import type { Plugin } from '@extism/extism';
import { OffscreenActionTypes } from '../Offscreen/types';
import { SidePanelActionTypes } from './types';
import { fetchP2PState, useClientId } from '../../reducers/p2p';
export default function SidePanel(): ReactElement {
const [config, setConfig] = useState<PluginConfig | null>(null);
const [url, setUrl] = 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();
const [hash, setHash] = useState('');
useEffect(() => {
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 getPluginConfigByUrl(data.pluginUrl));
setUrl(data.pluginUrl);
setParams(data.pluginParams);
setStarted(true);
break;
}
case SidePanelActionTypes.run_p2p_plugin_request: {
const { pluginHash, plugin } = data;
const config =
(await getPluginConfigByUrl(pluginHash)) ||
(await getPluginConfig(hexToArrayBuffer(plugin)));
setUrl(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);
setUrl('');
setHex('');
setStarted(false);
break;
}
}
});
(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: '' });
})();
}, []);
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 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 relative gap-2 h-9 p-2 cursor-default justify-center bg-slate-300 w-full">
<img className="h-5" src={logo} alt="logo" />
<button
className="button absolute right-2"
@@ -91,37 +47,18 @@ export default function SidePanel(): ReactElement {
Close
</button>
</div>
{/*{!config && <PluginList />}*/}
{started && config && (
<PluginBody
url={url}
hex={hex}
config={config}
p2p={p2p}
clientId={clientId}
presetParameterValues={params}
/>
)}
{!config && <PluginList />}
{config && <PluginBody hash={hash} config={config} />}
</div>
);
}
function PluginBody({
url,
hex,
config,
p2p,
clientId,
presetParameterValues,
}: {
function PluginBody(props: {
config: PluginConfig;
url: string;
hex?: string;
clientId?: string;
p2p?: boolean;
presetParameterValues?: Record<string, string>;
hash: string;
}): ReactElement {
const { title, description, icon, steps } = config;
const { hash } = props;
const { title, description, icon, steps } = props.config;
const [responses, setResponses] = useState<any[]>([]);
const [notarizationId, setNotarizationId] = useState('');
const notaryRequest = useRequestHistory(notarizationId);
@@ -135,7 +72,7 @@ function PluginBody({
setNotarizationId(response);
}
},
[url, responses],
[hash, responses],
);
useEffect(() => {
@@ -143,7 +80,7 @@ function PluginBody({
browser.runtime.sendMessage({
type: SidePanelActionTypes.execute_plugin_response,
data: {
url,
hash,
proof: notaryRequest.proof,
},
});
@@ -151,12 +88,12 @@ function PluginBody({
browser.runtime.sendMessage({
type: SidePanelActionTypes.execute_plugin_response,
data: {
url,
hash,
error: notaryRequest.error,
},
});
}
}, [url, notaryRequest?.status]);
}, [hash, notaryRequest?.status]);
return (
<div className="flex flex-col p-4">
@@ -172,17 +109,11 @@ function PluginBody({
<div className="flex flex-col items-start gap-8 mt-8">
{steps?.map((step, i) => (
<StepContent
key={i}
url={url}
config={config}
hex={hex}
hash={hash}
index={i}
setResponse={setResponse}
lastResponse={i > 0 ? responses[i - 1] : undefined}
responses={responses}
p2p={p2p}
clientId={clientId}
parameterValues={presetParameterValues}
{...step}
/>
))}
@@ -193,16 +124,11 @@ function PluginBody({
function StepContent(
props: StepConfig & {
url: string;
hex?: string;
clientId?: string;
hash: string;
index: number;
setResponse: (resp: any, i: number) => void;
responses: any[];
lastResponse?: any;
config: PluginConfig;
p2p?: boolean;
parameterValues?: Record<string, string>;
},
): ReactElement {
const {
@@ -214,12 +140,7 @@ function StepContent(
setResponse,
lastResponse,
prover,
url,
hex: _hex,
config,
p2p = false,
clientId = '',
parameterValues,
hash,
} = props;
const [completed, setCompleted] = useState(false);
const [pending, setPending] = useState(false);
@@ -228,10 +149,11 @@ function StepContent(
const notaryRequest = useRequestHistory(notarizationId);
const getPlugin = useCallback(async () => {
const hex = (await getPluginByUrl(url)) || _hex;
const hex = await getPluginByHash(hash);
const config = await getPluginConfigByHash(hash);
const arrayBuffer = hexToArrayBuffer(hex!);
return makePlugin(arrayBuffer, config, { p2p, clientId });
}, [url, _hex, config, p2p, clientId]);
return makePlugin(arrayBuffer, config!);
}, [hash]);
const processStep = useCallback(async () => {
const plugin = await getPlugin();
@@ -242,12 +164,7 @@ function StepContent(
setError('');
try {
const out = await plugin.call(
action,
index > 0
? JSON.stringify(lastResponse)
: JSON.stringify(parameterValues),
);
const out = await plugin.call(action, JSON.stringify(lastResponse));
const val = JSON.parse(out.string());
if (val && prover) {
setNotarizationId(val);
@@ -292,37 +209,13 @@ 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 (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) {
if (completed) {
btnContent = (
<button
className={classNames(
@@ -342,32 +235,15 @@ function StepContent(
)}
onClick={viewProofInPopup}
>
<span className="text-sm">View</span>
<span className="text-sm">View Proof</span>
</button>
);
} else if (notaryRequest?.status === 'pending' || pending || notarizationId) {
btnContent = (
<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>
<button className="button mt-2 w-fit flex flex-row flex-nowrap items-center gap-2 cursor-default">
<Icon className="animate-spin" fa="fa-solid fa-spinner" size={1} />
<span className="text-sm">{cta}</span>
</button>
);
} else {
btnContent = (

View File

@@ -1,10 +1,4 @@
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',
}

View File

@@ -1,74 +0,0 @@
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;
};

63
src/pages/Chat/chat.css Normal file
View File

@@ -0,0 +1,63 @@
.chat-container {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.chat-window {
height: 400px;
border: 1px solid #ccc;
overflow-y: auto;
padding: 10px;
margin-bottom: 20px;
}
.message {
margin-bottom: 10px;
padding: 8px 12px;
border-radius: 20px;
max-width: 70%;
}
.user {
background-color: #007bff;
color: white;
align-self: flex-end;
margin-left: auto;
}
.bot {
background-color: #f1f0f0;
color: black;
align-self: flex-start;
}
.chat-input {
display: flex;
}
.chat-input input {
flex-grow: 1;
padding: 10px;
font-size: 16px;
}
.chat-input button {
padding: 10px 20px;
font-size: 16px;
background-color: #007bff;
color: white;
border: none;
cursor: pointer;
}
.chat-buttons {
display: flex;
gap: 10px;
}
.clear-button {
background-color: #f44336;
color: white;
border: none;
}

303
src/pages/Chat/index.tsx Normal file
View File

@@ -0,0 +1,303 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import './Chat.css';
import { useRequests } from '../../reducers/requests';
import { extractBodyFromResponse } from '../../utils/misc';
interface Message {
id: number;
text: string;
sender: 'user' | 'bot';
}
interface CapturedData {
request: string;
headers: Record<string, string>;
response: string;
}
interface RequestData {
method: string;
url: string;
headers: Record<string, string>;
body?: string;
}
interface TabInfo {
url: string;
title: string;
favicon: string;
}
const Chat: React.FC = () => {
const [messages, setMessages] = useState<Message[]>(() => {
const savedMessages = localStorage.getItem('chatMessages');
return savedMessages ? JSON.parse(savedMessages) : [];
});
const [inputMessage, setInputMessage] = useState('');
const [allRequests, setAllRequests] = useState<RequestData[]>([]);
const [isConnected, setIsConnected] = useState(false);
const socketRef = useRef<WebSocket | null>(null);
const [chatId, setChatId] = useState<string | null>(null);
const requests = useRequests();
const [capturedData, setCapturedData] = useState<CapturedData[]>([]);
const [hasSetInitialTabInfo, setHasSetInitialTabInfo] = useState(false);
useEffect(() => {
localStorage.setItem('chatMessages', JSON.stringify(messages));
}, [messages]);
const getCurrentTabInfo = async (): Promise<TabInfo> => {
return new Promise((resolve) => {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
const currentTab = tabs[0];
resolve({
url: currentTab.url || '',
title: currentTab.title || '',
favicon: currentTab.favIconUrl || ''
});
});
});
};
const initializeChat = async () => {
const storedChatId = localStorage.getItem('chatId');
if (storedChatId) {
setChatId(storedChatId);
await connectWebSocket(storedChatId);
} else {
await fetchNewChatId();
}
};
useEffect(() => {
initializeChat();
return () => {
if (socketRef.current) {
socketRef.current.close();
}
};
}, []);
// Effect to set initial tab info when connection is established
useEffect(() => {
const setInitialTabInfo = async () => {
if (isConnected && messages.length === 0 && !hasSetInitialTabInfo) {
const tabInfo = await getCurrentTabInfo();
setInputMessage(`Current Page: ${tabInfo.title}\n website URL: ${tabInfo.url}`);
setHasSetInitialTabInfo(true);
// Send initial info to background script
chrome.runtime.sendMessage({
type: 'TAB_INFO',
data: tabInfo
});
}
};
setInitialTabInfo();
}, [isConnected, messages.length, hasSetInitialTabInfo]);
const fetchNewChatId = async () => {
try {
const response = await fetch('http://localhost:8000/get_chat_id');
const data = await response.json();
const newChatId = data.chat_id;
localStorage.setItem('chatId', newChatId);
setChatId(newChatId);
await connectWebSocket(newChatId);
} catch (error) {
console.error('Failed to fetch chat ID:', error);
}
};
const captureRequestAndResponse = useCallback(async (req: RequestData) => {
try {
const response = await fetch(req.url, {
method: req.method,
headers: req.headers,
body: req.body,
});
const responseText = await extractBodyFromResponse(response);
const headers: Record<string, string> = {};
response.headers.forEach((value, key) => {
headers[key] = value;
});
setCapturedData(prevData => [...prevData, {
request: `${req.method} ${req.url}`,
headers,
response: responseText,
}]);
} catch (error) {
console.error('Error capturing request and response:', error);
}
}, []);
const fetchMultipleRequests = async (requests: RequestData[]) => {
try {
const fetchPromises = requests.map(async (req) => {
if (req.headers === null || req.headers === undefined) {
req.headers = {};
}
if (req.body === null || req.body === undefined) {
req.body = '';
}
const response = await fetch(req.url, {
method: req.method,
headers: req.headers,
});
const responseText = await response.text();
return {
request: `${req.method} ${req.url}`,
headers: req.headers,
response: responseText,
};
});
const responses = await Promise.all(fetchPromises);
setCapturedData(prevData => [...prevData, ...responses]);
const response_message = responses.map(data => data.response).join('\n');
setInputMessage(response_message);
} catch (error) {
console.error('Error fetching multiple requests:', error);
}
};
const connectWebSocket = async (id: string) => {
return new Promise<void>((resolve, reject) => {
socketRef.current = new WebSocket(`ws://localhost:8000/ws/${id}`);
socketRef.current.onopen = () => {
console.log('WebSocket connection established');
setIsConnected(true);
resolve();
};
socketRef.current.onmessage = (event) => {
const botResponse: Message = {
id: Date.now(),
text: event.data,
sender: 'bot',
};
setMessages((prevMessages) => [...prevMessages, botResponse]);
if (botResponse.text.includes("send_request_function")) {
const updatedRequests = requests.map(req => ({
method: req.method,
url: req.url,
headers: req.requestHeaders.reduce((acc: { [key: string]: string }, h: any) => {
if (h.name && h.value) acc[h.name] = h.value;
return acc;
}, {}),
}));
setAllRequests(updatedRequests);
const requestDetails = updatedRequests.map(req =>
`${req.method} ${req.url}\nHeaders: ${JSON.stringify(req.headers, null, 2)}`
).join('\n\n');
setInputMessage(requestDetails);
}
if (botResponse.text.includes("send_response_function")) {
const regex = /"send_response_function"\s*:\s*(\[.*?\])/s;
const match = botResponse.text.match(regex);
if (match) {
const requestArrayString = match[1];
try {
const requestArray: RequestData[] = JSON.parse(requestArrayString);
fetchMultipleRequests(requestArray);
} catch (error) {
console.error("Error parsing JSON:", error);
}
}
}
};
socketRef.current.onclose = () => {
console.log('WebSocket connection closed');
setIsConnected(false);
};
socketRef.current.onerror = (error) => {
console.error('WebSocket error:', error);
reject(error);
};
});
};
useEffect(() => {
if (capturedData.length > 0 && isConnected) {
const capturedDataMessage = JSON.stringify(capturedData);
socketRef.current?.send(capturedDataMessage);
setCapturedData([]);
}
}, [capturedData, isConnected]);
const sendMessage = () => {
if (inputMessage.trim() === '' || !isConnected) return;
const newMessage: Message = {
id: Date.now(),
text: inputMessage,
sender: 'user',
};
setMessages((prevMessages) => [...prevMessages, newMessage]);
setInputMessage('');
if (socketRef.current && socketRef.current.readyState === WebSocket.OPEN) {
socketRef.current.send(inputMessage);
} else {
console.error('WebSocket is not connected');
}
};
const clearChat = () => {
setMessages([]);
setAllRequests([]);
setCapturedData([]);
setHasSetInitialTabInfo(false);
};
return (
<div className="chat-container">
<div className="chat-window">
{messages.map((message) => (
<div
key={message.id}
className={`message ${message.sender === 'user' ? 'user' : 'bot'}`}
>
{message.text}
</div>
))}
</div>
<div className="chat-input">
<input
type="text"
value={inputMessage}
onChange={(e) => setInputMessage(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
placeholder="Type your message..."
className="chat-input-field"
/>
<div className="chat-buttons">
<button
onClick={sendMessage}
className="send-button"
disabled={!isConnected}
>
Send
</button>
<button
onClick={clearChat}
className="clear-button"
style={{ backgroundColor: '#f44336', color: 'white', border: 'none' }}
>
Clear Chat
</button>
</div>
</div>
{!isConnected && <div className="connection-status">Disconnected</div>}
</div>
);
};
export default Chat;

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,32 +1,33 @@
import React, { ReactElement, useState, useCallback } from 'react';
import React, { ReactElement, useState, useCallback, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { useNavigate } from 'react-router';
import { useHistoryOrder, useRequestHistory } from '../../reducers/history';
import Icon from '../../components/Icon';
import NotarizeIcon from '../../assets/img/notarize.png';
import { urlify } from '../../utils/misc';
import {
BackgroundActiontype,
progressText,
RequestProgress,
} from '../../entries/Background/rpc';
useHistoryOrder,
useRequestHistory,
deleteRequestHistory,
} 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 Modal, { ModalContent } from '../../components/Modal/Modal';
import classNames from 'classnames';
import dayjs from 'dayjs';
import RequestMenu from './request-menu';
import copy from 'copy-to-clipboard';
import { EXPLORER_API } from '../../utils/constants';
import {
getNotaryRequest,
setNotaryRequestCid,
} from '../../entries/Background/db';
const charwise = require('charwise');
export default function History(): ReactElement {
const history = useHistoryOrder();
return (
<div className="flex flex-col flex-nowrap overflow-y-auto pb-36">
{history
.map((id) => {
return <OneRequestHistory key={id} requestId={id} />;
})
.reverse()}
<div className="flex flex-col flex-nowrap overflow-y-auto">
{history.map((id) => {
return <OneRequestHistory key={id} requestId={id} />;
})}
</div>
);
}
@@ -40,11 +41,42 @@ export function OneRequestHistory(props: {
const dispatch = useDispatch();
const request = useRequestHistory(props.requestId);
const [showingError, showError] = useState(false);
const [showingMenu, showMenu] = useState(false);
const [uploadError, setUploadError] = useState('');
const [showingShareConfirmation, setShowingShareConfirmation] =
useState(false);
const [cid, setCid] = useState<{ [key: string]: string }>({});
const [uploading, setUploading] = useState(false);
const navigate = useNavigate();
const { status } = request || {};
const requestUrl = urlify(request?.url || '');
useEffect(() => {
const fetchData = async () => {
try {
const request = await getNotaryRequest(props.requestId);
if (request && request.cid) {
setCid({ [props.requestId]: request.cid });
}
} catch (e) {
console.error('Error fetching data', e);
}
};
fetchData();
}, []);
const onRetry = useCallback(async () => {
const notaryUrl = await 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,
@@ -53,97 +85,151 @@ 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);
}, [showError]);
}, [setShowingShareConfirmation, showError]);
const day = dayjs(charwise.decode(props.requestId, 'hex'));
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]);
return (
<div
className={classNames(
'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',
},
'flex flex-row flex-nowrap border rounded-md p-2 gap-1 hover:bg-slate-50 cursor-pointer',
props.className,
)}
onClick={() => {
if (status === 'success') onView();
if (status === 'error') onShowError();
}}
>
<ShareConfirmationModal />
<ErrorModal />
<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 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>
<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>
</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 Proof"
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>
<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={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 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 ? (
@@ -154,7 +240,7 @@ export function OneRequestHistory(props: {
onClose={closeAllModal}
>
<ModalContent className="flex justify-center items-center text-slate-500">
{msg || request?.errorMessage}
{msg || 'Something went wrong :('}
</ModalContent>
<button
className="m-0 w-24 bg-red-100 text-red-300 hover:bg-red-200 hover:text-red-500"
@@ -165,4 +251,99 @@ 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

@@ -1,307 +0,0 @@
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

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

View File

@@ -1,104 +1,84 @@
import React, {
MouseEventHandler,
ReactElement,
useEffect,
useRef,
ReactNode,
useState,
} from 'react';
import Icon from '../../components/Icon';
import classNames from 'classnames';
import { useNavigate } from 'react-router';
import { useRequests } from '../../reducers/requests';
import { PluginList } from '../../components/PluginList';
import PluginUploadInfo from '../../components/PluginInfo';
import { ErrorModal } from '../../components/ErrorModal';
import History from '../History';
import './index.scss';
import Requests from '../Requests';
import { fetchPluginHashes } from '../../utils/rpc';
export default function Home(props: {
tab?: 'history' | 'network';
}): ReactElement {
export default function Home(): ReactElement {
const requests = useRequests();
const navigate = useNavigate();
const [error, showError] = useState('');
const [tab, setTab] = useState<'history' | 'network'>(props.tab || 'history');
const scrollableContent = useRef<HTMLDivElement | null>(null);
const [shouldFix, setFix] = useState(false);
useEffect(() => {
fetchPluginHashes();
}, []);
useEffect(() => {
const element = scrollableContent.current;
if (!element) return;
const onScroll = () => {
if (element.scrollTop > 0) {
setFix(true);
} else {
setFix(false);
}
};
element.addEventListener('scroll', onScroll);
return () => {
element.removeEventListener('scroll', onScroll);
};
}, [scrollableContent]);
return (
<div
id="home"
ref={scrollableContent}
className="flex flex-col flex-grow overflow-y-auto"
>
<div className="flex flex-col gap-4 py-4 overflow-y-auto">
{error && <ErrorModal onClose={() => showError('')} message={error} />}
<div
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')}
selected={tab === 'network'}
>
Network
</TabSelector>
<TabSelector
onClick={() => setTab('history')}
selected={tab === 'history'}
<div className="flex flex-col flex-nowrap justify-center gap-2 mx-4">
<NavButton fa="fa-solid fa-table" onClick={() => navigate('/requests')}>
<span>Requests</span>
<span>{`(${requests.length})`}</span>
</NavButton>
<NavButton fa="fa-solid fa-hammer" onClick={() => navigate('/custom')}>
Custom
</NavButton>
<NavButton
fa="fa-solid fa-certificate"
onClick={() => navigate('/verify')}
>
Verify
</NavButton>
<NavButton fa="fa-solid fa-list" onClick={() => navigate('/history')}>
History
</TabSelector>
</div>
<div className="flex-grow">
{tab === 'history' && <History />}
{tab === 'network' && <Requests shouldFix={shouldFix} />}
</NavButton>
<NavButton className="relative" fa="fa-solid fa-plus">
<PluginUploadInfo />
Add a plugin
</NavButton>
<NavButton fa="fa-solid fa-gear" onClick={() => navigate('/options')}>
Options
</NavButton>
<NavButton fa="fa-solid fa-comment-dots" onClick={() => navigate('/chat')}>
Chat
</NavButton>
</div>
<PluginList className="mx-4" />
</div>
);
}
function TabSelector(props: {
children: string;
function NavButton(props: {
fa: string;
children?: ReactNode;
onClick?: MouseEventHandler;
className?: string;
selected?: boolean;
onClick: MouseEventHandler;
disabled?: boolean;
}): ReactElement {
return (
<button
onClick={props.onClick}
className={classNames(
'flex flex-grow items-center justify-center p-2 font-semibold hover:text-slate-700 border-b-2 ',
'flex flex-row flex-nowrap items-center justify-center',
'text-white rounded px-2 py-1 gap-1',
{
'font-semibold text-slate-400 border-white': !props.selected,
'font-bold text-primary border-primary': props.selected,
'bg-primary/[.8] hover:bg-primary/[.7] active:bg-primary':
!props.disabled,
'bg-primary/[.5]': props.disabled,
},
props.className,
)}
onClick={props.onClick}
disabled={props.disabled}
>
{props.children}
<Icon className="flex-grow-0 flex-shrink-0" fa={props.fa} size={1} />
<span className="flex-grow flex-shrink w-0 flex-grow font-bold">
{props.children}
</span>
</button>
);
}

View File

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

View File

@@ -6,7 +6,6 @@ import React, {
ReactEventHandler,
useEffect,
useRef,
useMemo,
} from 'react';
import { useNavigate, useParams } from 'react-router';
import { notarizeRequest, useRequest } from '../../reducers/requests';
@@ -20,6 +19,8 @@ import {
} from '../../utils/storage';
import { useDispatch } from 'react-redux';
const maxTranscriptSize = 16384;
export default function Notarize(): ReactElement {
const params = useParams<{ requestId: string }>();
const req = useRequest(params.requestId);
@@ -57,6 +58,7 @@ export default function Notarize(): ReactElement {
body: req.requestBody,
maxSentData,
maxRecvData,
maxTranscriptSize,
notaryUrl,
websocketProxyUrl,
secretHeaders,
@@ -279,79 +281,34 @@ export function RevealHeaderTable(props: {
);
}
export function HideResponseStep(props: {
function HideResponseStep(props: {
onNext: () => void;
onCancel: () => void;
setSecretResps: (secrets: string[]) => void;
}): React.ReactElement {
}): ReactElement {
const params = useParams<{ requestId: string }>();
const req = useRequest(params.requestId);
const [responseText, setResponseText] = useState('');
const [redactedRanges, setRedactedRanges] = useState<
{ start: number; end: number }[]
>([]);
const [isRedactMode, setIsRedactMode] = useState(true);
const [start, setStart] = useState(0);
const [end, setEnd] = useState(0);
const taRef = useRef<HTMLTextAreaElement | null>(null);
const onSelectionChange: React.MouseEventHandler<HTMLTextAreaElement> =
useCallback(
(e) => {
const ta = e.currentTarget;
if (isRedactMode && ta.selectionEnd > ta.selectionStart) {
const newRange: { start: number; end: number } = {
start: ta.selectionStart,
end: ta.selectionEnd,
};
setRedactedRanges((prevRanges) => {
let updatedRanges = [...prevRanges, newRange].sort(
(a, b) => a.start - b.start,
);
updatedRanges = mergeRanges(updatedRanges);
const secretResps = updatedRanges
.map(({ start, end }) => responseText.substring(start, end))
.filter((d) => !!d);
props.setSecretResps(secretResps);
return updatedRanges;
});
} else if (!isRedactMode) {
const clickPosition = ta.selectionStart;
setRedactedRanges((prevRanges) => {
const updatedRanges = prevRanges.filter(
({ start, end }) => clickPosition < start || clickPosition > end,
);
const secretResps = updatedRanges
.map(({ start, end }) => responseText.substring(start, end))
.filter((d) => !!d);
props.setSecretResps(secretResps);
return updatedRanges;
});
}
},
[responseText, props, isRedactMode],
);
const mergeRanges = (
ranges: { start: number; end: number }[],
): { start: number; end: number }[] => {
if (ranges.length === 0) return [];
const mergedRanges: { start: number; end: number }[] = [ranges[0]];
for (let i = 1; i < ranges.length; i++) {
const lastRange = mergedRanges[mergedRanges.length - 1];
if (ranges[i].start <= lastRange.end) {
lastRange.end = Math.max(lastRange.end, ranges[i].end);
} else {
mergedRanges.push(ranges[i]);
const onSelectionChange: ReactEventHandler<HTMLTextAreaElement> = useCallback(
(e) => {
const ta = e.currentTarget;
if (ta.selectionEnd > ta.selectionStart) {
setStart(ta.selectionStart);
setEnd(ta.selectionEnd);
props.setSecretResps(
[
responseText.substring(0, ta.selectionStart),
responseText.substring(ta.selectionEnd, responseText.length),
].filter((d) => !!d),
);
}
}
return mergedRanges;
};
},
[responseText],
);
useEffect(() => {
if (!req) return;
@@ -387,46 +344,37 @@ export function HideResponseStep(props: {
if (current) {
current.focus();
current.setSelectionRange(start, end);
}
}, [taRef]);
}, [taRef, start, end]);
if (!req) return <></>;
const shieldedText = responseText.split('');
redactedRanges.forEach(({ start, end }) => {
for (let i = start; i < end; i++) {
shieldedText[i] = '*';
}
});
let shieldedText = '';
if (end > start) {
shieldedText = Array(start)
.fill('*')
.join('')
.concat(responseText.substring(start, end))
.concat(
Array(responseText.length - end)
.fill('*')
.join(''),
);
}
return (
<div className="flex flex-col flex-nowrap flex-shrink flex-grow h-0">
<div className="border bg-primary/[0.9] text-white border-slate-300 py-1 px-2 font-semibold">
Step 2 of 2:{' '}
{isRedactMode
? 'Highlight text to redact selected portions'
: 'Click redacted text to unredact'}
</div>
<div className="flex flex-row justify-end p-0.5 gap-2 border-t">
<button
className={`bg-${isRedactMode ? 'red-500' : 'green-500'} text-white font-bold hover:bg-${isRedactMode ? 'red-400' : 'green-400'} px-2 py-0.5 active:bg-${isRedactMode ? 'red-600' : 'green-600'}`}
onClick={() => setIsRedactMode(!isRedactMode)}
>
{isRedactMode ? 'Unredact Text' : 'Redact Text'}
</button>
<button
className="bg-gray-500 text-white font-bold hover:bg-gray-400 px-2 py-0.5 active:bg-gray-600"
onClick={() => setRedactedRanges([])}
>
Unredact All
</button>
Step 2 of 2: Highlight text to show only selected text from response
</div>
<div className="flex flex-col flex-grow flex-shrink h-0 overflow-y-auto p-2">
<textarea
ref={taRef}
className="flex-grow textarea bg-slate-100 font-mono"
value={shieldedText.join('')}
onMouseUp={onSelectionChange}
value={shieldedText || responseText}
onSelect={onSelectionChange}
/>
</div>
<div className="flex flex-row justify-end p-2 gap-2 border-t">

View File

@@ -17,8 +17,6 @@ import {
getProxyApi,
getLoggingFilter,
LOGGING_FILTER_KEY,
getRendezvousApi,
RENDEZVOUS_API_LS_KEY,
} from '../../utils/storage';
import {
EXPLORER_API,
@@ -26,7 +24,6 @@ import {
NOTARY_PROXY,
MAX_RECV,
MAX_SENT,
RENDEZVOUS_API,
} from '../../utils/constants';
import Modal, { ModalContent } from '../../components/Modal/Modal';
import browser from 'webextension-polyfill';
@@ -39,7 +36,6 @@ 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);
@@ -48,17 +44,11 @@ export default function Options(): ReactElement {
useEffect(() => {
(async () => {
setNotary(await getNotaryApi());
setProxy(await getProxyApi());
})();
}, []);
useEffect(() => {
(async () => {
setNotary((await getNotaryApi()) || NOTARY_API);
setProxy((await getProxyApi()) || NOTARY_PROXY);
setMaxReceived((await getMaxRecv()) || MAX_RECV);
setMaxSent((await getMaxSent()) || MAX_SENT);
setLoggingLevel((await getLoggingFilter()) || 'Info');
setRendezvous((await getRendezvousApi()) || RENDEZVOUS_API);
})();
}, [advanced]);
@@ -73,18 +63,9 @@ 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,
rendezvous,
shouldReload,
],
[notary, proxy, maxSent, maxReceived, loggingLevel, shouldReload],
);
const onSaveAndReload = useCallback(
@@ -99,12 +80,8 @@ export default function Options(): ReactElement {
setAdvanced(!advanced);
}, [advanced]);
const openInTab = useCallback((url: string) => {
browser.tabs.create({ url });
}, []);
return (
<div className="flex flex-col flex-nowrap flex-grow overflow-y-auto">
<div className="flex flex-col flex-nowrap flex-grow">
{showReloadModal && (
<Modal
className="flex flex-col items-center text-base cursor-default justify-center !w-auto mx-4 my-[50%] p-4 gap-4"
@@ -164,8 +141,6 @@ 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">
@@ -177,22 +152,6 @@ export default function Options(): ReactElement {
Save
</button>
</div>
<div className="flex flex-col w-full items-end gap-2 p-2">
<button
className="button"
onClick={() =>
openInTab('https://github.com/tlsnotary/tlsn-extension/issues/new')
}
>
File an issue
</button>
<button
className="button"
onClick={() => openInTab('https://discord.gg/9XwESXtcN7')}
>
Join our Discord
</button>
</div>
</div>
);
}
@@ -269,13 +228,11 @@ 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,
@@ -286,8 +243,6 @@ function AdvancedOptions(props: {
setLoggingLevel,
loggingLevel,
setShouldReload,
rendezvous,
setRendezvous,
} = props;
return (
@@ -312,15 +267,6 @@ 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

@@ -1,497 +0,0 @@
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 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

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

View File

@@ -7,17 +7,14 @@ 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
@@ -29,19 +26,8 @@ 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;
verifierKey?: string;
notaryKey?: string;
}
>({
.sendMessage<any, { recv: string; sent: string }>({
type: BackgroundActiontype.verify_proof,
data: proof,
})
@@ -62,15 +48,7 @@ export default function ProofUploader(): ReactElement {
);
if (proof) {
return (
<ProofViewer
recv={proof.recv}
sent={proof.sent}
verifierKey={proof.verifierKey}
notaryKey={proof.notaryKey}
info={metadata}
/>
);
return <ProofViewer recv={proof.recv} sent={proof.sent} />;
}
return (

View File

@@ -2,99 +2,41 @@ import React, {
ReactNode,
ReactElement,
useState,
useEffect,
MouseEventHandler,
useCallback,
} from 'react';
import { useParams, useNavigate } from 'react-router';
import c from 'classnames';
import {
deleteRequestHistory,
useRequestHistory,
} from '../../reducers/history';
import { useRequestHistory } from '../../reducers/history';
import Icon from '../../components/Icon';
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';
import { download } from '../../utils/misc';
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={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 w-full py-2 gap-2 flex-grow">
<div className="flex flex-col px-2">
<div className="flex flex-row gap-2 items-center">
{!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"
/>
)}
<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
@@ -107,12 +49,6 @@ export default function ProofViewer(props?: {
Download
</button>
)}
<button
className="button !text-red-500"
onClick={() => setShowRemoveModal(true)}
>
Delete
</button>
</div>
</div>
</div>
@@ -131,61 +67,11 @@ 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;
@@ -204,20 +90,3 @@ 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

@@ -192,6 +192,7 @@ export default function RequestBuilder(props?: {
maxRecvData,
secretHeaders: [],
secretResps: [],
maxTranscriptSize: 0,
notaryUrl,
websocketProxyUrl,
},

View File

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

View File

@@ -7,91 +7,62 @@ import { BackgroundActiontype } from '../../entries/Background/rpc';
import { BaseApproval } from '../BaseApproval';
import { PluginPermissions } from '../../components/PluginInfo';
import {
getPluginConfigByUrl,
getPluginMetadataByUrl,
getPluginByUrl,
getPluginConfigByHash,
getPluginMetadataByHash,
} from '../../entries/Background/db';
import { SidePanelActionTypes } from '../../entries/SidePanel/types';
import { deferredPromise } from '../../utils/promise';
import { installPlugin } from '../../entries/Background/plugins/utils';
import { runPlugin } from '../../utils/rpc';
export function RunPluginByUrlApproval(): ReactElement {
export function RunPluginApproval(): ReactElement {
const [params] = useSearchParams();
const origin = params.get('origin');
const favIconUrl = params.get('favIconUrl');
const url = params.get('url');
const pluginParams = params.get('params');
const hash = params.get('hash');
const hostname = urlify(origin || '')?.hostname;
const [error, showError] = useState('');
const [metadata, setPluginMetadata] = useState<PluginMetadata | null>(null);
const [pluginContent, setPluginContent] = useState<PluginConfig | null>(null);
const onCancel = useCallback(() => {
browser.runtime.sendMessage({
type: BackgroundActiontype.run_plugin_response,
data: false,
});
}, []);
const onAccept = useCallback(async () => {
if (!hash) return;
try {
const tab = await browser.tabs.create({
active: true,
});
await browser.storage.local.set({ plugin_hash: hash });
// @ts-ignore
if (chrome.sidePanel) await chrome.sidePanel.open({ tabId: tab.id });
browser.runtime.sendMessage({
type: BackgroundActiontype.run_plugin_response,
data: true,
});
} catch (e: any) {
showError(e.message);
}
}, [hash]);
useEffect(() => {
if (!url) return;
(async () => {
if (!hash) return;
try {
const hex = await getPluginByUrl(url);
if (!hex) {
await installPlugin(url);
}
const config = await getPluginConfigByUrl(url);
const metadata = await getPluginMetadataByUrl(url);
const config = await getPluginConfigByHash(hash);
const metadata = await getPluginMetadataByHash(hash);
setPluginContent(config);
setPluginMetadata(metadata);
} catch (e: any) {
showError(e?.message || 'Invalid Plugin');
}
})();
}, [url]);
const onCancel = useCallback(() => {
browser.runtime.sendMessage({
type: BackgroundActiontype.run_plugin_by_url_response,
data: false,
});
}, []);
const onAccept = useCallback(async () => {
if (!url) return;
try {
const tab = await browser.tabs.create({
active: true,
});
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: {
pluginUrl: url,
pluginParams: pluginParams ? JSON.parse(pluginParams) : undefined,
},
});
browser.runtime.sendMessage({
type: BackgroundActiontype.run_plugin_by_url_response,
data: true,
});
} catch (e: any) {
showError(e.message);
}
}, [url]);
}, [hash]);
return (
<BaseApproval

View File

@@ -1,7 +1,6 @@
import {
BackgroundActiontype,
RequestHistory,
RequestProgress,
} from '../entries/Background/rpc';
import { useSelector } from 'react-redux';
import { AppRootState } from './index';
@@ -9,9 +8,7 @@ import deepEqual from 'fast-deep-equal';
enum ActionType {
'/history/addRequest' = '/history/addRequest',
'/history/setRequests' = '/history/setRequests',
'/history/deleteRequest' = '/history/deleteRequest',
'/history/addRequestCid' = '/history/addRequestCid',
}
type Action<payload> = {
@@ -40,20 +37,6 @@ export const addRequestHistory = (request?: RequestHistory | null) => {
};
};
export const setRequests = (requests: RequestHistory[]) => {
return {
type: ActionType['/history/setRequests'],
payload: requests,
};
};
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,
@@ -77,9 +60,6 @@ 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,
@@ -92,25 +72,6 @@ export default function history(
order: newOrder,
};
}
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: newMap,
order: payload.map(({ id }) => id),
};
}
case ActionType['/history/deleteRequest']: {
const reqId: string = action.payload;
const newMap = { ...state.map };
@@ -122,20 +83,6 @@ 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,13 +2,11 @@ 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>;

View File

@@ -1,375 +0,0 @@
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

@@ -1,9 +1,6 @@
import { useSelector } from 'react-redux';
import { AppRootState } from './index';
import deepEqual from 'fast-deep-equal';
import { useEffect, useState } from 'react';
import { getPluginConfigByUrl } from '../entries/Background/db';
import { PluginConfig } from '../utils/misc';
enum ActionType {
'/plugin/addPlugin' = '/plugin/addPlugin',
@@ -55,13 +52,3 @@ export const usePluginHashes = (): string[] => {
return state.plugins.order;
}, deepEqual);
};
export const usePluginConfig = (hash: string) => {
const [config, setConfig] = useState<PluginConfig | null>(null);
useEffect(() => {
(async function () {
setConfig(await getPluginConfigByUrl(hash));
})();
}, [hash]);
return config;
};

View File

@@ -69,6 +69,7 @@ export const notarizeRequest = (options: RequestHistory) => async () => {
method: options.method,
headers: options.headers,
body: options.body,
maxTranscriptSize: options.maxTranscriptSize,
maxSentData,
maxRecvData,
secretHeaders: options.secretHeaders,

View File

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

View File

@@ -1,6 +1,5 @@
import {
BackgroundActiontype,
handleExecP2PPluginProver,
handleExecPluginProver,
RequestLog,
} from '../entries/Background/rpc';
@@ -14,12 +13,8 @@ import browser from 'webextension-polyfill';
import NodeCache from 'node-cache';
import { getNotaryApi, getProxyApi } from './storage';
import { minimatch } from 'minimatch';
import {
getCookiesByHost,
getHeadersByHost,
getLocalStorageByHost,
getSessionStorageByHost,
} from '../entries/Background/db';
import { getCookiesByHost, getHeadersByHost } from '../entries/Background/db';
const charwise = require('charwise');
export function urlify(
@@ -89,19 +84,6 @@ 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,
@@ -166,10 +148,6 @@ 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 });
@@ -221,45 +199,41 @@ export const makePlugin = async (
}
(async () => {
const { getSecretResponse, body: reqBody } = params;
const {
url,
method,
headers,
getSecretResponse,
body: reqBody,
} = params;
let secretResps;
const resp = await fetch(url, {
method,
headers,
body: reqBody,
});
const body = await extractBodyFromResponse(resp);
if (meta?.p2p) {
const pluginHex = Buffer.from(arrayBuffer).toString('hex');
const pluginUrl = await sha256(pluginHex);
handleExecP2PPluginProver({
type: BackgroundActiontype.execute_p2p_plugin_prover,
data: {
...params,
pluginUrl,
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,
},
});
if (getSecretResponse) {
const out = await plugin.call(getSecretResponse, body);
secretResps = JSON.parse(out.string());
}
handleExecPluginProver({
type: BackgroundActiontype.execute_plugin_prover,
data: {
...params,
body: reqBody,
secretResps,
now,
},
});
})();
return context.store(`${id}`);
},
};
const funcs: {
[key: string]: (callContext: CallContext, ...args: any[]) => any;
} = {};
@@ -276,61 +250,21 @@ 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: { [link: string]: { [key: string]: string } } = {};
for (const link of config.cookies) {
const cache = await getCookiesByHost(link);
cookies[link] = cache;
const cookies: { [hostname: string]: { [key: string]: string } } = {};
for (const host of config.cookies) {
const cache = await getCookiesByHost(host);
cookies[host] = cache;
}
// @ts-ignore
injectedConfig.cookies = JSON.stringify(cookies);
}
if (config?.headers) {
const headers: { [link: string]: { [key: string]: string } } = {};
for (const link of config.headers) {
const cache = await getHeadersByHost(link);
headers[link] = cache;
const headers: { [hostname: string]: { [key: string]: string } } = {};
for (const host of config.headers) {
const cache = await getHeadersByHost(host);
headers[host] = cache;
}
// @ts-ignore
injectedConfig.headers = JSON.stringify(headers);
@@ -350,26 +284,24 @@ export const makePlugin = async (
};
export type StepConfig = {
title: string; // Text for the step's title
description?: string; // Text for the step's description (optional)
cta: string; // Text for the step's call-to-action button
action: string; // The function name that this step will execute
prover?: boolean; // Boolean indicating if this step outputs a notarization (optional)
title: string;
description?: string;
cta: string;
action: string;
prover?: boolean;
};
export type PluginConfig = {
title: string; // The name of the plugin
description: string; // A description of the plugin 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)
title: string;
description: string;
icon?: string;
steps?: StepConfig[];
hostFunctions?: string[];
cookies?: string[];
headers?: string[];
requests: { method: string; url: string }[];
notaryUrls?: string[];
proxyUrls?: string[];
};
export type PluginMetadata = {
@@ -416,16 +348,7 @@ 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);
@@ -467,9 +390,3 @@ export function safeParseJSON(data?: string | null) {
return null;
}
}
export function isPopupWindow(): boolean {
return (
!!window.opener || window.matchMedia('(display-mode: standalone)').matches
);
}

View File

@@ -1,33 +0,0 @@
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', 'your peer'].concat(notaryUrls || []);
const notaries = ['default notary'].concat(notaryUrls || []);
const proxies = ['default proxy'].concat(proxyUrls || []);
return (

View File

@@ -2,10 +2,10 @@ import browser from 'webextension-polyfill';
import { BackgroundActiontype } from '../entries/Background/rpc';
import { PluginConfig } from './misc';
export async function addPlugin(hex: string, url: string) {
export async function addPlugin(hex: string) {
return browser.runtime.sendMessage({
type: BackgroundActiontype.add_plugin,
data: { hex, url },
data: hex,
});
}
@@ -38,12 +38,7 @@ export async function fetchPluginConfigByHash(
});
}
export async function runPlugin(
hash: string,
method: string,
params?: string,
meta?: any,
) {
export async function runPlugin(hash: string, method: string, params?: string) {
return browser.runtime.sendMessage({
type: BackgroundActiontype.run_plugin,
data: {
@@ -51,6 +46,5 @@ export async function runPlugin(
method,
params,
},
meta,
});
}

View File

@@ -1,13 +1,10 @@
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 });
@@ -21,25 +18,21 @@ export async function get(key: string, defaultValue?: string) {
}
export async function getMaxSent() {
return parseInt(await get(MAX_SENT_LS_KEY, MAX_SENT.toString()));
return parseInt(await get(MAX_SENT_LS_KEY, '4096'));
}
export async function getMaxRecv() {
return parseInt(await get(MAX_RECEIVED_LS_KEY, MAX_RECV.toString()));
return parseInt(await get(MAX_RECEIVED_LS_KEY, '16384'));
}
export async function getNotaryApi() {
return await get(NOTARY_API_LS_KEY, NOTARY_API);
return await get(NOTARY_API_LS_KEY, 'https://notary.pse.dev/v0.1.0-alpha.6');
}
export async function getProxyApi() {
return await get(PROXY_API_LS_KEY, NOTARY_PROXY);
return await get(PROXY_API_LS_KEY, 'wss://notary.pse.dev/proxy');
}
export async function getLoggingFilter(): Promise<LoggingLevel> {
return await get(LOGGING_FILTER_KEY, 'Info');
}
export async function getRendezvousApi(): Promise<string> {
return await get(RENDEZVOUS_API_LS_KEY, RENDEZVOUS_API);
}

View File

@@ -1,10 +1,18 @@
import { PresentationJSON as PresentationJSONa7 } from 'tlsn-js/build/types';
export type Proof = ProofV0 | ProofV1;
export type PresentationJSON = PresentationJSONa5 | PresentationJSONa7;
export type PresentationJSONa5 = {
export type ProofV0 = {
version?: undefined;
session: any;
substrings: any;
notaryUrl: string;
};
export type ProofV1 = {
version: '1.0';
data: string;
meta: {
notaryUrl: string;
websocketProxyUrl: string;
pluginUrl?: string;
};
};

View File

@@ -7,6 +7,7 @@ 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 || "/";
@@ -41,9 +42,6 @@ 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: {
@@ -85,9 +83,6 @@ var options = {
loader: "sass-loader",
options: {
sourceMap: true,
sassOptions: {
silenceDeprecations: ["legacy-js-api"],
}
},
},
],
@@ -113,6 +108,11 @@ var options = {
{
loader: require.resolve("ts-loader"),
options: {
getCustomTransformers: () => ({
before: [isDevelopment && ReactRefreshTypeScript()].filter(
Boolean
),
}),
transpileOnly: isDevelopment,
},
},

6
websockify_config Normal file
View File

@@ -0,0 +1,6 @@
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