Compare commits

...

18 Commits

Author SHA1 Message Date
tsukino
e0468a823c wip 2025-07-15 08:58:55 +08:00
tsukino
4e104d4f4a wip 2025-07-08 22:15:06 +08:00
Tanner
cc3264f058 Alpha.12 (#193) 2025-06-27 17:58:36 +01:00
Tanner
ea12322686 Developer Mode (#190)
* feat: adding developer mode toggle to add and display plugins

* chore: lint

* chore: cleanup
2025-06-26 17:54:22 +01:00
tsukino
5a9e5bc77d fix: query for headers and cookies (#192) 2025-06-26 17:38:30 +01:00
tsukino
f489800663 fix: refactor cache layer (#189) 2025-06-19 10:54:08 -04:00
yuroitaki
49056dc605 Correct url. (#191) 2025-06-12 15:30:56 +02:00
Tanner
9567590e47 Data: no longer duplicated in meta for downloaded attestations (#188)
* fix: data no longer duplicated in meta in downloads

* fix: cleanup
2025-06-10 08:24:38 -04:00
Tanner
008cb10b30 Display WSS errors in Sidepanel (#187)
* feat: display websocket proxy errors in sidepanel

* chore: cleanup
2025-06-10 08:23:35 -04:00
tsukino
6b8e9a3580 feat: clear db button (#186)
* feat: clear db

* add size calc
2025-06-10 08:23:13 -04:00
Brendan A. Miller
6aab10b7d0 Improve error handling for webpack build (#183) 2025-06-03 16:44:00 +02:00
Hendrik Eeckhaut
7de46bf590 chore: type cleanup (#185) 2025-06-02 21:30:52 +02:00
Hendrik Eeckhaut
c67b794f40 ci: do not unpack the zip file (#184) 2025-06-02 10:51:31 +02:00
Tanner
9872a376c1 Alpha.11 (#182)
Co-authored-by: Hendrik Eeckhaut <hendrik@eeckhaut.org>
2025-06-02 10:32:34 +02:00
Hendrik Eeckhaut
f34718f352 ci: Add missing checkout step in release job (#179) 2025-05-14 14:17:05 +02:00
tsukino
08520a90ff chore: update to alpha.10 (#177) 2025-04-30 16:18:51 +02:00
tsukino
264128d1fb feat: 1-click plugin (#175) 2025-04-30 03:01:54 -04:00
Hendrik Eeckhaut
dbb617f516 Update extism to v2.0.0-rc11 (#174) 2025-04-25 22:27:07 +02:00
37 changed files with 1978 additions and 2704 deletions

View File

@@ -43,11 +43,13 @@ jobs:
runs-on: ubuntu-latest
needs: build-lint-test
steps:
- name: Checkout
uses: actions/checkout@v4
- 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
path: .
- name: 📦 Add extension zip file to release
env:

1
.gitignore vendored
View File

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

View File

@@ -12,7 +12,7 @@
# Chrome Extension (MV3) for TLSNotary
> [!IMPORTANT]
> ⚠️ When running the extension against a [notary server](https://github.com/tlsnotary/tlsn/tree/dev/notary-server), please ensure that the server's version is the same as the version of this extension
> ⚠️ When running the extension against a [notary server](https://github.com/tlsnotary/tlsn/tree/main/crates/notary/server), please ensure that the server's version is the same as the version of this extension
## License
This repository is licensed under either of

1918
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.900",
"version": "0.1.0.1200",
"license": "MIT",
"repository": {
"type": "git",
@@ -16,7 +16,7 @@
"lint:fix": "eslint . --fix"
},
"dependencies": {
"@extism/extism": "^1.0.3",
"@extism/extism": "^2.0.0-rc11",
"@fortawesome/fontawesome-free": "^6.4.2",
"async-mutex": "^0.4.0",
"buffer": "^6.0.3",
@@ -40,8 +40,7 @@
"redux-logger": "^3.0.6",
"redux-thunk": "^2.4.2",
"tailwindcss": "^3.3.3",
"tlsn-js": "0.1.0-alpha.9",
"tlsn-js-v5": "npm:tlsn-js@0.1.0-alpha.5.4"
"tlsn-js": "0.1.0-alpha.12.0"
},
"devDependencies": {
"@babel/core": "^7.20.12",
@@ -95,4 +94,4 @@
"webpack-ext-reloader": "^1.1.12",
"zip-webpack-plugin": "^4.0.1"
}
}
}

View File

@@ -9,7 +9,6 @@ import Icon from '../Icon';
import browser from 'webextension-polyfill';
import classNames from 'classnames';
import { useNavigate } from 'react-router';
import PluginUploadInfo from '../PluginInfo';
export function MenuIcon(): ReactElement {
const [opened, setOpen] = useState(false);
@@ -54,24 +53,34 @@ export default function Menu(props: {
<div className="absolute top-[100%] right-0 rounded-md z-20">
<div className="flex flex-col bg-slate-200 w-40 shadow rounded-md py">
<MenuRow
fa="fa-solid fa-plus"
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');
}}
>
<PluginUploadInfo onPluginInstalled={() => props.setOpen(false)} />
<span>Install Plugin</span>
Verify
</MenuRow>
<MenuRow
fa="fa-solid fa-toolbox"
fa="fa-solid fa-network-wired"
className="border-b border-slate-300"
onClick={() => {
props.setOpen(false);
navigate('/plugins');
navigate('/p2p');
}}
>
Plugins
P2P
</MenuRow>
<MenuRow
className="lg:hidden"

View File

@@ -1,14 +1,4 @@
import React, {
ChangeEvent,
Children,
MouseEventHandler,
ReactElement,
ReactNode,
useCallback,
useState,
} from 'react';
import { makePlugin, getPluginConfig } from '../../utils/misc';
import { addPlugin } from '../../utils/rpc';
import React, { Children, MouseEventHandler, ReactNode } from 'react';
import Modal, {
ModalHeader,
ModalContent,
@@ -22,77 +12,9 @@ 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({
onPluginInstalled,
}: {
onPluginInstalled?: () => void;
}): 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);
onPluginInstalled?.();
} 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}
onClick={(e) => {
e.stopPropagation();
}}
/>
{error && <ErrorModal onClose={() => showError('')} message={error} />}
{pluginContent && (
<PluginInfoModal
pluginContent={pluginContent}
onClose={onClose}
onAddPlugin={onAddPlugin}
/>
)}
</>
);
}
export function PluginInfoModalHeader(props: {
className?: string;
children: ReactNode | ReactNode[];

View File

@@ -5,7 +5,12 @@ import React, {
useEffect,
useState,
} from 'react';
import { fetchPluginHashes, removePlugin, runPlugin } from '../../utils/rpc';
import {
fetchPluginHashes,
removePlugin,
runPlugin,
addPlugin,
} from '../../utils/rpc';
import { usePluginHashes } from '../../reducers/plugins';
import {
getPluginConfig,
@@ -23,7 +28,7 @@ import {
PluginInfoModalContent,
PluginInfoModalHeader,
} from '../PluginInfo';
import { getPluginConfigByHash } from '../../entries/Background/db';
import { getPluginConfigByUrl } from '../../entries/Background/db';
import { SidePanelActionTypes } from '../../entries/SidePanel/types';
import { openSidePanel } from '../../entries/utils';
@@ -31,20 +36,78 @@ export function PluginList({
className,
unremovable,
onClick,
showAddButton = false,
}: {
className?: string;
unremovable?: boolean;
onClick?: (hash: string) => void;
showAddButton?: boolean;
}): ReactElement {
const hashes = usePluginHashes();
const [uploading, setUploading] = useState(false);
useEffect(() => {
fetchPluginHashes();
}, []);
const handleFileUpload = useCallback(
async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
if (!file.name.endsWith('.wasm')) {
alert('Please select a .wasm file');
return;
}
setUploading(true);
try {
const arrayBuffer = await file.arrayBuffer();
const hex = Buffer.from(arrayBuffer).toString('hex');
const url = `file://${file.name}`;
await addPlugin(hex, url);
await fetchPluginHashes();
} catch (error: any) {
alert(`Failed to add plugin: ${error.message}`);
} finally {
setUploading(false);
e.target.value = '';
}
},
[],
);
return (
<div className={classNames('flex flex-col flex-nowrap gap-1', className)}>
{!hashes.length && (
{showAddButton && (
<div className="relative">
<input
type="file"
accept=".wasm"
onChange={handleFileUpload}
disabled={uploading}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10"
/>
<button
className="flex flex-row items-center justify-center gap-2 p-3 border-2 border-dashed border-slate-300 rounded-lg text-slate-500 hover:text-slate-700 hover:border-slate-400 transition-colors cursor-pointer w-full"
disabled={uploading}
>
{uploading ? (
<>
<Icon fa="fa-solid fa-spinner" className="animate-spin" />
<span>Adding Plugin...</span>
</>
) : (
<>
<Icon fa="fa-solid fa-plus" />
<span>Add Plugin (.wasm file)</span>
</>
)}
</button>
</div>
)}
{!hashes.length && !showAddButton && (
<div className="flex flex-col items-center justify-center text-slate-400 cursor-default select-none">
<div>No available plugins</div>
</div>
@@ -110,7 +173,7 @@ export function Plugin({
if (hex) {
setConfig(await getPluginConfig(hexToArrayBuffer(hex)));
} else {
setConfig(await getPluginConfigByHash(hash));
setConfig(await getPluginConfigByUrl(hash));
}
})();
}, [hash, hex]);

View File

@@ -1,28 +0,0 @@
import NodeCache from 'node-cache';
let RequestsLogs: {
[tabId: string]: NodeCache;
} = {};
export const deleteCacheByTabId = (tabId: number) => {
delete RequestsLogs[tabId];
};
export const getCacheByTabId = (tabId: number): NodeCache => {
RequestsLogs[tabId] =
RequestsLogs[tabId] ||
new NodeCache({
stdTTL: 60 * 5, // default 5m TTL
maxKeys: 1000000,
});
return RequestsLogs[tabId];
};
export const clearRequestCache = () => {
RequestsLogs = {};
};
export const clearCache = () => {
clearRequestCache();
};

View File

@@ -1,6 +1,12 @@
import { Level } from 'level';
import { AbstractSublevel } from 'abstract-level';
import { PluginConfig, PluginMetadata, sha256, urlify } from '../../utils/misc';
import { RequestHistory, RequestProgress } from './rpc';
import {
RequestHistory,
RequestLog,
RequestProgress,
UpsertRequestLog,
} from './rpc';
import mutex from './mutex';
import { minimatch } from 'minimatch';
const charwise = require('charwise');
@@ -23,12 +29,6 @@ const pluginMetadataDb = db.sublevel<string, PluginMetadata>('pluginMetadata', {
const connectionDb = db.sublevel<string, boolean>('connections', {
valueEncoding: 'json',
});
const cookiesDb = db.sublevel<string, boolean>('cookies', {
valueEncoding: 'json',
});
const headersDb = db.sublevel<string, boolean>('headers', {
valueEncoding: 'json',
});
const localStorageDb = db.sublevel<string, any>('sessionStorage', {
valueEncoding: 'json',
});
@@ -38,10 +38,81 @@ const sessionStorageDb = db.sublevel<string, any>('localStorage', {
const appDb = db.sublevel<string, any>('app', {
valueEncoding: 'json',
});
const requestDb = db.sublevel<string, any>('requests', {
valueEncoding: 'json',
});
enum AppDatabaseKey {
DefaultPluginsInstalled = 'DefaultPluginsInstalled',
}
export async function upsertRequestLog(request: UpsertRequestLog) {
const existing = await getRequestLog(request.requestId);
if (existing) {
await requestDb.put(request.requestId, {
...existing,
...request,
});
} else if (request.url) {
const host = urlify(request.url)?.host;
if (host) {
await requestDb.put(request.requestId, request);
await requestDb
.sublevel(request.tabId.toString())
.put(request.requestId, '');
await requestDb.sublevel(host).put(request.requestId, '');
}
}
}
export async function getRequestLog(
requestId: string,
): Promise<RequestLog | null> {
return requestDb.get(requestId).catch(() => null);
}
export async function removeRequestLog(requestId: string) {
const existing = await getRequestLog(requestId);
if (existing) {
await requestDb.del(requestId);
await requestDb.sublevel(existing.tabId.toString()).del(requestId);
const host = urlify(existing.url)?.host;
if (host) {
await requestDb.sublevel(host).del(requestId);
}
}
}
export async function removeRequestLogsByTabId(tabId: number) {
const requests = requestDb.sublevel(tabId.toString());
for await (const [requestId] of requests.iterator()) {
await removeRequestLog(requestId);
}
}
export async function getRequestLogsByTabId(tabId: number) {
const requests = requestDb.sublevel(tabId.toString());
const ret: RequestLog[] = [];
for await (const [requestId] of requests.iterator()) {
ret.push(await requestDb.get(requestId));
}
return ret;
}
export async function getRequestLogsByHost(host: string) {
const requests = requestDb.sublevel(host);
const ret: RequestLog[] = [];
for await (const [requestId] of requests.iterator()) {
ret.push(await requestDb.get(requestId));
}
return ret;
}
export async function clearAllRequestLogs() {
await requestDb.clear();
}
export async function addNotaryRequest(
now = Date.now(),
request: Omit<RequestHistory, 'status' | 'id'>,
@@ -188,41 +259,44 @@ export async function getPluginHashes(): Promise<string[]> {
return retVal;
}
export async function getPluginByHash(hash: string): Promise<string | null> {
export async function getPluginByUrl(url: string): Promise<string | null> {
try {
const plugin = await pluginDb.get(hash);
const plugin = await pluginDb.get(url);
return plugin;
} catch (e) {
return null;
}
}
export async function addPlugin(hex: string): Promise<string | null> {
export async function addPlugin(
hex: string,
url: string,
): Promise<string | null> {
const hash = await sha256(hex);
if (await getPluginByHash(hash)) {
return null;
if (await getPluginByUrl(url)) {
return url;
}
await pluginDb.put(hash, hex);
await pluginDb.put(url, hex);
return hash;
}
export async function removePlugin(hash: string): Promise<string | null> {
const existing = await pluginDb.get(hash);
export async function removePlugin(url: string): Promise<string | null> {
const existing = await pluginDb.get(url);
if (!existing) return null;
await pluginDb.del(hash);
await pluginDb.del(url);
return hash;
return url;
}
export async function getPluginConfigByHash(
hash: string,
export async function getPluginConfigByUrl(
url: string,
): Promise<PluginConfig | null> {
try {
const config = await pluginConfigDb.get(hash);
const config = await pluginConfigDb.get(url);
return config;
} catch (e) {
return null;
@@ -230,25 +304,25 @@ export async function getPluginConfigByHash(
}
export async function addPluginConfig(
hash: string,
url: string,
config: PluginConfig,
): Promise<PluginConfig | null> {
if (await getPluginConfigByHash(hash)) {
if (await getPluginConfigByUrl(url)) {
return null;
}
await pluginConfigDb.put(hash, config);
await pluginConfigDb.put(url, config);
return config;
}
export async function removePluginConfig(
hash: string,
url: string,
): Promise<PluginConfig | null> {
const existing = await pluginConfigDb.get(hash);
const existing = await pluginConfigDb.get(url);
if (!existing) return null;
await pluginConfigDb.del(hash);
await pluginConfigDb.del(url);
return existing;
}
@@ -259,8 +333,8 @@ export async function getPlugins(): Promise<
const hashes = await getPluginHashes();
const ret: (PluginConfig & { hash: string; metadata: PluginMetadata })[] = [];
for (const hash of hashes) {
const config = await getPluginConfigByHash(hash);
const metadata = await getPluginMetadataByHash(hash);
const config = await getPluginConfigByUrl(hash);
const metadata = await getPluginMetadataByUrl(hash);
if (config) {
ret.push({
...config,
@@ -281,11 +355,11 @@ export async function getPlugins(): Promise<
return ret;
}
export async function getPluginMetadataByHash(
hash: string,
export async function getPluginMetadataByUrl(
url: string,
): Promise<PluginMetadata | null> {
try {
const metadata = await pluginMetadataDb.get(hash);
const metadata = await pluginMetadataDb.get(url);
return metadata;
} catch (e) {
return null;
@@ -293,21 +367,21 @@ export async function getPluginMetadataByHash(
}
export async function addPluginMetadata(
hash: string,
url: string,
metadata: PluginMetadata,
): Promise<PluginMetadata | null> {
await pluginMetadataDb.put(hash, metadata);
await pluginMetadataDb.put(url, metadata);
return metadata;
}
export async function removePluginMetadata(
hash: string,
url: string,
): Promise<PluginMetadata | null> {
const existing = await pluginMetadataDb.get(hash);
const existing = await pluginMetadataDb.get(url);
if (!existing) return null;
await pluginMetadataDb.del(hash);
await pluginMetadataDb.del(url);
return existing;
}
@@ -336,48 +410,43 @@ export async function setConnection(origin: string) {
return true;
}
export async function setCookies(host: string, name: string, value: string) {
return mutex.runExclusive(async () => {
await cookiesDb.sublevel(host).put(name, value);
return true;
});
}
export async function clearCookies(host: string) {
return mutex.runExclusive(async () => {
await cookiesDb.sublevel(host).clear();
return true;
});
}
export async function getCookies(link: string, name: string) {
try {
const existing = await cookiesDb.sublevel(link).get(name);
return existing;
} catch (e) {
return null;
}
}
export async function getCookiesByHost(link: string) {
export async function getCookiesByHost(linkOrHost: string) {
const ret: { [key: string]: string } = {};
const links: { [k: string]: boolean } = {};
const url = urlify(link);
const url = urlify(linkOrHost);
const isHost = !url;
const host = isHost ? linkOrHost : url.host;
const requests = await getRequestLogsByHost(host);
for await (const sublevel of cookiesDb.keys({ keyEncoding: 'utf8' })) {
const l = sublevel.split('!')[1];
links[l] = true;
let filteredRequest: RequestLog | null = null;
for (const request of requests) {
if (isHost) {
if (!filteredRequest || filteredRequest.updatedAt > request.updatedAt) {
filteredRequest = request;
}
} else {
const { origin, pathname } = urlify(request.url) || {};
const link = [origin, pathname].join('');
if (
minimatch(link, linkOrHost) &&
(!filteredRequest || filteredRequest.updatedAt > request.updatedAt)
) {
filteredRequest = request;
}
}
}
const cookieLink = url
? Object.keys(links).filter((l) => minimatch(l, link))[0]
: Object.keys(links).filter((l) => urlify(l)?.host === link)[0];
if (!filteredRequest) return ret;
if (!cookieLink) return ret;
for await (const [key, value] of cookiesDb.sublevel(cookieLink).iterator()) {
ret[key] = value;
for (const header of filteredRequest.requestHeaders) {
if (header.name.toLowerCase() === 'cookie') {
header.value?.split(';').forEach((cookie) => {
const [name, value] = cookie.split('=');
ret[name.trim()] = value.trim();
});
}
}
return ret;
}
@@ -397,49 +466,40 @@ export async function getConnection(origin: string) {
return null;
}
}
export async function setHeaders(link: string, name: string, value?: string) {
if (!value) return null;
return mutex.runExclusive(async () => {
await headersDb.sublevel(link).put(name, value);
return true;
});
}
export async function clearHeaders(host: string) {
return mutex.runExclusive(async () => {
await headersDb.sublevel(host).clear();
return true;
});
}
export async function getHeaders(host: string, name: string) {
try {
const existing = await headersDb.sublevel(host).get(name);
return existing;
} catch (e) {
return null;
}
}
export async function getHeadersByHost(link: string) {
export async function getHeadersByHost(linkOrHost: string) {
const ret: { [key: string]: string } = {};
const url = urlify(link);
const url = urlify(linkOrHost);
const isHost = !url;
const host = isHost ? linkOrHost : url.host;
const requests = await getRequestLogsByHost(host);
const links: { [k: string]: boolean } = {};
for await (const sublevel of headersDb.keys({ keyEncoding: 'utf8' })) {
const l = sublevel.split('!')[1];
links[l] = true;
let filteredRequest: RequestLog | null = null;
for (const request of requests) {
if (isHost) {
if (!filteredRequest || filteredRequest.updatedAt > request.updatedAt) {
filteredRequest = request;
}
} else {
const { origin, pathname } = urlify(request.url) || {};
const link = [origin, pathname].join('');
if (
minimatch(link, linkOrHost) &&
(!filteredRequest || filteredRequest.updatedAt > request.updatedAt)
) {
filteredRequest = request;
}
}
}
const headerLink = url
? Object.keys(links).filter((l) => minimatch(l, link))[0]
: Object.keys(links).filter((l) => urlify(l)?.host === link)[0];
if (!filteredRequest) return ret;
if (!headerLink) return ret;
for await (const [key, value] of headersDb.sublevel(headerLink).iterator()) {
ret[key] = value;
for (const header of filteredRequest.requestHeaders) {
if (header.name.toLowerCase() !== 'cookie') {
ret[header.name] = header.value || '';
}
}
return ret;
}
@@ -512,3 +572,62 @@ export async function getAppState() {
defaultPluginsInstalled: await getDefaultPluginsInstalled(),
};
}
export async function resetDB() {
return mutex.runExclusive(async () => {
return Promise.all([
localStorageDb.clear(),
sessionStorageDb.clear(),
requestDb.clear(),
]);
});
}
export async function getDBSizeByRoot(
rootDB: AbstractSublevel<Level, any, any, any>,
): Promise<number> {
return new Promise(async (resolve, reject) => {
let size = 0;
for await (const sublevel of rootDB.keys({ keyEncoding: 'utf8' })) {
const link = sublevel.split('!')[1];
const sub = rootDB.sublevel(link);
for await (const [key, value] of sub.iterator()) {
size += key.length + value.length;
}
}
resolve(size);
});
}
export async function getRecursiveDBSize(
db: AbstractSublevel<Level, any, any, any>,
): Promise<number> {
return new Promise(async (resolve, reject) => {
let size = 0;
for await (const sublevel of db.keys({ keyEncoding: 'utf8' })) {
const parts = sublevel.split('!');
if (parts.length === 1) {
const value = await db.get(parts[0]);
size += parts[0].length + (value ? JSON.stringify(value).length : 0);
} else {
const sub = db.sublevel(parts[1]);
size +=
(await getRecursiveDBSize(
sub as unknown as AbstractSublevel<Level, any, any, any>,
)) + parts[1].length;
}
}
resolve(size);
});
}
export async function getDBSize(): Promise<number> {
const sizes = await Promise.all([
getDBSizeByRoot(localStorageDb),
getDBSizeByRoot(sessionStorageDb),
getRecursiveDBSize(requestDb),
]);
return sizes.reduce((a, b) => a + b, 0);
}

View File

@@ -1,10 +1,10 @@
import { getCacheByTabId } from './cache';
import { BackgroundActiontype, RequestLog } from './rpc';
import { BackgroundActiontype } from './rpc';
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 { getRequestLog, upsertRequestLog } from './db';
export const onSendHeaders = (
details: browser.WebRequest.OnSendHeadersDetailsType,
) => {
@@ -12,40 +12,22 @@ export const onSendHeaders = (
const { method, tabId, requestId } = details;
if (method !== 'OPTIONS') {
const cache = getCacheByTabId(tabId);
const existing = cache.get<RequestLog>(requestId);
const { origin, pathname } = urlify(details.url) || {};
const link = [origin, pathname].join('');
if (link && details.requestHeaders) {
details.requestHeaders.forEach((header) => {
const { name, value } = header;
if (/^cookie$/i.test(name) && value) {
value.split(';').forEach((cookieStr) => {
const index = cookieStr.indexOf('=');
if (index !== -1) {
const cookieName = cookieStr.slice(0, index).trim();
const cookieValue = cookieStr.slice(index + 1);
setCookies(link, cookieName, cookieValue);
}
});
} else {
setHeaders(link, name, value);
}
upsertRequestLog({
method: details.method as 'GET' | 'POST',
type: details.type,
url: details.url,
initiator: details.initiator || null,
requestHeaders: details.requestHeaders || [],
tabId: tabId,
requestId: requestId,
updatedAt: Date.now(),
});
}
cache.set(requestId, {
...existing,
method: details.method as 'GET' | 'POST',
type: details.type,
url: details.url,
initiator: details.initiator || null,
requestHeaders: details.requestHeaders || [],
tabId: tabId,
requestId: requestId,
});
}
});
};
@@ -59,24 +41,30 @@ export const onBeforeRequest = (
if (method === 'OPTIONS') return;
if (requestBody) {
const cache = getCacheByTabId(tabId);
const existing = cache.get<RequestLog>(requestId);
if (requestBody.raw && requestBody.raw[0]?.bytes) {
try {
cache.set(requestId, {
...existing,
await upsertRequestLog({
requestBody: Buffer.from(requestBody.raw[0].bytes).toString(
'utf-8',
),
requestId: requestId,
tabId: tabId,
updatedAt: Date.now(),
});
} catch (e) {
console.error(e);
}
} else if (requestBody.formData) {
cache.set(requestId, {
...existing,
formData: requestBody.formData,
await upsertRequestLog({
formData: Object.fromEntries(
Object.entries(requestBody.formData).map(([key, value]) => [
key,
Array.isArray(value) ? value : [value],
]),
),
requestId: requestId,
tabId: tabId,
updatedAt: Date.now(),
});
}
}
@@ -91,12 +79,7 @@ export const onResponseStarted = (
if (method === 'OPTIONS') return;
const cache = getCacheByTabId(tabId);
const existing = cache.get<RequestLog>(requestId);
const newLog: RequestLog = {
requestHeaders: [],
...existing,
await upsertRequestLog({
method: details.method,
type: details.type,
url: details.url,
@@ -104,9 +87,15 @@ export const onResponseStarted = (
tabId: tabId,
requestId: requestId,
responseHeaders,
};
updatedAt: Date.now(),
});
cache.set(requestId, newLog);
const newLog = await getRequestLog(requestId);
if (!newLog) {
console.error('Request log not found', requestId);
return;
}
chrome.runtime.sendMessage({
type: BackgroundActiontype.push_action,

View File

@@ -1,7 +1,11 @@
import { onBeforeRequest, onResponseStarted, onSendHeaders } from './handlers';
import { deleteCacheByTabId } from './cache';
import browser from 'webextension-polyfill';
import { getAppState, removePlugin, setDefaultPluginsInstalled } from './db';
import {
getAppState,
removePlugin,
removeRequestLogsByTabId,
setDefaultPluginsInstalled,
} from './db';
import { installPlugin } from './plugins/utils';
(async () => {
@@ -30,7 +34,7 @@ import { installPlugin } from './plugins/utils';
);
browser.tabs.onRemoved.addListener((tabId) => {
deleteCacheByTabId(tabId);
removeRequestLogsByTabId(tabId);
});
const { defaultPluginsInstalled } = await getAppState();

View File

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

View File

@@ -1,5 +1,4 @@
import browser from 'webextension-polyfill';
import { clearCache, getCacheByTabId } from './cache';
import { addRequestHistory, setRequests } from '../../reducers/history';
import {
addNotaryRequest,
@@ -12,16 +11,11 @@ import {
setNotaryRequestVerification,
addPlugin,
getPluginHashes,
getPluginByHash,
getPluginByUrl,
removePlugin,
addPluginConfig,
getPluginConfigByHash,
getPluginConfigByUrl,
removePluginConfig,
getConnection,
setConnection,
deleteConnection,
addPluginMetadata,
getPlugins,
getCookiesByHost,
getHeadersByHost,
getAppState,
@@ -29,6 +23,8 @@ import {
setLocalStorage,
setSessionStorage,
setNotaryRequestProgress,
getRequestLogsByTabId,
clearAllRequestLogs,
} from './db';
import { addOnePlugin, removeOnePlugin } from '../../reducers/plugins';
import {
@@ -36,7 +32,6 @@ import {
getPluginConfig,
hexToArrayBuffer,
makePlugin,
PluginConfig,
} from '../../utils/misc';
import {
getLoggingFilter,
@@ -47,7 +42,6 @@ import {
getRendezvousApi,
} from '../../utils/storage';
import { deferredPromise } from '../../utils/promise';
import { minimatch } from 'minimatch';
import { OffscreenActionTypes } from '../Offscreen/types';
import { SidePanelActionTypes } from '../SidePanel/types';
import { pushToRedux } from '../utils';
@@ -61,6 +55,7 @@ import {
sendMessage,
sendPairedMessage,
} from './ws';
import { parseHttpMessage } from '../../utils/parser';
import { mapStringToRange, subtractRanges } from 'tlsn-js';
import { PresentationJSON } from 'tlsn-js/build/types';
@@ -95,20 +90,10 @@ export enum BackgroundActiontype {
// Content Script
open_popup = 'open_popup',
change_route = 'change_route',
connect_request = 'connect_request',
connect_response = 'connect_response',
get_history_request = 'get_history_request',
get_history_response = 'get_history_response',
get_proof_request = 'get_proof_request',
get_proof_response = 'get_proof_response',
notarize_request = 'notarize_request',
notarize_response = 'notarize_response',
install_plugin_request = 'install_plugin_request',
install_plugin_response = 'install_plugin_response',
get_plugins_request = 'get_plugins_request',
get_plugins_response = 'get_plugins_response',
run_plugin_request = 'run_plugin_request',
run_plugin_response = 'run_plugin_response',
run_plugin_by_url_request = 'run_plugin_by_url_request',
run_plugin_by_url_response = 'run_plugin_by_url_response',
get_secrets_from_transcript = 'get_secrets_from_transcript',
// App State
get_logging_level = 'get_logging_level',
@@ -158,6 +143,23 @@ export type RequestLog = {
[k: string]: string[];
};
responseHeaders?: browser.WebRequest.HttpHeaders;
updatedAt: number;
};
export type UpsertRequestLog = {
requestId: string;
tabId: number;
method?: string;
type?: string;
url?: string;
initiator?: string | null;
requestHeaders?: browser.WebRequest.HttpHeaders;
requestBody?: string;
formData?: {
[k: string]: string[];
};
responseHeaders?: browser.WebRequest.HttpHeaders;
updatedAt: number;
};
export enum RequestProgress {
@@ -229,7 +231,7 @@ export const initRPC = () => {
case BackgroundActiontype.get_requests:
return handleGetRequests(request, sendResponse);
case BackgroundActiontype.clear_requests:
clearCache();
clearAllRequestLogs().then(() => pushToRedux(setRequests([])));
return sendResponse();
case BackgroundActiontype.get_prove_requests:
return handleGetProveRequests(request, sendResponse);
@@ -270,20 +272,10 @@ export const initRPC = () => {
return handleExecP2PPluginProver(request);
case BackgroundActiontype.open_popup:
return handleOpenPopup(request);
case BackgroundActiontype.connect_request:
return handleConnect(request);
case BackgroundActiontype.get_history_request:
return handleGetHistory(request);
case BackgroundActiontype.get_proof_request:
return handleGetProof(request);
case BackgroundActiontype.notarize_request:
return handleNotarizeRequest(request);
case BackgroundActiontype.install_plugin_request:
return handleInstallPluginRequest(request);
case BackgroundActiontype.get_plugins_request:
return handleGetPluginsRequest(request);
case BackgroundActiontype.run_plugin_request:
return handleRunPluginCSRequest(request);
case BackgroundActiontype.run_plugin_by_url_request:
return handleRunPluginByURLRequest(request);
case BackgroundActiontype.get_logging_level:
getLoggingFilter().then(sendResponse);
return true;
@@ -364,6 +356,7 @@ export const initRPC = () => {
pluginHash: request.data,
}).then(sendResponse);
return;
case BackgroundActiontype.get_p2p_state:
getP2PState();
return;
@@ -378,10 +371,7 @@ function handleGetRequests(
request: BackgroundAction,
sendResponse: (data?: any) => void,
): boolean {
const cache = getCacheByTabId(request.data);
const keys = cache.keys() || [];
const data = keys.map((key) => cache.get(key));
sendResponse(data);
getRequestLogsByTabId(request.data).then(sendResponse);
return true;
}
@@ -686,7 +676,7 @@ async function handleGetSecretsFromTranscript(
sendResponse: (data?: any) => void,
) {
const { pluginHash, pluginHex, p2p, transcript, method } = request.data;
const hex = (await getPluginByHash(pluginHash)) || pluginHex;
const hex = (await getPluginByUrl(pluginHash)) || pluginHex;
const arrayBuffer = hexToArrayBuffer(hex!);
const config = await getPluginConfig(arrayBuffer);
const plugin = await makePlugin(arrayBuffer, config, p2p);
@@ -701,7 +691,7 @@ async function handleGetSecretsFromTranscript(
...recvBody.map((body) => body.toString('utf-8')),
);
const secretResps = JSON.parse(out.string());
const secretResps = JSON.parse(out?.string() || '{}');
await browser.runtime.sendMessage({
type: OffscreenActionTypes.get_secrets_from_transcript_success,
data: {
@@ -712,7 +702,7 @@ async function handleGetSecretsFromTranscript(
async function runP2PPluginProver(request: BackgroundAction, now = Date.now()) {
const {
pluginHash,
pluginUrl,
pluginHex,
url,
method,
@@ -734,7 +724,7 @@ async function runP2PPluginProver(request: BackgroundAction, now = Date.now()) {
await browser.runtime.sendMessage({
type: OffscreenActionTypes.start_p2p_prover,
data: {
pluginHash,
pluginUrl,
pluginHex,
url,
method,
@@ -824,10 +814,10 @@ async function handleAddPlugin(
sendResponse: (data?: any) => void,
) {
try {
const config = await getPluginConfig(hexToArrayBuffer(request.data));
const config = await getPluginConfig(hexToArrayBuffer(request.data.hex));
if (config) {
const hash = await addPlugin(request.data);
const hash = await addPlugin(request.data.hex, request.data.url);
if (hash) {
await addPluginConfig(hash, config);
@@ -867,7 +857,7 @@ async function handleGetPluginByHash(
sendResponse: (data?: any) => void,
) {
const hash = request.data;
const hex = await getPluginByHash(hash);
const hex = await getPluginByUrl(hash);
return hex;
}
@@ -876,7 +866,7 @@ async function handleGetPluginConfigByHash(
sendResponse: (data?: any) => void,
) {
const hash = request.data;
const config = await getPluginConfigByHash(hash);
const config = await getPluginConfigByUrl(hash);
return config;
}
@@ -886,14 +876,14 @@ function handleRunPlugin(
) {
(async () => {
const { hash, method, params, meta } = request.data;
const hex = await getPluginByHash(hash);
const hex = await getPluginByUrl(hash);
const arrayBuffer = hexToArrayBuffer(hex!);
const config = await getPluginConfig(arrayBuffer);
const plugin = await makePlugin(arrayBuffer, config, meta?.p2p);
devlog(`plugin::${method}`, params);
const out = await plugin.call(method, params);
devlog(`plugin response: `, out.string());
sendResponse(JSON.parse(out.string()));
devlog(`plugin response: `, out?.string());
sendResponse(JSON.parse(out?.string() || '{}'));
})();
return true;
@@ -948,180 +938,6 @@ async function handleOpenPopup(request: BackgroundAction) {
}
}
async function handleConnect(request: BackgroundAction) {
const connection = await getConnection(request.data.origin);
const [currentTab] = await browser.tabs.query({
active: true,
currentWindow: true,
});
if (!connection) {
const defer = deferredPromise();
const { popup, tab } = await openPopup(
`connection-approval?origin=${encodeURIComponent(request.data.origin)}&favIconUrl=${encodeURIComponent(currentTab?.favIconUrl || '')}`,
request.data.position.left,
request.data.position.top,
);
const onMessage = async (req: BackgroundAction) => {
if (req.type === BackgroundActiontype.connect_response) {
defer.resolve(req.data);
if (req.data) {
await setConnection(request.data.origin);
} else {
await deleteConnection(request.data.origin);
}
browser.runtime.onMessage.removeListener(onMessage);
browser.tabs.remove(tab.id!);
}
};
const onPopUpClose = (windowId: number) => {
if (windowId === popup.id) {
defer.resolve(false);
browser.windows.onRemoved.removeListener(onPopUpClose);
}
};
browser.runtime.onMessage.addListener(onMessage);
browser.windows.onRemoved.addListener(onPopUpClose);
return defer.promise;
}
return true;
}
async function handleGetHistory(request: BackgroundAction) {
const [currentTab] = await browser.tabs.query({
active: true,
currentWindow: true,
});
const defer = deferredPromise();
const {
origin,
position,
method: filterMethod,
url: filterUrl,
metadata: filterMetadata,
} = request.data;
const { popup, tab } = await openPopup(
`get-history-approval?${filterMetadata ? `metadata=${JSON.stringify(filterMetadata)}&` : ''}method=${filterMethod}&url=${filterUrl}&origin=${encodeURIComponent(origin)}&favIconUrl=${encodeURIComponent(currentTab?.favIconUrl || '')}`,
position.left,
position.top,
);
const onMessage = async (req: BackgroundAction) => {
if (req.type === BackgroundActiontype.get_history_response) {
if (req.data) {
const response = await getNotaryRequests();
const result = response
.map(
({ id, method, url, notaryUrl, websocketProxyUrl, metadata }) => ({
id,
time: new Date(charwise.decode(id)),
method,
url,
notaryUrl,
websocketProxyUrl,
metadata,
}),
)
.filter(({ method, url, metadata }) => {
let matchedMetadata = true;
if (filterMetadata) {
matchedMetadata = Object.entries(
filterMetadata as { [k: string]: string },
).reduce((bool, [k, v]) => {
try {
return bool && minimatch(metadata![k], v);
} catch (e) {
return false;
}
}, matchedMetadata);
}
return (
minimatch(method, filterMethod, { nocase: true }) &&
minimatch(url, filterUrl) &&
matchedMetadata
);
});
defer.resolve(result);
} else {
defer.reject(new Error('user rejected.'));
}
browser.runtime.onMessage.removeListener(onMessage);
browser.tabs.remove(tab.id!);
}
};
const onPopUpClose = (windowId: number) => {
if (windowId === popup.id) {
defer.reject(new Error('user rejected.'));
browser.windows.onRemoved.removeListener(onPopUpClose);
}
};
browser.runtime.onMessage.addListener(onMessage);
browser.windows.onRemoved.addListener(onPopUpClose);
return defer.promise;
}
async function handleGetProof(request: BackgroundAction) {
const [currentTab] = await browser.tabs.query({
active: true,
currentWindow: true,
});
const defer = deferredPromise();
const { origin, position, id } = request.data;
const response = await getNotaryRequest(id);
if (!response) {
defer.reject(new Error('proof id not found.'));
return defer.promise;
}
const { popup, tab } = await openPopup(
`get-proof-approval?id=${id}&origin=${encodeURIComponent(origin)}&favIconUrl=${encodeURIComponent(currentTab?.favIconUrl || '')}`,
position.left,
position.top,
);
const onMessage = async (req: BackgroundAction) => {
if (req.type === BackgroundActiontype.get_proof_response) {
if (req.data) {
defer.resolve(response?.proof || null);
} else {
defer.reject(new Error('user rejected.'));
}
browser.runtime.onMessage.removeListener(onMessage);
browser.tabs.remove(tab.id!);
}
};
const onPopUpClose = (windowId: number) => {
if (windowId === popup.id) {
defer.reject(new Error('user rejected.'));
browser.windows.onRemoved.removeListener(onPopUpClose);
}
};
browser.runtime.onMessage.addListener(onMessage);
browser.windows.onRemoved.addListener(onPopUpClose);
return defer.promise;
}
async function handleNotarizeRequest(request: BackgroundAction) {
const [currentTab] = await browser.tabs.query({
active: true,
@@ -1226,171 +1042,33 @@ async function handleNotarizeRequest(request: BackgroundAction) {
return defer.promise;
}
async function handleInstallPluginRequest(request: BackgroundAction) {
async function handleRunPluginByURLRequest(request: BackgroundAction) {
const [currentTab] = await browser.tabs.query({
active: true,
currentWindow: true,
});
const defer = deferredPromise();
const { origin, position, url, metadata } = request.data;
const { origin, position, url, params } = request.data;
let arrayBuffer: ArrayBuffer, config: PluginConfig;
try {
const resp = await fetch(url);
arrayBuffer = await resp.arrayBuffer();
config = await getPluginConfig(arrayBuffer);
} catch (e) {
defer.reject(e);
return defer.promise;
}
const { popup, tab } = await openPopup(
`install-plugin-approval?${metadata ? `metadata=${JSON.stringify(metadata)}&` : ''}url=${url}&origin=${encodeURIComponent(origin)}&favIconUrl=${encodeURIComponent(currentTab?.favIconUrl || '')}`,
position.left,
position.top,
);
const onMessage = async (req: BackgroundAction) => {
if (req.type === BackgroundActiontype.install_plugin_response) {
if (req.data) {
try {
const hex = Buffer.from(arrayBuffer).toString('hex');
const hash = await addPlugin(hex);
if (!hash) {
throw new Error('Plugin already exist.');
}
await addPluginConfig(hash!, config);
await addPluginMetadata(hash!, {
...metadata,
origin,
filePath: url,
});
defer.resolve(hash);
} catch (e) {
defer.reject(e);
}
} else {
defer.reject(new Error('user rejected.'));
}
browser.runtime.onMessage.removeListener(onMessage);
browser.tabs.remove(tab.id!);
}
};
const onPopUpClose = (windowId: number) => {
if (windowId === popup.id) {
defer.reject(new Error('user rejected.'));
browser.windows.onRemoved.removeListener(onPopUpClose);
}
};
browser.runtime.onMessage.addListener(onMessage);
browser.windows.onRemoved.addListener(onPopUpClose);
return defer.promise;
}
async function handleGetPluginsRequest(request: BackgroundAction) {
const [currentTab] = await browser.tabs.query({
active: true,
currentWindow: true,
});
const defer = deferredPromise();
const {
origin,
position,
origin: filterOrigin,
url: filterUrl,
metadata: filterMetadata,
} = request.data;
const { popup, tab } = await openPopup(
`get-plugins-approval?${filterMetadata ? `metadata=${JSON.stringify(filterMetadata)}&` : ''}&filterOrigin=${filterOrigin}&url=${filterUrl}&origin=${encodeURIComponent(origin)}&favIconUrl=${encodeURIComponent(currentTab?.favIconUrl || '')}`,
position.left,
position.top,
);
const onMessage = async (req: BackgroundAction) => {
if (req.type === BackgroundActiontype.get_plugins_response) {
if (req.data) {
const response = await getPlugins();
const result = response.filter(({ metadata }) => {
let matchedMetadata = true;
if (filterMetadata) {
matchedMetadata = Object.entries(
filterMetadata as { [k: string]: string },
).reduce((bool, [k, v]) => {
try {
return bool && minimatch(metadata![k], v);
} catch (e) {
return false;
}
}, matchedMetadata);
}
return (
minimatch(metadata.filePath, filterUrl) &&
minimatch(metadata.origin, filterOrigin || '**') &&
matchedMetadata
);
});
defer.resolve(result);
} else {
defer.reject(new Error('user rejected.'));
}
browser.runtime.onMessage.removeListener(onMessage);
browser.tabs.remove(tab.id!);
}
};
const onPopUpClose = (windowId: number) => {
if (windowId === popup.id) {
defer.reject(new Error('user rejected.'));
browser.windows.onRemoved.removeListener(onPopUpClose);
}
};
browser.runtime.onMessage.addListener(onMessage);
browser.windows.onRemoved.addListener(onPopUpClose);
return defer.promise;
}
async function handleRunPluginCSRequest(request: BackgroundAction) {
const [currentTab] = await browser.tabs.query({
active: true,
currentWindow: true,
});
const defer = deferredPromise();
const { origin, position, hash, params } = request.data;
const plugin = await getPluginByHash(hash);
const config = await getPluginConfigByHash(hash);
// const plugin = await getPluginByHash(hash);
// const config = await getPluginConfigByHash(hash);
let isUserClose = true;
if (!plugin || !config) {
defer.reject(new Error('plugin not found.'));
return defer.promise;
}
// if (!plugin || !config) {
// defer.reject(new Error('plugin not found.'));
// return defer.promise;
// }
const { popup, tab } = await openPopup(
`run-plugin-approval?hash=${hash}&origin=${encodeURIComponent(origin)}&favIconUrl=${encodeURIComponent(currentTab?.favIconUrl || '')}&params=${encodeURIComponent(JSON.stringify(params) || '')}`,
`run-plugin-approval?url=${url}&origin=${encodeURIComponent(origin)}&favIconUrl=${encodeURIComponent(currentTab?.favIconUrl || '')}&params=${encodeURIComponent(JSON.stringify(params) || '')}`,
position.left,
position.top,
);
const onPluginRequest = async (req: any) => {
if (req.type !== SidePanelActionTypes.execute_plugin_response) return;
if (req.data.hash !== hash) return;
if (req.data.url !== url) return;
if (req.data.error) defer.reject(req.data.error);
if (req.data.proof) defer.resolve(req.data.proof);
@@ -1399,7 +1077,7 @@ async function handleRunPluginCSRequest(request: BackgroundAction) {
};
const onMessage = async (req: BackgroundAction) => {
if (req.type === BackgroundActiontype.run_plugin_response) {
if (req.type === BackgroundActiontype.run_plugin_by_url_response) {
if (req.data) {
browser.runtime.onMessage.addListener(onPluginRequest);
} else {

View File

@@ -17,7 +17,7 @@ import {
setPairing,
} from '../../reducers/p2p';
import { pushToRedux } from '../utils';
import { getPluginByHash } from './db';
import { getPluginByUrl } from './db';
import browser from 'webextension-polyfill';
import { OffscreenActionTypes } from '../Offscreen/types';
import { getMaxRecv, getMaxSent, getRendezvousApi } from '../../utils/storage';
@@ -69,6 +69,7 @@ export const connectSession = async () => {
if (state.socket) return;
const rendezvousAPI = await getRendezvousApi();
const socket = new WebSocket(rendezvousAPI);
socket.onopen = () => {
@@ -185,7 +186,7 @@ export const connectSession = async () => {
}
case 'request_proof_by_hash': {
const { pluginHash, from } = message.params;
const plugin = await getPluginByHash(pluginHash);
const plugin = await getPluginByUrl(pluginHash);
if (plugin) {
state.incomingProofRequests = [
...new Set(state.incomingProofRequests.concat(plugin)),
@@ -316,9 +317,26 @@ export const connectSession = async () => {
break;
}
};
socket.onerror = () => {
console.error('Error connecting to websocket');
socket.onerror = (error) => {
console.error('Error connecting to websocket:', error);
pushToRedux(setConnected(false));
pushToRedux(
setP2PError(
'Failed to connect to rendezvous server. Please check your connection and server URL.',
),
);
};
socket.onclose = (event) => {
console.log('WebSocket connection closed:', event.code, event.reason);
pushToRedux(setConnected(false));
if (event.code !== 1000 && event.code !== 1001) {
pushToRedux(
setP2PError(
`WebSocket connection lost: ${event.reason || 'Unknown error'}`,
),
);
}
};
};
@@ -336,7 +354,7 @@ async function handleRemoveIncomingProofRequest(message: {
params: { pluginHash: string };
}) {
const { pluginHash } = message.params;
const plugin = await getPluginByHash(pluginHash);
const plugin = await getPluginByUrl(pluginHash);
const incomingProofRequest = [];
for (const hex of state.incomingProofRequests) {
if (plugin) {
@@ -424,7 +442,7 @@ export async function sendPairedMessage(method: string, params?: any) {
}
export const requestProof = async (pluginHash: string) => {
const pluginHex = await getPluginByHash(pluginHash);
const pluginHex = await getPluginByUrl(pluginHash);
sendPairedMessage('request_proof', {
plugin: pluginHex,
pluginHash,
@@ -436,8 +454,8 @@ export const endProofRequest = async (data: {
proof: VerifierOutput;
}) => {
const transcript = new Transcript({
sent: data.proof.transcript.sent,
recv: data.proof.transcript.recv,
sent: data.proof.transcript?.sent || [],
recv: data.proof.transcript?.recv || [],
});
state.presentation = {

View File

@@ -1,40 +1,9 @@
import { ContentScriptTypes, RPCClient } from './rpc';
import { RequestHistory } from '../Background/rpc';
import { PluginConfig, PluginMetadata } from '../../utils/misc';
import { PresentationJSON } from '../../utils/types';
import { PresentationJSON } from 'tlsn-js/build/types';
const client = new RPCClient();
class TLSN {
async getHistory(
method: string,
url: string,
metadata?: {
[key: string]: string;
},
): Promise<
(Pick<
RequestHistory,
'id' | 'method' | 'notaryUrl' | 'url' | 'websocketProxyUrl'
> & { time: Date })[]
> {
const resp = await client.call(ContentScriptTypes.get_history, {
method,
url,
metadata,
});
return resp || [];
}
async getProof(id: string): Promise<PresentationJSON | null> {
const resp = await client.call(ContentScriptTypes.get_proof, {
id,
});
return resp || null;
}
async notarize(
url: string,
requestOptions?: {
@@ -67,37 +36,9 @@ class TLSN {
return resp;
}
async installPlugin(
url: string,
metadata?: { [k: string]: string },
): Promise<string> {
const resp = await client.call(ContentScriptTypes.install_plugin, {
async runPlugin(url: string, params?: Record<string, string>) {
const resp = await client.call(ContentScriptTypes.run_plugin_by_url, {
url,
metadata,
});
return resp;
}
async getPlugins(
url: string,
origin?: string,
metadata?: {
[key: string]: string;
},
): Promise<(PluginConfig & { hash: string; metadata: PluginMetadata })[]> {
const resp = await client.call(ContentScriptTypes.get_plugins, {
url,
origin,
metadata,
});
return resp;
}
async runPlugin(hash: string, params?: Record<string, string>) {
const resp = await client.call(ContentScriptTypes.run_plugin, {
hash,
params,
});
@@ -106,11 +47,7 @@ class TLSN {
}
const connect = async () => {
const resp = await client.call(ContentScriptTypes.connect);
if (resp) {
return new TLSN();
}
return new TLSN();
};
// @ts-ignore

View File

@@ -25,70 +25,6 @@ import { urlify } from '../../utils/misc';
}
});
server.on(ContentScriptTypes.connect, async () => {
const connected = await browser.runtime.sendMessage({
type: BackgroundActiontype.connect_request,
data: {
...getPopupData(),
},
});
if (!connected) throw new Error('user rejected.');
return connected;
});
server.on(
ContentScriptTypes.get_history,
async (
request: ContentScriptRequest<{
method: string;
url: string;
metadata?: { [k: string]: string };
}>,
) => {
const {
method: filterMethod,
url: filterUrl,
metadata,
} = request.params || {};
if (!filterMethod || !filterUrl)
throw new Error('params must include method and url.');
const response: RequestHistory[] = await browser.runtime.sendMessage({
type: BackgroundActiontype.get_history_request,
data: {
...getPopupData(),
method: filterMethod,
url: filterUrl,
metadata,
},
});
return response;
},
);
server.on(
ContentScriptTypes.get_proof,
async (request: ContentScriptRequest<{ id: string }>) => {
const { id } = request.params || {};
if (!id) throw new Error('params must include id.');
const proof = await browser.runtime.sendMessage({
type: BackgroundActiontype.get_proof_request,
data: {
...getPopupData(),
id,
},
});
return proof;
},
);
server.on(
ContentScriptTypes.notarize,
async (
@@ -139,78 +75,22 @@ import { urlify } from '../../utils/misc';
);
server.on(
ContentScriptTypes.install_plugin,
ContentScriptTypes.run_plugin_by_url,
async (
request: ContentScriptRequest<{
url: string;
metadata?: { [k: string]: string };
}>,
) => {
const { url, metadata } = request.params || {};
if (!url) throw new Error('params must include url.');
const response: RequestHistory[] = await browser.runtime.sendMessage({
type: BackgroundActiontype.install_plugin_request,
data: {
...getPopupData(),
url,
metadata,
},
});
return response;
},
);
server.on(
ContentScriptTypes.get_plugins,
async (
request: ContentScriptRequest<{
url: string;
origin?: string;
metadata?: { [k: string]: string };
}>,
) => {
const {
url: filterUrl,
origin: filterOrigin,
metadata,
} = request.params || {};
if (!filterUrl) throw new Error('params must include url.');
const response = await browser.runtime.sendMessage({
type: BackgroundActiontype.get_plugins_request,
data: {
...getPopupData(),
url: filterUrl,
origin: filterOrigin,
metadata,
},
});
return response;
},
);
server.on(
ContentScriptTypes.run_plugin,
async (
request: ContentScriptRequest<{
hash: string;
params?: Record<string, string>;
}>,
) => {
const { hash, params } = request.params || {};
const { url, params } = request.params || {};
if (!hash) throw new Error('params must include hash');
if (!url) throw new Error('params must include url');
const response = await browser.runtime.sendMessage({
type: BackgroundActiontype.run_plugin_request,
type: BackgroundActiontype.run_plugin_by_url_request,
data: {
...getPopupData(),
hash,
url,
params,
},
});

View File

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

View File

@@ -16,10 +16,8 @@ import {
} 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 { verify } from 'tlsn-js-v5';
import { PresentationJSON } from 'tlsn-js/build/types';
import { waitForEvent } from '../utils';
import {
setNotaryRequestError,
@@ -37,7 +35,10 @@ export const initThreads = async () => {
type: BackgroundActiontype.get_logging_level,
hardwareConcurrency: navigator.hardwareConcurrency,
});
await init({ loggingLevel });
await init({
loggingLevel,
hardwareConcurrency: navigator.hardwareConcurrency,
});
};
export const onNotarizationRequest = async (request: any) => {
const { id } = request.data;
@@ -116,31 +117,30 @@ export const onCreatePresentationRequest = async (request: any) => {
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,
},
},
},
const notarizationOutputs = await prover.reveal({
...commit,
server_identity: true,
});
console.log('notarizationOutputs', notarizationOutputs);
// const presentation = (await new Presentation({
// attestationHex: notarizationOutputs.attestation,
// secretsHex: notarizationOutputs.secrets,
// notaryUrl: notarizationOutputs.notaryUrl,
// websocketProxyUrl: notarizationOutputs.websocketProxyUrl,
// reveal: { ...commit, server_identity: false },
// })) as TPresentation;
// const json = await presentation.json();
// browser.runtime.sendMessage({
// type: BackgroundActiontype.finish_prove_request,
// data: {
// id,
// proof: {
// ...json,
// },
// },
// });
delete provers[id];
} catch (error: any) {
console.error(error);
@@ -249,7 +249,7 @@ export const startP2PVerifier = async (request: any) => {
export const startP2PProver = async (request: any) => {
const {
pluginHash,
pluginUrl,
pluginHex,
url,
method,
@@ -266,7 +266,7 @@ export const startP2PProver = async (request: any) => {
const hostname = urlify(url)?.hostname || '';
const prover: TProver = await new Prover({
id: pluginHash,
id: pluginUrl,
serverDns: hostname,
maxSentData,
maxRecvData,
@@ -275,7 +275,7 @@ export const startP2PProver = async (request: any) => {
browser.runtime.sendMessage({
type: BackgroundActiontype.prover_instantiated,
data: {
pluginHash,
pluginUrl,
},
});
@@ -288,7 +288,7 @@ export const startP2PProver = async (request: any) => {
browser.runtime.sendMessage({
type: BackgroundActiontype.prover_setup,
data: {
pluginHash,
pluginUrl,
},
});
@@ -296,10 +296,11 @@ export const startP2PProver = async (request: any) => {
browser.runtime.sendMessage({
type: BackgroundActiontype.prover_started,
data: {
pluginHash,
pluginUrl,
},
});
await proofRequestStart;
await prover.sendRequest(websocketProxyUrl + `?token=${hostname}`, {
url,
method,
@@ -315,7 +316,7 @@ export const startP2PProver = async (request: any) => {
browser.runtime.sendMessage({
type: BackgroundActiontype.get_secrets_from_transcript,
data: {
pluginHash,
pluginUrl,
pluginHex,
method: getSecretResponse,
transcript,
@@ -348,7 +349,7 @@ export const startP2PProver = async (request: any) => {
};
const endRequest = waitForEvent(OffscreenActionTypes.end_p2p_proof_request);
await prover.reveal(commit);
await prover.reveal({ ...commit, server_identity: false });
await endRequest;
};
@@ -366,7 +367,7 @@ async function createProof(options: {
id: string;
secretHeaders: string[];
secretResps: string[];
}): Promise<PresentationJSONa7> {
}): Promise<PresentationJSON> {
const {
url,
method = 'GET',
@@ -393,18 +394,37 @@ async function createProof(options: {
});
updateRequestProgress(id, RequestProgress.GettingSession);
const sessionUrl = await notary.sessionUrl(maxSentData, maxRecvData);
const resp = await fetch(`${notaryUrl}/session`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
clientType: 'Websocket',
maxRecvData,
maxSentData,
plugin: 'plugin-js',
}),
});
const { sessionId } = await resp.json();
const sessionUrl = `${notaryUrl}/notarize?sessionId=${sessionId}`;
// 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,
});
await handleProgress(
id,
RequestProgress.SendingRequest,
() =>
prover.sendRequest(websocketProxyUrl + `?token=${hostname}`, {
url,
method,
headers,
body,
}),
`Error connecting to websocket proxy: ${websocketProxyUrl}. Please check the proxy URL and ensure it's accessible.`,
);
updateRequestProgress(id, RequestProgress.ReadingTranscript);
const transcript = await prover.transcript();
@@ -434,17 +454,12 @@ async function createProof(options: {
secretsHex: notarizationOutputs.secrets,
notaryUrl: notarizationOutputs.notaryUrl,
websocketProxyUrl: notarizationOutputs.websocketProxyUrl,
reveal: commit,
reveal: { ...commit, server_identity: false },
})) as TPresentation;
const json = await presentation.json();
return {
...json,
meta: {
...json,
notaryUrl: notaryUrl,
websocketProxyUrl: websocketProxyUrl,
},
};
}
@@ -485,6 +500,7 @@ async function createProver(options: {
serverDns: hostname,
maxSentData,
maxRecvData,
serverIdentity: false,
}),
'Error creating prover',
);
@@ -492,7 +508,26 @@ async function createProver(options: {
const sessionUrl = await handleProgress(
id,
RequestProgress.GettingSession,
() => notary.sessionUrl(maxSentData, maxRecvData),
async () => {
const resp = await fetch(`${notaryUrl}/session`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
clientType: 'Websocket',
maxRecvData,
maxSentData,
plugin: 'plugin-js',
}),
});
const { sessionId } = await resp.json();
const url = new URL(notaryUrl);
const protocol = url.protocol === 'https:' ? 'wss' : 'ws';
const pathname = url.pathname;
return `${protocol}://${url.host}${pathname === '/' ? '' : pathname}/notarize?sessionId=${sessionId!}`;
},
'Error getting session from Notary',
);
@@ -513,7 +548,7 @@ async function createProver(options: {
headers,
body,
}),
'Error sending request',
`Error connecting to websocket proxy: ${websocketProxyUrl}. Please check the proxy URL and ensure it's accessible.`,
);
return prover;
@@ -536,36 +571,44 @@ async function verifyProof(proof: PresentationJSON): Promise<{
};
switch (proof.version) {
case undefined: {
case '0.1.0-alpha.12':
result = await verify(proof);
break;
}
case '0.1.0-alpha.7':
case '0.1.0-alpha.8':
case '0.1.0-alpha.9':
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(() => '');
default:
result = {
sent: transcript.sent(),
recv: transcript.recv(),
verifierKey: verifyingKey,
notaryKey: publicKey,
sent: 'version not supported',
recv: 'version not supported',
};
break;
}
return result;
return result!;
}
async function verify(proof: PresentationJSON) {
if (proof.version !== '0.1.0-alpha.12') {
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(
@@ -589,6 +632,75 @@ function updateRequestProgress(
});
}
function getWebsocketErrorMessage(
lowerError: string,
fallbackMessage: string,
): string {
const isWebsocketError =
lowerError.includes('websocket') ||
lowerError.includes('proxy') ||
lowerError.includes('connection') ||
lowerError.includes('network') ||
lowerError.includes('prover error') ||
lowerError.includes('io error') ||
lowerError.includes('certificate') ||
lowerError.includes('cert') ||
lowerError.includes('ssl') ||
lowerError.includes('tls');
if (!isWebsocketError) {
return fallbackMessage;
}
const errorPatterns = [
{
patterns: ['protocol', 'must use ws://', 'must use wss://'],
message:
'Invalid websocket proxy URL protocol. Please use ws:// or wss:// protocol in your websocket proxy URL settings.',
},
{
patterns: [
'not allowed',
'not whitelisted',
'forbidden',
'unauthorized',
'permission denied',
'access denied',
],
message:
'Target domain not allowed by websocket proxy. Please check if the website domain is supported by your proxy service.',
},
{
patterns: ['dns', 'resolve'],
message:
'Cannot resolve websocket proxy domain. Please check your websocket proxy URL in settings.',
},
{
patterns: ['timeout'],
message:
'Websocket proxy connection timeout. Please check your websocket proxy URL in settings and ensure the server is accessible.',
},
{
patterns: ['refused', 'unreachable'],
message:
'Cannot reach websocket proxy server. Please check your websocket proxy URL in settings and ensure the server is accessible.',
},
{
patterns: ['cert', 'certificate', 'certnotvalidforname'],
message:
'Cannot connect to websocket proxy server. Please check your websocket proxy URL in settings and ensure it points to a valid websocket proxy service.',
},
];
for (const { patterns, message } of errorPatterns) {
if (patterns.some((pattern) => lowerError.includes(pattern))) {
return message;
}
}
return 'Websocket proxy connection failed. Please check your websocket proxy URL in settings and ensure the server is accessible.';
}
async function handleProgress<T>(
id: string,
progress: RequestProgress,
@@ -599,12 +711,17 @@ async function handleProgress<T>(
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',
const specificError = error?.message || '';
const lowerError = specificError.toLowerCase();
const finalErrorMessage = getWebsocketErrorMessage(
lowerError,
errorMessage,
);
updateRequestProgress(id, RequestProgress.Error, finalErrorMessage);
await setNotaryRequestStatus(id, 'error');
await setNotaryRequestError(id, finalErrorMessage);
throw error;
}
}

View File

@@ -1,14 +1,8 @@
import React, { useEffect, useState } from 'react';
import { Navigate, Route, Routes, useNavigate } from 'react-router';
import { useDispatch } from 'react-redux';
import {
setActiveTab,
setRequests,
useActiveTab,
useActiveTabUrl,
} from '../../reducers/requests';
import { setActiveTab, setRequests } 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';
@@ -16,28 +10,15 @@ 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 { InstallPluginApproval } from '../../pages/InstallPluginApproval';
import { GetPluginsApproval } from '../../pages/GetPluginsApproval';
import { RunPluginApproval } from '../../pages/RunPluginApproval';
import Icon from '../../components/Icon';
import classNames from 'classnames';
import { getConnection } from '../Background/db';
import { useIsConnected, setConnection } from '../../reducers/requests';
import { MenuIcon } from '../../components/Menu';
import Plugins from '../../pages/Plugins';
import { P2PHome } from '../../pages/PeerToPeer';
import { fetchP2PState } from '../../reducers/p2p';
import { RunPluginByUrlApproval } from '../../pages/RunPluginByUrlApproval';
const Popup = () => {
const dispatch = useDispatch();
@@ -104,7 +85,6 @@ const Popup = () => {
<div className="flex flex-row flex-grow items-center justify-end gap-4">
{!isPopup && (
<>
<AppConnectionLogo />
<MenuIcon />
</>
)}
@@ -119,20 +99,13 @@ const Popup = () => {
<Route path="/requests" element={<Home tab="network" />} />
<Route path="/custom/*" element={<RequestBuilder />} />
<Route path="/options" element={<Options />} />
<Route path="/plugins" element={<Plugins />} />
<Route path="/home" element={<Home />} />
<Route path="/plugininfo" element={<PluginUploadInfo />} />
<Route path="/connection-approval" element={<ConnectionApproval />} />
<Route path="/get-history-approval" element={<GetHistoryApproval />} />
<Route path="/get-proof-approval" element={<GetProofApproval />} />
<Route path="/notarize-approval" element={<NotarizeApproval />} />
<Route path="/get-plugins-approval" element={<GetPluginsApproval />} />
<Route path="/run-plugin-approval" element={<RunPluginApproval />} />
<Route path="/p2p" element={<P2PHome />} />
<Route
path="/install-plugin-approval"
element={<InstallPluginApproval />}
path="/run-plugin-approval"
element={<RunPluginByUrlApproval />}
/>
<Route path="/p2p" element={<P2PHome />} />
<Route path="*" element={<Navigate to="/home" />} />
</Routes>
</div>
@@ -140,58 +113,3 @@ const Popup = () => {
};
export default Popup;
function AppConnectionLogo() {
const dispatch = useDispatch();
const activeTab = useActiveTab();
const url = useActiveTabUrl();
const [showConnectionDetails, setShowConnectionDetails] = useState(false);
const connected = useIsConnected();
useEffect(() => {
(async () => {
if (url) {
const isConnected: boolean | null = await getConnection(url?.origin);
dispatch(setConnection(!!isConnected));
}
})();
}, [url]);
return (
<div
className="flex flex-nowrap flex-row items-center gap-1 justify-center w-fit cursor-pointer"
onClick={() => setShowConnectionDetails(true)}
>
<div className="flex flex-row relative bg-black border-[1px] border-black rounded-full">
{!!activeTab?.favIconUrl ? (
<img
src={activeTab?.favIconUrl}
className="h-5 rounded-full"
alt="logo"
/>
) : (
<Icon
fa="fa-solid fa-globe"
className="bg-white text-slate-400 rounded-full"
size={1.25}
/>
)}
<div
className={classNames(
'absolute right-[-2px] bottom-[-2px] rounded-full h-[10px] w-[10px] border-[2px]',
{
'bg-green-500': connected,
'bg-slate-500': !connected,
},
)}
/>
</div>
{showConnectionDetails && (
<ConnectionDetailsModal
showConnectionDetails={showConnectionDetails}
setShowConnectionDetails={setShowConnectionDetails}
/>
)}
</div>
);
}

View File

@@ -18,13 +18,13 @@ import {
progressText,
RequestProgress,
} from '../Background/rpc';
import { getPluginByHash, getPluginConfigByHash } from '../Background/db';
import { getPluginByUrl, getPluginConfigByUrl } from '../Background/db';
import { SidePanelActionTypes } from './types';
import { fetchP2PState, useClientId } from '../../reducers/p2p';
export default function SidePanel(): ReactElement {
const [config, setConfig] = useState<PluginConfig | null>(null);
const [hash, setHash] = useState('');
const [url, setUrl] = useState('');
const [hex, setHex] = useState('');
const [p2p, setP2P] = useState(false);
const [params, setParams] = useState<Record<string, string> | undefined>();
@@ -44,8 +44,9 @@ export default function SidePanel(): ReactElement {
switch (type) {
case SidePanelActionTypes.execute_plugin_request: {
setConfig(await getPluginConfigByHash(data.pluginHash));
setHash(data.pluginHash);
const pluginIdentifier = data.pluginUrl || data.pluginHash;
setConfig(await getPluginConfigByUrl(pluginIdentifier));
setUrl(pluginIdentifier);
setParams(data.pluginParams);
setStarted(true);
break;
@@ -53,10 +54,10 @@ export default function SidePanel(): ReactElement {
case SidePanelActionTypes.run_p2p_plugin_request: {
const { pluginHash, plugin } = data;
const config =
(await getPluginConfigByHash(pluginHash)) ||
(await getPluginConfigByUrl(pluginHash)) ||
(await getPluginConfig(hexToArrayBuffer(plugin)));
setHash(pluginHash);
setUrl(pluginHash);
setHex(plugin);
setP2P(true);
setConfig(config);
@@ -71,7 +72,7 @@ export default function SidePanel(): ReactElement {
}
case SidePanelActionTypes.reset_panel: {
setConfig(null);
setHash('');
setUrl('');
setHex('');
setStarted(false);
break;
@@ -92,9 +93,10 @@ export default function SidePanel(): ReactElement {
</button>
</div>
{/*{!config && <PluginList />}*/}
{started && config && (
<PluginBody
hash={hash}
url={url}
hex={hex}
config={config}
p2p={p2p}
@@ -106,15 +108,21 @@ export default function SidePanel(): ReactElement {
);
}
function PluginBody(props: {
function PluginBody({
url,
hex,
config,
p2p,
clientId,
presetParameterValues,
}: {
config: PluginConfig;
hash: string;
url: string;
hex?: string;
clientId?: string;
p2p?: boolean;
presetParameterValues?: Record<string, string>;
}): ReactElement {
const { hash, hex, config, p2p, clientId, presetParameterValues } = props;
const { title, description, icon, steps } = config;
const [responses, setResponses] = useState<any[]>([]);
const [notarizationId, setNotarizationId] = useState('');
@@ -129,7 +137,7 @@ function PluginBody(props: {
setNotarizationId(response);
}
},
[hash, responses],
[url, responses],
);
useEffect(() => {
@@ -137,7 +145,7 @@ function PluginBody(props: {
browser.runtime.sendMessage({
type: SidePanelActionTypes.execute_plugin_response,
data: {
hash,
url,
proof: notaryRequest.proof,
},
});
@@ -145,12 +153,15 @@ function PluginBody(props: {
browser.runtime.sendMessage({
type: SidePanelActionTypes.execute_plugin_response,
data: {
hash,
error: notaryRequest.error,
url,
error:
notaryRequest.errorMessage ||
notaryRequest.error ||
'Notarization failed',
},
});
}
}, [hash, notaryRequest?.status]);
}, [url, notaryRequest?.status]);
return (
<div className="flex flex-col p-4">
@@ -167,7 +178,7 @@ function PluginBody(props: {
{steps?.map((step, i) => (
<StepContent
key={i}
hash={hash}
url={url}
config={config}
hex={hex}
index={i}
@@ -187,7 +198,7 @@ function PluginBody(props: {
function StepContent(
props: StepConfig & {
hash: string;
url: string;
hex?: string;
clientId?: string;
index: number;
@@ -208,7 +219,7 @@ function StepContent(
setResponse,
lastResponse,
prover,
hash,
url,
hex: _hex,
config,
p2p = false,
@@ -222,10 +233,10 @@ function StepContent(
const notaryRequest = useRequestHistory(notarizationId);
const getPlugin = useCallback(async () => {
const hex = (await getPluginByHash(hash)) || _hex;
const hex = (await getPluginByUrl(url)) || _hex;
const arrayBuffer = hexToArrayBuffer(hex!);
return makePlugin(arrayBuffer, config, { p2p, clientId });
}, [hash, _hex, config, p2p, clientId]);
}, [url, _hex, config, p2p, clientId]);
const processStep = useCallback(async () => {
const plugin = await getPlugin();
@@ -242,8 +253,7 @@ function StepContent(
? JSON.stringify(lastResponse)
: JSON.stringify(parameterValues),
);
console.log(out);
const val = JSON.parse(out.string());
const val = JSON.parse(out!.string());
if (val && prover) {
setNotarizationId(val);
} else {
@@ -252,7 +262,7 @@ function StepContent(
setResponse(val, index);
} catch (e: any) {
console.error(e);
setError(e?.message || 'Unkonwn error');
setError(e?.message || 'Unknown error');
} finally {
setPending(false);
}
@@ -344,12 +354,18 @@ function StepContent(
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 className="flex flex-col gap-1">
<div className="flex flex-row items-start gap-2 text-red-600">
<Icon
fa="fa-solid fa-triangle-exclamation"
size={1}
className="mt-0.5"
/>
<span className="text-sm">
{notaryRequest?.errorMessage ||
progressText(notaryRequest.progress)}
</span>
</div>
</div>
)}
{notaryRequest?.progress !== RequestProgress.Error && (
@@ -392,7 +408,18 @@ function StepContent(
{!!description && (
<div className="text-slate-500 text-sm">{description}</div>
)}
{!!error && <div className="text-red-500 text-sm">{error}</div>}
{!!error && (
<div className="flex flex-col gap-1">
<div className="flex flex-row items-start gap-2 text-red-600">
<Icon
fa="fa-solid fa-triangle-exclamation"
size={1}
className="mt-0.5"
/>
<div className="text-red-500 text-sm">{error}</div>
</div>
</div>
)}
{btnContent}
</div>
</div>

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,60 +1,44 @@
import React, {
MouseEventHandler,
ReactElement,
ReactNode,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import Icon from '../../components/Icon';
import classNames from 'classnames';
import { useNavigate } from 'react-router';
import { ErrorModal } from '../../components/ErrorModal';
import History from '../History';
import './index.scss';
import Requests from '../Requests';
import PluginUploadInfo from '../../components/PluginInfo';
import {
useOnPluginClick,
usePluginConfig,
usePluginHashes,
} from '../../reducers/plugins';
import { fetchPluginHashes } from '../../utils/rpc';
import DefaultPluginIcon from '../../assets/img/default-plugin-icon.png';
import { useClientId } from '../../reducers/p2p';
import { PluginList } from '../../components/PluginList';
import { getDeveloperMode } from '../../utils/storage';
export default function Home(props: {
tab?: 'history' | 'network';
}): ReactElement {
const [error, showError] = useState('');
const [tab, setTab] = useState<'history' | 'network'>(props.tab || 'history');
const [tab, setTab] = useState<'history' | 'network' | 'plugins'>(
props.tab || 'history',
);
const scrollableContent = useRef<HTMLDivElement | null>(null);
const [shouldFix, setFix] = useState(false);
const [actionPanelElement, setActionPanelElement] =
useState<HTMLDivElement | null>(null);
const [scrollTop, setScrollTop] = useState(0);
const [developerMode, setDeveloperMode] = useState(false);
useEffect(() => {
fetchPluginHashes();
getDeveloperMode().then(setDeveloperMode);
}, []);
useEffect(() => {
const element = scrollableContent.current;
if (!element) return;
if (!actionPanelElement) return;
let timer = Date.now();
const onScroll = () => {
const now = Date.now();
if (now - timer > 20) {
timer = now;
setScrollTop(element.scrollTop);
if (element.scrollTop >= actionPanelElement.clientHeight) {
setFix(true);
} else {
setFix(false);
}
if (element.scrollTop > 0) {
setFix(true);
} else {
setFix(false);
}
};
@@ -63,7 +47,7 @@ export default function Home(props: {
return () => {
element.removeEventListener('scroll', onScroll);
};
}, [scrollableContent, actionPanelElement]);
}, [scrollableContent]);
return (
<div
@@ -72,10 +56,6 @@ export default function Home(props: {
className="flex flex-col flex-grow overflow-y-auto"
>
{error && <ErrorModal onClose={() => showError('')} message={error} />}
<ActionPanel
setActionPanelElement={setActionPanelElement}
scrollTop={scrollTop}
/>
<div
className={classNames(
'flex flex-row justify-center items-center z-10',
@@ -97,167 +77,29 @@ export default function Home(props: {
>
History
</TabSelector>
{developerMode && (
<TabSelector
onClick={() => setTab('plugins')}
selected={tab === 'plugins'}
>
Plugins
</TabSelector>
)}
</div>
<div className="flex-grow">
{tab === 'history' && <History />}
{tab === 'network' && <Requests shouldFix={shouldFix} />}
{tab === 'plugins' && (
<PluginList
className="p-2 overflow-y-auto"
showAddButton={developerMode}
/>
)}
</div>
</div>
);
}
function ActionPanel({
setActionPanelElement,
scrollTop,
}: {
scrollTop: number;
setActionPanelElement: (el: HTMLDivElement) => void;
}) {
const pluginHashes = usePluginHashes();
const navigate = useNavigate();
const clientId = useClientId();
const container = useRef<HTMLDivElement | null>(null);
const [isOverflow, setOverflow] = useState(false);
const [expanded, setExpand] = useState(false);
const onCheckSize = useCallback(() => {
const element = container.current;
if (!element) return;
setActionPanelElement(element);
if (element.scrollWidth > element.clientWidth) {
setOverflow(true);
} else {
setOverflow(false);
}
}, [container]);
useEffect(() => {
onCheckSize();
window.addEventListener('resize', onCheckSize);
return () => {
window.removeEventListener('resize', onCheckSize);
};
}, [onCheckSize, pluginHashes]);
useEffect(() => {
const element = container.current;
if (!element) return;
if (scrollTop >= element.clientHeight) {
setExpand(false);
}
}, [container, scrollTop]);
return (
<div
ref={container}
className={classNames(
'flex flex-row justify-start items-center gap-4 p-4 border-b relative',
{
'flex-wrap': expanded,
'flex-nowrap': !expanded,
},
)}
>
<NavButton
fa="fa-solid fa-hammer"
onClick={() => navigate('/custom')}
title="Build a custom request"
>
Custom
</NavButton>
<NavButton
fa="fa-solid fa-certificate"
onClick={() => navigate('/verify')}
title="Visualize an attestation"
>
Verify
</NavButton>
<NavButton
className={'relative'}
fa="fa-solid fa-network-wired"
iconSize={0.5}
iconClassName={classNames({
'!text-green-500': clientId,
})}
onClick={() => navigate('/p2p')}
>
P2P
</NavButton>
{pluginHashes.map((hash) => (
<PluginIcon hash={hash} onCheckSize={onCheckSize} />
))}
<button
className={
'flex flex-row shrink-0 items-center justify-center self-start rounded relative border-2 border-dashed border-slate-300 hover:border-slate-400 text-slate-300 hover:text-slate-400 h-16 w-16 mx-1'
}
title="Install a plugin"
>
<PluginUploadInfo />
<Icon fa="fa-solid fa-plus" />
</button>
<button
className={classNames(
'absolute right-0 top-0 w-6 h-full bg-slate-100 hover:bg-slate-200 font-semibold',
'flex flex-row items-center justify-center gap-2 text-slate-500 hover:text-slate-700',
{
hidden: !isOverflow || expanded,
},
)}
onClick={() => setExpand(true)}
>
<Icon fa="fa-solid fa-caret-down" size={0.875} />
</button>
</div>
);
}
function PluginIcon({
hash,
onCheckSize,
}: {
hash: string;
onCheckSize: () => void;
}) {
const config = usePluginConfig(hash);
const onPluginClick = useOnPluginClick(hash);
const onClick = useCallback(() => {
if (!config) return;
onPluginClick();
}, [onPluginClick, config]);
if (!config) return null;
return (
<button
ref={() => {
onCheckSize();
}}
className={classNames(
'flex flex-col flex-nowrap items-center justify-center',
'text-white px-2 py-1 gap-1 opacity-90 hover:opacity-100 w-18',
)}
onClick={onClick}
>
<Icon
className="rounded-full flex flex-row items-center justify-center flex-grow-0 flex-shrink-0"
url={config?.icon || DefaultPluginIcon}
size={2}
/>
<span className="font-bold text-primary h-10 w-14 overflow-hidden text-ellipsis">
{config?.title}
</span>
</button>
);
}
function TabSelector(props: {
children: string;
className?: string;
@@ -280,39 +122,3 @@ function TabSelector(props: {
</button>
);
}
function NavButton(props: {
fa: string;
children?: ReactNode;
onClick?: MouseEventHandler;
className?: string;
title?: string;
iconClassName?: string;
disabled?: boolean;
iconSize?: number;
}): ReactElement {
return (
<button
className={classNames(
'flex flex-col flex-nowrap items-center justify-center',
'text-white px-2 py-1 gap-1 opacity-90 hover:opacity-100 w-18',
props.className,
)}
onClick={props.onClick}
disabled={props.disabled}
title={props.title}
>
<Icon
className={classNames(
'w-8 h-8 rounded-full bg-primary flex flex-row items-center justify-center flex-grow-0 flex-shrink-0',
props.iconClassName,
)}
fa={props.fa}
size={0.875}
/>
<span className="font-bold text-primary h-10 w-14 overflow-hidden text-ellipsis">
{props.children}
</span>
</button>
);
}

View File

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

View File

@@ -19,6 +19,8 @@ import {
LOGGING_FILTER_KEY,
getRendezvousApi,
RENDEZVOUS_API_LS_KEY,
getDeveloperMode,
DEVELOPER_MODE_LS_KEY,
} from '../../utils/storage';
import {
EXPLORER_API,
@@ -32,6 +34,7 @@ import Modal, { ModalContent } from '../../components/Modal/Modal';
import browser from 'webextension-polyfill';
import { LoggingLevel } from 'tlsn-js';
import { version } from '../../../package.json';
import { getDBSize, resetDB } from '../../entries/Background/db';
export default function Options(): ReactElement {
const [notary, setNotary] = useState(NOTARY_API);
@@ -40,16 +43,28 @@ export default function Options(): ReactElement {
const [maxReceived, setMaxReceived] = useState(MAX_RECV);
const [loggingLevel, setLoggingLevel] = useState<LoggingLevel>('Info');
const [rendezvous, setRendezvous] = useState(RENDEZVOUS_API);
const [developerMode, setDeveloperMode] = useState(false);
const [dirty, setDirty] = useState(false);
const [shouldReload, setShouldReload] = useState(false);
const [advanced, setAdvanced] = useState(false);
const [showReloadModal, setShowReloadModal] = useState(false);
const [dbSize, setDbSize] = useState(0);
const [isCalculatingDbSize, setIsCalculatingDbSize] = useState(false);
useEffect(() => {
(async () => {
setIsCalculatingDbSize(true);
setDbSize(await getDBSize());
setIsCalculatingDbSize(false);
})();
}, []);
useEffect(() => {
(async () => {
setNotary(await getNotaryApi());
setProxy(await getProxyApi());
setDeveloperMode(await getDeveloperMode());
})();
}, []);
@@ -74,6 +89,7 @@ export default function Options(): ReactElement {
await set(MAX_RECEIVED_LS_KEY, maxReceived.toString());
await set(LOGGING_FILTER_KEY, loggingLevel);
await set(RENDEZVOUS_API_LS_KEY, rendezvous);
await set(DEVELOPER_MODE_LS_KEY, developerMode.toString());
setDirty(false);
},
[
@@ -83,6 +99,7 @@ export default function Options(): ReactElement {
maxReceived,
loggingLevel,
rendezvous,
developerMode,
shouldReload,
],
);
@@ -103,6 +120,13 @@ export default function Options(): ReactElement {
browser.tabs.create({ url });
}, []);
const onCleanCache = useCallback(async () => {
setIsCalculatingDbSize(true);
await resetDB();
setDbSize(await getDBSize());
setIsCalculatingDbSize(false);
}, []);
return (
<div className="flex flex-col flex-nowrap flex-grow overflow-y-auto">
{showReloadModal && (
@@ -139,6 +163,8 @@ export default function Options(): ReactElement {
proxy={proxy}
setProxy={setProxy}
setDirty={setDirty}
developerMode={developerMode}
setDeveloperMode={setDeveloperMode}
/>
<div className="justify-left px-2 pt-3 gap-2">
<button className="font-bold" onClick={onAdvanced}>
@@ -192,6 +218,15 @@ export default function Options(): ReactElement {
>
Join our Discord
</button>
<button className="button" onClick={onCleanCache}>
<span>Clean Cache (</span>
{isCalculatingDbSize ? (
<i className="fa-solid fa-spinner fa-spin"></i>
) : (
<span>{(dbSize / 1024 / 1024).toFixed(2)} MB</span>
)}
<span>)</span>
</button>
</div>
</div>
);
@@ -228,8 +263,18 @@ function NormalOptions(props: {
proxy: string;
setProxy: (value: string) => void;
setDirty: (value: boolean) => void;
developerMode: boolean;
setDeveloperMode: (value: boolean) => void;
}) {
const { notary, setNotary, proxy, setProxy, setDirty } = props;
const {
notary,
setNotary,
proxy,
setProxy,
setDirty,
developerMode,
setDeveloperMode,
} = props;
return (
<div>
@@ -261,6 +306,33 @@ function NormalOptions(props: {
<div className="font-semibold">Explorer URL</div>
<div className="input border bg-slate-100">{EXPLORER_API}</div>
</div>
<div className="flex flex-row items-center py-3 px-2 gap-2">
<div className="font-semibold">Developer Mode</div>
<div className="relative inline-block w-9 h-5">
<input
type="checkbox"
id="developer-mode"
checked={developerMode}
onChange={(e) => {
setDeveloperMode(e.target.checked);
setDirty(true);
}}
className="sr-only"
/>
<label
htmlFor="developer-mode"
className={`block h-5 rounded-full cursor-pointer transition-all duration-300 ease-in-out ${
developerMode ? 'bg-blue-500' : 'bg-gray-300'
}`}
>
<span
className={`absolute top-0.5 left-0.5 w-4 h-4 bg-white rounded-full shadow-sm transform transition-all duration-300 ease-in-out ${
developerMode ? 'translate-x-4' : 'translate-x-0'
}`}
/>
</label>
</div>
</div>
</div>
);
}

View File

@@ -38,7 +38,6 @@ import browser from 'webextension-polyfill';
import { sha256 } from '../../utils/misc';
import { openSidePanel } from '../../entries/utils';
import { SidePanelActionTypes } from '../../entries/SidePanel/types';
import { verify } from 'tlsn-js-v5';
import ProofViewer from '../ProofViewer';
export function P2PHome(): ReactElement {

View File

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

View File

@@ -1,13 +1,9 @@
import { useSelector } from 'react-redux';
import { AppRootState } from './index';
import deepEqual from 'fast-deep-equal';
import { useCallback, useEffect, useState } from 'react';
import { getPluginConfigByHash } from '../entries/Background/db';
import { useEffect, useState } from 'react';
import { getPluginConfigByUrl } from '../entries/Background/db';
import { PluginConfig } from '../utils/misc';
import { runPlugin } from '../utils/rpc';
import browser from 'webextension-polyfill';
import { openSidePanel } from '../entries/utils';
import { SidePanelActionTypes } from '../entries/SidePanel/types';
enum ActionType {
'/plugin/addPlugin' = '/plugin/addPlugin',
@@ -64,27 +60,8 @@ export const usePluginConfig = (hash: string) => {
const [config, setConfig] = useState<PluginConfig | null>(null);
useEffect(() => {
(async function () {
setConfig(await getPluginConfigByHash(hash));
setConfig(await getPluginConfigByUrl(hash));
})();
}, [hash]);
return config;
};
export const useOnPluginClick = (hash: string) => {
return useCallback(async () => {
await browser.storage.local.set({ plugin_hash: hash });
await openSidePanel();
browser.runtime.sendMessage({
type: SidePanelActionTypes.execute_plugin_request,
data: {
pluginHash: hash,
},
});
await runPlugin(hash, 'start');
window.close();
}, [hash]);
};

View File

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

View File

@@ -225,11 +225,12 @@ export const makePlugin = async (
if (meta?.p2p) {
const pluginHex = Buffer.from(arrayBuffer).toString('hex');
const pluginUrl = await sha256(pluginHex);
handleExecP2PPluginProver({
type: BackgroundActiontype.execute_p2p_plugin_prover,
data: {
...params,
pluginHash: await sha256(pluginHex),
pluginUrl,
pluginHex,
body: reqBody,
now,
@@ -246,7 +247,7 @@ export const makePlugin = async (
return new Promise((resolve) => {
setTimeout(async () => {
const out = await plugin.call(getSecretResponse, body);
resolve(JSON.parse(out.string()));
resolve(JSON.parse(out?.string() || '{}'));
}, 0);
});
},

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) {
export async function addPlugin(hex: string, url: string) {
return browser.runtime.sendMessage({
type: BackgroundActiontype.add_plugin,
data: hex,
data: { hex, url },
});
}

View File

@@ -8,6 +8,7 @@ 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 const DEVELOPER_MODE_LS_KEY = 'developer-mode';
export async function set(key: string, value: string) {
return chrome.storage.sync.set({ [key]: value });
@@ -43,3 +44,8 @@ export async function getLoggingFilter(): Promise<LoggingLevel> {
export async function getRendezvousApi(): Promise<string> {
return await get(RENDEZVOUS_API_LS_KEY, RENDEZVOUS_API);
}
export async function getDeveloperMode(): Promise<boolean> {
const value = await get(DEVELOPER_MODE_LS_KEY, 'false');
return value === 'true';
}

View File

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

View File

@@ -22,6 +22,28 @@ config.plugins = (config.plugins || []).concat(
}),
);
webpack(config, function (err) {
if (err) throw err;
webpack(config, function (err, stats) {
if (err) {
console.error('Webpack error:', err);
process.exit(1);
}
if (stats.hasErrors()) {
console.error('Build failed with errors:');
const info = stats.toJson();
console.error(info.errors.map((e) => e.message).join('\n\n'));
process.exit(1);
}
if (stats.hasWarnings()) {
console.warn('Build completed with warnings:');
const info = stats.toJson();
console.warn(info.warnings.map((w) => w.message).join('\n\n'));
}
console.log('Build completed successfully!');
console.log(`Output: ${path.join(__dirname, '../', 'build')}`);
console.log(
`Zip: ${path.join(__dirname, '../', 'zip', `${packageInfo.name}-${packageInfo.version}.zip`)}`,
);
});