Compare commits

...

78 Commits

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

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

* response capture works

* code refactor
2024-10-22 10:34:39 +02:00
jayssj11
4ad394419b chat window added to the extension (#1) 2024-10-02 16:47:38 +02:00
Tanner
25f35d0051 feat: moving requestbuilder.tsx from utils to it's own component (#92) 2024-08-28 04:10:28 -04:00
tsukino
5ccdd9b06a feat: add default plugins to extension bundle (#93) 2024-08-28 04:05:56 -04:00
Hendrik Eeckhaut
186f77d3cb fix: add body to request (#89) 2024-08-23 03:22:21 -04:00
Hendrik Eeckhaut
08c4f74479 chore: moved all example plugins to the boilerplate repository (#90)
https://github.com/tlsnotary/tlsn-plugin-boilerplate
2024-08-23 03:21:53 -04:00
tsukino
737cc10af7 fix: add default empty array for secret values (#86) 2024-08-22 04:36:24 -04:00
tsukino
d1cbc34126 fix: empty body string should be undefined (#85) 2024-08-21 10:52:39 -04:00
Tanner
30a7b7b36b feat: adding connection status to requests store (#82) 2024-08-21 07:12:30 -04:00
tsukino
4c78625f12 fix: update logging level to work with alpha6; update verify method in background; add version to option page (#79) 2024-08-14 07:57:27 -04:00
tsukino
d15d021b4a feat: upgrade to alpha.6 (#78)
* feat: integrate with alpha.6

* fix: replace deleted type

* chore: update package-lock
2024-08-13 12:50:51 -04:00
Tanner
be27560631 Content Script disconnect (#75)
* feat: Modal to disconnect from origin

* refactor: styling

* chore: linting

* refactor: styling connections modal

* feat: moved ConnectionDetailsModal to its own component

* chore: linting

* fix: styling update

---------

Co-authored-by: tsukino <0xtsukino@gmail.com>
2024-08-13 12:43:22 -04:00
Ryan MacArthur
a018acb7bf [chore] MV3 migration: remove persistent key from background' (#76) 2024-08-13 11:00:26 -04:00
Tanner
d9dacdfb14 feat: adding disclaimer readme to example plugins (#74) 2024-07-13 12:58:04 +02:00
tsukino
998c9c091e fix: overwrite headers and cookies (#73) 2024-07-12 13:27:13 +02:00
tsukino
c04556620c fix: overwrite metadata when installing plugin (#72) 2024-07-04 03:26:28 -04:00
Tanner
4b473273dc fix: Request Builder fixes (#65)
* feat: wip fixing requestbuilder

* refactor: refactored RequestBuilder and RequestDetail components

* refactor: WIP requestbuilder/responsedetail methods working now

* refactor: dynamically change content-type for body

* refactor: formatting body for request

* feat: adding notarize button to RequestBuilder

* feat: adding x-www-form-urlencoded option to body

* refactor: major refactor of request builder

* feat: added parseResponse function; refactor: adjusted formatForRequest to properly handle different body types

* refactor: improving styling and readability

* refactor: fixing method/type logic for body and headers

* refactor: cleaning up code in requestbuilder utils

* chore: wip

* refactor: fixing GET/HEAD sending a body

* refactor: fix for type and method changes

* refactor: fixing more bugs with GET/POST & x-www-form-urlencoded requests

* feat: update content-type silent value instead of removing on method change
2024-07-04 02:10:37 -04:00
tsukino
585a8f2d3d feat: add injected client to content script (#69)
* feat: connection approval screen and injected script

* fix: favicon and origin on connection request

* feat: add get history to injected client

* feat: add get history approval ux

* fix: styling fix

* feat: add get proof

* fix: throw error when proof not found

* wip: adding notarize

* feat: add notarization request

* feat: add metadata to notarization request

* feat: add install plugin content script

* feat: add get plugins content script

* feat: add execute plugin

* fix: remove unused method
2024-07-04 02:06:35 -04:00
Hendrik Eeckhaut
1c29fee920 Add example plugin in Typescript (#71)
* WIP: plugins in typescript

* Convert icon to base64 with esbuild step

* code clean up

* Added comments + move config to the front

* Corrected package.json (name, description, etc)

* Call extism-js in esbuild.js

* Moved config file to separate json file

+ various cleanups
2024-06-25 06:02:12 -04:00
Hendrik Eeckhaut
884e55dccf feat: Twitter DM plugin + use minimatch to allow patterns in config request urls (#66) 2024-06-17 06:17:21 -04:00
Tanner
87e96c0f50 feat: Display Plugin Information (#62)
* feat: added info button and confirmation to remove button

* refactor: working on plugin info modal

* refactor: adding remove and plugininfo to callback dependencies

* refactor: working on plugin info modal

* feat: show plugin information on plugin upload

* refactor: Plugin info and plugin installation modals restyled

* refactor: linting

* refactor: plugin installation modal refactor

* refactor: plugin upload refactor

* wip: restyling and add user friendly text

* fix: restyling and retexting

* refactor PluginInfo

* fix error message and twitter profile plugin

---------

Co-authored-by: John Shaw <codetrauma@Johns-MacBook-Pro.local>
Co-authored-by: tsukino <0xtsukino@gmail.com>
2024-06-17 04:19:40 -04:00
Hendrik Eeckhaut
3b8cd0fba3 fix: off by one error + updated README 2024-06-13 11:21:50 +02:00
tsukino
c41b4ff401 feat: add redaction back to plugin (#64)
* feat: add redaction back to plugin

* chore: compile wasm

* fix: pass in correct request body
2024-06-11 10:19:09 -04:00
tsukino
047eb673f3 feat: add logging options and reload extension on save (#61)
* feat: add logging options and reload extension on save

* chore: update lockfiles
2024-05-28 05:41:05 -04:00
Tanner
b8d2ba06d7 Store CID to prevent having to upload multiple times (#53)
* feature: store cid

* chore: renamed error state

* chore: fixing linter errors

* refactor: refactored cid store from chrome storage to db

* fix: typo OneRequestHistory

* fix: update lockfiles

---------

Co-authored-by: John Shaw <codetrauma@Johns-MacBook-Pro.local>
Co-authored-by: tsukino <0xtsukino@gmail.com>
2024-05-21 09:45:56 -04:00
Hendrik Eeckhaut
ca1ea2b34e fix: pass maxSentData and maxRecvData correctly (#60)
+ use user settings in Twitter plugin
2024-05-21 09:35:42 -04:00
tsukino
53ba6f69b8 feat: implement plugin system and add demo plugin for twitter profile (#59)
* wip: testing extism host function

* wip

* wip

* wip: adding ui for plugin

* feat: add cache for headers and cookies by host

* feat: add plugin stores

* feat: add a plugin and render plugin list

* feat: add plugin config db

* feat: add basic plugin steps ui

* feat: add completion status to steps ui

* fix: refactor twitter profile plugin

* fix: refactor steps execution

* fix: testing plugin

* Added README to plugins folder

* feat: fix twitter profile plugin

* improved README: document how to run the twitter plugin example

* remove ddos

* WIP: Reddit plugin

* fix: steps circular reference

* feat: open popup

* Reddit plugin (result too big)

* feat: add view proof

* chore: add plugins to eslint ignore

* fix: twitter plugin

* fix: remove logs

* feat: add permission for approved request, notary, and proxy url in plugin

---------

Co-authored-by: Hendrik Eeckhaut <hendrik@eeckhaut.org>
2024-05-17 06:04:47 -04:00
Tanner
217824f2bf feat: Advanced Options to set MAX_SENT and MAX_RECV (#58)
* feature: adding advanced options for setting max_sent and max_recv

* refactor: changed MAX_RECV and MAX_SENT to be numbers instead

* feature: setting default values for max_sent and max_recv

* feat: added Advanced options tab to RequestDetails

* refactor: remove props from advanced route - useEffect refactored for advanced options

* chore: linting

* refactor: moved advanced options to drop down

* refactor: added helper functions to get stored values - refactored useEffect in Options

* refactor: refactored useEffect in RequestDetails advanced options

* chore: linting

* chore: linting

* refactor: applying new get functions where applicable

* chore: linting

* feat: adding maxSentData and maxRecv data to prover

* chore: linting

---------

Co-authored-by: John Shaw <codetrauma@Johns-MacBook-Pro.local>
2024-05-17 04:48:05 -04:00
Hendrik Eeckhaut
869fa5eeaa docs: Add license information (#57) 2024-05-07 11:05:51 -04:00
Tanner
92f9d65c23 Merge pull request #54 from tlsnotary/notary-api
Notary Server Url Constant
2024-04-09 10:35:08 -07:00
Tanner Shaw
c481ee6bbf chore: fixing linter issues 2024-04-09 10:33:24 -07:00
Tanner
810e7bf415 Merge branch 'main' into notary-api 2024-04-09 10:32:31 -07:00
Tanner Shaw
8348756f0a chore: bumping tlsn-js version 2024-04-09 09:50:50 -07:00
tsukino
9890604391 fix: bump version to alpha.5.2 (#56) 2024-04-09 12:50:45 -04:00
tsukino
763335b0b1 chore: version bump to 0.1.0-alpha.5 (#55)
* chore: version bump to 0.1.0-alpha.5

* fix: version bump for tlsn-js@alpha.5.1

* fix: add type for redux-thunk
2024-04-09 12:08:42 -04:00
Tanner Shaw
71cef56356 chore: updating notary api url constant to alpha.5 2024-04-08 12:03:23 -07:00
Tanner Shaw
25689017b0 refactor: removing version from notary proxy 2024-04-03 13:25:51 -07:00
Tanner Shaw
c68e2e1548 chore: fixing linter issues 2024-04-03 10:45:12 -07:00
Tanner Shaw
8bb76ad969 feature: added notary proxy to constants 2024-04-03 10:43:05 -07:00
Tanner Shaw
d55279501e refactor: just changing to .5 now, can push when released 2024-04-02 12:15:52 -07:00
Tanner Shaw
2331074c9c feature: adding constant for versioning for the notary, will change to .5 when released 2024-04-02 11:42:29 -07:00
tsukino
a42bb2eabd fix: clean up styling and code for sharing a proof (#51)
* fix: clean up styling and code for sharing a proof

* chore: update lockfiles

* fix: use constant for explorer api
2024-03-26 07:16:41 -04:00
tsukino
76c6acd998 chore: update lockfiles (#50) 2024-03-26 05:49:07 -04:00
Richard Liang
d0024077f9 add ext reloader to fix infinite refresh (#49) 2024-03-26 05:38:34 -04:00
Tanner
42ab67eb24 Merge pull request #47 from tlsnotary/upload-proof-to-ipfs
Upload proof to IPFS
2024-03-20 18:20:19 -07:00
Tanner Shaw
028a3b5444 chore: fixing linting errors 2024-03-20 18:18:44 -07:00
Tanner Shaw
d60d6a3ff3 refactor: modal events 2024-03-05 14:32:43 -08:00
Tanner Shaw
33ad9acca5 refactor: changing styling to modal 2024-03-05 13:34:59 -08:00
Tanner Shaw
6503281d75 refactor: adding styling to modal 2024-03-04 13:28:41 -08:00
Tanner Shaw
5f7bc6dae0 refactor: trying to fix some of the GH action checks 2024-03-03 20:30:33 -08:00
Tanner Shaw
bc4e77b8f1 feature: added modal for user acceptance to upload proof to IPFS 2024-03-03 16:09:32 -08:00
Tanner Shaw
8ec3e37c92 feature: added button to share proofs through ipfs 2024-03-03 14:15:57 -08:00
tsukino
220c138290 chore: update tlsn-js (#46) 2024-02-22 11:33:23 -05:00
Hendrik Eeckhaut
eb505cf234 Merge pull request #45 from tlsnotary/notary-server-documentation
Update readme for version constraint
2024-02-20 14:33:05 +01:00
Christopher Chong
04485168e1 Update readme for notary version. 2024-02-19 20:29:20 +08:00
tsukino
fec058fd7c feat: upgrade to alpha.4 (#43) 2024-02-16 11:46:21 -05:00
tsukino
042fba9c09 Update README.md 2024-02-15 04:32:54 -05:00
tsukino
1da4f45564 feat: add error handling to prover (#42) 2024-02-06 09:09:56 -05:00
Hendrik Eeckhaut
c8f2b541d6 build: added cocogitto config file (#40) 2024-01-09 10:22:38 -05:00
tsukino
610b07f4b3 update: version bump 2024-01-09 00:34:57 -04:00
tsukino
217a18ea9d fix: update default urls (#39) 2024-01-08 23:23:17 -05:00
tsukino
a9e0924a1d fix: webpack config and return data when handling rpc requests (#38)
* upgrade tlsn-js and fix webpack config

* fix: return data when handling rpc request

* fix: fetch history and requests
2024-01-08 23:05:04 -05:00
Hendrik Eeckhaut
c2b3b9dcfa Chore/wasm removal cleanup (#37)
* Remove old wasm leftovers

* use wildcard for tlsn-js wasm entry

* Remove unused ttlcache dependency
2024-01-06 22:08:51 -05:00
tsukino
72efee69ff fix: ignore ts compilation error on production build (#36) 2023-12-22 02:25:29 -05:00
tsukino
821f2734f9 refactor: modularize background and offscreen (#35) 2023-12-20 21:09:10 -05:00
Kevin Mai-Husan Chia
51dcb29e7e fix: config max_transcript_size in ProverConfig (#34) 2023-12-18 12:44:15 -05:00
Hendrik Eeckhaut
629a519112 Cleanup (#33)
* Added update:wasm script + resolved Rust warnings

* resolved last warning + added helper script

* Resolved lint warning
2023-12-14 13:39:54 -05:00
Hendrik Eeckhaut
99d1922aa8 Removed Ring patch + updated wasm-bindgen (#31)
* Removed Ring patch + updated wasm-bindgen

* Update wasm/prover/Cargo.toml

Co-authored-by: Kevin Mai-Husan Chia <mhchia@users.noreply.github.com>

* Move ring dependency forward

* cleaned up Rust dependencies

---------

Co-authored-by: Kevin Mai-Husan Chia <mhchia@users.noreply.github.com>
2023-12-14 01:46:52 -05:00
0xtsukino
bb204533db add multi-target files and auto tokens to webproxy url 2023-11-16 12:14:59 +03:00
0xtsukino
3be3b5997c udpate readme 2023-11-15 09:03:09 +03:00
0xtsukino
0379540ca8 update readme 2023-11-15 08:14:28 +03:00
tsukino
5acf70686e basic plugin for twitter and reddit (#26)
* basic plugin for twitter and reddit

* remove timeout

* linter fix
2023-11-14 15:48:34 +03:00
tsukino
d09aeca74b update tlsn to latest (#25) 2023-11-14 13:00:03 +03:00
tsukino
03b54a53d6 Add UI for verifying a proof (#24)
* ellipsify text in notarization history view

* finish verify flow

* linter fix

* unhide download button
2023-11-14 10:55:34 +03:00
0xtsukino
d9e8e813b0 fix: override Accept-Endcoding and Connection for all request 2023-11-11 02:20:36 -08:00
tsukino
33ff881ec8 add selective disclosure to notarization flow (#23)
* selective disclosure ui

* wip: notarize with selective disclosure

* ready
2023-11-11 01:52:23 -08:00
tsukino
7c95e9e6f8 minor refactoring and bugfixes (#22) 2023-11-10 00:10:12 -08:00
106 changed files with 34586 additions and 6608 deletions

View File

@@ -1,10 +0,0 @@
[target.wasm32-unknown-unknown]
rustflags = [
"-C",
"target-feature=+atomics,+bulk-memory,+mutable-globals",
"-C",
"link-arg=--max-memory=4294967296"
]
[unstable]
build-std = ["panic_abort", "std"]

View File

@@ -1,5 +1,5 @@
{
root: true,
"root": true,
"extends": ["prettier", "plugin:@typescript-eslint/recommended"],
"plugins": ["prettier", "@typescript-eslint"],
"parser": "@typescript-eslint/parser",
@@ -7,6 +7,7 @@
"prettier/prettier": "error",
"@typescript-eslint/no-explicit-any": 1,
"@typescript-eslint/no-var-requires": 0,
"@typescript-eslint/ban-ts-comment": 0,
"no-undef": "error",
"padding-line-between-statements": "error"
},
@@ -30,6 +31,7 @@
"wasm",
"tlsn",
"util",
"plugins",
"webpack.config.js"
]
}

2
.gitignore vendored
View File

@@ -2,11 +2,9 @@
**/*.rs.bk
Cargo.lock
bin/
wasm-pack.log
**/node_modules
**/.DS_Store
.idea
build
tlsn/
zip
wasm/prover/target/

View File

@@ -1,7 +1,28 @@
![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.
## Installing and Running
### Procedures:
@@ -9,23 +30,25 @@
1. Check if your [Node.js](https://nodejs.org/) version is >= **18**.
2. Clone this repository.
3. Run `npm install` to install the dependencies.
4. Run `npm run build:wasm`
5. Run `npm run dev`
6. Load your extension on Chrome following:
4. Run `npm run dev`
5. Load your extension on Chrome following:
1. Access `chrome://extensions/`
2. Check `Developer mode`
3. Click on `Load unpacked extension`
4. Select the `build` folder.
7. Happy hacking.
## Webpack auto-reload and HRM
To make your workflow much more efficient this boilerplate uses the [webpack server](https://webpack.github.io/docs/webpack-dev-server.html) to development (started with `npm start`) with auto reload feature that reloads the browser automatically every time that you save some file in your editor.
You can run the dev mode on other port if you want. Just specify the env var `port` like this:
6. Happy hacking.
## Building Websockify Docker Image
```
$ PORT=6002 npm run start
$ git clone https://github.com/novnc/websockify && cd websockify
$ ./docker/build.sh
$ docker run -it --rm -p 55688:80 novnc/websockify 80 api.x.com:443
```
## Running Websockify Docker Image
```
$ cd tlsn-extension
$ docker run -it --rm -p 55688:80 novnc/websockify 80 api.twitter.com:443
```
## Packing
@@ -42,5 +65,3 @@ Now, the content of `build` folder will be the extension ready to be submitted t
- [Webpack documentation](https://webpack.js.org/concepts/)
- [Chrome Extension documentation](https://developer.chrome.com/extensions/getstarted)
- [wasm-bindgen-rayon](https://github.com/GoogleChromeLabs/wasm-bindgen-rayon)
- [wasm-unsafe-eval CSP](https://bugs.chromium.org/p/chromium/issues/detail?id=1173354#c60)

33
cog.toml Normal file
View File

@@ -0,0 +1,33 @@
branch_whitelist = []
disable_changelog = false
from_latest_tag = false
generate_mono_repository_global_tag = true
ignore_merge_commits = false
post_bump_hooks = []
post_package_bump_hooks = []
pre_bump_hooks = [
"echo {{version}}",
]
pre_package_bump_hooks = []
skip_ci = "[skip ci]"
skip_untracked = false
[git_hooks]
[commit_types]
[changelog]
authors = [
{username = "0xtsukino", signature = "tsukino"},
{username = "heeckhau", signature = "Hendrik Eeckhaut"},
{username = "mhchia", signature = "Kevin Mai-Husan Chia"},
]
owner = "TLSNotary"
path = "CHANGELOG.md"
remote = "github.com"
repository = "tlsn-extension"
template = "remote"
[bump_profiles]
[packages]

23989
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "tlsn-extension",
"version": "0.0.1",
"version": "0.1.0.6",
"license": "MIT",
"repository": {
"type": "git",
@@ -8,22 +8,28 @@
},
"scripts": {
"clone:tlsn": "bash ./utils/download-tlsn.sh",
"build:wasm": "wasm-pack build --target web wasm/prover",
"build": "NODE_ENV=production node utils/build.js",
"build:webpack": "NODE_ENV=production webpack --config webpack.config.js",
"websockify": "docker run -it --rm -p 55688:80 -v $(pwd):/app novnc/websockify 80 --target-config /app/websockify_config",
"dev": "NODE_ENV=development node utils/webserver.js",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
},
"dependencies": {
"@extism/extism": "^1.0.2",
"@fortawesome/fontawesome-free": "^6.4.2",
"@types/axios": "^0.14.0",
"async-mutex": "^0.4.0",
"axios": "^1.7.7",
"buffer": "^6.0.3",
"charwise": "^3.0.1",
"classnames": "^2.3.2",
"comlink": "^4.4.1",
"copy-to-clipboard": "^3.3.3",
"fast-deep-equal": "^3.1.3",
"fuse.js": "^6.6.2",
"level": "^8.0.0",
"minimatch": "^9.0.4",
"node-cache": "^5.1.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@@ -33,7 +39,9 @@
"redux": "^4.2.1",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.4.2",
"tailwindcss": "^3.3.3"
"tailwindcss": "^3.3.3",
"tlsn-js": "0.1.0-alpha.6.2",
"tlsn-js-v5": "npm:tlsn-js@0.1.0-alpha.5.4"
},
"devDependencies": {
"@babel/core": "^7.20.12",
@@ -47,6 +55,7 @@
"@types/react-dom": "^18.0.10",
"@types/react-router-dom": "^5.3.3",
"@types/redux-logger": "^3.0.9",
"@types/webextension-polyfill": "^0.10.7",
"babel-eslint": "^10.1.0",
"babel-loader": "^9.1.2",
"babel-preset-react-app": "^10.0.1",
@@ -77,12 +86,13 @@
"style-loader": "^3.3.1",
"terser-webpack-plugin": "^5.3.6",
"ts-loader": "^9.4.2",
"ttlcache": "link:@types/@isaacs/ttlcache",
"type-fest": "^3.5.2",
"typescript": "^4.9.4",
"webextension-polyfill": "^0.10.0",
"webpack": "^5.75.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.11.1",
"webpack-ext-reloader": "^1.1.12",
"zip-webpack-plugin": "^4.0.1"
}
}

5506
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +0,0 @@
[toolchain]
# channel = "nightly-2022-12-12"
# channel = "stable"
# channel = "nightly-x86_64-apple-darwin"
channel = "nightly"

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,70 @@
import React, { useCallback, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import {
useActiveTabUrl,
setConnection,
useIsConnected,
} from '../../reducers/requests';
import Modal, { ModalHeader, ModalContent } from '../../components/Modal/Modal';
import { deleteConnection, getConnection } from '../../entries/Background/db';
const ConnectionDetailsModal = (props: {
showConnectionDetails: boolean;
setShowConnectionDetails: (show: boolean) => void;
}) => {
const dispatch = useDispatch();
const activeTabOrigin = useActiveTabUrl();
const connected = useIsConnected();
useEffect(() => {
(async () => {
if (activeTabOrigin) {
const isConnected: boolean | null = await getConnection(
activeTabOrigin.origin,
);
dispatch(setConnection(!!isConnected));
}
})();
}, [activeTabOrigin, dispatch]);
const handleDisconnect = useCallback(async () => {
if (activeTabOrigin?.origin) {
await deleteConnection(activeTabOrigin.origin);
props.setShowConnectionDetails(false);
dispatch(setConnection(false));
}
}, [activeTabOrigin?.origin, dispatch, props]);
return (
<Modal
onClose={() => props.setShowConnectionDetails(false)}
className="flex flex-col gap-2 items-center text-base cursor-default justify-center mx-4 min-h-24"
>
<ModalHeader
className="w-full rounded-t-lg pb-0 border-b-0"
onClose={() => props.setShowConnectionDetails(false)}
>
<span className="text-lg font-semibold">
{activeTabOrigin?.hostname || 'Connections'}
</span>
</ModalHeader>
<ModalContent className="w-full gap-2 flex-grow flex flex-col items-center justify-between px-4 pt-0 pb-4">
<div className="flex flex-row gap-2 items-start w-full text-xs font-semibold text-slate-800">
{connected
? 'TLSN Extension is connected to this site.'
: 'TLSN Extension is not connected to this site. To connect to this site, find and click the connect button.'}
</div>
{connected && (
<button
className="button disabled:opacity-50 self-end"
onClick={handleDisconnect}
>
Disconnect
</button>
)}
</ModalContent>
</Modal>
);
};
export default ConnectionDetailsModal;

View File

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

View File

@@ -1,130 +0,0 @@
import React, { ReactElement, useState, useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { useNavigate } from 'react-router';
import {
useHistoryOrder,
useRequestHistory,
deleteRequestHistory,
} from '../../reducers/history';
import Icon from '../../components/Icon';
import { get, NOTARY_API_LS_KEY, PROXY_API_LS_KEY } from '../../utils/storage';
import { urlify, download } from '../../utils/misc';
import { BackgroundActiontype } from '../../pages/Background/actionTypes';
export default function History(): ReactElement {
const history = useHistoryOrder();
return (
<div className="flex flex-col flex-nowrap overflow-y-auto">
{history.map((id) => {
return <OneRequestHistory key={id} requestId={id} />;
})}
</div>
);
}
function OneRequestHistory(props: { requestId: string }): ReactElement {
const dispatch = useDispatch();
const request = useRequestHistory(props.requestId);
const navigate = useNavigate();
const { status } = request || {};
const requestUrl = urlify(request?.url || '');
const onRetry = useCallback(async () => {
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: {
id: props.requestId,
notaryUrl,
websocketProxyUrl,
},
});
}, [props.requestId]);
const onView = useCallback(() => {
chrome.runtime.sendMessage<any, string>({
type: BackgroundActiontype.verify_prove_request,
data: request,
});
navigate('/verify/' + request?.id);
}, [request]);
const onDelete = useCallback(async () => {
dispatch(deleteRequestHistory(props.requestId));
}, [props.requestId]);
return (
<div className="flex flex-row flex-nowrap border rounded-md p-2 gap-1 hover:bg-slate-50 cursor-pointer">
<div className="flex flex-col flex-nowrap flex-grow">
<div className="flex flex-row items-center text-xs">
<div className="bg-slate-200 text-slate-400 px-1 py-0.5 rounded-sm">
{request?.method}
</div>
<div className="text-black font-bold px-2 py-1 rounded-md">
{requestUrl?.pathname}
</div>
</div>
<div className="flex flex-row">
<div className="font-bold text-slate-400">Host:</div>
<div className="ml-2 text-slate-800">
{requestUrl?.host}
</div>
</div>
<div className="flex flex-row">
<div className="font-bold text-slate-400">Notary API:</div>
<div className="ml-2 text-slate-800">{request?.notaryUrl}</div>
</div>
<div className="flex flex-row">
<div className="font-bold text-slate-400">TLS Proxy API: </div>
<div className="ml-2 text-slate-800">
{request?.websocketProxyUrl}
</div>
</div>
</div>
<div className="flex flex-col gap-1">
{status === 'success' && (
<>
<div
className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-slate-600 text-slate-200 hover:bg-slate-500 hover:text-slate-100 hover:font-bold"
onClick={onView}
>
<Icon className="" fa="fa-solid fa-receipt" size={1} />
<span className="text-xs font-bold">View Proof</span>
</div>
<div
className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-slate-100 text-slate-300 hover:bg-slate-200 hover:text-slate-500 hover:font-bold"
onClick={() => download(`${request?.id}.json`, JSON.stringify(request?.proof))}
>
<Icon className="" fa="fa-solid fa-download" size={1} />
<span className="text-xs font-bold">Download</span>
</div>
</>
)}
{(!status || status === 'error') && (
<div
className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-slate-100 text-slate-300 hover:bg-slate-200 hover:text-slate-500 hover:font-bold"
onClick={onRetry}
>
<Icon fa="fa-solid fa-arrows-rotate" size={1} />
<span className="text-xs font-bold">Retry</span>
</div>
)}
{status === 'pending' && (
<div className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-slate-100 text-slate-300 font-bold">
<Icon className="animate-spin" fa="fa-solid fa-spinner" size={1} />
<span className="text-xs font-bold">Pending</span>
</div>
)}
<div
className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-slate-100 text-slate-300 hover:bg-red-100 hover:text-red-500 hover:font-bold"
onClick={onDelete}
>
<Icon className="" fa="fa-solid fa-trash" size={1} />
<span className="text-xs font-bold">Delete</span>
</div>
</div>
</div>
);
}

View File

@@ -37,13 +37,19 @@ export default function Modal(props: Props): ReactElement {
}
type HeaderProps = {
className?: string;
onClose?: () => void;
children: ReactNode;
children?: ReactNode;
};
export function ModalHeader(props: HeaderProps): ReactElement {
return (
<div className={classNames('border-b modal__header border-gray-100')}>
<div
className={classNames(
'border-b modal__header border-gray-100',
props.className,
)}
>
<div className="modal__header__title">{props.children}</div>
<div className="modal__header__content">
{props.onClose && (
@@ -84,7 +90,7 @@ export function ModalFooter(props: FooterProps): ReactElement {
return (
<div
className={classNames(
'border-t modal__footer border-gray-100',
'border-t modal__footer border-gray-100 w-full',
props.className,
)}
>

View File

@@ -1,15 +0,0 @@
import React, { ReactElement } from 'react';
import { useParams } from 'react-router';
import { useRequestHistory } from '../../reducers/history';
import RequestBuilder from '../../pages/RequestBuilder';
export default function Notarize(): ReactElement {
const params = useParams<{ requestId: string }>();
const request = useRequestHistory(params.requestId);
return (
<div className="flex flex-col flex-nowrap flex-grow">
{request?.id}
</div>
);
}

View File

@@ -1,69 +0,0 @@
import React, { ReactElement, useState, useEffect, useCallback } from 'react';
import {
set,
get,
NOTARY_API_LS_KEY,
PROXY_API_LS_KEY,
} from '../../utils/storage';
export default function Options(): ReactElement {
const [notary, setNotary] = useState('http://localhost:7047');
const [proxy, setProxy] = useState('ws://127.0.0.1:55688');
const [dirty, setDirty] = useState(false);
useEffect(() => {
(async () => {
setNotary(await get(NOTARY_API_LS_KEY));
setProxy(await get(PROXY_API_LS_KEY));
})();
}, []);
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">
<div className="flex flex-row flex-nowrap py-1 px-2 gap-2 font-bold text-base">
Settings
</div>
<div className="flex flex-col flex-nowrap py-1 px-2 gap-2">
<div className="font-semibold">Notary API</div>
<input
type="text"
className="input border"
placeholder="http://localhost:7047"
onChange={(e) => {
setNotary(e.target.value);
setDirty(true);
}}
value={notary}
/>
</div>
<div className="flex flex-col flex-nowrap py-1 px-2 gap-2">
<div className="font-semibold">Proxy API</div>
<input
type="text"
className="input border"
placeholder="ws://127.0.0.1:55688"
onChange={(e) => {
setProxy(e.target.value);
setDirty(true);
}}
value={proxy}
/>
</div>
<div className="flex flex-row flex-nowrap justify-end gap-2 p-2">
<button
className="button !bg-primary/[0.9] hover:bg-primary/[0.8] active:bg-primary !text-white"
disabled={!dirty}
onClick={onSave}
>
Save
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,20 @@
.custom-modal {
height: 100%;
max-width: 800px;
max-height: 100vh;
display: flex;
margin: 0 auto;
flex-direction: column;
}
.custom-modal-content {
flex-grow: 2;
overflow-y: auto;
max-height: 90%;
}
.modal__overlay {
display: flex;
align-items: center;
justify-content: center;
}

View File

@@ -0,0 +1,208 @@
import React, {
ChangeEvent,
Children,
MouseEventHandler,
ReactElement,
ReactNode,
useCallback,
useState,
} from 'react';
import { makePlugin, getPluginConfig } from '../../utils/misc';
import { addPlugin } from '../../utils/rpc';
import Modal, {
ModalHeader,
ModalContent,
ModalFooter,
} from '../../components/Modal/Modal';
import type { PluginConfig } from '../../utils/misc';
import './index.scss';
import logo from '../../assets/img/icon-128.png';
import {
HostFunctionsDescriptions,
MultipleParts,
PermissionDescription,
} from '../../utils/plugins';
import { ErrorModal } from '../ErrorModal';
import classNames from 'classnames';
import DefaultPluginIcon from '../../assets/img/default-plugin-icon.png';
export default function PluginUploadInfo(): ReactElement {
const [error, showError] = useState('');
const [pluginBuffer, setPluginBuffer] = useState<ArrayBuffer | any>(null);
const [pluginContent, setPluginContent] = useState<PluginConfig | null>(null);
const onAddPlugin = useCallback(
async (evt: React.MouseEvent<HTMLButtonElement>) => {
try {
await addPlugin(Buffer.from(pluginBuffer).toString('hex'));
setPluginContent(null);
} catch (e: any) {
showError(e?.message || 'Invalid Plugin');
}
},
[pluginContent, pluginBuffer],
);
const onPluginInfo = useCallback(
async (evt: ChangeEvent<HTMLInputElement>) => {
if (!evt.target.files) return;
try {
const [file] = evt.target.files;
const arrayBuffer = await file.arrayBuffer();
const plugin = await makePlugin(arrayBuffer);
setPluginContent(await getPluginConfig(plugin));
setPluginBuffer(arrayBuffer);
} catch (e: any) {
showError(e?.message || 'Invalid Plugin');
} finally {
evt.target.value = '';
}
},
[setPluginContent, setPluginBuffer],
);
const onClose = useCallback(() => {
setPluginContent(null);
setPluginBuffer(null);
}, []);
return (
<>
<input
className="opacity-0 absolute top-0 right-0 h-full w-full cursor-pointer"
type="file"
onChange={onPluginInfo}
/>
{error && <ErrorModal onClose={() => showError('')} message={error} />}
{pluginContent && (
<PluginInfoModal
pluginContent={pluginContent}
onClose={onClose}
onAddPlugin={onAddPlugin}
/>
)}
</>
);
}
export function PluginInfoModalHeader(props: {
className?: string;
children: ReactNode | ReactNode[];
}) {
return <div className={props.className}>{props.children}</div>;
}
export function PluginInfoModalContent(props: {
className?: string;
children: ReactNode | ReactNode[];
}) {
return <div className={props.className}>{props.children}</div>;
}
export function PluginInfoModal(props: {
pluginContent: PluginConfig;
onClose: () => void;
onAddPlugin?: MouseEventHandler;
children?: ReactNode | ReactNode[];
}) {
const { pluginContent, onClose, onAddPlugin, children } = props;
const header = Children.toArray(children).filter(
(c: any) => c.type.name === 'PluginInfoModalHeader',
)[0];
const content = Children.toArray(children).filter(
(c: any) => c.type.name === 'PluginInfoModalContent',
)[0];
return (
<Modal
onClose={onClose}
className="custom-modal !rounded-none flex items-center justify-center gap-4 cursor-default"
>
<ModalHeader className="w-full p-2 border-gray-200 text-gray-500">
{header || (
<div className="flex flex-row items-end justify-start gap-2">
<img className="h-5" src={logo || DefaultPluginIcon} alt="logo" />
<span className="font-semibold">{`Installing ${pluginContent.title}`}</span>
</div>
)}
</ModalHeader>
<ModalContent className="flex flex-col flex-grow-0 flex-shrink-0 items-center px-8 py-2 gap-2 w-full max-h-none">
{content || (
<>
<img
className="w-12 h-12"
src={pluginContent.icon || DefaultPluginIcon}
alt="Plugin Icon"
/>
<span className="text-3xl text-center">
<span>
<span className="text-blue-600 font-semibold">
{pluginContent.title}
</span>{' '}
wants access to your browser
</span>
</span>
</>
)}
</ModalContent>
<div className="flex-grow flex-shrink overflow-y-auto w-full px-8">
<PluginPermissions pluginContent={pluginContent} />
</div>
<ModalFooter className="flex justify-end gap-2 p-4">
<button className="button" onClick={onClose}>
Cancel
</button>
{onAddPlugin && (
<button className="button button--primary" onClick={onAddPlugin}>
Allow
</button>
)}
</ModalFooter>
</Modal>
);
}
export function PluginPermissions({
pluginContent,
className,
}: {
pluginContent: PluginConfig;
className?: string;
}) {
return (
<div className={classNames('flex flex-col p-2 gap-5', className)}>
{pluginContent.hostFunctions?.map((hostFunction: string) => {
const HFComponent = HostFunctionsDescriptions[hostFunction];
return <HFComponent key={hostFunction} {...pluginContent} />;
})}
{pluginContent.cookies && (
<PermissionDescription fa="fa-solid fa-cookie-bite">
<span className="cursor-default">
<span className="mr-1">Access cookies from</span>
<MultipleParts parts={pluginContent.cookies} />
</span>
</PermissionDescription>
)}
{pluginContent.headers && (
<PermissionDescription fa="fa-solid fa-envelope">
<span className="cursor-default">
<span className="mr-1">Access headers from</span>
<MultipleParts parts={pluginContent.headers} />
</span>
</PermissionDescription>
)}
{pluginContent.requests && (
<PermissionDescription fa="fa-solid fa-globe">
<span className="cursor-default">
<span className="mr-1">Submit network requests to</span>
<MultipleParts
parts={pluginContent?.requests.map(({ url }) => url)}
/>
</span>
</PermissionDescription>
)}
</div>
);
}

View File

@@ -0,0 +1,45 @@
.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;
}
}
}
}
.custom-modal {
width: 100vw;
height: 100vh;
max-width: 800px;
max-height: 90vh;
display: flex;
margin: 1rem auto;
flex-direction: column;
}
.custom-modal-content {
flex-grow: 2;
overflow-y: auto;
max-height: 90%;
}
.modal__overlay {
display: flex;
align-items: center;
justify-content: center;
}

View File

@@ -0,0 +1,217 @@
import React, {
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';
import {
PluginInfoModal,
PluginInfoModalContent,
PluginInfoModalHeader,
} from '../PluginInfo';
import { getPluginConfigByHash } from '../../entries/Background/db';
export function PluginList(props: { className?: string }): ReactElement {
const hashes = usePluginHashes();
useEffect(() => {
fetchPluginHashes();
}, []);
return (
<div
className={classNames('flex flex-col flex-nowrap gap-1', props.className)}
>
{!hashes.length && (
<div className="flex flex-col items-center justify-center text-slate-400 cursor-default select-none">
<div>No available plugins</div>
</div>
)}
{hashes.map((hash) => (
<Plugin key={hash} hash={hash} />
))}
</div>
);
}
export function Plugin(props: {
hash: string;
onClick?: () => void;
}): ReactElement {
const [error, showError] = useState('');
const [config, setConfig] = useState<PluginConfig | null>(null);
const [pluginInfo, showPluginInfo] = useState(false);
const [remove, showRemove] = useState(false);
const onClick = useCallback(async () => {
if (!config || remove) 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, remove]);
useEffect(() => {
(async function () {
setConfig(await getPluginConfigByHash(props.hash));
})();
}, [props.hash]);
const onRemove: MouseEventHandler = useCallback(
(e) => {
e.stopPropagation();
removePlugin(props.hash);
showRemove(false);
},
[props.hash, remove],
);
const onConfirmRemove: MouseEventHandler = useCallback(
(e) => {
e.stopPropagation();
showRemove(true);
},
[props.hash, remove],
);
const onPluginInfo: MouseEventHandler = useCallback(
(e) => {
e.stopPropagation();
showPluginInfo(true);
},
[props.hash, pluginInfo],
);
if (!config) return <></>;
return (
<div
className={classNames(
'flex flex-row justify-center border rounded border-slate-300 p-2 gap-2 plugin-box',
'cursor-pointer hover:bg-slate-100 hover:border-slate-400 active:bg-slate-200',
)}
onClick={onClick}
>
{!!error && <ErrorModal onClose={() => showError('')} message={error} />}
{!remove ? (
<div className="flex flex-row w-full gap-2">
<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}
<div className="flex flex-row items-center justify-center">
<Icon
fa="fa-solid fa-circle-info"
className="flex flex-row items-center justify-center cursor-pointer plugin-box__remove-icon"
onClick={onPluginInfo}
/>
<Icon
fa="fa-solid fa-xmark"
className="flex flex-row items-center justify-center cursor-pointer text-red-500 bg-red-200 rounded-full plugin-box__remove-icon"
onClick={onConfirmRemove}
/>
</div>
</div>
<div>{config.description}</div>
</div>
</div>
) : (
<RemovePlugin
onRemove={onRemove}
showRemove={showRemove}
config={config}
/>
)}
{pluginInfo && (
<PluginInfoModal
pluginContent={config}
onClose={() => showPluginInfo(false)}
>
<PluginInfoModalHeader>
<div className="flex flex-row items-end justify-start gap-2">
<Icon
className="text-slate-500 hover:text-slate-700 cursor-pointer"
size={1}
fa="fa-solid fa-caret-left"
onClick={() => showPluginInfo(false)}
/>
</div>
</PluginInfoModalHeader>
<PluginInfoModalContent className="flex flex-col items-center cursor-default">
<img
className="w-12 h-12 mb-2"
src={config.icon || DefaultPluginIcon}
alt="Plugin Icon"
/>
<span className="text-3xl text-blue-600 font-semibold">
{config.title}
</span>
<div className="text-slate-500 text-lg">{config.description}</div>
</PluginInfoModalContent>
</PluginInfoModal>
)}
</div>
);
}
function RemovePlugin(props: {
onRemove: MouseEventHandler;
showRemove: (show: boolean) => void;
config: PluginConfig;
}): ReactElement {
const { onRemove, showRemove, config } = props;
const onCancel: MouseEventHandler = useCallback((e) => {
e.stopPropagation();
showRemove(false);
}, []);
return (
<div className="flex flex-col items-center w-full gap-1">
<div className="font-bold text-red-700">
{`Are you sure you want to remove "${config.title}" plugin?`}
</div>
<div className="mb-1">Warning: this cannot be undone.</div>
<div className="flex flex-row w-full gap-1">
<button className="flex-grow button p-1" onClick={onCancel}>
Cancel
</button>
<button
className="flex-grow font-bold bg-red-500 hover:bg-red-600 text-white rounded p-1"
onClick={onRemove}
>
Remove
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,154 @@
import React, { useCallback } from 'react';
import c from 'classnames';
export function InputBody(props: {
body: string;
setBody: (body: string) => void;
}) {
return (
<textarea
className="textarea h-[90%] w-full resize-none"
value={props.body}
onChange={(e) => props.setBody(e.target.value)}
/>
);
}
export function FormBodyTable(props: {
formBody: [string, string, boolean?][];
setFormBody: (formBody: [string, string, boolean?][]) => void;
}) {
const toggleKV = useCallback(
(index: number) => {
const newFormBody = [...props.formBody];
newFormBody[index][2] = !newFormBody[index][2];
props.setFormBody(newFormBody);
},
[props.formBody],
);
const setKV = useCallback(
(index: number, key: string, value: string) => {
const newFormBody = [...props.formBody];
newFormBody[index] = [key, value];
props.setFormBody(newFormBody);
if (index === props.formBody.length - 1 && (key || value)) {
props.setFormBody([...newFormBody, ['', '', true]]);
}
},
[props.formBody],
);
const last = props.formBody.length - 1;
return (
<table className="border border-slate-300 border-collapse table-fixed w-full">
<tbody>
{props.formBody.map(([key, value, silent], i) => (
<tr
key={i}
className={c('border-b border-slate-200', {
'opacity-30': !!silent,
})}
>
<td className="w-8 text-center pt-2">
{last !== i && (
<input
type="checkbox"
onChange={() => toggleKV(i)}
checked={!silent}
/>
)}
</td>
<td className="border border-slate-300 font-bold align-top break-all w-fit">
<input
className="input py-1 px-2 w-full"
type="text"
value={key}
placeholder="Key"
onChange={(e) => {
setKV(i, e.target.value, value);
}}
/>
</td>
<td className="border border-slate-300 break-all align-top">
<input
className="input py-1 px-2 w-full"
type="text"
value={value}
placeholder="Value"
onChange={(e) => {
setKV(i, key, e.target.value);
}}
/>
</td>
</tr>
))}
</tbody>
</table>
);
}
export function formatForRequest(
input: string | [string, string, boolean?][],
type: string,
): string {
try {
let pairs: [string, string][] = [];
if (typeof input === 'string') {
const lines = input.split('\n').filter((line) => line.trim() !== '');
pairs = lines.map((line) => {
const [key, value] = line.split('=').map((part) => part.trim());
return [key, value];
});
} else {
pairs = input
.filter(([, , silent]) => silent !== true)
.map(([key, value]) => [key, value]);
}
if (type === 'text/plain') {
return JSON.stringify(input as string);
}
if (type === 'application/json') {
const jsonObject = JSON.parse(input as string);
return JSON.stringify(jsonObject);
}
if (type === 'application/x-www-form-urlencoded') {
const searchParams = new URLSearchParams();
pairs.forEach(([key, value]) => {
searchParams.append(key, value);
});
return searchParams.toString();
}
return pairs.map(([key, value]) => `${key}=${value}`).join('&');
} catch (e) {
console.error('Error formatting for request:', e);
return '';
}
}
export async function parseResponse(contentType: string, res: Response) {
const parsedResponseData = {
json: '',
text: '',
img: '',
headers: Array.from(res.headers.entries()),
};
if (contentType?.includes('application/json')) {
parsedResponseData.json = await res.json();
} else if (contentType?.includes('text')) {
parsedResponseData.text = await res.text();
} else if (contentType?.includes('image')) {
const blob = await res.blob();
parsedResponseData.img = URL.createObjectURL(blob);
} else {
parsedResponseData.text = await res.text();
}
return parsedResponseData;
}

View File

@@ -5,18 +5,10 @@ import React, {
useEffect,
useState,
} from 'react';
import {
BackgroundActiontype,
RequestLog,
} from '../../pages/Background/actionTypes';
import {
notarizeRequest,
useRequest,
} from '../../reducers/requests';
import { notarizeRequest, useRequest } from '../../reducers/requests';
import classNames from 'classnames';
import { useDispatch } from 'react-redux';
import {
Navigate,
Route,
Routes,
useLocation,
@@ -25,61 +17,36 @@ import {
} from 'react-router';
import Icon from '../Icon';
import NavigateWithParams from '../NavigateWithParams';
import { get, NOTARY_API_LS_KEY, PROXY_API_LS_KEY } from '../../utils/storage';
import {
set,
get,
MAX_SENT_LS_KEY,
MAX_RECEIVED_LS_KEY,
getMaxRecv,
getMaxSent,
} from '../../utils/storage';
import { MAX_RECV, MAX_SENT } from '../../utils/constants';
import { urlify } from '../../utils/misc';
type Props = {
requestId: string;
};
const maxTranscriptSize = 16384;
const authToken = 'a28cae3969369c26c1410f5bded83c3f4f914fbc';
const accessToken =
'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA';
const csrfToken =
'b73b3488687683372af2ea77486a444ccaa5327bbabad709df1b5161a6b83c8d7ec19106a82cb8dd5f8569632ee95ab4c6dc2abf5ad2ed7fa11b8340fcbe86a8fc00df28db6c4109a807f7cb12dd19da';
const userAgent =
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36';
export default function RequestDetail(props: Props): ReactElement {
const request = useRequest(props.requestId);
const navigate = useNavigate();
const dispatch = useDispatch();
const notarize = useCallback(async () => {
const notaryUrl = await get(NOTARY_API_LS_KEY);
const websocketProxyUrl = await get(PROXY_API_LS_KEY);
const headers = request
.requestHeaders.reduce((acc, h) => {
if (!(/^(origin|referer|Accept-Language|Accept-EncodingAccept)$|^(sec-|x-twitter-)/i.test(h.name))) {
acc[h.name] = h.value;
}
return acc;
}, { Host: urlify(request.url)?.hostname });
if (!request) return;
//TODO: for some reason, these needs to be override for twitter to work
headers['Accept-Encoding'] = 'identity';
headers['Connection'] = 'close';
dispatch(notarizeRequest({
url: request.url,
method: request.method,
headers,
body: request.body,
maxTranscriptSize,
notaryUrl,
websocketProxyUrl,
}))
navigate(`/history`);
}, [request]);
navigate('/notary/' + request.requestId);
}, [request, props.requestId]);
if (!request) return <></>;
return (
<>
<div className="flex flex-row flex-nowrap relative items-center bg-slate-300 py-1 px-2 gap-2">
<div className="flex flex-row flex-nowrap relative items-center bg-slate-300 py-2 px-2 gap-2">
<Icon
className="cursor-point text-slate-400 hover:text-slate-700"
fa="fa-solid fa-xmark"
@@ -94,6 +61,9 @@ export default function RequestDetail(props: Props): ReactElement {
<RequestDetailsHeaderTab path="/response">
Response
</RequestDetailsHeaderTab>
<RequestDetailsHeaderTab path="/advanced">
Advanced
</RequestDetailsHeaderTab>
<button
className="absolute right-2 bg-primary/[0.9] text-white font-bold px-2 py-0.5 hover:bg-primary/[0.8] active:bg-primary"
onClick={notarize}
@@ -102,9 +72,19 @@ export default function RequestDetail(props: Props): ReactElement {
</button>
</div>
<Routes>
<Route path="headers" element={<RequestHeaders requestId={props.requestId} />} />
<Route path="payloads" element={<RequestPayload requestId={props.requestId} />} />
<Route path="response" element={<WebResponse requestId={props.requestId} />} />
<Route
path="headers"
element={<RequestHeaders requestId={props.requestId} />}
/>
<Route
path="payloads"
element={<RequestPayload requestId={props.requestId} />}
/>
<Route
path="response"
element={<WebResponse requestId={props.requestId} />}
/>
<Route path="advanced" element={<AdvancedOptions />} />
<Route path="/" element={<NavigateWithParams to="/headers" />} />
</Routes>
</>
@@ -132,6 +112,62 @@ function RequestDetailsHeaderTab(props: {
);
}
function AdvancedOptions(): ReactElement {
const [maxSent, setMaxSent] = useState(MAX_SENT);
const [maxRecv, setMaxRecv] = useState(MAX_RECV);
const [dirty, setDirty] = useState(false);
useEffect(() => {
(async () => {
setMaxRecv((await getMaxRecv()) || MAX_RECV);
setMaxSent((await getMaxSent()) || MAX_SENT);
})();
}, []);
const onSave = useCallback(async () => {
await set(MAX_RECEIVED_LS_KEY, maxRecv.toString());
await set(MAX_SENT_LS_KEY, maxSent.toString());
setDirty(false);
}, [maxSent, maxRecv]);
return (
<div className="flex flex-col flex-nowrap py-1 px-2 gap-2">
<div className="font-semibold">Max Sent Data</div>
<input
type="number"
className="input border"
value={maxSent}
min={0}
onChange={(e) => {
setMaxSent(parseInt(e.target.value));
setDirty(true);
}}
/>
<div className="font-semibold">Max Received Data</div>
<input
type="number"
className="input border"
value={maxRecv}
min={0}
onChange={(e) => {
setMaxRecv(parseInt(e.target.value));
setDirty(true);
}}
/>
<div className="flex flex-row flex-nowrap justify-end gap-2 p-2">
<button
className="button !bg-primary/[0.9] hover:bg-primary/[0.8] active:bg-primary !text-white"
disabled={!dirty}
onClick={onSave}
>
Save
</button>
</div>
</div>
);
}
function RequestPayload(props: Props): ReactElement {
const data = useRequest(props.requestId);
const [url, setUrl] = useState<URL | null>();
@@ -280,6 +316,7 @@ function WebResponse(props: Props): ReactElement {
const options = {
method: data.method,
headers: data.requestHeaders.reduce(
// @ts-ignore
(acc: { [key: string]: string }, h: chrome.webRequest.HttpHeader) => {
if (typeof h.name !== 'undefined' && typeof h.value !== 'undefined') {
acc[h.name] = h.value;
@@ -295,6 +332,7 @@ function WebResponse(props: Props): ReactElement {
options.body = formData.toString();
}
// @ts-ignore
const resp = await fetch(data.url, options);
setResponse(resp);

View File

@@ -1,8 +1,5 @@
import React, { ReactElement, useCallback, useState } from 'react';
import {
BackgroundActiontype,
RequestLog,
} from '../../pages/Background/actionTypes';
import { BackgroundActiontype, RequestLog } from '../../entries/Background/rpc';
import { useNavigate } from 'react-router';
import Fuse from 'fuse.js';
import Icon from '../Icon';

View File

@@ -1,63 +1,15 @@
import classNames from 'classnames';
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import React, { ReactElement } from 'react';
export default function ResponseDetail(props: {
response: Response | null;
responseData: {
json: any | null;
text: string | null;
img: string | null;
headers: [string, string][] | null;
} | null;
className?: string;
}): ReactElement {
const [json, setJSON] = useState<any | null>(null);
const [text, setText] = useState<string | null>(null);
const [img, setImg] = useState<string | null>(null);
const [formData, setFormData] = useState<URLSearchParams | null>(null);
useEffect(() => {
const resp = props.response;
if (!resp) return;
const contentType =
resp.headers.get('content-type') || resp.headers.get('Content-Type');
if (contentType?.includes('application/json')) {
resp
.json()
.then((json) => {
if (json) {
setJSON(json);
}
})
.catch();
} else if (contentType?.includes('text')) {
resp
.text()
.then((_text) => {
if (_text) {
setText(_text);
}
})
.catch();
} else if (contentType?.includes('image')) {
resp
.blob()
.then((blob) => {
if (blob) {
setImg(URL.createObjectURL(blob));
}
})
.catch();
} else {
resp
.blob()
.then((blob) => blob.text())
.then((_text) => {
if (_text) {
setText(_text);
}
})
.catch();
}
}, [props.response]);
return (
<div
className={classNames(
@@ -66,7 +18,7 @@ export default function ResponseDetail(props: {
)}
>
<table className="border border-slate-300 border-collapse table-fixed w-full">
{!!json && (
{!!props.responseData?.json && (
<>
<thead className="bg-slate-200">
<tr>
@@ -80,13 +32,13 @@ export default function ResponseDetail(props: {
<textarea
rows={16}
className="w-full bg-slate-100 text-slate-600 p-2 text-xs break-all h-full outline-none font-mono"
value={JSON.stringify(json, null, 2)}
value={JSON.stringify(props.responseData.json, null, 2)}
></textarea>
</td>
</tr>
</>
)}
{!!text && (
{!!props.responseData?.text && (
<>
<thead className="bg-slate-200">
<tr>
@@ -100,13 +52,13 @@ export default function ResponseDetail(props: {
<textarea
rows={16}
className="w-full bg-slate-100 text-slate-600 p-2 text-xs break-all h-full outline-none font-mono"
value={text}
value={props.responseData.text}
></textarea>
</td>
</tr>
</>
)}
{!!img && (
{!!props.responseData?.img && (
<>
<thead className="bg-slate-200">
<tr>
@@ -117,12 +69,12 @@ export default function ResponseDetail(props: {
</thead>
<tr>
<td className="bg-slate-100" colSpan={2}>
<img src={img} />
<img src={props.responseData.img} />
</td>
</tr>
</>
)}
{!!props.response?.headers && (
{!!props.responseData?.headers && (
<>
<thead className="bg-slate-200">
<tr>
@@ -132,20 +84,18 @@ export default function ResponseDetail(props: {
</tr>
</thead>
<tbody>
{Array.from(props.response.headers.entries()).map(
([name, value]) => {
return (
<tr className="border-b border-slate-200">
<td className="border border-slate-300 font-bold align-top py-1 px-2 whitespace-nowrap">
{name}
</td>
<td className="border border-slate-300 break-all align-top py-1 px-2">
{value}
</td>
</tr>
);
},
)}
{props.responseData?.headers.map(([name, value]) => {
return (
<tr className="border-b border-slate-200">
<td className="border border-slate-300 font-bold align-top py-1 px-2 whitespace-nowrap">
{name}
</td>
<td className="border border-slate-300 break-all align-top py-1 px-2">
{value}
</td>
</tr>
);
})}
</tbody>
</>
)}

View File

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

@@ -0,0 +1,396 @@
import { Level } from 'level';
import type { RequestHistory } from './rpc';
import { PluginConfig, PluginMetadata, sha256 } from '../../utils/misc';
import mutex from './mutex';
const charwise = require('charwise');
export const db = new Level('./ext-db', {
valueEncoding: 'json',
});
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',
});
const pluginMetadataDb = db.sublevel<string, PluginMetadata>('pluginMetadata', {
valueEncoding: 'json',
});
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 appDb = db.sublevel<string, any>('app', {
valueEncoding: 'json',
});
enum AppDatabaseKey {
DefaultPluginsInstalled = 'DefaultPluginsInstalled',
}
export async function addNotaryRequest(
now = Date.now(),
request: Omit<RequestHistory, 'status' | 'id'>,
): Promise<RequestHistory> {
const id = charwise.encode(now).toString('hex');
const newReq: RequestHistory = {
...request,
id,
status: '',
};
await historyDb.put(id, newReq);
return newReq;
}
export async function addNotaryRequestProofs(
id: string,
proof: { session: any; substrings: any },
): Promise<RequestHistory | null> {
const existing = await historyDb.get(id);
if (!existing) return null;
const newReq: RequestHistory = {
...existing,
proof,
status: 'success',
};
await historyDb.put(id, newReq);
return newReq;
}
export async function setNotaryRequestStatus(
id: string,
status: '' | 'pending' | 'success' | 'error',
): Promise<RequestHistory | null> {
const existing = await historyDb.get(id);
if (!existing) return null;
const newReq = {
...existing,
status,
};
await historyDb.put(id, newReq);
return newReq;
}
export async function setNotaryRequestError(
id: string,
error: any,
): Promise<RequestHistory | null> {
const existing = await historyDb.get(id);
if (!existing) return null;
const newReq: RequestHistory = {
...existing,
error,
status: 'error',
};
await historyDb.put(id, newReq);
return newReq;
}
export async function setNotaryRequestVerification(
id: string,
verification: { sent: string; recv: string },
): Promise<RequestHistory | null> {
const existing = await historyDb.get(id);
if (!existing) return null;
const newReq = {
...existing,
verification,
};
await historyDb.put(id, newReq);
return newReq;
}
export async function removeNotaryRequest(
id: string,
): Promise<RequestHistory | null> {
const existing = await historyDb.get(id);
if (!existing) return null;
await historyDb.del(id);
return existing;
}
export async function getNotaryRequests(): Promise<RequestHistory[]> {
const retVal = [];
for await (const [key, value] of historyDb.iterator()) {
retVal.push(value);
}
return retVal;
}
export async function getNotaryRequest(
id: string,
): Promise<RequestHistory | null> {
return historyDb.get(id).catch(() => null);
}
export async function getPluginHashes(): Promise<string[]> {
const retVal: string[] = [];
for await (const [key] of pluginDb.iterator()) {
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 getPlugins(): Promise<
(PluginConfig & { hash: string; metadata: PluginMetadata })[]
> {
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);
if (config) {
ret.push({
...config,
hash,
metadata: metadata || {
filePath: '',
origin: '',
},
});
}
}
return ret;
}
export async function getPluginMetadataByHash(
hash: string,
): Promise<PluginMetadata | null> {
try {
const metadata = await pluginMetadataDb.get(hash);
return metadata;
} catch (e) {
return null;
}
}
export async function addPluginMetadata(
hash: string,
metadata: PluginMetadata,
): Promise<PluginMetadata | null> {
await pluginMetadataDb.put(hash, metadata);
return metadata;
}
export async function removePluginMetadata(
hash: string,
): Promise<PluginMetadata | null> {
const existing = await pluginMetadataDb.get(hash);
if (!existing) return null;
await pluginMetadataDb.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;
}
export async function setConnection(origin: string) {
if (await getConnection(origin)) return null;
await connectionDb.put(origin, true);
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(host: string, name: string) {
try {
const existing = await cookiesDb.sublevel(host).get(name);
return existing;
} catch (e) {
return null;
}
}
export async function getCookiesByHost(host: string) {
const ret: { [key: string]: string } = {};
for await (const [key, value] of cookiesDb.sublevel(host).iterator()) {
ret[key] = value;
}
return ret;
}
export async function deleteConnection(origin: string) {
return mutex.runExclusive(async () => {
if (await getConnection(origin)) {
await connectionDb.del(origin);
}
});
}
export async function getConnection(origin: string) {
try {
const existing = await connectionDb.get(origin);
return existing;
} catch (e) {
return null;
}
}
export async function setHeaders(host: string, name: string, value?: string) {
if (!value) return null;
return mutex.runExclusive(async () => {
await headersDb.sublevel(host).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(host: string) {
const ret: { [key: string]: string } = {};
for await (const [key, value] of headersDb.sublevel(host).iterator()) {
ret[key] = value;
}
return ret;
}
async function getDefaultPluginsInstalled(): Promise<boolean> {
return appDb.get(AppDatabaseKey.DefaultPluginsInstalled).catch(() => false);
}
export async function setDefaultPluginsInstalled(installed = false) {
return mutex.runExclusive(async () => {
await appDb.put(AppDatabaseKey.DefaultPluginsInstalled, installed);
});
}
export async function getAppState() {
return {
defaultPluginsInstalled: await getDefaultPluginsInstalled(),
};
}

View File

@@ -0,0 +1,117 @@
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';
import { setCookies, setHeaders } from './db';
export const onSendHeaders = (
details: browser.WebRequest.OnSendHeadersDetailsType,
) => {
return mutex.runExclusive(async () => {
const { method, tabId, requestId } = details;
if (method !== 'OPTIONS') {
const cache = getCacheByTabId(tabId);
const existing = cache.get<RequestLog>(requestId);
const { hostname } = urlify(details.url) || {};
if (hostname && details.requestHeaders) {
details.requestHeaders.forEach((header) => {
const { name, value } = header;
if (/^cookie$/i.test(name) && value) {
value
.split(';')
.map((v) => v.split('='))
.forEach((cookie) => {
setCookies(hostname, cookie[0].trim(), cookie[1]);
});
} else {
setHeaders(hostname, name, value);
}
});
}
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,
});
}
});
};
export const onBeforeRequest = (
details: browser.WebRequest.OnBeforeRequestDetailsType,
) => {
mutex.runExclusive(async () => {
const { method, requestBody, tabId, requestId } = details;
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,
requestBody: Buffer.from(requestBody.raw[0].bytes).toString(
'utf-8',
),
});
} catch (e) {
console.error(e);
}
} else if (requestBody.formData) {
cache.set(requestId, {
...existing,
formData: requestBody.formData,
});
}
}
});
};
export const onResponseStarted = (
details: browser.WebRequest.OnResponseStartedDetailsType,
) => {
mutex.runExclusive(async () => {
const { method, responseHeaders, tabId, requestId } = details;
if (method === 'OPTIONS') return;
const cache = getCacheByTabId(tabId);
const existing = cache.get<RequestLog>(requestId);
const newLog: RequestLog = {
requestHeaders: [],
...existing,
method: details.method,
type: details.type,
url: details.url,
initiator: details.initiator || null,
tabId: tabId,
requestId: requestId,
responseHeaders,
};
cache.set(requestId, newLog);
chrome.runtime.sendMessage({
type: BackgroundActiontype.push_action,
data: {
tabId: details.tabId,
request: newLog,
},
action: addRequest(newLog),
});
});
};

View File

@@ -0,0 +1,78 @@
import { onBeforeRequest, onResponseStarted, onSendHeaders } from './handlers';
import { deleteCacheByTabId } from './cache';
import browser from 'webextension-polyfill';
import { getAppState, setDefaultPluginsInstalled } from './db';
import { installPlugin } from './plugins/utils';
(async () => {
browser.webRequest.onSendHeaders.addListener(
onSendHeaders,
{
urls: ['<all_urls>'],
},
['requestHeaders', 'extraHeaders'],
);
browser.webRequest.onBeforeRequest.addListener(
onBeforeRequest,
{
urls: ['<all_urls>'],
},
['requestBody'],
);
browser.webRequest.onResponseStarted.addListener(
onResponseStarted,
{
urls: ['<all_urls>'],
},
['responseHeaders', 'extraHeaders'],
);
browser.tabs.onRemoved.addListener((tabId) => {
deleteCacheByTabId(tabId);
});
const { defaultPluginsInstalled } = await getAppState();
if (!defaultPluginsInstalled) {
try {
const twitterProfileUrl = browser.runtime.getURL('twitter_profile.wasm');
const discordDmUrl = browser.runtime.getURL('discord_dm.wasm');
await installPlugin(twitterProfileUrl);
await installPlugin(discordDmUrl);
} finally {
await setDefaultPluginsInstalled(true);
}
}
const { initRPC } = await import('./rpc');
await createOffscreenDocument();
initRPC();
})();
let creatingOffscreen: any;
async function createOffscreenDocument() {
const offscreenUrl = browser.runtime.getURL('offscreen.html');
// @ts-ignore
const existingContexts = await browser.runtime.getContexts({
contextTypes: ['OFFSCREEN_DOCUMENT'],
documentUrls: [offscreenUrl],
});
if (existingContexts.length > 0) {
return;
}
if (creatingOffscreen) {
await creatingOffscreen;
} else {
creatingOffscreen = (chrome as any).offscreen.createDocument({
url: 'offscreen.html',
reasons: ['WORKERS'],
justification: 'workers for multithreading',
});
await creatingOffscreen;
creatingOffscreen = null;
}
}

View File

@@ -0,0 +1,5 @@
import { Mutex } from 'async-mutex';
const mutex = new Mutex();
export default mutex;

View File

@@ -0,0 +1,29 @@
import { addPlugin, addPluginConfig, addPluginMetadata } from '../db';
import { getPluginConfig } from '../../../utils/misc';
export async function installPlugin(
urlOrBuffer: ArrayBuffer | 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 config = await getPluginConfig(arrayBuffer);
const hex = Buffer.from(arrayBuffer).toString('hex');
const hash = await addPlugin(hex);
await addPluginConfig(hash!, config);
await addPluginMetadata(hash!, {
...metadata,
origin,
filePath,
});
return hash;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,120 @@
import { ContentScriptTypes, RPCClient } from './rpc';
import { RequestHistory } from '../Background/rpc';
import { PluginConfig, PluginMetadata } from '../../utils/misc';
import { Proof } from '../../utils/types';
const client = new RPCClient();
class TLSN {
async getHistory(
method: string,
url: string,
metadata?: {
[key: string]: string;
},
): Promise<
(Pick<
RequestHistory,
'id' | 'method' | 'notaryUrl' | 'url' | 'websocketProxyUrl'
> & { time: Date })[]
> {
const resp = await client.call(ContentScriptTypes.get_history, {
method,
url,
metadata,
});
return resp || [];
}
async getProof(id: string): Promise<Proof | null> {
const resp = await client.call(ContentScriptTypes.get_proof, {
id,
});
return resp || null;
}
async notarize(
url: string,
requestOptions?: {
method?: string;
headers?: { [key: string]: string };
body?: string;
},
proofOptions?: {
notaryUrl?: string;
websocketProxyUrl?: string;
maxSentData?: number;
maxRecvData?: number;
maxTranscriptSize?: number;
metadata?: {
[k: string]: string;
};
},
): Promise<Proof> {
const resp = await client.call(ContentScriptTypes.notarize, {
url,
method: requestOptions?.method,
headers: requestOptions?.headers,
body: requestOptions?.body,
maxSentData: proofOptions?.maxSentData,
maxRecvData: proofOptions?.maxRecvData,
maxTranscriptSize: proofOptions?.maxTranscriptSize,
notaryUrl: proofOptions?.notaryUrl,
websocketProxyUrl: proofOptions?.websocketProxyUrl,
metadata: proofOptions?.metadata,
});
return resp;
}
async installPlugin(
url: string,
metadata?: { [k: string]: string },
): Promise<string> {
const resp = await client.call(ContentScriptTypes.install_plugin, {
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) {
const resp = await client.call(ContentScriptTypes.run_plugin, {
hash,
});
return resp;
}
}
const connect = async () => {
const resp = await client.call(ContentScriptTypes.connect);
if (resp) {
return new TLSN();
}
};
// @ts-ignore
window.tlsn = {
connect,
};

View File

@@ -0,0 +1,218 @@
import browser from 'webextension-polyfill';
import { ContentScriptRequest, ContentScriptTypes, RPCServer } from './rpc';
import { BackgroundActiontype, RequestHistory } from '../Background/rpc';
import { urlify } from '../../utils/misc';
(async () => {
loadScript('content.bundle.js');
const server = new RPCServer();
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 (
request: ContentScriptRequest<{
url: string;
method?: string;
headers?: { [key: string]: string };
metadata?: { [key: string]: string };
body?: string;
notaryUrl?: string;
websocketProxyUrl?: string;
maxSentData?: number;
maxRecvData?: number;
maxTranscriptSize?: number;
}>,
) => {
const {
url,
method,
headers,
body,
maxSentData,
maxRecvData,
maxTranscriptSize,
notaryUrl,
websocketProxyUrl,
metadata,
} = request.params || {};
if (!url || !urlify(url)) throw new Error('invalid url.');
const proof = await browser.runtime.sendMessage({
type: BackgroundActiontype.notarize_request,
data: {
...getPopupData(),
url,
method,
headers,
body,
maxSentData,
maxRecvData,
maxTranscriptSize,
notaryUrl,
websocketProxyUrl,
metadata,
},
});
return proof;
},
);
server.on(
ContentScriptTypes.install_plugin,
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 }>) => {
const { hash } = request.params || {};
if (!hash) throw new Error('params must include hash');
const response = await browser.runtime.sendMessage({
type: BackgroundActiontype.run_plugin_request,
data: {
...getPopupData(),
hash,
},
});
return response;
},
);
})();
function loadScript(filename: string) {
const url = browser.runtime.getURL(filename);
const script = document.createElement('script');
script.setAttribute('type', 'text/javascript');
script.setAttribute('src', url);
document.body.appendChild(script);
}
function getPopupData() {
return {
origin: window.origin,
position: {
left: window.screen.width / 2 - 240,
top: window.screen.height / 2 - 300,
},
};
}

118
src/entries/Content/rpc.ts Normal file
View File

@@ -0,0 +1,118 @@
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',
}
export type ContentScriptRequest<params> = {
tlsnrpc: string;
} & RPCRequest<ContentScriptTypes, params>;
export type ContentScriptResponse = {
tlsnrpc: string;
} & RPCResponse;
export type RPCRequest<method, params> = {
id: number;
method: method;
params?: params;
};
export type RPCResponse = {
id: number;
result?: never;
error?: never;
};
export class RPCServer {
#handlers: Map<
ContentScriptTypes,
(message: ContentScriptRequest<any>) => Promise<any>
> = new Map();
constructor() {
window.addEventListener(
'message',
async (event: MessageEvent<ContentScriptRequest<never>>) => {
const data = event.data;
if (data.tlsnrpc !== '1.0') return;
if (!data.method) return;
const handler = this.#handlers.get(data.method);
if (handler) {
try {
const result = await handler(data);
window.postMessage({
tlsnrpc: '1.0',
id: data.id,
result,
});
} catch (error) {
window.postMessage({
tlsnrpc: '1.0',
id: data.id,
error,
});
}
} else {
throw new Error(`unknown method - ${data.method}`);
}
},
);
}
on(
method: ContentScriptTypes,
handler: (message: ContentScriptRequest<any>) => Promise<any>,
) {
this.#handlers.set(method, handler);
}
}
export class RPCClient {
#requests: Map<number, PromiseResolvers> = new Map();
#id = 0;
get id() {
return this.#id++;
}
constructor() {
window.addEventListener(
'message',
(event: MessageEvent<ContentScriptResponse>) => {
const data = event.data;
if (data.tlsnrpc !== '1.0') return;
const promise = this.#requests.get(data.id);
if (promise) {
if (typeof data.result !== 'undefined') {
promise.resolve(data.result);
this.#requests.delete(data.id);
} else if (typeof data.error !== 'undefined') {
promise.reject(data.error);
this.#requests.delete(data.id);
}
}
},
);
}
async call(method: ContentScriptTypes, params?: any): Promise<never> {
const request = { tlsnrpc: '1.0', id: this.id, method, params };
const defer = deferredPromise();
this.#requests.set(request.id, defer);
window.postMessage(request, '*');
return defer.promise;
}
}

View File

@@ -0,0 +1,300 @@
import React, { useEffect } from 'react';
import * as Comlink from 'comlink';
import { OffscreenActionTypes } from './types';
import {
NotaryServer,
Prover as _Prover,
NotarizedSession as _NotarizedSession,
TlsProof as _TlsProof,
} from 'tlsn-js';
import { verify } from 'tlsn-js-v5';
import { urlify } from '../../utils/misc';
import { BackgroundActiontype } from '../Background/rpc';
import browser from 'webextension-polyfill';
import { Proof, ProofV1 } from '../../utils/types';
import { Method } from 'tlsn-js/wasm/pkg';
const { init, Prover, NotarizedSession, TlsProof }: any = Comlink.wrap(
new Worker(new URL('./worker.ts', import.meta.url)),
);
const Offscreen = () => {
useEffect(() => {
(async () => {
const loggingLevel = await browser.runtime.sendMessage({
type: BackgroundActiontype.get_logging_level,
});
await init({ loggingLevel });
// @ts-ignore
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
switch (request.type) {
case OffscreenActionTypes.notarization_request: {
const { id } = request.data;
(async () => {
try {
const proof = await createProof(request.data);
browser.runtime.sendMessage({
type: BackgroundActiontype.finish_prove_request,
data: {
id,
proof,
},
});
browser.runtime.sendMessage({
type: OffscreenActionTypes.notarization_response,
data: {
id,
proof,
},
});
} catch (error) {
console.error(error);
browser.runtime.sendMessage({
type: BackgroundActiontype.finish_prove_request,
data: {
id,
error,
},
});
browser.runtime.sendMessage({
type: OffscreenActionTypes.notarization_response,
data: {
id,
error,
},
});
}
})();
break;
}
case BackgroundActiontype.process_prove_request: {
const { id } = request.data;
(async () => {
try {
const proof = await createProof(request.data);
browser.runtime.sendMessage({
type: BackgroundActiontype.finish_prove_request,
data: {
id,
proof: proof,
},
});
} catch (error) {
console.error(error);
browser.runtime.sendMessage({
type: BackgroundActiontype.finish_prove_request,
data: {
id,
error,
},
});
}
})();
break;
}
case BackgroundActiontype.verify_proof: {
(async () => {
const result = await verifyProof(request.data);
sendResponse(result);
})();
return true;
}
case BackgroundActiontype.verify_prove_request: {
(async () => {
const proof: Proof = request.data.proof;
const result: { sent: string; recv: string } =
await verifyProof(proof);
chrome.runtime.sendMessage<any, string>({
type: BackgroundActiontype.finish_prove_request,
data: {
id: request.data.id,
verification: {
sent: result.sent,
recv: result.recv,
},
},
});
})();
break;
}
default:
break;
}
});
})();
}, []);
return <div className="App" />;
};
export default Offscreen;
function subtractRanges(
ranges: { start: number; end: number },
negatives: { start: number; end: number }[],
): { start: number; end: number }[] {
const returnVal: { start: number; end: number }[] = [ranges];
negatives
.sort((a, b) => (a.start < b.start ? -1 : 1))
.forEach(({ start, end }) => {
const last = returnVal.pop()!;
if (start < last.start || end > last.end) {
console.error('invalid ranges');
return;
}
if (start === last.start && end === last.end) {
return;
}
if (start === last.start && end < last.end) {
returnVal.push({ start: end, end: last.end });
return;
}
if (start > last.start && end < last.end) {
returnVal.push({ start: last.start, end: start });
returnVal.push({ start: end, end: last.end });
return;
}
if (start > last.start && end === last.end) {
returnVal.push({ start: last.start, end: start });
return;
}
});
return returnVal;
}
async function createProof(options: {
url: string;
notaryUrl: string;
websocketProxyUrl: string;
method?: Method;
headers?: {
[name: string]: string;
};
body?: any;
maxSentData?: number;
maxRecvData?: number;
id: string;
secretHeaders: string[];
secretResps: string[];
}): Promise<ProofV1> {
const {
url,
method = 'GET',
headers = {},
body,
maxSentData,
maxRecvData,
notaryUrl,
websocketProxyUrl,
id,
secretHeaders = [],
secretResps = [],
} = options;
const hostname = urlify(url)?.hostname || '';
const notary = NotaryServer.from(notaryUrl);
const prover: _Prover = await new Prover({
id,
serverDns: hostname,
maxSentData,
maxRecvData,
});
await prover.setup(await notary.sessionUrl(maxSentData, maxRecvData));
await prover.sendRequest(websocketProxyUrl + `?token=${hostname}`, {
url,
method,
headers,
body,
});
const transcript = await prover.transcript();
const commit = {
sent: subtractRanges(
transcript.ranges.sent.all,
secretHeaders
.map((secret: string) => {
const index = transcript.sent.indexOf(secret);
return index > -1
? {
start: index,
end: index + secret.length,
}
: null;
})
.filter((data: any) => !!data) as { start: number; end: number }[],
),
recv: subtractRanges(
transcript.ranges.recv.all,
secretResps
.map((secret: string) => {
const index = transcript.recv.indexOf(secret);
return index > -1
? {
start: index,
end: index + secret.length,
}
: null;
})
.filter((data: any) => !!data) as { start: number; end: number }[],
),
};
const session: _NotarizedSession = await new NotarizedSession(
await prover.notarize(commit),
);
const proofHex = await session.proof(commit);
const proof: ProofV1 = {
version: '1.0',
meta: {
notaryUrl,
websocketProxyUrl,
},
data: proofHex,
};
return proof;
}
async function verifyProof(
proof: Proof,
): Promise<{ sent: string; recv: string }> {
let result: { sent: string; recv: string };
switch (proof.version) {
case undefined: {
result = await verify(proof);
break;
}
case '1.0': {
const tlsProof: _TlsProof = await new TlsProof(proof.data);
result = await tlsProof.verify({
typ: 'P256',
key: await NotaryServer.from(proof.meta.notaryUrl).publicKey(),
});
break;
}
}
return result;
}

View File

@@ -0,0 +1,4 @@
export enum OffscreenActionTypes {
notarization_request = 'offscreen/notarization_request',
notarization_response = 'offscreen/notarization_response',
}

View File

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

View File

@@ -0,0 +1,9 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import Options from '../../pages/Options';
import './index.scss';
const container = document.getElementById('app-container');
const root = createRoot(container!); // createRoot(container!) if you use TypeScript
root.render(<Options />);

181
src/entries/Popup/Popup.tsx Normal file
View File

@@ -0,0 +1,181 @@
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 { BackgroundActiontype } from '../Background/rpc';
import Requests from '../../pages/Requests';
import Options from '../../pages/Options';
import Request from '../../pages/Requests/Request';
import Home from '../../pages/Home';
import Chat from '../../pages/Chat';
import logo from '../../assets/img/icon-128.png';
import RequestBuilder from '../../pages/RequestBuilder';
import Notarize from '../../pages/Notarize';
import ProofViewer from '../../pages/ProofViewer';
import History from '../../pages/History';
import ProofUploader from '../../pages/ProofUploader';
import browser from 'webextension-polyfill';
import store from '../../utils/store';
import 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';
const Popup = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
useEffect(() => {
(async () => {
const [tab] = await browser.tabs.query({
active: true,
lastFocusedWindow: true,
});
dispatch(setActiveTab(tab || null));
const logs = await browser.runtime.sendMessage({
type: BackgroundActiontype.get_requests,
data: tab?.id,
});
dispatch(setRequests(logs));
await browser.runtime.sendMessage({
type: BackgroundActiontype.get_prove_requests,
data: tab?.id,
});
})();
}, []);
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">
<img
className="absolute left-2 h-5 cursor-pointer"
src={logo}
alt="logo"
onClick={() => navigate('/')}
/>
<AppConnectionLogo />
</div>
<Routes>
<Route path="/requests/:requestId/*" element={<Request />} />
<Route path="/notary/:requestId" element={<Notarize />} />
<Route path="/verify/:requestId/*" element={<ProofViewer />} />
<Route path="/verify" element={<ProofUploader />} />
<Route path="/history" element={<History />} />
<Route path="/requests" element={<Requests />} />
<Route path="/custom/*" element={<RequestBuilder />} />
<Route path="/options" element={<Options />} />
<Route path="/home" element={<Home />} />
<Route path="/chat" element={<Chat />} />
<Route path="/plugininfo" element={<PluginUploadInfo />} />
<Route path="/connection-approval" element={<ConnectionApproval />} />
<Route path="/get-history-approval" element={<GetHistoryApproval />} />
<Route path="/get-proof-approval" element={<GetProofApproval />} />
<Route path="/notarize-approval" element={<NotarizeApproval />} />
<Route path="/get-plugins-approval" element={<GetPluginsApproval />} />
<Route path="/run-plugin-approval" element={<RunPluginApproval />} />
<Route
path="/install-plugin-approval"
element={<InstallPluginApproval />}
/>
<Route path="*" element={<Navigate to="/home" />} />
</Routes>
</div>
);
};
export default Popup;
function AppConnectionLogo() {
const dispatch = useDispatch();
const activeTab = useActiveTab();
const url = useActiveTabUrl();
const [showConnectionDetails, setShowConnectionDetails] = useState(false);
const connected = useIsConnected();
useEffect(() => {
(async () => {
if (url) {
const isConnected: boolean | null = await getConnection(url?.origin);
dispatch(setConnection(!!isConnected));
}
})();
}, [url]);
return (
<div
className="absolute right-2 flex flex-nowrap flex-row items-center gap-1 justify-center w-fit cursor-pointer"
onClick={() => setShowConnectionDetails(true)}
>
<div className="flex flex-row relative bg-black border-[1px] border-black rounded-full">
{!!activeTab?.favIconUrl ? (
<img
src={activeTab?.favIconUrl}
className="h-5 rounded-full"
alt="logo"
/>
) : (
<Icon
fa="fa-solid fa-globe"
className="bg-white text-slate-400 rounded-full"
size={1.25}
/>
)}
<div
className={classNames(
'absolute right-[-2px] bottom-[-2px] rounded-full h-[10px] w-[10px] border-[2px]',
{
'bg-green-500': connected,
'bg-slate-500': !connected,
},
)}
/>
</div>
{showConnectionDetails && (
<ConnectionDetailsModal
showConnectionDetails={showConnectionDetails}
setShowConnectionDetails={setShowConnectionDetails}
/>
)}
</div>
);
}

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Popup</title>
<title>TLSN Extension</title>
</head>
<body>

View File

@@ -4,10 +4,10 @@
$fa-font-path: "~@fortawesome/fontawesome-free/webfonts";
@import "@fortawesome/fontawesome-free/scss/fontawesome";
@import "@fortawesome/fontawesome-free/scss/brands";
@import "@fortawesome/fontawesome-free/scss/solid";
@import "@fortawesome/fontawesome-free/scss/regular";
@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";
body {
width: 480px;
@@ -47,6 +47,21 @@ code {
@apply text-slate-700;
}
&--primary {
@apply bg-primary/[0.8];
@apply text-white;
&:hover {
@apply bg-primary/[0.9];
@apply text-white;
}
&:active {
@apply bg-primary;
@apply text-white;
}
}
&:disabled {
@apply opacity-50;
@apply select-none;

View File

@@ -0,0 +1,18 @@
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';
import store from '../../utils/store';
const container = document.getElementById('app-container');
const root = createRoot(container!); // createRoot(container!) if you use TypeScript
root.render(
<Provider store={store}>
<HashRouter>
<Popup />
</HashRouter>
</Provider>,
);

View File

@@ -0,0 +1,281 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import './sidePanel.scss';
import browser from 'webextension-polyfill';
import { fetchPluginConfigByHash, runPlugin } from '../../utils/rpc';
import {
getPluginConfig,
hexToArrayBuffer,
makePlugin,
PluginConfig,
StepConfig,
} from '../../utils/misc';
import { PluginList } from '../../components/PluginList';
import DefaultPluginIcon from '../../assets/img/default-plugin-icon.png';
import logo from '../../assets/img/icon-128.png';
import classNames from 'classnames';
import Icon from '../../components/Icon';
import { useRequestHistory } from '../../reducers/history';
import { BackgroundActiontype } from '../Background/rpc';
import { getPluginByHash, getPluginConfigByHash } from '../Background/db';
import type { Plugin } from '@extism/extism';
import { OffscreenActionTypes } from '../Offscreen/types';
import { SidePanelActionTypes } from './types';
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 getPluginConfigByHash(plugin_hash);
setHash(plugin_hash);
setConfig(config);
// await browser.storage.local.set({ plugin_hash: '' });
})();
}, []);
return (
<div className="flex flex-col bg-slate-100 w-screen h-screen">
<div className="relative flex flex-nowrap flex-shrink-0 flex-row items-center 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 [notarizationId, setNotarizationId] = useState('');
const notaryRequest = useRequestHistory(notarizationId);
const setResponse = useCallback(
(response: any, i: number) => {
const result = responses.concat();
result[i] = response;
setResponses(result);
if (i === steps!.length - 1 && !!response) {
setNotarizationId(response);
}
},
[hash, responses],
);
useEffect(() => {
if (notaryRequest?.status === 'success') {
browser.runtime.sendMessage({
type: SidePanelActionTypes.execute_plugin_response,
data: {
hash,
proof: notaryRequest.proof,
},
});
} else if (notaryRequest?.status === 'error') {
browser.runtime.sendMessage({
type: SidePanelActionTypes.execute_plugin_response,
data: {
hash,
error: notaryRequest.error,
},
});
}
}, [hash, notaryRequest?.status]);
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,
setResponse,
lastResponse,
prover,
hash,
} = props;
const [completed, setCompleted] = useState(false);
const [pending, setPending] = useState(false);
const [error, setError] = useState('');
const [notarizationId, setNotarizationId] = useState('');
const notaryRequest = useRequestHistory(notarizationId);
const getPlugin = useCallback(async () => {
const hex = await getPluginByHash(hash);
const config = await getPluginConfigByHash(hash);
const arrayBuffer = hexToArrayBuffer(hex!);
return makePlugin(arrayBuffer, config!);
}, [hash]);
const processStep = useCallback(async () => {
const plugin = await getPlugin();
if (!plugin) return;
if (index > 0 && !lastResponse) return;
setPending(true);
setError('');
try {
const out = await plugin.call(action, JSON.stringify(lastResponse));
const val = JSON.parse(out.string());
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);
}
}, [action, index, lastResponse, prover, getPlugin]);
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

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

View File

@@ -1,12 +1,9 @@
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';
import SidePanel from './SidePanel';
import store from '../../utils/store';
import { BackgroundActiontype } from '../Background/actionTypes';
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
@@ -27,8 +24,6 @@ chrome.runtime.onMessage.addListener((request) => {
root.render(
<Provider store={store}>
<HashRouter>
<Popup />
</HashRouter>
<SidePanel />
</Provider>,
);

View File

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

View File

@@ -0,0 +1,4 @@
export enum SidePanelActionTypes {
execute_plugin_request = 'sidePanel/execute_plugin_request',
execute_plugin_response = 'sidePanel/execute_plugin_response',
}

View File

@@ -3,11 +3,16 @@
"name": "TLSN Extension",
"description": "A chrome extension for TLSN",
"options_page": "options.html",
"background": { "service_worker": "background.bundle.js" },
"background": {
"service_worker": "background.bundle.js"
},
"action": {
"default_popup": "popup.html",
"default_icon": "icon-34.png"
},
"side_panel": {
"default_path": "sidePanel.html"
},
"icons": {
"128": "icon-128.png"
},
@@ -23,18 +28,16 @@
],
"web_accessible_resources": [
{
"resources": ["content.styles.css", "icon-128.png", "icon-34.png"],
"matches": []
"resources": ["content.styles.css", "icon-128.png", "icon-34.png", "content.bundle.js", "discord_dm.wasm", "twitter_profile.wasm"],
"matches": ["http://*/*", "https://*/*", "<all_urls>"]
}
],
"host_permissions": ["<all_urls>"],
"permissions": [
"offscreen",
"http://*/",
"https://*/",
"storage",
"webRequest",
"activeTab",
"<all_urls>"
"sidePanel"
]
}

View File

@@ -1,53 +0,0 @@
export enum BackgroundActiontype {
get_requests = 'get_requests',
clear_requests = 'clear_requests',
push_action = 'push_action',
get_prove_requests = 'get_prove_requests',
prove_request_start = 'prove_request_start',
process_prove_request = 'process_prove_request',
finish_prove_request = 'finish_prove_request',
verify_prove_request = 'verify_prove_request',
delete_prove_request = 'delete_prove_request',
retry_prove_request = 'retry_prove_request',
}
export type BackgroundAction = {
type: BackgroundActiontype;
data?: any;
meta?: any;
error?: boolean;
};
export type RequestLog = {
requestId: string;
tabId: number;
method: string;
type: string;
url: string;
initiator: string | null;
requestHeaders: chrome.webRequest.HttpHeader[];
requestBody?: string;
formData?: {
[k: string]: string[];
};
responseHeaders?: chrome.webRequest.HttpHeader[];
};
export type RequestHistory = {
id: string;
url: string;
method: string;
headers: { [key: string]: string };
body: string;
maxTranscriptSize: string;
notaryUrl: string;
websocketProxyUrl: string;
status: '' | 'pending' | 'success' | 'error';
error?: any;
proof?: { session: any; substrings: any };
requestBody?: any;
verification?: {
sent: string;
recv: string;
};
};

View File

@@ -1,441 +0,0 @@
import {
BackgroundActiontype,
RequestLog,
type RequestHistory,
} from './actionTypes';
import { Mutex } from 'async-mutex';
import NodeCache from 'node-cache';
import { addRequest } from '../../reducers/requests';
import { addRequestHistory } from '../../reducers/history';
import { Level } from 'level';
import charwise from 'charwise';
let RequestsLogs: {
[tabId: string]: NodeCache;
} = {};
const mutex = new Mutex();
const cache = new NodeCache({
stdTTL: 60 * 5, // default 5m TTL
maxKeys: 1000000,
});
let creatingOffscreen;
chrome.tabs.onActivated.addListener((tabs) => {
RequestsLogs[tabs.tabId] =
RequestsLogs[tabs.tabId] ||
new NodeCache({
stdTTL: 60 * 5, // default 5m TTL
maxKeys: 1000000,
});
});
chrome.tabs.onRemoved.addListener((tab) => {
delete RequestsLogs[tab];
});
(async () => {
const offscreenUrl = chrome.runtime.getURL('offscreen.html');
const existingContexts = await chrome.runtime.getContexts({
contextTypes: ['OFFSCREEN_DOCUMENT'],
documentUrls: [offscreenUrl],
});
if (existingContexts.length > 0) {
return;
}
if (creatingOffscreen) {
await creatingOffscreen;
} else {
creatingOffscreen = (chrome as any).offscreen.createDocument({
url: 'offscreen.html',
reasons: ['WORKERS'],
justification: 'workers for multithreading',
});
await creatingOffscreen;
creatingOffscreen = null;
}
chrome.webRequest.onSendHeaders.addListener(
(details) => {
mutex.runExclusive(async () => {
const { method, tabId, requestId } = details;
if (method !== 'OPTIONS') {
RequestsLogs[tabId] =
RequestsLogs[tabId] ||
new NodeCache({
stdTTL: 60 * 5, // default 5m TTL
maxKeys: 1000000,
});
const existing = RequestsLogs[tabId].get<RequestLog>(requestId);
RequestsLogs[tabId].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,
});
}
});
},
{
urls: ['<all_urls>'],
},
['requestHeaders', 'extraHeaders'],
);
chrome.webRequest.onBeforeRequest.addListener(
(details) => {
mutex.runExclusive(async () => {
const { method, requestBody, tabId, requestId } = details;
if (method === 'OPTIONS') return;
if (requestBody) {
RequestsLogs[tabId] =
RequestsLogs[tabId] ||
new NodeCache({
stdTTL: 60 * 5, // default 5m TTL
maxKeys: 1000000,
});
const existing = RequestsLogs[tabId].get<RequestLog>(requestId);
if (requestBody.raw && requestBody.raw[0]?.bytes) {
try {
RequestsLogs[details.tabId].set(requestId, {
...existing,
requestBody: Buffer.from(requestBody.raw[0].bytes).toString(
'utf-8',
),
});
} catch (e) {
console.error(e);
}
} else if (requestBody.formData) {
RequestsLogs[details.tabId].set(requestId, {
...existing,
formData: requestBody.formData,
});
}
}
});
},
{
urls: ['<all_urls>'],
},
['requestBody'],
);
chrome.webRequest.onResponseStarted.addListener(
(details) => {
mutex.runExclusive(async () => {
const { method, responseHeaders, tabId, requestId } = details;
if (method === 'OPTIONS') return;
RequestsLogs[tabId] =
RequestsLogs[tabId] ||
new NodeCache({
stdTTL: 60 * 5, // default 5m TTL
maxKeys: 1000000,
});
const existing = RequestsLogs[tabId].get<RequestLog>(requestId);
const newLog: RequestLog = {
requestHeaders: [],
...existing,
method: details.method,
type: details.type,
url: details.url,
initiator: details.initiator || null,
tabId: tabId,
requestId: requestId,
responseHeaders,
};
RequestsLogs[tabId].set(requestId, newLog);
chrome.runtime.sendMessage({
type: BackgroundActiontype.push_action,
data: {
tabId: details.tabId,
request: newLog,
},
action: addRequest(newLog),
});
});
},
{
urls: ['<all_urls>'],
},
['responseHeaders', 'extraHeaders'],
);
chrome.runtime.onMessage.addListener(
async (request, sender, sendResponse) => {
switch (request.type) {
case BackgroundActiontype.get_requests: {
const keys = RequestsLogs[request.data]?.keys() || [];
const data = keys.map((key) => RequestsLogs[request.data]?.get(key));
return sendResponse(data);
}
case BackgroundActiontype.clear_requests: {
RequestsLogs = {};
return sendResponse();
}
case BackgroundActiontype.get_prove_requests: {
getNotaryRequests().then((reqs) => {
for (const req of reqs) {
chrome.runtime.sendMessage({
type: BackgroundActiontype.push_action,
data: {
tabId: 'background',
},
action: addRequestHistory(req),
});
}
});
return sendResponse();
}
case BackgroundActiontype.finish_prove_request: {
const { id, proof, error, verification } = request.data;
if (proof) {
const newReq = await addNotaryRequestProofs(id, proof);
if (!newReq) return;
chrome.runtime.sendMessage({
type: BackgroundActiontype.push_action,
data: {
tabId: 'background',
},
action: addRequestHistory(await getNotaryRequest(id)),
});
}
if (error) {
const newReq = await setNotaryRequestError(id, error);
if (!newReq) return;
chrome.runtime.sendMessage({
type: BackgroundActiontype.push_action,
data: {
tabId: 'background',
},
action: addRequestHistory(await getNotaryRequest(id)),
});
}
if (verification) {
const newReq = await setNotaryRequestVerification(id, verification);
if (!newReq) return;
chrome.runtime.sendMessage({
type: BackgroundActiontype.push_action,
data: {
tabId: 'background',
},
action: addRequestHistory(await getNotaryRequest(id)),
});
}
return sendResponse();
}
case BackgroundActiontype.delete_prove_request: {
const id = request.data;
await removeNotaryRequest(id);
return sendResponse();
}
case BackgroundActiontype.retry_prove_request: {
const { id, notaryUrl, websocketProxyUrl } = request.data;
await setNotaryRequestStatus(id, 'pending');
const req = await getNotaryRequest(id);
chrome.runtime.sendMessage<any, string>({
type: BackgroundActiontype.process_prove_request,
data: {
...req,
notaryUrl,
websocketProxyUrl,
},
});
return sendResponse();
}
case BackgroundActiontype.prove_request_start: {
const {
url,
method,
headers,
body,
maxTranscriptSize,
notaryUrl,
websocketProxyUrl,
} = request.data;
const { id } = await addNotaryRequest(Date.now(), {
url,
method,
headers,
body,
maxTranscriptSize,
notaryUrl,
websocketProxyUrl,
});
await setNotaryRequestStatus(id, 'pending');
chrome.runtime.sendMessage({
type: BackgroundActiontype.push_action,
data: {
tabId: 'background',
},
action: addRequestHistory(await getNotaryRequest(id)),
});
chrome.runtime.sendMessage<any, string>({
type: BackgroundActiontype.process_prove_request,
data: {
id,
url,
method,
headers,
body,
maxTranscriptSize,
notaryUrl,
websocketProxyUrl,
},
});
return sendResponse();
}
default:
break;
}
},
);
})();
const db = new Level('./ext-db', {
valueEncoding: 'json',
});
const historyDb = db.sublevel('history', { valueEncoding: 'json' });
async function addNotaryRequest(
now = Date.now(),
request: RequestHistory,
): Promise<RequestHistory> {
const id = charwise.encode(now).toString('hex');
const newReq = {
...request,
id,
};
await historyDb.put(id, newReq);
return newReq;
}
async function addNotaryRequestProofs(
id: string,
proof: { session: any; substrings: any },
): Promise<RequestHistory | null> {
const existing = await historyDb.get(id);
if (!existing) return null;
const newReq = {
...existing,
proof,
status: 'success',
};
await historyDb.put(id, newReq);
return newReq;
}
async function setNotaryRequestStatus(
id: string,
status: '' | 'pending' | 'success' | 'error',
): Promise<RequestHistory | null> {
const existing = await historyDb.get(id);
if (!existing) return null;
const newReq = {
...existing,
status,
};
await historyDb.put(id, newReq);
return newReq;
}
async function setNotaryRequestError(
id: string,
error: any,
): Promise<RequestHistory | null> {
const existing = await historyDb.get(id);
if (!existing) return null;
const newReq = {
...existing,
error,
status: 'error',
};
await historyDb.put(id, newReq);
return newReq;
}
async function setNotaryRequestVerification(
id: string,
verification: { sent: string; recv: string },
): Promise<RequestHistory | null> {
const existing = await historyDb.get(id);
if (!existing) return null;
const newReq = {
...existing,
verification,
};
await historyDb.put(id, newReq);
return newReq;
}
async function removeNotaryRequest(id: string): Promise<RequestHistory | null> {
const existing = await historyDb.get(id);
if (!existing) return null;
await historyDb.del(id);
return existing;
}
async function getNotaryRequests(): Promise<RequestHistory[]> {
const retVal = [];
for await (const [key, value] of historyDb.iterator()) {
retVal.push(value);
}
return retVal;
}
async function getNotaryRequest(id: string): Promise<RequestHistory | null> {
return historyDb.get(id);
}

View File

@@ -0,0 +1,44 @@
import React, { ReactElement, ReactNode } from 'react';
import logo from '../../assets/img/icon-128.png';
export function BaseApproval({
onSecondaryClick,
onPrimaryClick,
header,
children,
secondaryCTAText = 'Cancel',
primaryCTAText = 'Accept',
}: {
header: ReactNode;
children: ReactNode;
onSecondaryClick: () => void;
onPrimaryClick: () => void;
secondaryCTAText?: string;
primaryCTAText?: string;
}): ReactElement {
return (
<div className="absolute flex flex-col items-center w-screen h-screen bg-white gap-2 cursor-default">
<div className="w-full p-2 border-b border-gray-200 text-gray-500">
<div className="flex flex-row items-end justify-start gap-2">
<img className="h-5" src={logo} alt="logo" />
<span className="font-semibold">{header}</span>
</div>
</div>
<div className="flex flex-col flex-grow gap-2 overflow-y-auto w-full">
{children}
</div>
<div className="flex flex-row w-full gap-2 justify-end border-t p-4">
{!!onSecondaryClick && !!secondaryCTAText && (
<button className="button" onClick={onSecondaryClick}>
{secondaryCTAText}
</button>
)}
{!!onPrimaryClick && !!primaryCTAText && (
<button className="button button--primary" onClick={onPrimaryClick}>
{primaryCTAText}
</button>
)}
</div>
</div>
);
}

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

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

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

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

View File

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

View File

@@ -1,9 +0,0 @@
window.onerror = (error) => {
console.log('error');
console.log(error);
};
(async () => {
console.log('Content script works!');
console.log('Must reload extension for modifications to take effect.');
})();

View File

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

View File

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

View File

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

349
src/pages/History/index.tsx Normal file
View File

@@ -0,0 +1,349 @@
import React, { ReactElement, useState, useCallback, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { useNavigate } from 'react-router';
import {
useHistoryOrder,
useRequestHistory,
deleteRequestHistory,
} from '../../reducers/history';
import Icon from '../../components/Icon';
import { getNotaryApi, getProxyApi } from '../../utils/storage';
import { urlify, download, upload } from '../../utils/misc';
import { BackgroundActiontype } from '../../entries/Background/rpc';
import Modal, { ModalContent } from '../../components/Modal/Modal';
import classNames from 'classnames';
import copy from 'copy-to-clipboard';
import { EXPLORER_API } from '../../utils/constants';
import {
getNotaryRequest,
setNotaryRequestCid,
} from '../../entries/Background/db';
const charwise = require('charwise');
export default function History(): ReactElement {
const history = useHistoryOrder();
return (
<div className="flex flex-col flex-nowrap overflow-y-auto">
{history.map((id) => {
return <OneRequestHistory key={id} requestId={id} />;
})}
</div>
);
}
export function OneRequestHistory(props: {
requestId: string;
className?: string;
hideActions?: string[];
}): ReactElement {
const { hideActions = [] } = props;
const dispatch = useDispatch();
const request = useRequestHistory(props.requestId);
const [showingError, showError] = useState(false);
const [uploadError, setUploadError] = useState('');
const [showingShareConfirmation, setShowingShareConfirmation] =
useState(false);
const [cid, setCid] = useState<{ [key: string]: string }>({});
const [uploading, setUploading] = useState(false);
const navigate = useNavigate();
const { status } = request || {};
const requestUrl = urlify(request?.url || '');
useEffect(() => {
const fetchData = async () => {
try {
const request = await getNotaryRequest(props.requestId);
if (request && request.cid) {
setCid({ [props.requestId]: request.cid });
}
} catch (e) {
console.error('Error fetching data', e);
}
};
fetchData();
}, []);
const onRetry = useCallback(async () => {
const notaryUrl = await getNotaryApi();
const websocketProxyUrl = await getProxyApi();
chrome.runtime.sendMessage<any, string>({
type: BackgroundActiontype.retry_prove_request,
data: {
id: props.requestId,
notaryUrl,
websocketProxyUrl,
},
});
}, [props.requestId]);
const onView = useCallback(() => {
chrome.runtime.sendMessage<any, string>({
type: BackgroundActiontype.verify_prove_request,
data: request,
});
navigate('/verify/' + request?.id);
}, [request]);
const onDelete = useCallback(async () => {
dispatch(deleteRequestHistory(props.requestId));
}, [props.requestId]);
const onShowError = useCallback(async () => {
showError(true);
}, [request?.error, showError]);
const closeAllModal = useCallback(() => {
setShowingShareConfirmation(false);
showError(false);
}, [setShowingShareConfirmation, showError]);
const handleUpload = useCallback(async () => {
setUploading(true);
try {
const data = await upload(
`${request?.id}.json`,
JSON.stringify(request?.proof),
);
setCid((prevCid) => ({ ...prevCid, [props.requestId]: data }));
await setNotaryRequestCid(props.requestId, data);
} catch (e: any) {
setUploadError(e.message);
} finally {
setUploading(false);
}
}, [props.requestId, request, cid]);
return (
<div
className={classNames(
'flex flex-row flex-nowrap border rounded-md p-2 gap-1 hover:bg-slate-50 cursor-pointer',
props.className,
)}
>
<ShareConfirmationModal />
<ErrorModal />
<div className="flex flex-col flex-nowrap flex-grow flex-shrink w-0">
<div className="flex flex-row items-center text-xs">
<div className="bg-slate-200 text-slate-400 px-1 py-0.5 rounded-sm">
{request?.method}
</div>
<div className="text-black font-bold px-2 py-1 rounded-md overflow-hidden text-ellipsis">
{requestUrl?.pathname}
</div>
</div>
<div className="flex flex-row">
<div className="font-bold text-slate-400">Time:</div>
<div className="ml-2 text-slate-800">
{new Date(charwise.decode(props.requestId, 'hex')).toISOString()}
</div>
</div>
<div className="flex flex-row">
<div className="font-bold text-slate-400">Host:</div>
<div className="ml-2 text-slate-800">{requestUrl?.host}</div>
</div>
<div className="flex flex-row">
<div className="font-bold text-slate-400">Notary API:</div>
<div className="ml-2 text-slate-800">{request?.notaryUrl}</div>
</div>
<div className="flex flex-row">
<div className="font-bold text-slate-400">TLS Proxy API:</div>
<div className="ml-2 text-slate-800">
{request?.websocketProxyUrl}
</div>
</div>
</div>
<div className="flex flex-col gap-1">
{status === 'success' && (
<>
<ActionButton
className="bg-slate-600 text-slate-200 hover:bg-slate-500 hover:text-slate-100"
onClick={onView}
fa="fa-solid fa-receipt"
ctaText="View Proof"
hidden={hideActions.includes('view')}
/>
<ActionButton
className="bg-slate-100 text-slate-300 hover:bg-slate-200 hover:text-slate-500"
onClick={() =>
download(`${request?.id}.json`, JSON.stringify(request?.proof))
}
fa="fa-solid fa-download"
ctaText="Download"
hidden={hideActions.includes('download')}
/>
<ActionButton
className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-slate-100 text-slate-300 hover:bg-slate-200 hover:text-slate-500 hover:font-bold"
onClick={() => setShowingShareConfirmation(true)}
fa="fa-solid fa-upload"
ctaText="Share"
hidden={hideActions.includes('share')}
/>
</>
)}
{status === 'error' && !!request?.error && (
<ErrorButton hidden={hideActions.includes('error')} />
)}
{(!status || status === 'error') && (
<RetryButton hidden={hideActions.includes('retry')} />
)}
{status === 'pending' && (
<button className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-slate-100 text-slate-300 font-bold">
<Icon className="animate-spin" fa="fa-solid fa-spinner" size={1} />
<span className="text-xs font-bold">Pending</span>
</button>
)}
<ActionButton
className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-slate-100 text-slate-300 hover:bg-red-100 hover:text-red-500 hover:font-bold"
onClick={onDelete}
fa="fa-solid fa-trash"
ctaText="Delete"
hidden={hideActions.includes('delete')}
/>
</div>
</div>
);
function RetryButton(p: { hidden?: boolean }): ReactElement {
if (p.hidden) return <></>;
return (
<button
className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-slate-100 text-slate-300 hover:bg-slate-200 hover:text-slate-500 hover:font-bold"
onClick={onRetry}
>
<Icon fa="fa-solid fa-arrows-rotate" size={1} />
<span className="text-xs font-bold">Retry</span>
</button>
);
}
function ErrorButton(p: { hidden?: boolean }): ReactElement {
if (p.hidden) return <></>;
return (
<button
className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-red-100 text-red-300 hover:bg-red-200 hover:text-red-500 hover:font-bold"
onClick={onShowError}
>
<Icon fa="fa-solid fa-circle-exclamation" size={1} />
<span className="text-xs font-bold">Error</span>
</button>
);
}
function ErrorModal(): ReactElement {
const msg = typeof request?.error === 'string' && request?.error;
return !showingError ? (
<></>
) : (
<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"
onClose={closeAllModal}
>
<ModalContent className="flex justify-center items-center text-slate-500">
{msg || 'Something went wrong :('}
</ModalContent>
<button
className="m-0 w-24 bg-red-100 text-red-300 hover:bg-red-200 hover:text-red-500"
onClick={closeAllModal}
>
OK
</button>
</Modal>
);
}
function ShareConfirmationModal(): ReactElement {
return !showingShareConfirmation ? (
<></>
) : (
<Modal
className="flex flex-col items-center text-base cursor-default justify-center !w-auto mx-4 my-[50%] p-4 gap-4"
onClose={closeAllModal}
>
<ModalContent className="flex flex-col w-full gap-4 items-center text-base justify-center">
{!cid[props.requestId] ? (
<p className="text-slate-500 text-center">
{uploadError ||
'This will make your proof publicly accessible by anyone with the CID'}
</p>
) : (
<input
className="input w-full bg-slate-100 border border-slate-200"
readOnly
value={`${EXPLORER_API}/ipfs/${cid[props.requestId]}`}
onFocus={(e) => e.target.select()}
/>
)}
</ModalContent>
<div className="flex flex-row gap-2 justify-center">
{!cid[props.requestId] ? (
<>
{!uploadError && (
<button
onClick={handleUpload}
className="button button--primary flex flex-row items-center justify-center gap-2 m-0"
disabled={uploading}
>
{uploading && (
<Icon
className="animate-spin"
fa="fa-solid fa-spinner"
size={1}
/>
)}
I understand
</button>
)}
<button
className="m-0 w-24 bg-slate-100 text-slate-400 hover:bg-slate-200 hover:text-slate-600 font-bold"
onClick={closeAllModal}
>
Close
</button>
</>
) : (
<>
<button
onClick={() =>
copy(`${EXPLORER_API}/ipfs/${cid[props.requestId]}`)
}
className="m-0 w-24 bg-slate-600 text-slate-200 hover:bg-slate-500 hover:text-slate-100 font-bold"
>
Copy
</button>
<button
className="m-0 w-24 bg-slate-100 text-slate-400 hover:bg-slate-200 hover:text-slate-600 font-bold"
onClick={closeAllModal}
>
Close
</button>
</>
)}
</div>
</Modal>
);
}
}
function ActionButton(props: {
onClick: () => void;
fa: string;
ctaText: string;
className?: string;
hidden?: boolean;
}): ReactElement {
if (props.hidden) return <></>;
return (
<button
className={classNames(
'flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 hover:font-bold',
props.className,
)}
onClick={props.onClick}
>
<Icon className="" fa={props.fa} size={1} />
<span className="text-xs font-bold">{props.ctaText}</span>
</button>
);
}

View File

@@ -2,99 +2,53 @@ import React, {
MouseEventHandler,
ReactElement,
ReactNode,
useCallback,
useState,
} from 'react';
import Icon from '../../components/Icon';
import classNames from 'classnames';
import { useNavigate } from 'react-router';
import { useActiveTabUrl, useRequests } from '../../reducers/requests';
import { Link } from 'react-router-dom';
import { filterByBookmarks } from '../../../utils/bookmark';
import { useRequests } from '../../reducers/requests';
import { PluginList } from '../../components/PluginList';
import PluginUploadInfo from '../../components/PluginInfo';
import { ErrorModal } from '../../components/ErrorModal';
export default function Home(): ReactElement {
const requests = useRequests();
const url = useActiveTabUrl();
const navigate = useNavigate();
const suggestions = filterByBookmarks(requests);
const [error, showError] = useState('');
return (
<div className="flex flex-col gap-4 py-4 overflow-y-auto">
{error && <ErrorModal onClose={() => showError('')} message={error} />}
<div className="flex flex-col flex-nowrap justify-center gap-2 mx-4">
<NavButton fa="fa-solid fa-table" onClick={() => navigate('/requests')}>
<span>Requests</span>
<span>{`(${requests.length})`}</span>
</NavButton>
<NavButton
fa="fa-solid fa-magnifying-glass"
onClick={() => navigate('/custom')}
>
<NavButton fa="fa-solid fa-hammer" onClick={() => navigate('/custom')}>
Custom
</NavButton>
<NavButton
fa="fa-solid fa-magnifying-glass"
fa="fa-solid fa-certificate"
onClick={() => navigate('/verify')}
disabled
>
Verify
</NavButton>
<NavButton fa="fa-solid fa-list" onClick={() => navigate('/history')}>
History
</NavButton>
<NavButton className="relative" fa="fa-solid fa-plus">
<PluginUploadInfo />
Add a plugin
</NavButton>
<NavButton fa="fa-solid fa-gear" onClick={() => navigate('/options')}>
Options
</NavButton>
<NavButton fa="fa-solid fa-comment-dots" onClick={() => navigate('/chat')}>
Chat
</NavButton>
</div>
{!suggestions.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">
{suggestions.map((bm, i) => {
try {
const reqs = requests.filter((req) => {
return req?.url?.includes(bm.url);
});
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>
<div className="text-slate-300">
Found {reqs.length} request
</div>
{reqs.map((r) => (
<Link
to={`/requests/${r.requestId}`}
className="break-all text-slate-500 truncate"
>
{r.url}
</Link>
))}
</div>
);
} catch (e) {
return null;
}
})}
</div>
<PluginList className="mx-4" />
</div>
);
}

View File

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

View File

@@ -0,0 +1,515 @@
import classNames from 'classnames';
import React, {
ReactElement,
useState,
useCallback,
ReactEventHandler,
useEffect,
useRef,
} from 'react';
import { useNavigate, useParams } from 'react-router';
import { notarizeRequest, useRequest } from '../../reducers/requests';
import Icon from '../../components/Icon';
import { urlify } from '../../utils/misc';
import {
getNotaryApi,
getProxyApi,
getMaxSent,
getMaxRecv,
} from '../../utils/storage';
import { useDispatch } from 'react-redux';
const maxTranscriptSize = 16384;
export default function Notarize(): ReactElement {
const params = useParams<{ requestId: string }>();
const req = useRequest(params.requestId);
const navigate = useNavigate();
const dispatch = useDispatch();
const [step, setStep] = useState(0);
const [secretHeaders, setSecretHeaders] = useState<string[]>([]);
const [secretResps, setSecretResps] = useState<string[]>([]);
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 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,
body: req.requestBody,
maxSentData,
maxRecvData,
maxTranscriptSize,
notaryUrl,
websocketProxyUrl,
secretHeaders,
secretResps,
}),
);
navigate(`/history`);
}, [req, secretHeaders, secretResps]);
if (!req) return <></>;
let body;
switch (step) {
case 0:
body = (
<RevealHeaderStep
onNext={() => setStep(1)}
onCancel={() => navigate(-1)}
setSecretHeaders={setSecretHeaders}
/>
);
break;
case 1:
body = (
<HideResponseStep
onNext={notarize}
onCancel={() => setStep(0)}
setSecretResps={setSecretResps}
/>
);
break;
default:
body = null;
break;
}
return (
<div className="flex flex-col flex-nowrap flex-grow">
<div className="flex flex-row flex-nowrap relative items-center bg-slate-300 py-2 px-2 gap-2">
<Icon
className="cursor-point text-slate-400 hover:text-slate-700"
fa="fa-solid fa-xmark"
onClick={() => navigate(-1)}
/>
<div className="flex flex-col flex-shrink flex-grow mr-20 w-0 select-none">
<span className="font-bold text-slate-700">
{`Notarizing a ${req.method} request`}
</span>
<span
className="text-ellipsis whitespace-nowrap overflow-hidden"
title={req.url}
>
{req.url}
</span>
</div>
</div>
{body}
</div>
);
}
export function RevealHeaderStep(props: {
onNext: () => void;
onCancel: () => void;
setSecretHeaders: (secrets: string[]) => void;
}): ReactElement {
const params = useParams<{ requestId: string }>();
const req = useRequest(params.requestId);
const [revealed, setRevealed] = useState<{ [key: string]: boolean }>({});
const headers = req?.requestHeaders;
useEffect(() => {
if (!req) return;
props.setSecretHeaders(
req.requestHeaders
.map((h) => {
if (!revealed[h.name]) {
return `${h.name.toLowerCase()}: ${h.value || ''}` || '';
}
return '';
})
.filter((d) => !!d),
);
}, [revealed]);
const changeHeaderKey = useCallback(
(key: string, shouldReveal: boolean) => {
if (!req) return;
setRevealed({
...revealed,
[key]: shouldReveal,
});
},
[revealed, req],
);
if (!headers) return <></>;
return (
<div className="flex flex-col flex-nowrap flex-shrink flex-grow h-0">
<div className="border bg-primary/[0.9] text-white border-slate-300 py-1 px-2 font-semibold">
`Step 1 of 2: Select which request headers you want to reveal`
</div>
<div className="flex-grow flex-shrink h-0 overflow-y-auto">
<table className="border border-slate-300 border-collapse table-fixed">
<tbody className="bg-slate-200">
{headers.map((h) => (
<tr
key={h.name}
className={classNames('border-b border-slate-200 text-xs', {
'bg-slate-50': revealed[h.name],
})}
>
<td className="border border-slate-300 py-1 px-2 align-top">
<input
type="checkbox"
className="cursor-pointer"
onChange={(e) => changeHeaderKey(h.name, e.target.checked)}
checked={revealed[h.name]}
/>
</td>
<td className="border border-slate-300 font-bold align-top py-1 px-2 whitespace-nowrap">
{h.name}
</td>
<td className="border border-slate-300 break-all align-top py-1 px-2">
{revealed[h.name]
? h.value
: Array(h.value?.length || 0)
.fill('*')
.join('')}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="flex flex-row justify-end p-2 gap-2 border-t">
<button className="button" onClick={props.onCancel}>
Cancel
</button>
<button
className="bg-primary/[0.9] text-white font-bold hover:bg-primary/[0.8] px-2 py-0.5 active:bg-primary"
onClick={props.onNext}
>
Next
</button>
</div>
</div>
);
}
export function RevealHeaderTable(props: {
headers: { name: string; value: string }[];
className?: string;
onChange: (revealed: { [key: string]: boolean }) => void;
}) {
const { headers } = props;
const [revealed, setRevealed] = useState<{ [key: string]: boolean }>({});
const changeHeaderKey = useCallback(
(key: string, shouldReveal: boolean) => {
const result = {
...revealed,
[key]: shouldReveal,
};
setRevealed(result);
props.onChange(result);
},
[revealed],
);
return (
<table
className={classNames(
'border border-slate-300 border-collapse table-fixed',
props.className,
)}
>
<thead className="bg-slate-200">
<th className="border border-slate-300 py-1 px-2 align-middle w-8"></th>
<th className="border border-slate-300 py-1 px-2 align-middle">Name</th>
<th className="border border-slate-300 py-1 px-2 align-middle">
Value
</th>
</thead>
<tbody className="bg-slate-100">
{headers.map((h) => (
<tr
key={h.name}
className={classNames('border-b border-slate-200 text-xs', {
'bg-slate-50': revealed[h.name],
})}
>
<td className="border border-slate-300 py-1 px-2 align-top w-8">
<input
type="checkbox"
className="cursor-pointer"
onChange={(e) => changeHeaderKey(h.name, e.target.checked)}
checked={revealed[h.name]}
/>
</td>
<td className="border border-slate-300 font-bold align-top py-1 px-2 whitespace-nowrap">
{h.name}
</td>
<td className="border border-slate-300 break-all align-top py-1 px-2">
{revealed[h.name]
? h.value
: Array(h.value?.length || 0)
.fill('*')
.join('')}
</td>
</tr>
))}
</tbody>
</table>
);
}
function HideResponseStep(props: {
onNext: () => void;
onCancel: () => void;
setSecretResps: (secrets: string[]) => void;
}): ReactElement {
const params = useParams<{ requestId: string }>();
const req = useRequest(params.requestId);
const [responseText, setResponseText] = useState('');
const [start, setStart] = useState(0);
const [end, setEnd] = useState(0);
const taRef = useRef<HTMLTextAreaElement | null>(null);
const onSelectionChange: ReactEventHandler<HTMLTextAreaElement> = useCallback(
(e) => {
const ta = e.currentTarget;
if (ta.selectionEnd > ta.selectionStart) {
setStart(ta.selectionStart);
setEnd(ta.selectionEnd);
props.setSecretResps(
[
responseText.substring(0, ta.selectionStart),
responseText.substring(ta.selectionEnd, responseText.length),
].filter((d) => !!d),
);
}
},
[responseText],
);
useEffect(() => {
if (!req) return;
const options = {
method: req.method,
headers: req.requestHeaders.reduce(
// @ts-ignore
(acc: { [key: string]: string }, h: chrome.webRequest.HttpHeader) => {
if (typeof h.name !== 'undefined' && typeof h.value !== 'undefined') {
acc[h.name] = h.value;
}
return acc;
},
{},
),
body: req.requestBody,
};
if (req?.formData) {
const formData = new URLSearchParams();
Object.entries(req.formData).forEach(([key, values]) => {
values.forEach((v) => formData.append(key, v));
});
options.body = formData.toString();
}
replay(req.url, options).then((resp) => setResponseText(resp));
}, [req]);
useEffect(() => {
const current = taRef.current;
if (current) {
current.focus();
current.setSelectionRange(start, end);
}
}, [taRef, start, end]);
if (!req) return <></>;
let shieldedText = '';
if (end > start) {
shieldedText = Array(start)
.fill('*')
.join('')
.concat(responseText.substring(start, end))
.concat(
Array(responseText.length - end)
.fill('*')
.join(''),
);
}
return (
<div className="flex flex-col flex-nowrap flex-shrink flex-grow h-0">
<div className="border bg-primary/[0.9] text-white border-slate-300 py-1 px-2 font-semibold">
Step 2 of 2: Highlight text to show only selected text from response
</div>
<div className="flex flex-col flex-grow flex-shrink h-0 overflow-y-auto p-2">
<textarea
ref={taRef}
className="flex-grow textarea bg-slate-100 font-mono"
value={shieldedText || responseText}
onSelect={onSelectionChange}
/>
</div>
<div className="flex flex-row justify-end p-2 gap-2 border-t">
<button className="button" onClick={props.onCancel}>
Back
</button>
<button
className="bg-primary/[0.9] text-white font-bold hover:bg-primary/[0.8] px-2 py-0.5 active:bg-primary"
onClick={props.onNext}
>
Notarize
</button>
</div>
</div>
);
}
export function RedactBodyTextarea(props: {
className?: string;
onChange: (secretResponse: string[]) => void;
request: {
url: string;
method?: string;
headers?: { [name: string]: string };
formData?: { [k: string]: string[] };
body?: string;
};
}) {
const { className, onChange, request } = props;
const [loading, setLoading] = useState(false);
const [responseText, setResponseText] = useState('');
const [start, setStart] = useState(0);
const [end, setEnd] = useState(0);
const taRef = useRef<HTMLTextAreaElement | null>(null);
const onSelectionChange: ReactEventHandler<HTMLTextAreaElement> = useCallback(
(e) => {
const ta = e.currentTarget;
if (ta.selectionEnd > ta.selectionStart) {
setStart(ta.selectionStart);
setEnd(ta.selectionEnd);
onChange(
[
responseText.substring(0, ta.selectionStart),
responseText.substring(ta.selectionEnd, responseText.length),
].filter((d) => !!d),
);
}
},
[responseText],
);
useEffect(() => {
const options = {
method: request.method,
headers: request.headers,
body: request.body,
};
if (request?.formData) {
const formData = new URLSearchParams();
Object.entries(request.formData).forEach(([key, values]) => {
values.forEach((v) => formData.append(key, v));
});
options.body = formData.toString();
}
setLoading(true);
replay(request.url, options).then((resp) => {
setResponseText(resp);
setLoading(false);
});
}, [request]);
useEffect(() => {
const current = taRef.current;
if (current) {
current.focus();
current.setSelectionRange(start, end);
}
}, [taRef, start, end]);
let shieldedText = '';
if (end > start) {
shieldedText = Array(start)
.fill('*')
.join('')
.concat(responseText.substring(start, end))
.concat(
Array(responseText.length - end)
.fill('*')
.join(''),
);
}
if (loading) {
return (
<div className="flex flex-col items-center !pt-4 flex-grow textarea bg-slate-100">
<Icon
className="animate-spin w-fit text-slate-500"
fa="fa-solid fa-spinner"
size={1}
/>
</div>
);
}
return (
<textarea
ref={taRef}
className={classNames(
'flex-grow textarea bg-slate-100 font-mono',
className,
)}
value={shieldedText || responseText}
onSelect={onSelectionChange}
/>
);
}
const replay = async (url: string, options: any) => {
const resp = await fetch(url, options);
const contentType =
resp?.headers.get('content-type') || resp?.headers.get('Content-Type');
if (contentType?.includes('application/json')) {
return resp.text();
} else if (contentType?.includes('text')) {
return resp.text();
} else if (contentType?.includes('image')) {
return resp.blob().then((blob) => blob.text());
} else {
return resp.blob().then((blob) => blob.text());
}
};

View File

@@ -0,0 +1,178 @@
import React, { ReactElement, useCallback, useState } 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 { RedactBodyTextarea, RevealHeaderTable } from '../Notarize';
export function NotarizeApproval(): ReactElement {
const [params] = useSearchParams();
const origin = params.get('origin');
const favIconUrl = params.get('favIconUrl');
const config = JSON.parse(params.get('config')!);
const hostname = urlify(origin || '')?.hostname;
const [step, setStep] = useState<'overview' | 'headers' | 'response'>(
'overview',
);
const [revealed, setRevealed] = useState<{ [key: string]: boolean }>({});
const [secretResps, setSecretResps] = useState<string[]>([]);
const headerList = Object.entries(config.headers || {}).map(
([name, value]) => ({
name,
value: String(value),
}),
);
const onCancel = useCallback(() => {
if (step === 'headers') return setStep('overview');
if (step === 'response') return setStep('headers');
browser.runtime.sendMessage({
type: BackgroundActiontype.notarize_response,
data: false,
});
}, [step]);
const onAccept = useCallback(() => {
if (step === 'overview') return setStep('headers');
if (step === 'headers') return setStep('response');
const secretHeaders = headerList
.map((h) => {
if (!revealed[h.name]) {
return `${h.name.toLowerCase()}: ${h.value || ''}` || '';
}
return '';
})
.filter((d) => !!d);
browser.runtime.sendMessage({
type: BackgroundActiontype.notarize_response,
data: {
...config,
secretHeaders,
secretResps,
},
});
}, [revealed, step, secretResps, config]);
let body, headerText, primaryCta, secondaryCta;
switch (step) {
case 'overview':
headerText = 'Notarizing Request';
primaryCta = 'Next';
secondaryCta = 'Cancel';
body = (
<>
<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 notarize the
following request:
</div>
</div>
<div className="flex flex-col items-center gap-4 text-sm px-8 text-center flex-grow break-all">
<table className="border border-collapse table-auto rounded text-xs w-full">
<tbody>
<TableRow label="Method" value={config.method?.toUpperCase()} />
<TableRow label="Request URL" value={config.url} />
<TableRow label="Notary URL" value={config.notaryUrl} />
<TableRow label="Proxy URL" value={config.websocketProxyUrl} />
<TableRow label="Max Sent" value={config.maxSentData} />
<TableRow label="Max Recv" value={config.maxRecvData} />
{config.metadata && (
<TableRow
label="Metadata"
value={JSON.stringify(config.metadata)}
/>
)}
</tbody>
</table>
</div>
<div className="text-xs px-8 pb-2 text-center text-slate-500">
You will be able to review and redact headers and response body.
</div>
</>
);
break;
case 'headers':
headerText = 'Step 1 of 2: Select headers to reveal';
primaryCta = 'Next';
secondaryCta = 'Back';
body = (
<div className="px-2 flex flex-col">
<RevealHeaderTable
className="w-full"
onChange={setRevealed}
headers={headerList}
/>
</div>
);
break;
case 'response':
headerText = 'Step 2 of 2: Highlight response to keep';
primaryCta = 'Notarize';
secondaryCta = 'Back';
body = (
<div className="px-2 flex flex-col flex-grow">
<RedactBodyTextarea
className="w-full "
onChange={setSecretResps}
request={{
url: config.url,
method: config.method,
headers: config.headers,
body: config.body,
formData: config.formData,
}}
/>
</div>
);
break;
}
return (
<BaseApproval
header={headerText}
onSecondaryClick={onCancel}
onPrimaryClick={onAccept}
primaryCTAText={primaryCta}
secondaryCTAText={secondaryCta}
>
{body}
</BaseApproval>
);
}
function TableRow({ label, value }: { label: string; value: string }) {
return (
<tr>
<td className="px-2 py-1 border border-slate-300 bg-slate-100 text-slate-500 align-top text-left w-24">
{label}
</td>
<td className="px-2 py-1 border border-slate-300 font-semibold text-slate-800 text-left">
<input
className="outline-0 flex-grow cursor-default w-full"
type="text"
value={value}
/>
</td>
</tr>
);
}

View File

@@ -1,97 +0,0 @@
import React, { useEffect } from 'react';
import * as Comlink from 'comlink';
import { BackgroundActiontype } from '../Background/actionTypes';
const TLSN: any = Comlink.wrap(
new Worker(new URL('./worker.ts', import.meta.url)),
);
let tlsn: any | null = null;
async function getTLSN(): Promise<any | null> {
if (tlsn) return tlsn;
tlsn = await new TLSN();
return tlsn;
}
const Offscreen = () => {
useEffect(() => {
chrome.runtime.onMessage.addListener(
async (request, sender, sendResponse) => {
switch (request.type) {
case BackgroundActiontype.process_prove_request: {
const {
url,
method,
headers,
body = '',
maxTranscriptSize,
notaryUrl,
websocketProxyUrl,
id,
} = request.data;
const tlsn = await getTLSN();
try {
const proof = await tlsn.prover(url, {
method,
headers,
body,
maxTranscriptSize,
notaryUrl,
websocketProxyUrl,
});
chrome.runtime.sendMessage<any, string>({
type: BackgroundActiontype.finish_prove_request,
data: {
id,
proof,
},
});
} catch (error) {
chrome.runtime.sendMessage<any, string>({
type: BackgroundActiontype.finish_prove_request,
data: {
id,
error,
},
});
}
break;
}
case BackgroundActiontype.verify_prove_request: {
const tlsn = await getTLSN();
const result = await tlsn.verify(
request.data.proof,
`-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEBv36FI4ZFszJa0DQFJ3wWCXvVLFr\ncRzMG5kaTeHGoSzDu6cFqx3uEWYpFGo6C0EOUgf+mEgbktLrXocv5yHzKg==\n-----END PUBLIC KEY-----`,
);
if (result) {
chrome.runtime.sendMessage<any, string>({
type: BackgroundActiontype.finish_prove_request,
data: {
id: request.data.id,
verification: {
sent: result.sent,
recv: result.recv,
},
},
});
}
break;
}
default:
break;
}
},
);
}, []);
return <div className="App" />;
};
export default Offscreen;

View File

@@ -1,83 +0,0 @@
import * as Comlink from 'comlink';
import { urlify, devlog } from '../../utils/misc';
import init, {
initThreadPool,
prover,
verify,
} from '../../../wasm/prover/pkg/tlsn_extension_rs';
class TLSN {
private startPromise: any;
private resolveStart: any;
constructor() {
console.log('worker module initiated.');
this.startPromise = new Promise((resolve) => {
this.resolveStart = resolve;
});
this.start();
}
async start() {
devlog('start');
const numConcurrency = navigator.hardwareConcurrency;
devlog('!@# navigator.hardwareConcurrency=', numConcurrency);
const res = await init();
devlog('!@# res.memory=', res.memory);
// 6422528 ~= 6.12 mb
devlog('!@# res.memory.buffer.length=', res.memory.buffer.byteLength);
await initThreadPool(numConcurrency);
this.resolveStart();
}
async waitForStart() {
return this.startPromise;
}
async prover(
url: string,
options?: {
method?: string;
headers?: { [key: string]: string };
body?: string;
maxTranscriptSize?: number;
notaryUrl?: string;
websocketProxyUrl?: string;
},
) {
try {
await this.waitForStart();
console.log('worker', url, {
...options,
notaryUrl: options.notaryUrl,
websocketProxyUrl: options.websocketProxyUrl,
});
const resProver = await prover(url, {
...options,
notaryUrl: options.notaryUrl,
websocketProxyUrl: options.websocketProxyUrl,
});
const resJSON = JSON.parse(resProver);
devlog('!@# resProver,resJSON=', { resProver, resJSON });
devlog('!@# resAfter.memory=', resJSON.memory);
// 1105920000 ~= 1.03 gb
devlog(
'!@# resAfter.memory.buffer.length=',
resJSON.memory?.buffer?.byteLength,
);
return resJSON;
} catch (e: any) {
devlog(e);
return e;
}
}
async verify(proof: any, pubkey: string) {
await this.waitForStart();
const raw = await verify(JSON.stringify(proof), pubkey);
return JSON.parse(raw);
}
}
Comlink.expose(TLSN);

View File

@@ -1,9 +1,291 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import React, {
ReactElement,
useState,
useEffect,
useCallback,
MouseEvent,
} from 'react';
import {
set,
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,
} from '../../utils/constants';
import Modal, { ModalContent } from '../../components/Modal/Modal';
import browser from 'webextension-polyfill';
import { LoggingLevel } from 'tlsn-js';
import { version } from '../../../package.json';
import Options from '../../components/Options';
import './index.css';
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<LoggingLevel>('Info');
const container = document.getElementById('app-container');
const root = createRoot(container!); // createRoot(container!) if you use TypeScript
root.render(<Options />);
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()) || 'Info');
})();
}, [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]);
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>
<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-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 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 cursor-default">{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>
<div className="flex flex-col flex-nowrap py-1 px-2 gap-2 cursor-default">
<div className="font-semibold">Version</div>
<div className="input border bg-slate-100">{version}</div>
</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 cursor-default">
<div className="font-semibold">Explorer URL</div>
<div className="input border bg-slate-100">{EXPLORER_API}</div>
</div>
</div>
);
}
function AdvancedOptions(props: {
maxSent: number;
maxReceived: number;
loggingLevel: LoggingLevel;
setShouldReload: (reload: boolean) => void;
setMaxSent: (value: number) => void;
setMaxReceived: (value: number) => void;
setDirty: (value: boolean) => void;
setLoggingLevel: (level: LoggingLevel) => 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 as LoggingLevel);
setDirty(true);
setShouldReload(true);
}}
value={loggingLevel}
>
<option value="Error">Error</option>
<option value="Warn">Warn</option>
<option value="Info">Info</option>
<option value="Debug">Debug</option>
<option value="Trace">Trace</option>
</select>
</div>
<div className="flex flex-row flex-nowrap justify-end gap-2 p-2"></div>
</div>
);
}

View File

@@ -1,83 +0,0 @@
import React, { useEffect } from 'react';
import { Navigate, Route, Routes, useNavigate } from 'react-router';
import { useDispatch } from 'react-redux';
import {
setActiveTab,
setRequests,
useActiveTab,
useActiveTabUrl,
} from '../../reducers/requests';
import { BackgroundActiontype } from '../Background/actionTypes';
import Requests from '../Requests';
import Options from '../../components/Options';
import Request from '../Requests/Request';
import Home from '../Home';
import logo from '../../assets/img/icon-128.png';
import RequestBuilder from '../RequestBuilder';
import Notarize from '../../components/Notarize';
import ProofViewer from '../../components/ProofViewer';
import History from '../../components/History';
const Popup = () => {
const dispatch = useDispatch();
const activeTab = useActiveTab();
const url = useActiveTabUrl();
const navigate = useNavigate();
useEffect(() => {
(async () => {
const [tab] = await chrome.tabs.query({
active: true,
lastFocusedWindow: true,
});
dispatch(setActiveTab(tab || null));
const logs = await chrome.runtime.sendMessage({
type: BackgroundActiontype.get_requests,
data: tab?.id,
});
dispatch(setRequests(logs));
const history = await chrome.runtime.sendMessage({
type: BackgroundActiontype.get_prove_requests,
data: tab?.id,
});
})();
}, []);
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">
<img
className="absolute left-2 h-5 cursor-pointer"
src={logo}
alt="logo"
onClick={() => navigate('/')}
/>
<div className="absolute right-2 flex flex-nowrap flex-row items-center gap-1 justify-center w-fit">
<img
src={activeTab?.favIconUrl}
className="h-5 rounded-full"
alt="logo"
/>
<div className="text-xs">{url?.hostname}</div>
</div>
</div>
<Routes>
<Route path="/requests/:requestId/*" element={<Request />} />
<Route path="/notary/:requestId/*" element={<Notarize />} />
<Route path="/verify/:requestId/*" element={<ProofViewer />} />
<Route path="/history" element={<History />} />
<Route path="/requests" element={<Requests />} />
<Route path="/custom/*" element={<RequestBuilder />} />
<Route path="/options" element={<Options />} />
<Route path="/home" element={<Home />} />
<Route path="*" element={<Navigate to="/home" />} />
</Routes>
</div>
);
};
export default Popup;

View File

@@ -0,0 +1,82 @@
import React, {
ReactElement,
useState,
useCallback,
ChangeEventHandler,
} from 'react';
import Icon from '../../components/Icon';
import { BackgroundActiontype } from '../../entries/Background/rpc';
import ProofViewer from '../ProofViewer';
export default function ProofUploader(): ReactElement {
const [proof, setProof] = useState<{
recv: string;
sent: string;
} | null>(null);
const [uploading, setUploading] = useState(false);
const onFileUpload: ChangeEventHandler<HTMLInputElement> = useCallback(
async (e) => {
// @ts-ignore
const [file] = e.target.files || [];
if (file) {
const reader = new FileReader();
reader.addEventListener('load', async (event) => {
const result = event.target?.result;
if (result) {
const proof = JSON.parse(result as string);
const res = await chrome.runtime
.sendMessage<any, { recv: string; sent: string }>({
type: BackgroundActiontype.verify_proof,
data: proof,
})
.catch(() => null);
if (proof) {
setUploading(false);
setProof(res);
}
}
});
setUploading(true);
reader.readAsText(file);
}
},
[],
);
if (proof) {
return <ProofViewer recv={proof.recv} sent={proof.sent} />;
}
return (
<div className="flex flex-col flex-nowrap flex-grow flex-shrink h-0 overflow-y-auto">
<div className="flex flex-col items-center justify-center relative border-slate-400 border-2 text-slate-500 border-dashed flex-grow flex-shrink h-0 m-2 bg-slate-200">
<input
type="file"
className="absolute w-full h-full top-0 left-0 opacity-0 z-10"
onChange={onFileUpload}
accept=".json"
disabled={uploading}
/>
{uploading ? (
<Icon className="animate-spin" fa="fa-solid fa-spinner" size={2} />
) : (
<>
<Icon className="mb-4" fa="fa-solid fa-upload" size={2} />
<div className="text-lg">Drop your proof here to continue</div>
<div className="text-sm">or</div>
<button
className="button !bg-primary/[.8] !hover:bg-primary/[.7] !active:bg-primary !text-white cursor-pointer"
onClick={() => null}
>
Browse Files
</button>
</>
)}
</div>
</div>
);
}

View File

@@ -1,18 +1,24 @@
import React, { ReactNode, ReactElement, useState } from 'react';
import { useParams, useLocation, useNavigate } from 'react-router';
import React, {
ReactNode,
ReactElement,
useState,
MouseEventHandler,
} from 'react';
import { useParams, useNavigate } from 'react-router';
import c from 'classnames';
import { useRequestHistory } from '../../reducers/history';
import RequestBuilder from '../../pages/RequestBuilder';
import Icon from '../../components/Icon';
import { download } from '../../utils/misc';
export default function ProofViewer(): ReactElement {
const {requestId} = useParams<{ requestId: string }>();
export default function ProofViewer(props?: {
recv?: string;
sent?: string;
}): ReactElement {
const { requestId } = useParams<{ requestId: string }>();
const request = useRequestHistory(requestId);
const navigate = useNavigate();
const [ tab, setTab ] = useState('sent');
const loc = useLocation();
const [tab, setTab] = useState('sent');
return (
<div className="flex flex-col w-full py-2 gap-2 flex-grow">
<div className="flex flex-col px-2">
@@ -25,25 +31,24 @@ export default function ProofViewer(): ReactElement {
onClick={() => navigate(-1)}
fa="fa-solid fa-xmark"
/>
<TabLabel
onClick={() => setTab('sent')}
active={tab === 'sent'}
>
<TabLabel onClick={() => setTab('sent')} active={tab === 'sent'}>
Sent
</TabLabel>
<TabLabel
onClick={() => setTab('recv')}
active={tab === 'recv'}
>
<TabLabel onClick={() => setTab('recv')} active={tab === 'recv'}>
Recv
</TabLabel>
<div className="flex flex-row flex-grow items-center justify-end">
<button
className="button"
onClick={() => download(request.id, JSON.stringify(request.proof))}
>
Download
</button>
{!props?.recv && (
<button
className="button"
onClick={() => {
if (!request) return;
download(request.id, JSON.stringify(request.proof));
}}
>
Download
</button>
)}
</div>
</div>
</div>
@@ -51,19 +56,21 @@ export default function ProofViewer(): ReactElement {
{tab === 'sent' && (
<textarea
className="w-full resize-none bg-slate-100 text-slate-800 border p-2 text-[10px] break-all h-full outline-none font-mono"
value={request.verification?.sent}
value={props?.sent || request?.verification?.sent}
readOnly
></textarea>
)}
{tab === 'recv' && (
<textarea
className="w-full resize-none bg-slate-100 text-slate-800 border p-2 text-[10px] break-all h-full outline-none font-mono"
value={request.verification?.recv}
value={props?.recv || request?.verification?.recv}
readOnly
></textarea>
)}
</div>
</div>
);
};
}
function TabLabel(props: {
children: ReactNode;
@@ -74,7 +81,8 @@ function TabLabel(props: {
<button
className={c('px-1 select-none cursor-pointer font-bold', {
'text-slate-800 border-b-2 border-green-500': props.active,
'text-slate-400 border-b-2 border-transparent hover:text-slate-500': !props.active,
'text-slate-400 border-b-2 border-transparent hover:text-slate-500':
!props.active,
})}
onClick={props.onClick}
>

View File

@@ -11,6 +11,20 @@ import { Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import NavigateWithParams from '../../components/NavigateWithParams';
import ResponseDetail from '../../components/ResponseDetail';
import { urlify } from '../../utils/misc';
import { notarizeRequest } from '../../reducers/requests';
import {
getMaxRecv,
getMaxSent,
getNotaryApi,
getProxyApi,
} from '../../utils/storage';
import { useDispatch } from 'react-redux';
import {
formatForRequest,
InputBody,
FormBodyTable,
parseResponse,
} from '../../components/RequestBuilder';
enum TabType {
Params = 'Params',
@@ -25,25 +39,33 @@ export default function RequestBuilder(props?: {
headers?: [string, string, boolean?][];
body?: string;
method?: string;
response?: Response;
}): ReactElement {
const loc = useLocation();
const navigate = useNavigate();
const dispatch = useDispatch();
const subpath = props.subpath || '/custom';
const [_url, setUrl] = useState(props.url || '');
const subpath = props?.subpath || '/custom';
const [_url, setUrl] = useState(props?.url || '');
const [params, setParams] = useState<[string, string, boolean?][]>(
props.params || [],
props?.params || [],
);
const [body, setBody] = useState<string | undefined>(props?.body);
const [formBody, setFormBody] = useState<[string, string, boolean?][]>([
['', '', true],
]);
const [method, setMethod] = useState<string>(props?.method || 'GET');
const [type, setType] = useState<string>('text/plain');
const [headers, setHeaders] = useState<[string, string, boolean?][]>(
props.headers || [],
);
const [body, setBody] = useState<string | undefined>(props.body);
const [method, setMethod] = useState<string>(props.method || 'GET');
const [response, setResponse] = useState<Response | null>(
props.response || null,
props?.headers || [['Content-Type', type, true]],
);
const [responseData, setResponseData] = useState<{
json: any | null;
text: string | null;
img: string | null;
headers: [string, string][] | null;
} | null>(null);
const url = urlify(_url);
const href = !url
@@ -57,6 +79,26 @@ export default function RequestBuilder(props?: {
setParams(Array.from(url?.searchParams || []));
}, [_url]);
useEffect(() => {
updateContentType(type);
}, [type, method]);
const updateContentType = useCallback(
(type: string) => {
const updateHeaders = headers.filter(
([key]) => key.toLowerCase() !== 'content-type',
);
if (method === 'GET' || method === 'HEAD') {
updateHeaders.push(['Content-Type', type, true]);
} else {
updateHeaders.push(['Content-Type', type, false]);
}
setHeaders(updateHeaders);
},
[method, type, headers],
);
const toggleParam = useCallback(
(i: number) => {
params[i][2] = !params[i][2];
@@ -91,7 +133,7 @@ export default function RequestBuilder(props?: {
const sendRequest = useCallback(async () => {
if (!href) return;
setResponseData(null);
// eslint-disable-next-line no-undef
const opts: RequestInit = {
method,
@@ -102,9 +144,13 @@ export default function RequestBuilder(props?: {
return map;
}, {}),
};
if (body) opts.body = body;
if (method !== 'GET' && method !== 'HEAD') {
if (type === 'application/x-www-form-urlencoded') {
opts.body = formatForRequest(formBody, type);
} else {
opts.body = formatForRequest(body!, type);
}
}
const cookie = headers.find(([key]) => key === 'Cookie');
if (cookie) {
@@ -114,15 +160,65 @@ export default function RequestBuilder(props?: {
const res = await fetch(href, opts);
setResponse(res);
const contentType =
res.headers.get('content-type') || res.headers.get('Content-Type');
setResponseData(await parseResponse(contentType!, res));
navigate(subpath + '/response');
}, [href, method, headers, body]);
}, [href, method, headers, body, type]);
const onNotarize = useCallback(async () => {
const maxSentData = await getMaxSent();
const maxRecvData = await getMaxRecv();
const notaryUrl = await getNotaryApi();
const websocketProxyUrl = await getProxyApi();
dispatch(
notarizeRequest(
//@ts-ignore
{
url: href || '',
method,
headers: headers.reduce((map: { [key: string]: string }, [k, v]) => {
if (k !== 'Cookie') {
map[k] = v;
}
return map;
}, {}),
body: body ? formatForRequest(body, type) : undefined,
maxSentData,
maxRecvData,
secretHeaders: [],
secretResps: [],
maxTranscriptSize: 0,
notaryUrl,
websocketProxyUrl,
},
),
);
navigate('/history');
}, [href, method, headers, body, type]);
const onMethod = useCallback(
(e: React.ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value;
if (value === 'GET' || value === 'HEAD') {
setType('');
setMethod(value);
} else {
setMethod(value);
}
},
[method, type],
);
return (
<div className="flex flex-col w-full py-2 gap-2 flex-grow">
<div className="flex flex-row px-2">
<select className="select" onChange={(e) => setMethod(e.target.value)}>
<select className="select" onChange={(e) => onMethod(e)}>
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
@@ -134,8 +230,14 @@ export default function RequestBuilder(props?: {
<input
className="input border flex-grow"
type="text"
value={url ? href : _url}
value={_url}
onChange={(e) => setUrl(e.target.value)}
onBlur={() => {
const formattedUrl = urlify(_url);
if (formattedUrl) {
setUrl(formattedUrl.href);
}
}}
/>
<button className="button" disabled={!url} onClick={sendRequest}>
Send
@@ -161,13 +263,19 @@ export default function RequestBuilder(props?: {
>
Body
</TabLabel>
{response && (
<TabLabel
onClick={() => navigate(subpath + '/response')}
active={loc.pathname.includes('response')}
>
Response
</TabLabel>
{responseData && (
<div className="flex flex-row justify-between w-full">
<TabLabel
onClick={() => navigate(subpath + '/response')}
active={loc.pathname.includes('response')}
>
Response
</TabLabel>
<button className="button" onClick={onNotarize}>
Notarize
</button>
</div>
)}
</div>
</div>
@@ -197,16 +305,38 @@ export default function RequestBuilder(props?: {
<Route
path="body"
element={
<textarea
className="textarea h-full w-full resize-none"
value={body}
onChange={(e) => setBody(e.target.value)}
/>
<div className="h-full">
<select
className={c('select', {
'w-[80px]':
type === 'application/json' ||
type === 'text/plain' ||
type === '',
'w-[200px]': type === 'application/x-www-form-urlencoded',
})}
value={type}
onChange={(e) => setType(e.target.value)}
>
<option value="text/plain">Text</option>
<option value="application/json">JSON</option>
<option value="application/x-www-form-urlencoded">
x-www-form-urlencoded
</option>
</select>
{type === 'application/x-www-form-urlencoded' ? (
<FormBodyTable
formBody={formBody}
setFormBody={setFormBody}
/>
) : (
<InputBody body={body!} setBody={setBody} />
)}
</div>
}
/>
<Route
path="response"
element={<ResponseDetail response={response} />}
element={<ResponseDetail responseData={responseData} />}
/>
<Route path="/" element={<NavigateWithParams to="/params" />} />
</Routes>

View File

@@ -1,14 +1,11 @@
import React, { ReactElement } from 'react';
import RequestDetail from '../../components/RequestDetail';
import { useParams } from 'react-router';
import { useRequest } from '../../reducers/requests';
export default function Request(): ReactElement {
const params = useParams<{ requestId: string }>();
return (
<>
<RequestDetail requestId={params.requestId} />
</>
<>{!!params.requestId && <RequestDetail requestId={params.requestId} />}</>
);
}

View File

@@ -0,0 +1,119 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import Icon from '../../components/Icon';
import { useSearchParams } from 'react-router-dom';
import { type PluginConfig, PluginMetadata, urlify } from '../../utils/misc';
import browser from 'webextension-polyfill';
import { BackgroundActiontype } from '../../entries/Background/rpc';
import { BaseApproval } from '../BaseApproval';
import { PluginPermissions } from '../../components/PluginInfo';
import {
getPluginConfigByHash,
getPluginMetadataByHash,
} from '../../entries/Background/db';
import { runPlugin } from '../../utils/rpc';
export function RunPluginApproval(): ReactElement {
const [params] = useSearchParams();
const origin = params.get('origin');
const favIconUrl = params.get('favIconUrl');
const hash = params.get('hash');
const hostname = urlify(origin || '')?.hostname;
const [error, showError] = useState('');
const [metadata, setPluginMetadata] = useState<PluginMetadata | null>(null);
const [pluginContent, setPluginContent] = useState<PluginConfig | null>(null);
const onCancel = useCallback(() => {
browser.runtime.sendMessage({
type: BackgroundActiontype.run_plugin_response,
data: false,
});
}, []);
const onAccept = useCallback(async () => {
if (!hash) return;
try {
const tab = await browser.tabs.create({
active: true,
});
await browser.storage.local.set({ plugin_hash: hash });
// @ts-ignore
if (chrome.sidePanel) await chrome.sidePanel.open({ tabId: tab.id });
browser.runtime.sendMessage({
type: BackgroundActiontype.run_plugin_response,
data: true,
});
} catch (e: any) {
showError(e.message);
}
}, [hash]);
useEffect(() => {
(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]);
return (
<BaseApproval
header={`Execute 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 execute 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 gap-4 border border-slate-300 p-4 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-2xl text-blue-600 font-semibold">
{pluginContent.title}
</span>
<div className="text-slate-500 text-base">
{pluginContent.description}
</div>
</div>
</div>
)}
</BaseApproval>
);
}

View File

@@ -1,7 +1,7 @@
import {
BackgroundActiontype,
RequestHistory,
} from '../pages/Background/actionTypes';
} from '../entries/Background/rpc';
import { useSelector } from 'react-redux';
import { AppRootState } from './index';
import deepEqual from 'fast-deep-equal';
@@ -30,7 +30,7 @@ const initialState: State = {
order: [],
};
export const addRequestHistory = (request: RequestHistory) => {
export const addRequestHistory = (request?: RequestHistory | null) => {
return {
type: ActionType['/history/addRequest'],
payload: request,
@@ -56,6 +56,9 @@ export default function history(
switch (action.type) {
case ActionType['/history/addRequest']: {
const payload: RequestHistory = action.payload;
if (!payload) return state;
const existing = state.map[payload.id];
const newMap = {
...state.map,
@@ -91,8 +94,15 @@ export const useHistoryOrder = (): string[] => {
}, deepEqual);
};
export const useRequestHistory = (id: string): RequestHistory | undefined => {
export const useAllProofHistory = (): RequestHistory[] => {
return useSelector((state: AppRootState) => {
return state.history.order.map((id) => state.history.map[id]);
}, deepEqual);
};
export const useRequestHistory = (id?: string): RequestHistory | undefined => {
return useSelector((state: AppRootState) => {
if (!id) return undefined;
return state.history.map[id];
}, deepEqual);
};

View File

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

54
src/reducers/plugins.tsx Normal file
View File

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

View File

@@ -1,14 +1,24 @@
import { type RequestLog, type RequestHistory } from '../pages/Background/actionTypes';
import {
type RequestLog,
type RequestHistory,
} from '../entries/Background/rpc';
import { useSelector } from 'react-redux';
import { AppRootState } from './index';
import deepEqual from 'fast-deep-equal';
import { get, NOTARY_API_LS_KEY, PROXY_API_LS_KEY } from '../utils/storage';
import { BackgroundActiontype } from '../pages/Background/actionTypes';
import {
getNotaryApi,
getProxyApi,
getMaxSent,
getMaxRecv,
} from '../utils/storage';
import { BackgroundActiontype } from '../entries/Background/rpc';
import browser from 'webextension-polyfill';
enum ActionType {
'/requests/setRequests' = '/requests/setRequests',
'/requests/addRequest' = '/requests/addRequest',
'/requests/setActiveTab' = '/requests/setActiveTab',
'/requests/isConnected' = '/requests/isConnected',
}
type Action<payload> = {
@@ -23,11 +33,22 @@ type State = {
[requestId: string]: RequestLog;
};
activeTab: chrome.tabs.Tab | null;
isConnected: boolean;
};
const initialState: State = {
map: {},
activeTab: null,
isConnected: false,
};
export const setConnection = (isConnected: boolean): Action<boolean> => ({
type: ActionType['/requests/isConnected'],
payload: isConnected,
});
export const isConnected = (isConnected: boolean) => async () => {
return isConnected;
};
export const setRequests = (requests: RequestLog[]): Action<RequestLog[]> => ({
@@ -36,21 +57,10 @@ export const setRequests = (requests: RequestLog[]): Action<RequestLog[]> => ({
});
export const notarizeRequest = (options: RequestHistory) => async () => {
const notaryUrl = await get(NOTARY_API_LS_KEY);
const websocketProxyUrl = await get(PROXY_API_LS_KEY);
console.log({
type: BackgroundActiontype.prove_request_start,
data: {
url: options.url,
method: options.method,
headers: options.headers,
body: options.body,
maxTranscriptSize: options.maxTranscriptSize,
notaryUrl,
websocketProxyUrl,
},
})
const notaryUrl = await getNotaryApi();
const websocketProxyUrl = await getProxyApi();
const maxSentData = await getMaxSent();
const maxRecvData = await getMaxRecv();
chrome.runtime.sendMessage<any, string>({
type: BackgroundActiontype.prove_request_start,
@@ -60,15 +70,19 @@ export const notarizeRequest = (options: RequestHistory) => async () => {
headers: options.headers,
body: options.body,
maxTranscriptSize: options.maxTranscriptSize,
maxSentData,
maxRecvData,
secretHeaders: options.secretHeaders,
secretResps: options.secretResps,
notaryUrl,
websocketProxyUrl,
},
});
}
};
export const setActiveTab = (
activeTab: chrome.tabs.Tab | null,
): Action<chrome.tabs.Tab | null> => ({
activeTab: browser.Tabs.Tab | null,
): Action<browser.Tabs.Tab | null> => ({
type: ActionType['/requests/setActiveTab'],
payload: activeTab,
});
@@ -87,7 +101,7 @@ export default function requests(
return {
...state,
map: {
...(action.payload || []).reduce(
...(action?.payload || []).reduce(
(acc: { [requestId: string]: RequestLog }, req: RequestLog) => {
if (req) {
acc[req.requestId] = req;
@@ -111,6 +125,11 @@ export default function requests(
[action.payload.requestId]: action.payload,
},
};
case ActionType['/requests/isConnected']:
return {
...state,
isConnected: action.payload,
};
default:
return state;
}
@@ -140,3 +159,7 @@ export const useActiveTabUrl = (): URL | null => {
return activeTab?.url ? new URL(activeTab.url) : null;
}, deepEqual);
};
export const useIsConnected = (): boolean => {
return useSelector((state: AppRootState) => state.requests.isConnected);
};

5
src/utils/constants.ts Normal file
View File

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

View File

@@ -1,3 +1,22 @@
import {
BackgroundActiontype,
handleExecPluginProver,
RequestLog,
} from '../entries/Background/rpc';
import { EXPLORER_API } from './constants';
import createPlugin, {
CallContext,
ExtismPluginOptions,
Plugin,
} from '@extism/extism';
import browser from 'webextension-polyfill';
import NodeCache from 'node-cache';
import { getNotaryApi, getProxyApi } from './storage';
import { minimatch } from 'minimatch';
import { getCookiesByHost, getHeadersByHost } from '../entries/Background/db';
const charwise = require('charwise');
export function urlify(
text: string,
params?: [string, string, boolean?][],
@@ -17,15 +36,18 @@ export function urlify(
}
}
export function devlog(text: string) {
export function devlog(...args: any[]) {
if (process.env.NODE_ENV === 'development') {
console.log(text);
console.log(...args);
}
}
export function download(filename: string, content: string) {
var element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(content));
const element = document.createElement('a');
element.setAttribute(
'href',
'data:text/plain;charset=utf-8,' + encodeURIComponent(content),
);
element.setAttribute('download', filename);
element.style.display = 'none';
@@ -35,3 +57,336 @@ export function download(filename: string, content: string) {
document.body.removeChild(element);
}
export async function upload(filename: string, content: string) {
const formData = new FormData();
formData.append(
'file',
new Blob([content], { type: 'application/json' }),
filename,
);
const response = await fetch(`${EXPLORER_API}/api/upload`, {
method: 'POST',
body: formData,
});
if (!response.ok) {
throw new Error('Failed to upload');
}
const data = await response.json();
return data;
}
export const copyText = async (text: string): Promise<void> => {
try {
await navigator.clipboard.writeText(text);
} catch (e) {
console.error(e);
}
};
export async function replayRequest(req: RequestLog): Promise<string> {
const options = {
method: req.method,
headers: req.requestHeaders.reduce(
// @ts-ignore
(acc: { [key: string]: string }, h: chrome.webRequest.HttpHeader) => {
if (typeof h.name !== 'undefined' && typeof h.value !== 'undefined') {
acc[h.name] = h.value;
}
return acc;
},
{},
),
body: req.requestBody,
};
if (req?.formData) {
const formData = new URLSearchParams();
Object.entries(req.formData).forEach(([key, values]) => {
values.forEach((v) => formData.append(key, v));
});
options.body = formData.toString();
}
// @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');
if (contentType?.includes('application/json')) {
return resp.text();
} else if (contentType?.includes('text')) {
return resp.text();
} else if (contentType?.includes('image')) {
return resp.blob().then((blob) => blob.text());
} 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 || 'x://x',
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 && minimatch(params.url, 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}`);
}
(async () => {
const {
url,
method,
headers,
getSecretResponse,
body: reqBody,
} = params;
let secretResps;
const resp = await fetch(url, {
method,
headers,
body: reqBody,
});
const body = await extractBodyFromResponse(resp);
if (getSecretResponse) {
const out = await plugin.call(getSecretResponse, body);
secretResps = JSON.parse(out.string());
}
handleExecPluginProver({
type: BackgroundActiontype.execute_plugin_prover,
data: {
...params,
body: reqBody,
secretResps,
now,
},
});
})();
return context.store(`${id}`);
},
};
const funcs: {
[key: string]: (callContext: CallContext, ...args: any[]) => any;
} = {};
for (const fn of Object.keys(VALID_HOST_FUNCS)) {
funcs[fn] = function (context: CallContext) {
throw new Error(`no permission for ${fn}`);
};
}
if (config?.hostFunctions) {
for (const fn of config.hostFunctions) {
funcs[fn] = HostFunctions[fn];
}
}
if (config?.cookies) {
const cookies: { [hostname: string]: { [key: string]: string } } = {};
for (const host of config.cookies) {
const cache = await getCookiesByHost(host);
cookies[host] = cache;
}
// @ts-ignore
injectedConfig.cookies = JSON.stringify(cookies);
}
if (config?.headers) {
const headers: { [hostname: string]: { [key: string]: string } } = {};
for (const host of config.headers) {
const cache = await getHeadersByHost(host);
headers[host] = cache;
}
// @ts-ignore
injectedConfig.headers = JSON.stringify(headers);
}
const pluginConfig: ExtismPluginOptions = {
useWasi: true,
config: injectedConfig,
// allowedHosts: approvedRequests.map((r) => urlify(r.url)?.origin),
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 type PluginMetadata = {
origin: string;
filePath: string;
} & { [k: string]: 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;
}, {});
};
export function safeParseJSON(data?: string | null) {
try {
return JSON.parse(data!);
} catch (e) {
return null;
}
}

87
src/utils/plugins.tsx Normal file
View File

@@ -0,0 +1,87 @@
import { PluginConfig } from './misc';
import React, { ReactElement, ReactNode } from 'react';
import Icon from '../components/Icon';
export const HostFunctionsDescriptions: {
[key: string]: (pluginContent: PluginConfig) => ReactElement;
} = {
redirect: () => {
return (
<PermissionDescription fa="fa-solid fa-diamond-turn-right">
<span>Redirect your current tab to any URL</span>
</PermissionDescription>
);
},
notarize: ({ notaryUrls, proxyUrls }) => {
const notaries = ['default notary'].concat(notaryUrls || []);
const proxies = ['default proxy'].concat(proxyUrls || []);
return (
<>
<PermissionDescription fa="fa-solid fa-route">
<span className="cursor-default">
<span className="mr-1">Proxy notarization requests thru</span>
<MultipleParts parts={proxies} />
</span>
</PermissionDescription>
<PermissionDescription fa="fa-solid fa-stamp">
<span className="cursor-default">
<span className="mr-1">Submit notarization requests to</span>
<MultipleParts parts={notaries} />
</span>
</PermissionDescription>
</>
);
},
};
export function PermissionDescription({
fa,
children,
}: {
fa: string;
children?: ReactNode;
}): ReactElement {
return (
<div className="flex flex-row gap-4 items-start cursor-default">
<Icon className="" size={1.6125} fa={fa} />
<div className="text-sm mt-[0.125rem]">{children}</div>
</div>
);
}
export function MultipleParts({ parts }: { parts: string[] }): ReactElement {
const content = [];
if (parts.length > 1) {
for (let i = 0; i < parts.length; i++) {
content.push(
<span key={i} className="text-blue-600">
{parts[i]}
</span>,
);
if (parts.length - i === 2) {
content.push(
<span key={i + 'separator'} className="inline-block mx-1">
and
</span>,
);
} else if (parts.length - i > 1) {
content.push(
<span key={i + 'separator'} className="inline-block mr-1">
,
</span>,
);
}
}
} else {
content.push(
<span key={0} className="text-blue-600">
{parts[0]}
</span>,
);
}
return <>{content}</>;
}

17
src/utils/promise.ts Normal file
View File

@@ -0,0 +1,17 @@
export const deferredPromise = (): {
promise: Promise<never>;
resolve: (data?: any) => void;
reject: (reason?: any) => void;
} => {
let resolve: (data?: any) => void, reject: (reason?: any) => void;
const promise = new Promise((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
});
// @ts-ignore
return { promise, resolve, reject };
};
export type PromiseResolvers = ReturnType<typeof deferredPromise>;

50
src/utils/rpc.ts Normal file
View File

@@ -0,0 +1,50 @@
import browser from 'webextension-polyfill';
import { BackgroundActiontype } from '../entries/Background/rpc';
import { PluginConfig } from './misc';
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,14 +1,38 @@
import { LoggingLevel } from 'tlsn-js';
export const NOTARY_API_LS_KEY = 'notary-api';
export const PROXY_API_LS_KEY = 'proxy-api';
export const HISTORY_LS_KEY = 'history';
export const MAX_SENT_LS_KEY = 'max-sent';
export const MAX_RECEIVED_LS_KEY = 'max-received';
export const LOGGING_FILTER_KEY = 'logging-filter-2';
export async function set(key: string, value: string) {
return chrome.storage.sync.set({ [key]: value });
}
export async function get(key: string) {
export async function get(key: string, defaultValue?: string) {
return chrome.storage.sync
.get(key)
.then((json: any) => json[key])
.catch(() => '');
.then((json: any) => json[key] || defaultValue)
.catch(() => defaultValue);
}
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.6');
}
export async function getProxyApi() {
return await get(PROXY_API_LS_KEY, 'wss://notary.pse.dev/proxy');
}
export async function getLoggingFilter(): Promise<LoggingLevel> {
return await get(LOGGING_FILTER_KEY, 'Info');
}

View File

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

18
src/utils/types.ts Normal file
View File

@@ -0,0 +1,18 @@
export type Proof = ProofV0 | ProofV1;
export type ProofV0 = {
version?: undefined;
session: any;
substrings: any;
notaryUrl: string;
};
export type ProofV1 = {
version: '1.0';
data: string;
meta: {
notaryUrl: string;
websocketProxyUrl: string;
pluginUrl?: string;
};
};

View File

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

View File

@@ -1,16 +0,0 @@
[
{
"url": "https://api.twitter.com/1.1/account/settings.json",
"method": "GET",
"type": "xmlhttprequest",
"title": "Twitter Profile",
"description": "Notarize ownership of a twitter profile. To start, go to your own profile"
},
{
"url": "https://gateway.reddit.com/desktopapi/v1/prefs",
"method": "GET",
"type": "xmlhttprequest",
"title": "Reddit Profile",
"description": "Notarize ownership of a reddit profile. To start, go to reddit.com/settings"
}
]

View File

@@ -1,34 +0,0 @@
import bookmarks from './bookmarks.json';
import { RequestLog } from '../../src/pages/Background/actionTypes';
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,22 +0,0 @@
#!/bin/bash
# Set the directory path where you want to clone the repository
target_directory="tlsn"
# GitHub repository URL
github_url="https://github.com/tlsnotary/tlsn.git"
# Check if the target directory exists
if [ -d "$target_directory" ]; then
echo "tlsn already exists."
else
# Clone the repository if the directory doesn't exist
git clone "$github_url" "$target_directory"
# Check if the cloning was successful
if [ $? -eq 0 ]; then
echo "tlsn cloned successfully."
else
echo "Error cloning repository."
fi
fi

View File

@@ -1,111 +0,0 @@
[package]
name = "tlsn-extension-rs"
version = "0.1.0"
authors = ["The tlsn-extension Developers"]
edition = "2018"
rust-version = "1.56"
[lib]
crate-type = ["cdylib"]
[dependencies]
rayon = "1.5"
wasm-bindgen-rayon = "1.0"
wasm-bindgen = "0.2.87"
js-sys = "0.3.64"
tracing = "0.1"
getrandom = { version = "0.2", features = ["js"] }
ws_stream_wasm = "0.7.4"
wasm-bindgen-futures = "0.4.37"
tokio-util = "0.7"
futures = "0.3"
serde_json = "1.0"
serde = { version = "1.0.147", features = ["derive"] }
serde-wasm-bindgen = "0.4"
url = { version = "2.0", features = ["serde"] }
futures-util = "0.3.28"
chrono = "0.4"
elliptic-curve = { version = "0.13.5", features = ["pkcs8"] }
p256 = { version = "0.13", features = ["pem", "ecdsa"] }
hyper = { version = "0.14", features = ["client", "http1"] }
console_error_panic_hook = "0.1.7"
tracing-web = "0.1.2"
tracing-subscriber = { version = "0.3", features = ["time"] }
# time crate: https://crates.io/crates/time
# NOTE: It is required, otherwise "time not implemented on this platform" error happens right after "!@# 2".
# Probably due to tokio's time feature is used in tlsn-prover?
time = { version = "0.3", features = ["wasm-bindgen"] }
# Used to calculate elapsed time.
web-time = "0.2"
# tlsn-prover = { path = "../tlsn/tlsn/tlsn-prover", features = ["tracing"] }
[dependencies.tlsn-prover]
git = "https://github.com/mhchia/tlsn.git"
branch = "dev-20231006-webtime-2"
package = "tlsn-prover"
features = ["tracing"]
[dependencies.tlsn-core]
git = "https://github.com/mhchia/tlsn.git"
branch = "dev-20231006-webtime-2"
package = "tlsn-core"
[dependencies.web-sys]
version = "0.3.4"
features = [
"BinaryType",
"Blob",
"ErrorEvent",
"FileReader",
"MessageEvent",
"ProgressEvent",
"WebSocket",
"console",
'Document',
'HtmlElement',
'HtmlInputElement',
'Window',
'Worker',
'Headers',
'Request',
'RequestInit',
'RequestMode',
'Response',
]
# Replace ring with the forked version `ring-xous` implemented in pure rust
# to make it compiled to wasm.
# Refs:
# - Rationale for `ring-xous`: https://www.bunniestudios.com/blog/?p=6521
# - Issue for wasm comptability: https://github.com/briansmith/ring/issues/918
[patch.crates-io.ring]
git="https://github.com/betrusted-io/ring-xous"
branch="0.16.20-cleanup"
[patch.crates-io.ws_stream_wasm]
# path = "../../../others/ws_stream_wasm"
# Use the patched ws_stream_wasm to fix the issue https://github.com/najamelan/ws_stream_wasm/issues/12#issuecomment-1711902958
git="https://github.com/mhchia/ws_stream_wasm"
branch="dev"
# [patch.'https://github.com/tlsnotary/tlsn-utils']
# # Use single cpu backend
# tlsn-utils = { git = 'https://www.github.com/mhchia/tlsn-utils.git', rev = "46327f0" }
# tlsn-utils-aio = { git = 'https://www.github.com/mhchia/tlsn-utils.git', rev = "46327f0" }
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
console_error_panic_hook = { version = "0.1.7", optional = true }
[dev-dependencies]
wasm-bindgen-test = "0.3.34"
[profile.release]
# Tell `rustc` to optimize for small code size.
[package.metadata.wasm-pack.profile.release]
wasm-opt = false

Some files were not shown because too many files have changed in this diff Show More