Compare commits

..

20 Commits

Author SHA1 Message Date
John Shaw
da2ec501f3 chatbox fix 2024-04-13 18:15:58 +02:00
tsukino
871c765963 fix: auto scroll messages 2024-04-13 18:13:54 +02:00
John Shaw
31ca65ccd2 chatbox styling 2024-04-13 17:48:31 +02:00
John Shaw
8ad4c0b4f8 wip 2024-04-13 16:52:06 +02:00
tsukino
3d8aec4083 fix: styling change 2024-04-13 16:51:27 +02:00
tsukino
e63d21e394 fix: use correct method name 2024-04-13 16:40:44 +02:00
John Shaw
fca19846b4 wip 2024-04-13 16:30:45 +02:00
tsukino
12e6806fa8 wip: move create session to p2p 2024-04-13 16:27:53 +02:00
tsukino
b6d801a45e wip 2024-04-13 16:27:04 +02:00
John Shaw
b8f67d0adf feature: implementing client connect 2024-04-13 16:21:34 +02:00
tsukino
b3009023b3 append message to list 2024-04-13 16:14:09 +02:00
tsukino
02398eb253 wip: use pair id in chatbox 2024-04-13 16:12:31 +02:00
tsukino
f2e298943f wip: consolidate logic to ChatBox 2024-04-13 16:11:38 +02:00
tsukino
743b073fbe wip: add new rpc method to p2p 2024-04-13 16:06:37 +02:00
tsukino
78fc4bc452 wip: add chatbox 2024-04-13 15:53:51 +02:00
tsukino
c0fb13d5c8 wip: adding verifier ux 2024-04-13 14:16:51 +02:00
John Shaw
6ffe529144 refactor: p2p chatting 2024-04-13 13:40:10 +02:00
John Shaw
82d5b77969 feature: websocket connections + chatting 2024-04-13 13:29:31 +02:00
John Shaw
f58431cfb2 refactor: p2p component 2024-04-13 12:35:22 +02:00
John Shaw
86f21ac8a1 feature: creating the p2p ui component 2024-04-13 12:24:45 +02:00
55 changed files with 6611 additions and 7633 deletions

View File

@@ -31,7 +31,6 @@
"wasm",
"tlsn",
"util",
"plugins",
"webpack.config.js"
]
}

1
.gitignore vendored
View File

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

View File

@@ -1,27 +1,9 @@
![MIT licensed][mit-badge]
![Apache licensed][apache-badge]
[![Build Status][actions-badge]][actions-url]
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
[apache-badge]: https://img.shields.io/github/license/saltstack/salt
[actions-badge]: https://github.com/tlsnotary/tlsn-extension/actions/workflows/build.yaml/badge.svg
[actions-url]: https://github.com/tlsnotary/tlsn-extension/actions?query=workflow%3Abuild+branch%3Amain++
<img src="src/assets/img/icon-128.png" width="64"/>
# Chrome Extension (MV3) for TLSNotary
> [!IMPORTANT]
> ⚠️ When running the extension against a [notary server](https://github.com/tlsnotary/tlsn/tree/dev/notary-server), please ensure that the server's version is the same as the version of this extension
## License
This repository is licensed under either of
- [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0)
- [MIT license](http://opensource.org/licenses/MIT)
at your option.
### ⚠️ Notice
- When running the extension against a [notary server](https://github.com/tlsnotary/tlsn/tree/dev/notary-server), please ensure that the server's version is the same as the version of this extension
## Installing and Running

4566
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.5",
"version": "0.1.0.4",
"license": "MIT",
"repository": {
"type": "git",
@@ -16,7 +16,6 @@
"lint:fix": "eslint . --fix"
},
"dependencies": {
"@extism/extism": "^1.0.2",
"@fortawesome/fontawesome-free": "^6.4.2",
"async-mutex": "^0.4.0",
"buffer": "^6.0.3",
@@ -37,7 +36,7 @@
"redux-logger": "^3.0.6",
"redux-thunk": "^2.4.2",
"tailwindcss": "^3.3.3",
"tlsn-js": "0.1.0-alpha.5.3"
"tlsn-js": "0.1.0-alpha.4.1"
},
"devDependencies": {
"@babel/core": "^7.20.12",

View File

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

View File

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

View File

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

Binary file not shown.

View File

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

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

Binary file not shown.

2488
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

View File

@@ -0,0 +1,114 @@
import React, { useCallback, useEffect, useState } from 'react';
import {
sendChat,
useChatMessages,
useClientId,
usePairId,
} from '../../reducers/p2p';
import { useDispatch } from 'react-redux';
import classNames from 'classnames';
import Icon from '../Icon';
export default function ChatBox() {
const messages = useChatMessages();
const dispatch = useDispatch();
const clientId = useClientId();
const [text, setText] = useState('');
const pairId = usePairId();
const onSend = useCallback(() => {
if (text && pairId) {
dispatch(
sendChat({
text,
from: clientId,
to: pairId,
}),
);
setText('');
console.log('after sending');
}
}, [text, pairId]);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === 'Enter' && text && pairId) {
onSend();
setText('');
}
},
[text],
);
const isClient = (msg: any) => {
return msg.from === clientId;
};
return (
<div className="flex flex-col flex-nowrap flex-grow gap-1 p-2 flex-shrink h-0 ">
<div className="flex flex-row gap-1 font-semibold text-xs align-center">
<div>Client ID:</div>
{clientId ? (
<div className="text-green-500">{clientId}</div>
) : (
<Icon
className="animate-spin text-gray-500"
fa="fa-solid fa-spinner"
size={1}
/>
)}
</div>
<div className="flex flex-row gap-1 font-semibold text-xs align-center">
<div>Peer ID:</div>
{pairId ? (
<div className="text-red-500">{pairId}</div>
) : (
<div className="flex flex-row gap-1">
<span className="text-slate-500">Waiting for Peer</span>
<Icon
className="animate-spin text-slate-500 w-fit"
fa="fa-solid fa-spinner"
size={1}
/>
</div>
)}
</div>
<div className="flex flex-col flex-grow flex-shrink h-0 gap-1">
<div className="flex flex-col border gap-1 border-slate-200 flex-grow overflow-y-auto">
{messages.map((msg) => {
return (
<div
className={`rounded-lg p-2 max-w-[50%] break-all ${isClient(msg) ? 'mr-auto bg-blue-600' : 'ml-auto bg-slate-300'}`}
>
<div
className={`${isClient(msg) ? 'text-white' : 'text-black'}`}
>
{msg.text}
</div>
</div>
);
})}
</div>
<div className="flex flex-row w-full gap-1">
<input
className="input border border-slate-200 focus:border-slate-400 flex-grow p-2"
onChange={(e) => setText(e.target.value)}
onKeyDown={handleKeyDown}
value={text}
autoFocus
/>
<button
className={classNames('button', {
'button--primary': !!text && !!pairId,
})}
disabled={!text || !pairId}
onClick={onSend}
>
Send
</button>
</div>
</div>
</div>
);
}

View File

@@ -1,26 +0,0 @@
import React, { ReactElement } from 'react';
import Modal, { ModalContent } from '../Modal/Modal';
export function ErrorModal(props: {
onClose: () => void;
message: string;
}): ReactElement {
const { onClose, message } = props;
return (
<Modal
className="flex flex-col gap-4 items-center text-base cursor-default justify-center !w-auto mx-4 my-[50%] min-h-24 p-4 border border-red-500 !bg-red-100"
onClose={onClose}
>
<ModalContent className="flex justify-center items-center text-red-500">
{message || 'Something went wrong :('}
</ModalContent>
<button
className="m-0 w-24 bg-red-200 text-red-400 hover:bg-red-200 hover:text-red-500"
onClick={onClose}
>
OK
</button>
</Modal>
);
}

View File

@@ -1,23 +0,0 @@
.plugin-box {
&__remove-icon {
opacity: 0;
height: 0;
width: 0;
padding: 0;
overflow: hidden;
transition: 200ms opacity;
}
&:hover {
.plugin-box__remove-icon {
height: 1.25rem;
width: 1.25rem;
padding: .5rem;
opacity: .5;
&:hover {
opacity: 1;
}
}
}
}

View File

@@ -1,113 +0,0 @@
import React, {
ChangeEvent,
MouseEventHandler,
ReactElement,
useCallback,
useEffect,
useState,
} from 'react';
import {
fetchPluginHashes,
removePlugin,
fetchPluginConfigByHash,
runPlugin,
} from '../../utils/rpc';
import { usePluginHashes } from '../../reducers/plugins';
import { PluginConfig } from '../../utils/misc';
import DefaultPluginIcon from '../../assets/img/default-plugin-icon.png';
import classNames from 'classnames';
import Icon from '../Icon';
import './index.scss';
import browser from 'webextension-polyfill';
import { ErrorModal } from '../ErrorModal';
export function PluginList(props: { className?: string }): ReactElement {
const hashes = usePluginHashes();
useEffect(() => {
fetchPluginHashes();
}, []);
return (
<div className={classNames('flex flex-col flex-nowrap', props.className)}>
{!hashes.length && (
<div className="flex flex-col items-center justify-center text-slate-400 cursor-default select-none">
<div>No available plugins</div>
</div>
)}
{hashes.map((hash) => (
<Plugin key={hash} hash={hash} />
))}
</div>
);
}
export function Plugin(props: {
hash: string;
onClick?: () => void;
}): ReactElement {
const [error, showError] = useState('');
const [config, setConfig] = useState<PluginConfig | null>(null);
const onClick = useCallback(async () => {
if (!config) return;
try {
await runPlugin(props.hash, 'start');
const [tab] = await browser.tabs.query({
active: true,
currentWindow: true,
});
await browser.storage.local.set({ plugin_hash: props.hash });
// @ts-ignore
if (chrome.sidePanel) await chrome.sidePanel.open({ tabId: tab.id });
window.close();
} catch (e: any) {
showError(e.message);
}
}, [props.hash, config]);
useEffect(() => {
(async function () {
setConfig(await fetchPluginConfigByHash(props.hash));
})();
}, [props.hash]);
const onRemove: MouseEventHandler = useCallback(
(e) => {
e.stopPropagation();
removePlugin(props.hash);
},
[props.hash],
);
if (!config) return <></>;
return (
<button
className={classNames(
'flex flex-row border rounded border-slate-300 p-2 gap-2 plugin-box',
'cursor-pointer hover:bg-slate-100 hover:border-slate-400 active:bg-slate-200',
)}
onClick={onClick}
>
{!!error && <ErrorModal onClose={() => showError('')} message={error} />}
<img className="w-12 h-12" src={config.icon || DefaultPluginIcon} />
<div className="flex flex-col w-full items-start">
<div className="font-bold flex flex-row h-6 items-center justify-between w-full">
{config.title}
<Icon
fa="fa-solid fa-xmark"
className="flex flex-row items-center justify-center cursor-pointer text-red-500 bg-red-200 rounded-full plugin-box__remove-icon"
onClick={onRemove}
/>
</div>
<div>{config.description}</div>
</div>
</button>
);
}

View File

@@ -5,8 +5,9 @@ import React, {
useEffect,
useState,
} from 'react';
import { useRequest } from '../../reducers/requests';
import { notarizeRequest, useRequest } from '../../reducers/requests';
import classNames from 'classnames';
import { useDispatch } from 'react-redux';
import {
Route,
Routes,
@@ -16,14 +17,8 @@ import {
} from 'react-router';
import Icon from '../Icon';
import NavigateWithParams from '../NavigateWithParams';
import {
set,
MAX_SENT_LS_KEY,
MAX_RECEIVED_LS_KEY,
getMaxRecv,
getMaxSent,
} from '../../utils/storage';
import { MAX_RECV, MAX_SENT } from '../../utils/constants';
import { get, NOTARY_API_LS_KEY, PROXY_API_LS_KEY } from '../../utils/storage';
import { urlify } from '../../utils/misc';
type Props = {
requestId: string;
@@ -36,6 +31,7 @@ export default function RequestDetail(props: Props): ReactElement {
const notarize = useCallback(async () => {
if (!request) return;
console.log('/notary/' + props.requestId);
navigate('/notary/' + request.requestId);
}, [request, props.requestId]);
@@ -58,9 +54,6 @@ export default function RequestDetail(props: Props): ReactElement {
<RequestDetailsHeaderTab path="/response">
Response
</RequestDetailsHeaderTab>
<RequestDetailsHeaderTab path="/advanced">
Advanced
</RequestDetailsHeaderTab>
<button
className="absolute right-2 bg-primary/[0.9] text-white font-bold px-2 py-0.5 hover:bg-primary/[0.8] active:bg-primary"
onClick={notarize}
@@ -81,7 +74,6 @@ export default function RequestDetail(props: Props): ReactElement {
path="response"
element={<WebResponse requestId={props.requestId} />}
/>
<Route path="advanced" element={<AdvancedOptions />} />
<Route path="/" element={<NavigateWithParams to="/headers" />} />
</Routes>
</>
@@ -109,62 +101,6 @@ function RequestDetailsHeaderTab(props: {
);
}
function AdvancedOptions(): ReactElement {
const [maxSent, setMaxSent] = useState(MAX_SENT);
const [maxRecv, setMaxRecv] = useState(MAX_RECV);
const [dirty, setDirty] = useState(false);
useEffect(() => {
(async () => {
setMaxRecv((await getMaxRecv()) || MAX_RECV);
setMaxSent((await getMaxSent()) || MAX_SENT);
})();
}, []);
const onSave = useCallback(async () => {
await set(MAX_RECEIVED_LS_KEY, maxRecv.toString());
await set(MAX_SENT_LS_KEY, maxSent.toString());
setDirty(false);
}, [maxSent, maxRecv]);
return (
<div className="flex flex-col flex-nowrap py-1 px-2 gap-2">
<div className="font-semibold">Max Sent Data</div>
<input
type="number"
className="input border"
value={maxSent}
min={0}
onChange={(e) => {
setMaxSent(parseInt(e.target.value));
setDirty(true);
}}
/>
<div className="font-semibold">Max Received Data</div>
<input
type="number"
className="input border"
value={maxRecv}
min={0}
onChange={(e) => {
setMaxRecv(parseInt(e.target.value));
setDirty(true);
}}
/>
<div className="flex flex-row flex-nowrap justify-end gap-2 p-2">
<button
className="button !bg-primary/[0.9] hover:bg-primary/[0.8] active:bg-primary !text-white"
disabled={!dirty}
onClick={onSave}
>
Save
</button>
</div>
</div>
);
}
function RequestPayload(props: Props): ReactElement {
const data = useRequest(props.requestId);
const [url, setUrl] = useState<URL | null>();

View File

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

View File

@@ -1,6 +1,5 @@
import { Level } from 'level';
import type { RequestHistory } from './rpc';
import { PluginConfig, sha256 } from '../../utils/misc';
const charwise = require('charwise');
const db = new Level('./ext-db', {
@@ -9,12 +8,6 @@ const db = new Level('./ext-db', {
const historyDb = db.sublevel<string, RequestHistory>('history', {
valueEncoding: 'json',
});
const pluginDb = db.sublevel<string, string>('plugin', {
valueEncoding: 'hex',
});
const pluginConfigDb = db.sublevel<string, PluginConfig>('pluginConfig', {
valueEncoding: 'json',
});
export async function addNotaryRequest(
now = Date.now(),
@@ -129,96 +122,3 @@ export async function getNotaryRequest(
): Promise<RequestHistory | null> {
return historyDb.get(id);
}
export async function getPluginHashes(): Promise<string[]> {
const retVal: string[] = [];
for await (const [key] of pluginDb.iterator()) {
// pluginDb.del(key);
// pluginConfigDb.del(key);
retVal.push(key);
}
return retVal;
}
export async function getPluginByHash(hash: string): Promise<string | null> {
try {
const plugin = await pluginDb.get(hash);
return plugin;
} catch (e) {
return null;
}
}
export async function addPlugin(hex: string): Promise<string | null> {
const hash = await sha256(hex);
if (await getPluginByHash(hash)) {
return null;
}
await pluginDb.put(hash, hex);
return hash;
}
export async function removePlugin(hash: string): Promise<string | null> {
const existing = await pluginDb.get(hash);
if (!existing) return null;
await pluginDb.del(hash);
return hash;
}
export async function getPluginConfigByHash(
hash: string,
): Promise<PluginConfig | null> {
try {
const config = await pluginConfigDb.get(hash);
return config;
} catch (e) {
return null;
}
}
export async function addPluginConfig(
hash: string,
config: PluginConfig,
): Promise<PluginConfig | null> {
if (await getPluginConfigByHash(hash)) {
return null;
}
await pluginConfigDb.put(hash, config);
return config;
}
export async function removePluginConfig(
hash: string,
): Promise<PluginConfig | null> {
const existing = await pluginConfigDb.get(hash);
if (!existing) return null;
await pluginConfigDb.del(hash);
return existing;
}
export async function setNotaryRequestCid(
id: string,
cid: string,
): Promise<RequestHistory | null> {
const existing = await historyDb.get(id);
if (!existing) return null;
const newReq = {
...existing,
cid,
};
await historyDb.put(id, newReq);
return newReq;
}

View File

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

View File

@@ -1,51 +1,21 @@
import browser from 'webextension-polyfill';
import {
clearCache,
getCacheByTabId,
getCookieStoreByHost,
getHeaderStoreByHost,
} from './cache';
import { clearCache, getCacheByTabId } from './cache';
import { addRequestHistory } from '../../reducers/history';
import {
getNotaryRequests,
addNotaryRequest,
addNotaryRequestProofs,
getNotaryRequest,
getNotaryRequests,
removeNotaryRequest,
setNotaryRequestError,
setNotaryRequestStatus,
setNotaryRequestError,
setNotaryRequestVerification,
addPlugin,
getPluginHashes,
getPluginByHash,
removePlugin,
addPluginConfig,
getPluginConfigByHash,
removePluginConfig,
removeNotaryRequest,
} from './db';
import { addOnePlugin, removeOnePlugin } from '../../reducers/plugins';
import {
devlog,
extractBodyFromResponse,
getPluginConfig,
hexToArrayBuffer,
makePlugin,
} from '../../utils/misc';
import {
getLoggingFilter,
getMaxRecv,
getMaxSent,
getNotaryApi,
getProxyApi,
} from '../../utils/storage';
const charwise = require('charwise');
export enum BackgroundActiontype {
get_requests = 'get_requests',
clear_requests = 'clear_requests',
push_action = 'push_action',
execute_plugin_prover = 'execute_plugin_prover',
get_prove_requests = 'get_prove_requests',
prove_request_start = 'prove_request_start',
process_prove_request = 'process_prove_request',
@@ -54,16 +24,6 @@ export enum BackgroundActiontype {
verify_proof = 'verify_proof',
delete_prove_request = 'delete_prove_request',
retry_prove_request = 'retry_prove_request',
get_cookies_by_hostname = 'get_cookies_by_hostname',
get_headers_by_hostname = 'get_headers_by_hostname',
add_plugin = 'add_plugin',
remove_plugin = 'remove_plugin',
get_plugin_by_hash = 'get_plugin_by_hash',
get_plugin_config_by_hash = 'get_plugin_config_by_hash',
run_plugin = 'run_plugin',
get_plugin_hashes = 'get_plugin_hashes',
open_popup = 'open_popup',
change_route = 'change_route',
}
export type BackgroundAction = {
@@ -95,8 +55,6 @@ export type RequestHistory = {
headers: { [key: string]: string };
body?: string;
maxTranscriptSize: number;
maxSentData: number;
maxRecvData: number;
notaryUrl: string;
websocketProxyUrl: string;
status: '' | 'pending' | 'success' | 'error';
@@ -109,7 +67,6 @@ export type RequestHistory = {
};
secretHeaders?: string[];
secretResps?: string[];
cid?: string;
};
export const initRPC = () => {
@@ -132,26 +89,6 @@ export const initRPC = () => {
return handleRetryProveReqest(request, sendResponse);
case BackgroundActiontype.prove_request_start:
return handleProveRequestStart(request, sendResponse);
case BackgroundActiontype.get_cookies_by_hostname:
return handleGetCookiesByHostname(request, sendResponse);
case BackgroundActiontype.get_headers_by_hostname:
return handleGetHeadersByHostname(request, sendResponse);
case BackgroundActiontype.add_plugin:
return handleAddPlugin(request, sendResponse);
case BackgroundActiontype.remove_plugin:
return handleRemovePlugin(request, sendResponse);
case BackgroundActiontype.get_plugin_hashes:
return handleGetPluginHashes(request, sendResponse);
case BackgroundActiontype.get_plugin_by_hash:
return handleGetPluginByHash(request, sendResponse);
case BackgroundActiontype.get_plugin_config_by_hash:
return handleGetPluginConfigByHash(request, sendResponse);
case BackgroundActiontype.run_plugin:
return handleRunPlugin(request, sendResponse);
case BackgroundActiontype.execute_plugin_prover:
return handleExecPluginProver(request);
case BackgroundActiontype.open_popup:
return handleOpenPopup(request);
default:
break;
}
@@ -259,7 +196,6 @@ async function handleRetryProveReqest(
...req,
notaryUrl,
websocketProxyUrl,
loggingFilter: await getLoggingFilter(),
},
});
@@ -276,8 +212,6 @@ async function handleProveRequestStart(
headers,
body,
maxTranscriptSize,
maxSentData,
maxRecvData,
notaryUrl,
websocketProxyUrl,
secretHeaders,
@@ -289,8 +223,6 @@ async function handleProveRequestStart(
method,
headers,
body,
maxSentData,
maxRecvData,
maxTranscriptSize,
notaryUrl,
websocketProxyUrl,
@@ -317,240 +249,12 @@ async function handleProveRequestStart(
headers,
body,
maxTranscriptSize,
maxSentData,
maxRecvData,
notaryUrl,
websocketProxyUrl,
secretHeaders,
secretResps,
loggingFilter: await getLoggingFilter(),
},
});
return sendResponse();
}
async function runPluginProver(request: BackgroundAction, now = Date.now()) {
const {
url,
method,
headers,
notaryUrl: _notaryUrl,
websocketProxyUrl: _websocketProxyUrl,
maxSentData: _maxSentData,
maxRecvData: _maxRecvData,
} = request.data;
const resp = await fetch(url, {
method,
headers,
});
const body = await extractBodyFromResponse(resp);
const notaryUrl = _notaryUrl || (await getNotaryApi());
const websocketProxyUrl = _websocketProxyUrl || (await getProxyApi());
const maxSentData = _maxSentData || (await getMaxSent());
const maxRecvData = _maxRecvData || (await getMaxRecv());
const maxTranscriptSize = 16384;
const { id } = await addNotaryRequest(now, {
url,
method,
headers,
body,
maxTranscriptSize,
notaryUrl,
websocketProxyUrl,
maxRecvData,
maxSentData,
});
await setNotaryRequestStatus(id, 'pending');
await browser.runtime.sendMessage({
type: BackgroundActiontype.push_action,
data: {
tabId: 'background',
},
action: addRequestHistory(await getNotaryRequest(id)),
});
await browser.runtime.sendMessage({
type: BackgroundActiontype.process_prove_request,
data: {
id,
url,
method,
headers,
body,
maxTranscriptSize,
notaryUrl,
websocketProxyUrl,
maxRecvData,
maxSentData,
loggingFilter: await getLoggingFilter(),
},
});
}
export async function handleExecPluginProver(request: BackgroundAction) {
const now = request.data.now;
const id = charwise.encode(now).toString('hex');
runPluginProver(request, now);
return id;
}
function handleGetCookiesByHostname(
request: BackgroundAction,
sendResponse: (data?: any) => void,
) {
const cache = getCookieStoreByHost(request.data);
const keys = cache.keys() || [];
const data = keys.reduce((acc: { [k: string]: string }, key) => {
acc[key] = cache.get(key) || '';
return acc;
}, {});
return data;
}
function handleGetHeadersByHostname(
request: BackgroundAction,
sendResponse: (data?: any) => void,
) {
const cache = getHeaderStoreByHost(request.data);
const keys = cache.keys() || [];
const data = keys.reduce((acc: { [k: string]: string }, key) => {
acc[key] = cache.get(key) || '';
return acc;
}, {});
return data;
}
async function handleAddPlugin(
request: BackgroundAction,
sendResponse: (data?: any) => void,
) {
try {
const config = await getPluginConfig(hexToArrayBuffer(request.data));
if (config) {
const hash = await addPlugin(request.data);
if (hash) {
await addPluginConfig(hash, config);
await browser.runtime.sendMessage({
type: BackgroundActiontype.push_action,
data: {
tabId: 'background',
},
action: addOnePlugin(hash),
});
}
}
} finally {
return sendResponse();
}
}
async function handleRemovePlugin(
request: BackgroundAction,
sendResponse: (data?: any) => void,
) {
await removePlugin(request.data);
await removePluginConfig(request.data);
await browser.runtime.sendMessage({
type: BackgroundActiontype.push_action,
data: {
tabId: 'background',
},
action: removeOnePlugin(request.data),
});
return sendResponse();
}
async function handleGetPluginHashes(
request: BackgroundAction,
sendResponse: (data?: any) => void,
) {
const hashes = await getPluginHashes();
for (const hash of hashes) {
await browser.runtime.sendMessage({
type: BackgroundActiontype.push_action,
data: {
tabId: 'background',
},
action: addOnePlugin(hash),
});
}
return sendResponse();
}
async function handleGetPluginByHash(
request: BackgroundAction,
sendResponse: (data?: any) => void,
) {
const hash = request.data;
const hex = await getPluginByHash(hash);
return hex;
}
async function handleGetPluginConfigByHash(
request: BackgroundAction,
sendResponse: (data?: any) => void,
) {
const hash = request.data;
const config = await getPluginConfigByHash(hash);
return config;
}
async function handleRunPlugin(
request: BackgroundAction,
sendResponse: (data?: any) => void,
) {
const { hash, method, params } = request.data;
const hex = await getPluginByHash(hash);
const arrayBuffer = hexToArrayBuffer(hex!);
const config = await getPluginConfig(arrayBuffer);
const plugin = await makePlugin(arrayBuffer, config);
devlog(`plugin::${method}`, params);
const out = await plugin.call(method, params);
devlog(`plugin response: `, out.string());
return JSON.parse(out.string());
}
let cachePopup: browser.Windows.Window | null = null;
async function handleOpenPopup(request: BackgroundAction) {
if (cachePopup) {
browser.windows.update(cachePopup.id!, {
focused: true,
});
} else {
const tab = await browser.tabs.create({
url: browser.runtime.getURL('popup.html') + '#' + request.data.route,
active: false,
});
const popup = await browser.windows.create({
tabId: tab.id,
type: 'popup',
focused: true,
width: 480,
height: 640,
left: request.data.position.left,
top: request.data.position.top,
});
cachePopup = popup;
const onPopUpClose = (windowId: number) => {
if (windowId === popup.id) {
cachePopup = null;
browser.windows.onRemoved.removeListener(onPopUpClose);
}
};
browser.windows.onRemoved.addListener(onPopUpClose);
}
}

View File

@@ -1,10 +1,8 @@
import React, { useEffect } from 'react';
import { BackgroundActiontype } from '../Background/rpc';
import { prove, set_logging_filter, verify } from 'tlsn-js';
import { prove, verify } from 'tlsn-js';
import { urlify } from '../../utils/misc';
import browser from 'webextension-polyfill';
import { getLoggingFilter } from '../../utils/storage';
import { LOGGING_LEVEL_INFO } from '../../utils/constants';
const Offscreen = () => {
useEffect(() => {
@@ -17,27 +15,21 @@ const Offscreen = () => {
method,
headers,
body = '',
maxSentData,
maxRecvData,
maxTranscriptSize,
notaryUrl,
websocketProxyUrl,
id,
secretHeaders,
secretResps,
loggingFilter = LOGGING_LEVEL_INFO,
} = request.data;
(async () => {
try {
const token = urlify(url)?.hostname || '';
await set_logging_filter(loggingFilter);
const proof = await prove(url, {
method,
headers,
body,
maxSentData,
maxRecvData,
maxTranscriptSize,
notaryUrl,
websocketProxyUrl: websocketProxyUrl + `?token=${token}`,

View File

@@ -18,8 +18,10 @@ import Notarize from '../../pages/Notarize';
import ProofViewer from '../../pages/ProofViewer';
import History from '../../pages/History';
import ProofUploader from '../../pages/ProofUploader';
import Connect from '../../pages/Connect';
import browser from 'webextension-polyfill';
import store from '../../utils/store';
import P2P from '../../pages/P2P';
import CreateSession from '../../pages/CreateSession';
const Popup = () => {
const dispatch = useDispatch();
@@ -50,28 +52,6 @@ const Popup = () => {
})();
}, []);
useEffect(() => {
chrome.runtime.onMessage.addListener((request) => {
switch (request.type) {
case BackgroundActiontype.push_action: {
if (
request.data.tabId === store.getState().requests.activeTab?.id ||
request.data.tabId === 'background'
) {
store.dispatch(request.action);
}
break;
}
case BackgroundActiontype.change_route: {
if (request.data.tabId === 'background') {
navigate(request.route);
break;
}
}
}
});
}, []);
return (
<div className="flex flex-col w-full h-full overflow-hidden">
<div className="flex flex-nowrap flex-shrink-0 flex-row items-center relative gap-2 h-9 p-2 cursor-default justify-center bg-slate-300 w-full">
@@ -102,6 +82,9 @@ const Popup = () => {
<Route path="/custom/*" element={<RequestBuilder />} />
<Route path="/options" element={<Options />} />
<Route path="/home" element={<Home />} />
<Route path="/connect-session" element={<Connect />} />
<Route path="/create-session" element={<CreateSession />} />
<Route path="/p2p" element={<P2P />} />
<Route path="*" element={<Navigate to="/home" />} />
</Routes>
</div>

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { HashRouter } from 'react-router-dom';
import Popup from './Popup';
import './index.scss';
import { Provider } from 'react-redux';
@@ -10,6 +11,20 @@ import { BackgroundActiontype } from '../Background/rpc';
const container = document.getElementById('app-container');
const root = createRoot(container!); // createRoot(container!) if you use TypeScript
chrome.runtime.onMessage.addListener((request) => {
switch (request.type) {
case BackgroundActiontype.push_action: {
if (
request.data.tabId === store.getState().requests.activeTab?.id ||
request.data.tabId === 'background'
) {
store.dispatch(request.action);
}
break;
}
}
});
root.render(
<Provider store={store}>
<HashRouter>

View File

@@ -1,235 +0,0 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import './sidePanel.scss';
import browser from 'webextension-polyfill';
import { fetchPluginConfigByHash, runPlugin } from '../../utils/rpc';
import { PluginConfig, StepConfig } from '../../utils/misc';
import { PluginList } from '../../components/PluginList';
import DefaultPluginIcon from '../../assets/img/default-plugin-icon.png';
import logo from '../../assets/img/icon-128.png';
import classNames from 'classnames';
import Icon from '../../components/Icon';
import { useRequestHistory } from '../../reducers/history';
import { BackgroundActiontype } from '../Background/rpc';
export default function SidePanel(): ReactElement {
const [config, setConfig] = useState<PluginConfig | null>(null);
const [hash, setHash] = useState('');
useEffect(() => {
(async function () {
const result = await browser.storage.local.get('plugin_hash');
const { plugin_hash } = result;
const config = await fetchPluginConfigByHash(plugin_hash);
setHash(plugin_hash);
setConfig(config);
// await browser.storage.local.set({ plugin_hash: '' });
})();
}, []);
return (
<div className="flex flex-col bg-slate-100 w-screen h-screen">
<div className="relative flex flex-nowrap flex-shrink-0 flex-row items-center relative gap-2 h-9 p-2 cursor-default justify-center bg-slate-300 w-full">
<img className="h-5" src={logo} alt="logo" />
<button
className="button absolute right-2"
onClick={() => window.close()}
>
Close
</button>
</div>
{!config && <PluginList />}
{config && <PluginBody hash={hash} config={config} />}
</div>
);
}
function PluginBody(props: {
config: PluginConfig;
hash: string;
}): ReactElement {
const { hash } = props;
const { title, description, icon, steps } = props.config;
const [responses, setResponses] = useState<any[]>([]);
const setResponse = useCallback(
(response: any, i: number) => {
const result = responses.concat();
result[i] = response;
setResponses(result);
},
[responses],
);
return (
<div className="flex flex-col p-4">
<div className="flex flex-row items-center gap-4">
<img className="w-12 h-12 self-start" src={icon || DefaultPluginIcon} />
<div className="flex flex-col w-full items-start">
<div className="font-bold flex flex-row h-6 items-center justify-between w-full text-base">
{title}
</div>
<div className="text-slate-500 text-sm">{description}</div>
</div>
</div>
<div className="flex flex-col items-start gap-8 mt-8">
{steps?.map((step, i) => (
<StepContent
hash={hash}
index={i}
setResponse={setResponse}
lastResponse={i > 0 ? responses[i - 1] : undefined}
responses={responses}
{...step}
/>
))}
</div>
</div>
);
}
function StepContent(
props: StepConfig & {
hash: string;
index: number;
setResponse: (resp: any, i: number) => void;
responses: any[];
lastResponse?: any;
},
): ReactElement {
const {
index,
title,
description,
cta,
action,
hash,
setResponse,
lastResponse,
prover,
} = props;
const [completed, setCompleted] = useState(false);
const [pending, setPending] = useState(false);
const [error, setError] = useState('');
const [notarizationId, setNotarizationId] = useState('');
const notaryRequest = useRequestHistory(notarizationId);
const processStep = useCallback(async () => {
if (index > 0 && !lastResponse) return;
setPending(true);
try {
setError('');
const val = await runPlugin(hash, action, JSON.stringify(lastResponse));
if (val && prover) {
setNotarizationId(val);
} else {
setCompleted(!!val);
setResponse(val, index);
}
} catch (e: any) {
console.error(e);
setError(e?.message || 'Unkonwn error');
} finally {
setPending(false);
}
}, [hash, action, index, lastResponse, prover]);
const onClick = useCallback(() => {
if (
pending ||
completed ||
notaryRequest?.status === 'pending' ||
notaryRequest?.status === 'success'
)
return;
processStep();
}, [processStep, pending, completed, notaryRequest]);
const viewProofInPopup = useCallback(async () => {
if (!notaryRequest) return;
chrome.runtime.sendMessage<any, string>({
type: BackgroundActiontype.verify_prove_request,
data: notaryRequest,
});
await browser.runtime.sendMessage({
type: BackgroundActiontype.open_popup,
data: {
position: {
left: window.screen.width / 2 - 240,
top: window.screen.height / 2 - 300,
},
route: `/verify/${notaryRequest.id}`,
},
});
}, [notaryRequest, notarizationId]);
useEffect(() => {
processStep();
}, [processStep]);
let btnContent = null;
if (completed) {
btnContent = (
<button
className={classNames(
'button mt-2 w-fit flex flex-row flex-nowrap items-center gap-2',
'!bg-green-200 !text-black cursor-default border border-green-500 rounded',
)}
>
<Icon className="text-green-600" fa="fa-solid fa-check" />
<span className="text-sm">DONE</span>
</button>
);
} else if (notaryRequest?.status === 'success') {
btnContent = (
<button
className={classNames(
'button button--primary mt-2 w-fit flex flex-row flex-nowrap items-center gap-2',
)}
onClick={viewProofInPopup}
>
<span className="text-sm">View Proof</span>
</button>
);
} else if (notaryRequest?.status === 'pending' || pending || notarizationId) {
btnContent = (
<button className="button mt-2 w-fit flex flex-row flex-nowrap items-center gap-2 cursor-default">
<Icon className="animate-spin" fa="fa-solid fa-spinner" size={1} />
<span className="text-sm">{cta}</span>
</button>
);
} else {
btnContent = (
<button
className={classNames(
'button mt-2 w-fit flex flex-row flex-nowrap items-center gap-2',
)}
disabled={index > 0 && typeof lastResponse === 'undefined'}
onClick={onClick}
>
<span className="text-sm">{cta}</span>
</button>
);
}
return (
<div className="flex flex-row gap-4 text-base w-full">
<div className="text-slate-500 self-start">{index + 1}.</div>
<div className="flex flex-col flex-grow flex-shrink w-0">
<div
className={classNames('font-semibold', {
'line-through text-slate-500': completed,
})}
>
{title}
</div>
{!!description && (
<div className="text-slate-500 text-sm">{description}</div>
)}
{!!error && <div className="text-red-500 text-sm">{error}</div>}
{btnContent}
</div>
</div>
);
}

View File

@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en" width="480px">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Side Panel</title>
</head>
<body>
<div id="app-container"></div>
<div id="modal-root"></div>
</body>
</html>

View File

@@ -1,29 +0,0 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import SidePanel from './SidePanel';
import store from '../../utils/store';
import { Provider } from 'react-redux';
import { BackgroundActiontype } from '../Background/rpc';
const container = document.getElementById('app-container');
const root = createRoot(container!); // createRoot(container!) if you use TypeScript
chrome.runtime.onMessage.addListener((request) => {
switch (request.type) {
case BackgroundActiontype.push_action: {
if (
request.data.tabId === store.getState().requests.activeTab?.id ||
request.data.tabId === 'background'
) {
store.dispatch(request.action);
}
break;
}
}
});
root.render(
<Provider store={store}>
<SidePanel />
</Provider>,
);

View File

@@ -1,28 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
$fa-font-path: "~@fortawesome/fontawesome-free/webfonts";
@import "../../../node_modules/@fortawesome/fontawesome-free/scss/fontawesome";
@import "../../../node_modules/@fortawesome/fontawesome-free/scss/brands";
@import "../../../node_modules/@fortawesome/fontawesome-free/scss/solid";
@import "../../../node_modules/@fortawesome/fontawesome-free/scss/regular";
@import "../Popup/index.scss";
html {
width: 100vw;
height: 100vh;
}
body {
padding: 0;
margin: 0;
width: 100vw;
height: 100vh;
}
#app-container {
width: 100vw;
height: 100vh;
}

View File

@@ -3,16 +3,11 @@
"name": "TLSN Extension",
"description": "A chrome extension for TLSN",
"options_page": "options.html",
"background": { "service_worker": "background.bundle.js",
"persistent": true
},
"background": { "service_worker": "background.bundle.js" },
"action": {
"default_popup": "popup.html",
"default_icon": "icon-34.png"
},
"side_panel": {
"default_path": "sidePanel.html"
},
"icons": {
"128": "icon-128.png"
},
@@ -37,7 +32,6 @@
"offscreen",
"storage",
"webRequest",
"activeTab",
"sidePanel"
"activeTab"
]
}

View File

@@ -0,0 +1,64 @@
import React, { useEffect, useState, useCallback } from 'react';
import { sendPairRequest } from '../../reducers/p2p';
import { useDispatch } from 'react-redux';
import { useNavigate } from 'react-router';
import { usePairId } from '../../reducers/p2p';
import Icon from '../../components/Icon';
export default function Connect() {
const dispatch = useDispatch();
const [peerId, setPeerId] = useState('');
const [loading, setLoading] = useState(false);
const pairId = usePairId();
const navigate = useNavigate();
useEffect(() => {
if (pairId && loading) {
console.log('Connected to peer', pairId);
setLoading(false);
navigate('/create-session');
}
}, [pairId]);
const connect = useCallback(() => {
if (peerId) {
console.log('Connecting to peer', peerId);
dispatch(sendPairRequest(peerId));
setLoading(true);
} else {
console.log('No peer ID provided');
}
}, [peerId]);
return (
<div className="flex flex-col justify-center items-center bg-slate-200 p-4 rounded border-slate-400 m-4 gap-2">
<h1 className="text-base font-semibold">Enter peer ID to connect to</h1>
<input
className="input border border-slate-200 focus:border-slate-400 w-full"
type="text"
value={peerId}
onChange={(e) => setPeerId(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') connect();
}}
autoFocus
/>
<button
className="button button--primary"
disabled={!peerId || loading}
onClick={connect}
>
{loading ? (
<Icon
className="animate-spin text-white"
fa="fa-solid fa-spinner"
size={1}
/>
) : (
'Connect'
)}
</button>
</div>
);
}

View File

@@ -0,0 +1,7 @@
import type {} from 'redux-thunk/extend-redux';
import React, { ReactElement, useEffect } from 'react';
import ChatBox from '../../components/ChatBox';
export default function CreateSession(): ReactElement {
return <ChatBox />;
}

View File

@@ -1,4 +1,4 @@
import React, { ReactElement, useState, useCallback, useEffect } from 'react';
import React, { ReactElement, useState, useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { useNavigate } from 'react-router';
import {
@@ -7,23 +7,13 @@ import {
deleteRequestHistory,
} from '../../reducers/history';
import Icon from '../../components/Icon';
import {
get,
NOTARY_API_LS_KEY,
PROXY_API_LS_KEY,
getNotaryApi,
getProxyApi,
} from '../../utils/storage';
import { get, NOTARY_API_LS_KEY, PROXY_API_LS_KEY } from '../../utils/storage';
import { urlify, download, upload } from '../../utils/misc';
import { BackgroundActiontype } from '../../entries/Background/rpc';
import Modal, { ModalContent } from '../../components/Modal/Modal';
import classNames from 'classnames';
import copy from 'copy-to-clipboard';
import { EXPLORER_API } from '../../utils/constants';
import {
getNotaryRequest,
setNotaryRequestCid,
} from '../../entries/Background/db';
export default function History(): ReactElement {
const history = useHistoryOrder();
@@ -44,29 +34,15 @@ function OneRequestHistory(props: { requestId: string }): ReactElement {
const [uploadError, setUploadError] = useState('');
const [showingShareConfirmation, setShowingShareConfirmation] =
useState(false);
const [cid, setCid] = useState<{ [key: string]: string }>({});
const [cid, setCid] = useState('');
const [uploading, setUploading] = useState(false);
const navigate = useNavigate();
const { status } = request || {};
const requestUrl = urlify(request?.url || '');
useEffect(() => {
const fetchData = async () => {
try {
const request = await getNotaryRequest(props.requestId);
if (request && request.cid) {
setCid({ [props.requestId]: request.cid });
}
} catch (e) {
console.error('Error fetching data', e);
}
};
fetchData();
}, []);
const onRetry = useCallback(async () => {
const notaryUrl = await getNotaryApi();
const websocketProxyUrl = await getProxyApi();
const notaryUrl = await get(NOTARY_API_LS_KEY);
const websocketProxyUrl = await get(PROXY_API_LS_KEY);
chrome.runtime.sendMessage<any, string>({
type: BackgroundActiontype.retry_prove_request,
data: {
@@ -105,14 +81,13 @@ function OneRequestHistory(props: { requestId: string }): ReactElement {
`${request?.id}.json`,
JSON.stringify(request?.proof),
);
setCid((prevCid) => ({ ...prevCid, [props.requestId]: data }));
await setNotaryRequestCid(props.requestId, data);
setCid(data);
} catch (e: any) {
setUploadError(e.message);
} finally {
setUploading(false);
}
}, [props.requestId, request, cid]);
}, []);
return (
<div className="flex flex-row flex-nowrap border rounded-md p-2 gap-1 hover:bg-slate-50 cursor-pointer">
@@ -240,7 +215,7 @@ function OneRequestHistory(props: { requestId: string }): ReactElement {
onClose={closeAllModal}
>
<ModalContent className="flex flex-col w-full gap-4 items-center text-base justify-center">
{!cid[props.requestId] ? (
{!cid ? (
<p className="text-slate-500 text-center">
{uploadError ||
'This will make your proof publicly accessible by anyone with the CID'}
@@ -249,13 +224,13 @@ function OneRequestHistory(props: { requestId: string }): ReactElement {
<input
className="input w-full bg-slate-100 border border-slate-200"
readOnly
value={`${EXPLORER_API}/ipfs/${cid[props.requestId]}`}
value={`${EXPLORER_API}/ipfs/${cid}`}
onFocus={(e) => e.target.select()}
/>
)}
</ModalContent>
<div className="flex flex-row gap-2 justify-center">
{!cid[props.requestId] ? (
{!cid ? (
<>
{!uploadError && (
<button
@@ -283,9 +258,7 @@ function OneRequestHistory(props: { requestId: string }): ReactElement {
) : (
<>
<button
onClick={() =>
copy(`${EXPLORER_API}/ipfs/${cid[props.requestId]}`)
}
onClick={() => copy(`${EXPLORER_API}/ipfs/${cid}`)}
className="m-0 w-24 bg-slate-600 text-slate-200 hover:bg-slate-500 hover:text-slate-100 font-bold"
>
Copy

View File

@@ -1,5 +1,4 @@
import React, {
ChangeEvent,
MouseEventHandler,
ReactElement,
ReactNode,
@@ -9,46 +8,38 @@ import React, {
import Icon from '../../components/Icon';
import classNames from 'classnames';
import { useNavigate } from 'react-router';
import { useRequests } from '../../reducers/requests';
import { makePlugin, getPluginConfig } from '../../utils/misc';
import { addPlugin } from '../../utils/rpc';
import { PluginList } from '../../components/PluginList';
import { ErrorModal } from '../../components/ErrorModal';
import {
notarizeRequest,
useActiveTabUrl,
useRequests,
} from '../../reducers/requests';
import { Link } from 'react-router-dom';
import bookmarks from '../../../utils/bookmark/bookmarks.json';
import { replayRequest, urlify } from '../../utils/misc';
import { useDispatch } from 'react-redux';
import { get, NOTARY_API_LS_KEY, PROXY_API_LS_KEY } from '../../utils/storage';
export default function Home(): ReactElement {
const requests = useRequests();
const url = useActiveTabUrl();
const navigate = useNavigate();
const [error, showError] = useState('');
const onAddPlugin = useCallback(
async (evt: ChangeEvent<HTMLInputElement>) => {
if (!evt.target.files) return;
try {
const [file] = evt.target.files;
const arrayBuffer = await file.arrayBuffer();
const plugin = await makePlugin(arrayBuffer);
await getPluginConfig(plugin);
await addPlugin(Buffer.from(arrayBuffer).toString('hex'));
} catch (e: any) {
showError(e?.message || 'Invalid Plugin');
}
},
[],
);
const dispatch = useDispatch();
return (
<div className="flex flex-col gap-4 py-4 overflow-y-auto">
{error && <ErrorModal onClose={() => showError('')} message={error} />}
<div className="flex flex-col flex-nowrap justify-center gap-2 mx-4">
<NavButton fa="fa-solid fa-table" onClick={() => navigate('/requests')}>
<span>Requests</span>
<span>{`(${requests.length})`}</span>
</NavButton>
<NavButton fa="fa-solid fa-hammer" onClick={() => navigate('/custom')}>
<NavButton
fa="fa-solid fa-magnifying-glass"
onClick={() => navigate('/custom')}
>
Custom
</NavButton>
<NavButton
fa="fa-solid fa-certificate"
fa="fa-solid fa-magnifying-glass"
onClick={() => navigate('/verify')}
>
Verify
@@ -56,19 +47,134 @@ export default function Home(): ReactElement {
<NavButton fa="fa-solid fa-list" onClick={() => navigate('/history')}>
History
</NavButton>
<NavButton className="relative" fa="fa-solid fa-plus">
<input
className="opacity-0 absolute top-0 right-0 h-full w-full"
type="file"
onChange={onAddPlugin}
/>
Add a plugin
</NavButton>
<NavButton fa="fa-solid fa-gear" onClick={() => navigate('/options')}>
Options
</NavButton>
<NavButton fa="fa-solid fa-wifi" onClick={() => navigate('/p2p')}>
Prove to peer
</NavButton>
</div>
{!bookmarks.length && (
<div className="flex flex-col flex-nowrap">
<div className="flex flex-col items-center justify-center text-slate-300 cursor-default select-none">
<div>No available notarization for {url?.hostname}</div>
<div>
Browse <Link to="/requests">Requests</Link>
</div>
</div>
</div>
)}
<div className="flex flex-col px-4 gap-4">
{bookmarks.map((bm, i) => {
try {
const reqs = requests.filter((req) => {
return req?.url?.includes(bm.url);
});
const bmHost = urlify(bm.targetUrl)?.host;
const isReady = !!reqs.length;
return (
<div
key={i}
className="flex flex-col flex-nowrap border rounded-md p-2 gap-1 hover:bg-slate-50 cursor-pointer"
>
<div className="flex flex-row items-center text-xs">
<div className="bg-slate-200 text-slate-400 px-1 py-0.5 rounded-sm">
{bm.method}
</div>
<div className="text-slate-400 px-2 py-1 rounded-md">
{bm.type}
</div>
</div>
<div className="font-bold">{bm.title}</div>
<div className="italic">{bm.description}</div>
{isReady && (
<button
className="button button--primary w-fit self-end mt-2"
onClick={async () => {
if (!isReady) return;
const req = reqs[0];
const res = await replayRequest(req);
const secretHeaders = req.requestHeaders
.map((h) => {
return (
`${h.name.toLowerCase()}: ${h.value || ''}` || ''
);
})
.filter((d) => !!d);
const selectedValue = res.match(
new RegExp(bm.responseSelector, 'g'),
);
if (selectedValue) {
const revealed = bm.valueTransform.replace(
'%s',
selectedValue[0],
);
const selectionStart = res.indexOf(revealed);
const selectionEnd =
selectionStart + revealed.length - 1;
const secretResps = [
res.substring(0, selectionStart),
res.substring(selectionEnd, res.length),
].filter((d) => !!d);
const hostname = urlify(req.url)?.hostname;
const notaryUrl = await get(NOTARY_API_LS_KEY);
const websocketProxyUrl = await get(PROXY_API_LS_KEY);
const headers: { [k: string]: string } =
req.requestHeaders.reduce(
(acc: any, h) => {
acc[h.name] = h.value;
return acc;
},
{ Host: hostname },
);
//TODO: for some reason, these needs to be override to work
headers['Accept-Encoding'] = 'identity';
headers['Connection'] = 'close';
dispatch(
// @ts-ignore
notarizeRequest({
url: req.url,
method: req.method,
headers: headers,
body: req.requestBody,
maxTranscriptSize: 16384,
notaryUrl,
websocketProxyUrl,
secretHeaders,
secretResps,
}),
);
navigate(`/history`);
}
}}
>
Notarize
</button>
)}
{!isReady && (
<button
className="button w-fit self-end mt-2"
onClick={() => chrome.tabs.update({ url: bm.targetUrl })}
>
{`Go to ${bmHost}`}
</button>
)}
</div>
);
} catch (e) {
return null;
}
})}
</div>
<PluginList className="mx-4" />
</div>
);
}

View File

@@ -12,15 +12,7 @@ import { useLocation, useNavigate, useParams } from 'react-router';
import { notarizeRequest, useRequest } from '../../reducers/requests';
import Icon from '../../components/Icon';
import { urlify } from '../../utils/misc';
import {
get,
NOTARY_API_LS_KEY,
PROXY_API_LS_KEY,
getNotaryApi,
getProxyApi,
getMaxSent,
getMaxRecv,
} from '../../utils/storage';
import { get, NOTARY_API_LS_KEY, PROXY_API_LS_KEY } from '../../utils/storage';
import { useDispatch } from 'react-redux';
const maxTranscriptSize = 16384;
@@ -37,10 +29,9 @@ export default function Notarize(): ReactElement {
const notarize = useCallback(async () => {
if (!req) return;
const hostname = urlify(req.url)?.hostname;
const notaryUrl = await getNotaryApi();
const websocketProxyUrl = await getProxyApi();
const maxSentData = await getMaxSent();
const maxRecvData = await getMaxRecv();
const notaryUrl = await get(NOTARY_API_LS_KEY);
const websocketProxyUrl = await get(PROXY_API_LS_KEY);
const headers: { [k: string]: string } = req.requestHeaders.reduce(
(acc: any, h) => {
acc[h.name] = h.value;
@@ -52,9 +43,7 @@ export default function Notarize(): ReactElement {
//TODO: for some reason, these needs to be override to work
headers['Accept-Encoding'] = 'identity';
headers['Connection'] = 'close';
if (req.requestBody) {
headers['Content-Length'] = req.requestBody.length.toString();
}
dispatch(
// @ts-ignore
notarizeRequest({
@@ -62,8 +51,6 @@ export default function Notarize(): ReactElement {
method: req.method,
headers,
body: req.requestBody,
maxSentData,
maxRecvData,
maxTranscriptSize,
notaryUrl,
websocketProxyUrl,
@@ -142,6 +129,7 @@ function RevealHeaderStep(props: {
props.setSecretHeaders(
req.requestHeaders
.map((h) => {
console.log(h.name, !revealed[h.name]);
if (!revealed[h.name]) {
return `${h.name.toLowerCase()}: ${h.value || ''}` || '';
}

View File

@@ -1,150 +1,76 @@
import React, {
ReactElement,
useState,
useEffect,
useCallback,
MouseEvent,
} from 'react';
import React, { ReactElement, useState, useEffect, useCallback } from 'react';
import {
set,
get,
NOTARY_API_LS_KEY,
PROXY_API_LS_KEY,
MAX_SENT_LS_KEY,
MAX_RECEIVED_LS_KEY,
getMaxSent,
getMaxRecv,
getNotaryApi,
getProxyApi,
getLoggingFilter,
LOGGING_FILTER_KEY,
} from '../../utils/storage';
import {
EXPLORER_API,
NOTARY_API,
NOTARY_PROXY,
MAX_RECV,
MAX_SENT,
LOGGING_LEVEL_INFO,
LOGGING_LEVEL_NONE,
LOGGING_LEVEL_DEBUG,
LOGGING_LEVEL_TRACE,
} from '../../utils/constants';
import Modal, { ModalContent } from '../../components/Modal/Modal';
import browser from 'webextension-polyfill';
export default function Options(): ReactElement {
const [notary, setNotary] = useState(NOTARY_API);
const [proxy, setProxy] = useState(NOTARY_PROXY);
const [maxSent, setMaxSent] = useState(MAX_SENT);
const [maxReceived, setMaxReceived] = useState(MAX_RECV);
const [loggingLevel, setLoggingLevel] = useState(LOGGING_LEVEL_INFO);
const [notary, setNotary] = useState('https://notary.pse.dev');
const [proxy, setProxy] = useState('wss://notary.pse.dev/proxy');
const [rendezvous, setRendezvous] = useState(
'wss://notary.pse.dev/rendezvous',
);
const [dirty, setDirty] = useState(false);
const [shouldReload, setShouldReload] = useState(false);
const [advanced, setAdvanced] = useState(false);
const [showReloadModal, setShowReloadModal] = useState(false);
useEffect(() => {
(async () => {
setNotary((await getNotaryApi()) || NOTARY_API);
setProxy((await getProxyApi()) || NOTARY_PROXY);
setMaxReceived((await getMaxRecv()) || MAX_RECV);
setMaxSent((await getMaxSent()) || MAX_SENT);
setLoggingLevel((await getLoggingFilter()) || LOGGING_LEVEL_INFO);
setNotary(await get(NOTARY_API_LS_KEY));
setProxy(await get(PROXY_API_LS_KEY));
})();
}, [advanced]);
}, []);
const onSave = useCallback(
async (e: MouseEvent<HTMLButtonElement>, skipCheck = false) => {
if (!skipCheck && shouldReload) {
setShowReloadModal(true);
return;
}
await set(NOTARY_API_LS_KEY, notary);
await set(PROXY_API_LS_KEY, proxy);
await set(MAX_SENT_LS_KEY, maxSent.toString());
await set(MAX_RECEIVED_LS_KEY, maxReceived.toString());
await set(LOGGING_FILTER_KEY, loggingLevel);
setDirty(false);
},
[notary, proxy, maxSent, maxReceived, loggingLevel, shouldReload],
);
const onSaveAndReload = useCallback(
async (e: MouseEvent<HTMLButtonElement>) => {
await onSave(e, true);
browser.runtime.reload();
},
[onSave],
);
const onAdvanced = useCallback(() => {
setAdvanced(!advanced);
}, [advanced]);
const onSave = useCallback(async () => {
await set(NOTARY_API_LS_KEY, notary);
await set(PROXY_API_LS_KEY, proxy);
setDirty(false);
}, [notary, proxy]);
return (
<div className="flex flex-col flex-nowrap flex-grow">
{showReloadModal && (
<Modal
className="flex flex-col items-center text-base cursor-default justify-center !w-auto mx-4 my-[50%] p-4 gap-4"
onClose={() => setShowReloadModal(false)}
>
<ModalContent className="flex flex-col w-full gap-4 items-center text-base justify-center">
Modifying your logging your will require your extension to reload.
Do you want to proceed?
</ModalContent>
<div className="flex flex-row justify-end items-center gap-2 w-full">
<button
className="button"
onClick={() => setShowReloadModal(false)}
>
No
</button>
<button
className="button button--primary"
onClick={onSaveAndReload}
>
Yes
</button>
</div>
</Modal>
)}
<div className="flex flex-row flex-nowrap justify-between items-between py-1 px-2 gap-2">
<p className="font-bold text-base">Settings</p>
<div className="flex flex-row flex-nowrap py-1 px-2 gap-2 font-bold text-base">
Settings
</div>
<NormalOptions
notary={notary}
setNotary={setNotary}
proxy={proxy}
setProxy={setProxy}
setDirty={setDirty}
/>
<div className="justify-left px-2 pt-3 gap-2">
<button className="font-bold" onClick={onAdvanced}>
<i
className={
advanced
? 'fa-solid fa-caret-down pr-1'
: 'fa-solid fa-caret-right pr-1'
}
></i>
Advanced
</button>
</div>
{!advanced ? (
<></>
) : (
<AdvancedOptions
maxSent={maxSent}
setMaxSent={setMaxSent}
maxReceived={maxReceived}
setMaxReceived={setMaxReceived}
setDirty={setDirty}
loggingLevel={loggingLevel}
setLoggingLevel={setLoggingLevel}
setShouldReload={setShouldReload}
<div className="flex flex-col flex-nowrap py-1 px-2 gap-2">
<div className="font-semibold">Notary API</div>
<input
type="text"
className="input border"
placeholder="http://localhost:7047"
onChange={(e) => {
setNotary(e.target.value);
setDirty(true);
}}
value={notary}
/>
)}
</div>
<div className="flex flex-col flex-nowrap py-1 px-2 gap-2">
<div className="font-semibold">Proxy API</div>
<input
type="text"
className="input border"
placeholder="ws://127.0.0.1:55688"
onChange={(e) => {
setProxy(e.target.value);
setDirty(true);
}}
value={proxy}
/>
</div>
<div className="flex flex-col flex-nowrap py-1 px-2 gap-2">
<div className="font-semibold">Rendezvous API</div>
<input
type="text"
className="input border"
placeholder="wss://notary.pse.dev/rendezvous"
onChange={(e) => {
setRendezvous(e.target.value);
setDirty(true);
}}
value={rendezvous}
/>
</div>
<div className="flex flex-row flex-nowrap justify-end gap-2 p-2">
<button
className="button !bg-primary/[0.9] hover:bg-primary/[0.8] active:bg-primary !text-white"
@@ -157,132 +83,3 @@ export default function Options(): ReactElement {
</div>
);
}
function InputField(props: {
label?: string;
placeholder?: string;
value?: string;
type?: string;
min?: number;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}) {
const { label, placeholder, value, type, min, onChange } = props;
return (
<div className="flex flex-col flex-nowrap py-1 px-2 gap-2">
<div className="font-semibold">{label}</div>
<input
type={type}
className="input border"
onChange={onChange}
value={value}
min={min}
placeholder={placeholder}
/>
</div>
);
}
function NormalOptions(props: {
notary: string;
setNotary: (value: string) => void;
proxy: string;
setProxy: (value: string) => void;
setDirty: (value: boolean) => void;
}) {
const { notary, setNotary, proxy, setProxy, setDirty } = props;
return (
<div>
<InputField
label="Notary API"
placeholder="https://api.tlsnotary.org"
value={notary}
type="text"
onChange={(e) => {
setNotary(e.target.value);
setDirty(true);
}}
/>
<InputField
label="Proxy API"
placeholder="https://proxy.tlsnotary.org"
value={proxy}
type="text"
onChange={(e) => {
setProxy(e.target.value);
setDirty(true);
}}
/>
<div className="flex flex-col flex-nowrap py-1 px-2 gap-2">
<div className="font-semibold">Explorer URL</div>
<div className="input border">{EXPLORER_API}</div>
</div>
</div>
);
}
function AdvancedOptions(props: {
maxSent: number;
maxReceived: number;
loggingLevel: string;
setShouldReload: (reload: boolean) => void;
setMaxSent: (value: number) => void;
setMaxReceived: (value: number) => void;
setDirty: (value: boolean) => void;
setLoggingLevel: (level: string) => void;
}) {
const {
maxSent,
setMaxSent,
maxReceived,
setMaxReceived,
setDirty,
setLoggingLevel,
loggingLevel,
setShouldReload,
} = props;
return (
<div>
<InputField
label="Set Max Received Data"
value={maxReceived.toString()}
type="number"
min={0}
onChange={(e) => {
setMaxReceived(parseInt(e.target.value));
setDirty(true);
}}
/>
<InputField
label="Set Max Sent Data"
value={maxSent.toString()}
type="number"
min={0}
onChange={(e) => {
setMaxSent(parseInt(e.target.value));
setDirty(true);
}}
/>
<div className="flex flex-col flex-nowrap py-1 px-2 gap-2">
<div className="font-semibold">Logging Level</div>
<select
className="select !bg-white border !px-2 !py-1"
onChange={(e) => {
setLoggingLevel(e.target.value);
setDirty(true);
setShouldReload(true);
}}
value={loggingLevel}
>
<option value={LOGGING_LEVEL_NONE}>None</option>
<option value={LOGGING_LEVEL_INFO}>Info</option>
<option value={LOGGING_LEVEL_DEBUG}>Debug</option>
<option value={LOGGING_LEVEL_TRACE}>Trace</option>
</select>
</div>
<div className="flex flex-row flex-nowrap justify-end gap-2 p-2"></div>
</div>
);
}

30
src/pages/P2P/index.tsx Normal file
View File

@@ -0,0 +1,30 @@
import React, { ReactElement, useEffect } from 'react';
import { useNavigate } from 'react-router';
import { useDispatch } from 'react-redux';
import { connectSession } from '../../reducers/p2p';
export default function P2P(): ReactElement {
const navigate = useNavigate();
const dispatch = useDispatch();
useEffect(() => {
dispatch(connectSession());
}, []);
return (
<div className="flex flex-col flex-nowrap flex-grow gap-2 m-4">
<button
className="button button--primary mx-20 brea"
onClick={() => navigate('/create-session')}
>
Create Session
</button>
<button
className="button button--primary mx-20"
onClick={() => navigate('/connect-session')}
>
Connect to Session
</button>
</div>
);
}

View File

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

264
src/reducers/p2p.tsx Normal file
View File

@@ -0,0 +1,264 @@
import { useSelector } from 'react-redux';
import { AppRootState } from './index';
import deepEqual from 'fast-deep-equal';
import { safeParseJSON } from '../utils/misc';
import { Dispatch } from 'redux';
enum ActionType {
'/p2p/createSession' = '/p2p/createSession',
'/p2p/setConnected' = '/p2p/setConnected',
'/p2p/setClientId' = '/p2p/setClientId',
'/p2p/setSocket' = '/p2p/setSocket',
'/p2p/appendMessage' = '/p2p/appendMessage',
'/p2p/setMessages' = '/p2p/setMessages',
'/p2p/setPairing' = '/p2p/setPairing',
}
type Action<payload> = {
type: ActionType;
payload?: payload;
error?: boolean;
meta?: any;
};
type State = {
clientId: string;
pairing: string;
socket: WebSocket | null;
connected: boolean;
messages: Chat[];
};
type Chat = {
to: string;
from: string;
text: string;
};
const initialState: State = {
clientId: '',
pairing: '',
socket: null,
connected: false,
messages: [],
};
export const connectSession =
() => async (dispatch: Dispatch, getState: () => AppRootState) => {
const { p2p } = getState();
if (p2p.socket) return;
const socket = new WebSocket('ws://0.tcp.ngrok.io:14339?clientId=hi');
socket.onopen = () => {
console.log('Connected to websocket');
dispatch(setConnected(true));
dispatch(setSocket(socket));
};
socket.onmessage = async (event) => {
const message: any = safeParseJSON(await event.data.text());
console.log(message);
switch (message.method) {
case 'client_connect': {
const { clientId } = message.params;
dispatch(setClientId(clientId));
break;
}
case 'chat': {
const { to, from, text } = message.params;
dispatch(
appendMessage({
to,
from,
text,
}),
);
break;
}
case 'pair_request': {
const { from } = message.params;
dispatch(confirmPairRequest(from));
break;
}
case 'pair_request_success': {
const { from } = message.params;
dispatch(setPairing(from));
break;
}
default:
console.warn(`Unknown message type "${message.method}"`);
break;
}
};
socket.onerror = () => {
console.error('Error connecting to websocket');
dispatch(setConnected(false));
};
};
export const setConnected = (connected = false) => ({
type: ActionType['/p2p/setConnected'],
payload: connected,
});
export const setClientId = (clientId: string) => ({
type: ActionType['/p2p/setClientId'],
payload: clientId,
});
export const setSocket = (socket: WebSocket) => ({
type: ActionType['/p2p/setSocket'],
payload: socket,
});
export const setMessages = (messages: Chat[]) => ({
type: ActionType['/p2p/setMessages'],
payload: messages,
});
export const appendMessage = (message: Chat) => ({
type: ActionType['/p2p/appendMessage'],
payload: message,
});
export const setPairing = (clientId: string) => ({
type: ActionType['/p2p/setPairing'],
payload: clientId,
});
let id = 1;
export const sendChat =
(message: Chat) =>
async (dispatch: Dispatch, getState: () => AppRootState) => {
const {
p2p: { socket },
} = getState();
if (socket) {
socket.send(
Buffer.from(
JSON.stringify({
method: 'chat',
params: {
...message,
id: id++,
},
}),
),
);
dispatch(appendMessage(message));
}
};
export const sendPairRequest =
(target: string) =>
async (dispatch: Dispatch, getState: () => AppRootState) => {
const {
p2p: { socket, clientId },
} = getState();
if (socket && clientId) {
socket.send(
Buffer.from(
JSON.stringify({
method: 'pair_request',
params: {
from: clientId,
to: target,
id: id++,
},
}),
),
);
}
};
export const confirmPairRequest =
(target: string) => (dispatch: Dispatch, getState: () => AppRootState) => {
const {
p2p: { socket, clientId },
} = getState();
console.log({ target, clientId });
if (socket && clientId) {
socket.send(
Buffer.from(
JSON.stringify({
method: 'pair_request_success',
params: {
from: clientId,
to: target,
id: id++,
},
}),
),
);
dispatch(setPairing(target));
}
};
export default function p2p(state = initialState, action: Action<any>) {
switch (action.type) {
case ActionType['/p2p/setConnected']:
return {
...state,
connected: action.payload,
};
case ActionType['/p2p/setClientId']:
return {
...state,
clientId: action.payload,
};
case ActionType['/p2p/setSocket']:
return {
...state,
socket: action.payload,
};
case ActionType['/p2p/setMessages']:
return {
...state,
messages: action.payload,
};
case ActionType['/p2p/setPairing']:
return {
...state,
pairing: action.payload,
};
case ActionType['/p2p/appendMessage']:
return {
...state,
messages: state.messages.concat(action.payload),
};
default:
return state;
}
}
export function useClientId() {
return useSelector((state: AppRootState) => {
return state.p2p.clientId;
}, deepEqual);
}
export function useSocket() {
return useSelector((state: AppRootState) => {
return state.p2p.socket;
}, deepEqual);
}
export function useConnected() {
return useSelector((state: AppRootState) => {
return state.p2p.connected;
}, deepEqual);
}
export function useChatMessages(): Chat[] {
return useSelector((state: AppRootState) => {
return state.p2p.messages;
}, deepEqual);
}
export function usePairId(): string {
return useSelector((state: AppRootState) => {
return state.p2p.pairing;
}, deepEqual);
}

View File

@@ -1,54 +0,0 @@
import { useSelector } from 'react-redux';
import { AppRootState } from './index';
import deepEqual from 'fast-deep-equal';
enum ActionType {
'/plugin/addPlugin' = '/plugin/addPlugin',
'/plugin/removePlugin' = '/plugin/removePlugin',
}
type Action<payload> = {
type: ActionType;
payload?: payload;
error?: boolean;
meta?: any;
};
type State = {
order: string[];
};
const initState: State = {
order: [],
};
export const addOnePlugin = (hash: string): Action<string> => ({
type: ActionType['/plugin/addPlugin'],
payload: hash,
});
export const removeOnePlugin = (hash: string): Action<string> => ({
type: ActionType['/plugin/removePlugin'],
payload: hash,
});
export default function plugins(state = initState, action: Action<any>): State {
switch (action.type) {
case ActionType['/plugin/addPlugin']:
return {
order: [...new Set(state.order.concat(action.payload))],
};
case ActionType['/plugin/removePlugin']:
return {
order: state.order.filter((h) => h !== action.payload),
};
default:
return state;
}
}
export const usePluginHashes = (): string[] => {
return useSelector((state: AppRootState) => {
return state.plugins.order;
}, deepEqual);
};

View File

@@ -5,12 +5,7 @@ import {
import { useSelector } from 'react-redux';
import { AppRootState } from './index';
import deepEqual from 'fast-deep-equal';
import {
getNotaryApi,
getProxyApi,
getMaxSent,
getMaxRecv,
} from '../utils/storage';
import { get, NOTARY_API_LS_KEY, PROXY_API_LS_KEY } from '../utils/storage';
import { BackgroundActiontype } from '../entries/Background/rpc';
import browser from 'webextension-polyfill';
@@ -45,10 +40,11 @@ export const setRequests = (requests: RequestLog[]): Action<RequestLog[]> => ({
});
export const notarizeRequest = (options: RequestHistory) => async () => {
const notaryUrl = await getNotaryApi();
const websocketProxyUrl = await getProxyApi();
const maxSentData = await getMaxSent();
const maxRecvData = await getMaxRecv();
const notaryUrl = await get(NOTARY_API_LS_KEY, 'https://notary.pse.dev');
const websocketProxyUrl = await get(
PROXY_API_LS_KEY,
'wss://notary.pse.dev/proxy',
);
chrome.runtime.sendMessage<any, string>({
type: BackgroundActiontype.prove_request_start,
@@ -58,8 +54,6 @@ export const notarizeRequest = (options: RequestHistory) => async () => {
headers: options.headers,
body: options.body,
maxTranscriptSize: options.maxTranscriptSize,
maxSentData,
maxRecvData,
secretHeaders: options.secretHeaders,
secretResps: options.secretResps,
notaryUrl,

View File

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

View File

@@ -1,19 +1,5 @@
import {
BackgroundActiontype,
handleExecPluginProver,
RequestLog,
} from '../entries/Background/rpc';
import { RequestLog } from '../entries/Background/rpc';
import { EXPLORER_API } from './constants';
import createPlugin, { CallContext, Plugin } from '@extism/extism';
import browser from 'webextension-polyfill';
import NodeCache from 'node-cache';
import {
getCookieStoreByHost,
getHeaderStoreByHost,
} from '../entries/Background/cache';
import { getNotaryApi, getProxyApi } from './storage';
const charwise = require('charwise');
export function urlify(
text: string,
@@ -58,6 +44,7 @@ export function download(filename: string, content: string) {
export async function upload(filename: string, content: string) {
const formData = new FormData();
formData.append(
'file',
new Blob([content], { type: 'application/json' }),
@@ -108,14 +95,8 @@ export async function replayRequest(req: RequestLog): Promise<string> {
// @ts-ignore
const resp = await fetch(req.url, options);
return extractBodyFromResponse(resp);
}
export const extractBodyFromResponse = async (
resp: Response,
): Promise<string> => {
const contentType =
resp.headers.get('content-type') || resp.headers.get('Content-Type');
resp?.headers.get('content-type') || resp?.headers.get('Content-Type');
if (contentType?.includes('application/json')) {
return resp.text();
@@ -126,225 +107,12 @@ export const extractBodyFromResponse = async (
} else {
return resp.blob().then((blob) => blob.text());
}
};
}
export const sha256 = async (data: string) => {
const encoder = new TextEncoder().encode(data);
const hashBuffer = await crypto.subtle.digest('SHA-256', encoder);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray
.map((bytes) => bytes.toString(16).padStart(2, '0'))
.join('');
return hashHex;
};
const VALID_HOST_FUNCS: { [name: string]: string } = {
redirect: 'redirect',
notarize: 'notarize',
};
export const makePlugin = async (
arrayBuffer: ArrayBuffer,
config?: PluginConfig,
) => {
const module = await WebAssembly.compile(arrayBuffer);
const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
const injectedConfig = {
tabUrl: tab?.url,
tabId: tab?.id,
};
const approvedRequests = config?.requests || [];
const approvedNotary = [await getNotaryApi()].concat(config?.notaryUrls);
const approvedProxy = [await getProxyApi()].concat(config?.proxyUrls);
const HostFunctions: {
[key: string]: (callContext: CallContext, ...args: any[]) => any;
} = {
redirect: function (context: CallContext, off: bigint) {
const r = context.read(off);
const url = r.text();
browser.tabs.update(tab.id, { url });
},
notarize: function (context: CallContext, off: bigint) {
const r = context.read(off);
const params = JSON.parse(r.text());
const now = Date.now();
const id = charwise.encode(now).toString('hex');
if (
!approvedRequests.find(
({ method, url }) => method === params.method && url === params.url,
)
) {
throw new Error(`Unapproved request - ${params.method}: ${params.url}`);
}
if (
params.notaryUrl &&
!approvedNotary.find((n) => n === params.notaryUrl)
) {
throw new Error(`Unapproved notary: ${params.notaryUrl}`);
}
if (
params.websocketProxyUrl &&
!approvedProxy.find((w) => w === params.websocketProxyUrl)
) {
throw new Error(`Unapproved proxy: ${params.websocketProxyUrl}`);
}
handleExecPluginProver({
type: BackgroundActiontype.execute_plugin_prover,
data: {
...params,
now,
},
});
return context.store(`${id}`);
},
};
const funcs: {
[key: string]: (callContext: CallContext, ...args: any[]) => any;
} = {};
for (const fn of Object.keys(VALID_HOST_FUNCS)) {
funcs[fn] = function (context: CallContext) {
throw new Error(`no permission for ${fn}`);
};
export function safeParseJSON(data: string) {
try {
return JSON.parse(data);
} catch (e) {
return null;
}
if (config?.hostFunctions) {
for (const fn of config.hostFunctions) {
funcs[fn] = HostFunctions[fn];
}
}
if (config?.cookies) {
const cookies: { [hostname: string]: { [key: string]: string } } = {};
for (const host of config.cookies) {
const cache = getCookieStoreByHost(host);
cookies[host] = cacheToMap(cache);
}
// @ts-ignore
injectedConfig.cookies = JSON.stringify(cookies);
}
if (config?.headers) {
const headers: { [hostname: string]: { [key: string]: string } } = {};
for (const host of config.headers) {
const cache = getHeaderStoreByHost(host);
headers[host] = cacheToMap(cache);
}
// @ts-ignore
injectedConfig.headers = JSON.stringify(headers);
}
const pluginConfig = {
useWasi: true,
config: injectedConfig,
functions: {
'extism:host/user': funcs,
},
};
const plugin = await createPlugin(module, pluginConfig);
return plugin;
};
export type StepConfig = {
title: string;
description?: string;
cta: string;
action: string;
prover?: boolean;
};
export type PluginConfig = {
title: string;
description: string;
icon?: string;
steps?: StepConfig[];
hostFunctions?: string[];
cookies?: string[];
headers?: string[];
requests: { method: string; url: string }[];
notaryUrls?: string[];
proxyUrls?: string[];
};
export const getPluginConfig = async (
data: Plugin | ArrayBuffer,
): Promise<PluginConfig> => {
const plugin = data instanceof ArrayBuffer ? await makePlugin(data) : data;
const out = await plugin.call('config');
const config: PluginConfig = JSON.parse(out.string());
assert(typeof config.title === 'string' && config.title.length);
assert(typeof config.description === 'string' && config.description.length);
assert(!config.icon || typeof config.icon === 'string');
for (const req of config.requests) {
assert(typeof req.method === 'string' && req.method);
assert(typeof req.url === 'string' && req.url);
}
if (config.hostFunctions) {
for (const func of config.hostFunctions) {
assert(typeof func === 'string' && !!VALID_HOST_FUNCS[func]);
}
}
if (config.notaryUrls) {
for (const notaryUrl of config.notaryUrls) {
assert(typeof notaryUrl === 'string' && notaryUrl);
}
}
if (config.proxyUrls) {
for (const proxyUrl of config.proxyUrls) {
assert(typeof proxyUrl === 'string' && proxyUrl);
}
}
if (config.cookies) {
for (const name of config.cookies) {
assert(typeof name === 'string' && name.length);
}
}
if (config.headers) {
for (const name of config.headers) {
assert(typeof name === 'string' && name.length);
}
}
if (config.steps) {
for (const step of config.steps) {
assert(typeof step.title === 'string' && step.title.length);
assert(!step.description || typeof step.description);
assert(typeof step.cta === 'string' && step.cta.length);
assert(typeof step.action === 'string' && step.action.length);
assert(!step.prover || typeof step.prover === 'boolean');
}
}
return config;
};
export const assert = (expr: any, msg = 'unknown error') => {
if (!expr) throw new Error(msg);
};
export const hexToArrayBuffer = (hex: string) =>
new Uint8Array(Buffer.from(hex, 'hex')).buffer;
export const cacheToMap = (cache: NodeCache) => {
const keys = cache.keys();
return keys.reduce((acc: { [k: string]: string }, key) => {
acc[key] = cache.get(key) || '';
return acc;
}, {});
};
}

View File

@@ -1,64 +0,0 @@
import browser from 'webextension-polyfill';
import { BackgroundActiontype } from '../entries/Background/rpc';
import { PluginConfig } from './misc';
export async function getCookiesByHost(hostname: string) {
return browser.runtime.sendMessage({
type: BackgroundActiontype.get_cookies_by_hostname,
data: hostname,
});
}
export async function getHeadersByHost(hostname: string) {
return browser.runtime.sendMessage({
type: BackgroundActiontype.get_headers_by_hostname,
data: hostname,
});
}
export async function addPlugin(hex: string) {
return browser.runtime.sendMessage({
type: BackgroundActiontype.add_plugin,
data: hex,
});
}
export async function removePlugin(hash: string) {
return browser.runtime.sendMessage({
type: BackgroundActiontype.remove_plugin,
data: hash,
});
}
export async function fetchPluginHashes() {
return browser.runtime.sendMessage({
type: BackgroundActiontype.get_plugin_hashes,
});
}
export async function fetchPluginByHash(hash: string) {
return browser.runtime.sendMessage({
type: BackgroundActiontype.get_plugin_by_hash,
data: hash,
});
}
export async function fetchPluginConfigByHash(
hash: string,
): Promise<PluginConfig | null> {
return browser.runtime.sendMessage({
type: BackgroundActiontype.get_plugin_config_by_hash,
data: hash,
});
}
export async function runPlugin(hash: string, method: string, params?: string) {
return browser.runtime.sendMessage({
type: BackgroundActiontype.run_plugin,
data: {
hash,
method,
params,
},
});
}

View File

@@ -1,10 +1,6 @@
import { LOGGING_LEVEL_INFO } from './constants';
export const NOTARY_API_LS_KEY = 'notary-api';
export const PROXY_API_LS_KEY = 'proxy-api';
export const MAX_SENT_LS_KEY = 'max-sent';
export const MAX_RECEIVED_LS_KEY = 'max-received';
export const LOGGING_FILTER_KEY = 'logging-filter';
export const HISTORY_LS_KEY = 'history';
export async function set(key: string, value: string) {
return chrome.storage.sync.set({ [key]: value });
@@ -16,23 +12,3 @@ export async function get(key: string, defaultValue?: string) {
.then((json: any) => json[key] || defaultValue)
.catch(() => '');
}
export async function getMaxSent() {
return parseInt(await get(MAX_SENT_LS_KEY, '4096'));
}
export async function getMaxRecv() {
return parseInt(await get(MAX_RECEIVED_LS_KEY, '16384'));
}
export async function getNotaryApi() {
return await get(NOTARY_API_LS_KEY, 'https://notary.pse.dev/v0.1.0-alpha.5');
}
export async function getProxyApi() {
return await get(PROXY_API_LS_KEY, 'wss://notary.pse.dev/proxy');
}
export async function getLoggingFilter() {
return await get(LOGGING_FILTER_KEY, LOGGING_LEVEL_INFO);
}

View File

@@ -1,4 +1,3 @@
import type {} from 'redux-thunk/extend-redux';
import { applyMiddleware, createStore } from 'redux';
import thunk from 'redux-thunk';
import { createLogger } from 'redux-logger';

View File

@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "esnext",
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": false,
"skipLibCheck": true,

View File

@@ -0,0 +1,22 @@
[
{
"url": "https://api.twitter.com/1.1/account/settings.json",
"targetUrl": "https://www.twitter.com",
"method": "GET",
"type": "xmlhttprequest",
"title": "Twitter Profile",
"description": "Notarize ownership of a twitter profile. To start, go to your own profile",
"responseSelector": "(?<=\"screen_name\":)\"(.*?)\"",
"valueTransform": "\"screen_name\":%s"
},
{
"url": "https://gateway.reddit.com/desktopapi/v1/prefs",
"targetUrl": "https://www.reddit.com/settings",
"method": "GET",
"type": "xmlhttprequest",
"title": "Reddit Profile",
"description": "Notarize ownership of a reddit profile. To start, go to reddit.com/settings",
"responseSelector": "(?<=\"displayText\": )\"(.*?)\"",
"valueTransform": "\"displayText\": %s"
}
]

34
utils/bookmark/index.ts Normal file
View File

@@ -0,0 +1,34 @@
import bookmarks from './bookmarks.json';
import { RequestLog } from '../../src/entries/Background/rpc';
type Bookmark = {
url: string;
title: string;
description: string;
method: string;
type: string;
};
export function findBookmarksByURL(url: URL | null): Bookmark[] {
if (!url) return [];
return bookmarks.filter((m) => {
const _url = new URL(m.url);
return url.host === _url.host;
});
}
export function filterByBookmarks(requests: RequestLog[]): Bookmark[] {
const hosts = requests
.map((r) => r.url && new URL(r.url).host)
.reduce((acc: { [host: string]: string }, host) => {
acc[host] = host;
return acc;
}, {});
return bookmarks.filter((bm) => {
if (hosts[new URL(bm.url).host]) {
return true;
}
});
}

View File

@@ -1,5 +1,5 @@
// tiny wrapper with default env vars
module.exports = {
NODE_ENV: process.env.NODE_ENV || 'development',
PORT: process.env.PORT || 3000,
PORT: process.env.PORT || 3001,
};

View File

@@ -49,7 +49,6 @@ var options = {
background: path.join(__dirname, "src", "entries", "Background", "index.ts"),
contentScript: path.join(__dirname, "src", "entries", "Content", "index.ts"),
offscreen: path.join(__dirname, "src", "entries", "Offscreen", "index.tsx"),
sidePanel: path.join(__dirname, "src", "entries", "SidePanel", "index.tsx"),
},
// chromeExtensionBoilerplate: {
// notHotReload: ["background", "contentScript", "devtools"],
@@ -147,9 +146,9 @@ var options = {
new webpack.ProgressPlugin(),
// expose and write the allowed env vars on the compiled bundle
new webpack.EnvironmentPlugin(["NODE_ENV"]),
// new ExtReloader({
// manifest: path.resolve(__dirname, "src/manifest.json")
// }),
new ExtReloader({
manifest: path.resolve(__dirname, "src/manifest.json")
}),
new CopyWebpackPlugin({
patterns: [
{
@@ -243,12 +242,6 @@ var options = {
chunks: ["offscreen"],
cache: false,
}),
new HtmlWebpackPlugin({
template: path.join(__dirname, "src", "entries", "SidePanel", "index.html"),
filename: "sidePanel.html",
chunks: ["sidePanel"],
cache: false,
}),
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
}),

4243
yarn.lock

File diff suppressed because it is too large Load Diff