Compare commits
264 Commits
v0.0.1
...
v0.1.0-dev
Author | SHA1 | Date |
---|---|---|
semantic-release-bot | 33f0b4c6d0 | |
Derrick Hammer | 062f5c9899 | |
Derrick Hammer | 47f768b20b | |
Derrick Hammer | c08e130e97 | |
Derrick Hammer | ec43f49562 | |
Derrick Hammer | 93c110c755 | |
Derrick Hammer | a0d33d3880 | |
Derrick Hammer | 382761e484 | |
Derrick Hammer | 188f7986b6 | |
Derrick Hammer | e317a3209d | |
Derrick Hammer | f1ca174022 | |
Derrick Hammer | f7ba77e04f | |
Derrick Hammer | e933cbe918 | |
Derrick Hammer | 20543a0612 | |
Derrick Hammer | 52c209dbb9 | |
Derrick Hammer | 4f920d6958 | |
Derrick Hammer | 69e999aef5 | |
Derrick Hammer | 533e741c15 | |
Derrick Hammer | 9f07b21566 | |
Derrick Hammer | d5f78e4d27 | |
Derrick Hammer | 6c9fe8208a | |
Derrick Hammer | 23f9e0f6bd | |
Derrick Hammer | b39d0dd7e7 | |
Derrick Hammer | 00837eb143 | |
Derrick Hammer | 2dafa592ed | |
Derrick Hammer | 7b78e882d8 | |
Derrick Hammer | 1350929bca | |
Derrick Hammer | 3d64870f17 | |
Derrick Hammer | a2055079d1 | |
Derrick Hammer | 6964c6bed2 | |
Derrick Hammer | c1e474e025 | |
Derrick Hammer | 0ebd876c67 | |
Derrick Hammer | c7e0e23950 | |
Derrick Hammer | b6a6bdc97f | |
Derrick Hammer | d06575e9ee | |
Derrick Hammer | ddc93ba71e | |
Derrick Hammer | 7ec8bb79db | |
Derrick Hammer | 1bd466cc20 | |
Derrick Hammer | 2e3fe286f9 | |
Derrick Hammer | 3facaddae8 | |
Derrick Hammer | abafc1c715 | |
Derrick Hammer | 6621f25b5e | |
Derrick Hammer | 6d2cffd869 | |
Derrick Hammer | b3fb59d283 | |
Derrick Hammer | c7f0dd586d | |
Derrick Hammer | c6d4ea5a8e | |
Derrick Hammer | c25bf102fe | |
Derrick Hammer | 79f981d789 | |
Derrick Hammer | 0736469296 | |
Derrick Hammer | 69e613075e | |
Derrick Hammer | 671c7ad6a1 | |
Derrick Hammer | 5439c9dc92 | |
Derrick Hammer | c26be980c5 | |
Derrick Hammer | d5a4956be7 | |
Derrick Hammer | a9366c915d | |
Derrick Hammer | 90e0f3f2c4 | |
Derrick Hammer | 38d6198628 | |
Derrick Hammer | 053eca0cf4 | |
Derrick Hammer | a0504443e6 | |
Derrick Hammer | 21887df639 | |
Derrick Hammer | a628a9a07f | |
Derrick Hammer | 1b7ad1a896 | |
Derrick Hammer | 3ac8e38f66 | |
Derrick Hammer | 356681af35 | |
Derrick Hammer | 714da70209 | |
Derrick Hammer | 3b1b6425ae | |
Derrick Hammer | 7f1dde272a | |
Derrick Hammer | f720f40f05 | |
Derrick Hammer | 2b12150d71 | |
Derrick Hammer | 556373c5bc | |
Derrick Hammer | a003da1606 | |
Derrick Hammer | 58e95806d0 | |
Derrick Hammer | 3600fbfdcf | |
Derrick Hammer | 2d30390fa2 | |
Derrick Hammer | d3d0f387b6 | |
Derrick Hammer | eb2a19121f | |
Derrick Hammer | d5138c3860 | |
Derrick Hammer | 594e8d82a1 | |
Derrick Hammer | 90d8bfba63 | |
Derrick Hammer | da14bac9b2 | |
Derrick Hammer | 8d52e40f20 | |
Derrick Hammer | 72c663795a | |
Derrick Hammer | 74c20c6042 | |
Derrick Hammer | ce23f0a7b8 | |
Derrick Hammer | 5c85ad2962 | |
Derrick Hammer | f8a698c656 | |
Derrick Hammer | 5dee9aeed4 | |
Derrick Hammer | 2ae556ad14 | |
Derrick Hammer | 523f2c04f0 | |
Derrick Hammer | ba86b3587b | |
Derrick Hammer | 831612bc77 | |
Derrick Hammer | 6a41daff62 | |
Derrick Hammer | d569c5fd8b | |
Derrick Hammer | 3a0ed24b9d | |
Derrick Hammer | f71d8f8685 | |
Derrick Hammer | 2c22c88e30 | |
Derrick Hammer | 14edcdc6ce | |
Derrick Hammer | be22b771f5 | |
Derrick Hammer | 1b78d0e696 | |
Derrick Hammer | 90ef9e3386 | |
Derrick Hammer | ca65db07a3 | |
Derrick Hammer | 1da240ecd6 | |
Derrick Hammer | bcc63ae15d | |
Derrick Hammer | 89b0942bc8 | |
Derrick Hammer | 7c9a7230fe | |
Derrick Hammer | 6a310a1e1f | |
Derrick Hammer | 47705c2f12 | |
Derrick Hammer | 24ec1e6526 | |
Derrick Hammer | af7e441c7c | |
Derrick Hammer | 13522532a3 | |
Derrick Hammer | 3d29026a22 | |
Derrick Hammer | 5a4537ffd3 | |
Derrick Hammer | aa2dfe0ba0 | |
Derrick Hammer | edda68506c | |
Derrick Hammer | 630dce7b05 | |
Derrick Hammer | 7b26856720 | |
Derrick Hammer | af4e6155bc | |
Derrick Hammer | 1e3b0e46fe | |
Derrick Hammer | caa72f93fc | |
Derrick Hammer | b9b9ae0afa | |
Derrick Hammer | 6fe25b7f17 | |
Derrick Hammer | 212edf184d | |
Derrick Hammer | 7b9c667749 | |
Derrick Hammer | d2acea6781 | |
Derrick Hammer | 9b9ff2118c | |
Derrick Hammer | b57644836f | |
Derrick Hammer | c207452c65 | |
Derrick Hammer | c139eb3165 | |
Derrick Hammer | 2bc9c03ea1 | |
Derrick Hammer | b7cd9ac5e2 | |
Derrick Hammer | a6eef21da0 | |
Derrick Hammer | 88a827276e | |
Derrick Hammer | 05223c84b8 | |
Derrick Hammer | ccd2b4fd78 | |
Derrick Hammer | bf8a3f9aa1 | |
Derrick Hammer | f86c924299 | |
Derrick Hammer | f164f7a6d3 | |
Derrick Hammer | 3fffc08d54 | |
Derrick Hammer | f597afac6a | |
Derrick Hammer | ef03883605 | |
Derrick Hammer | 84b69e09af | |
Derrick Hammer | f7a696a65f | |
Derrick Hammer | ce5d66095f | |
Derrick Hammer | b7fc6834eb | |
Derrick Hammer | 56673a6bc1 | |
Derrick Hammer | 457398b291 | |
Derrick Hammer | 6fa5ccd49a | |
Derrick Hammer | 480bfdd0d0 | |
Derrick Hammer | b50b8e8ced | |
Derrick Hammer | 37b0e824c4 | |
Derrick Hammer | a85b20769a | |
Derrick Hammer | 5fba29b3ee | |
Derrick Hammer | 7fad293bdc | |
Derrick Hammer | db4b61bdd9 | |
Derrick Hammer | d62ce9a4d3 | |
Derrick Hammer | 2db95ff746 | |
Derrick Hammer | f849579156 | |
Derrick Hammer | 2b5d3ef646 | |
Derrick Hammer | 523fe07028 | |
Derrick Hammer | fe14767e5a | |
Derrick Hammer | d2ca086c11 | |
Derrick Hammer | 0e39f3d658 | |
Derrick Hammer | f74c66ab5f | |
Derrick Hammer | 9d6a198bca | |
Derrick Hammer | d14320b9d0 | |
Derrick Hammer | 7fa2ec9f8e | |
Derrick Hammer | 621230ef2c | |
Derrick Hammer | b0fcc1d835 | |
Derrick Hammer | b2665df70b | |
Derrick Hammer | 3b1e31ea2d | |
Derrick Hammer | 014f92342a | |
Derrick Hammer | a062a522d3 | |
Derrick Hammer | 23e1eb79ff | |
Derrick Hammer | 6445ad0c82 | |
Derrick Hammer | 6273c634e9 | |
Derrick Hammer | 01de0d586b | |
Derrick Hammer | 06b3f03fec | |
Derrick Hammer | 1a99c3d8cd | |
Derrick Hammer | 8cf2770f42 | |
Derrick Hammer | 1c57d0af4a | |
Derrick Hammer | 38c85468a4 | |
Derrick Hammer | 8e8039eee6 | |
Derrick Hammer | 4867c9a986 | |
Derrick Hammer | 2700630a9c | |
Derrick Hammer | db9ada506b | |
Derrick Hammer | d7c506757c | |
Derrick Hammer | b6f1e4ba66 | |
Derrick Hammer | faec15f06d | |
Derrick Hammer | de38a16ac1 | |
Derrick Hammer | 4057f4d388 | |
Derrick Hammer | 7c873db91b | |
Derrick Hammer | 5e4f45180e | |
Derrick Hammer | 487ba0b5bd | |
Derrick Hammer | 58ff8f2f92 | |
Derrick Hammer | f33e77da54 | |
Derrick Hammer | 3736bf0e51 | |
Derrick Hammer | 58190128a0 | |
Derrick Hammer | d28868508b | |
Derrick Hammer | 485fa98f0e | |
Derrick Hammer | bd1226ad18 | |
Derrick Hammer | 55fa792bc9 | |
Derrick Hammer | 6413d97c61 | |
Derrick Hammer | 324106d54f | |
Derrick Hammer | 884ba62bde | |
Derrick Hammer | cd2f3a415c | |
Derrick Hammer | d83af7bf1a | |
Derrick Hammer | 15c127f26b | |
Derrick Hammer | 724a0f0135 | |
Derrick Hammer | 66995f5d7f | |
Derrick Hammer | c6652efeb0 | |
Derrick Hammer | 3835cf014e | |
Derrick Hammer | aad4a075cf | |
Derrick Hammer | d54fe4666f | |
Derrick Hammer | ce713cdb15 | |
Derrick Hammer | 4742c6844c | |
Derrick Hammer | d2d9c0445c | |
Derrick Hammer | 83af4fb226 | |
Derrick Hammer | 80a3354866 | |
Derrick Hammer | 7bd0a72113 | |
Derrick Hammer | c25e8b4aff | |
Derrick Hammer | 561be145ce | |
Derrick Hammer | 2a6a0bf94c | |
Derrick Hammer | ededa55b57 | |
Derrick Hammer | d8f0288370 | |
Derrick Hammer | 64b80001a0 | |
Derrick Hammer | b4e09c05cf | |
Derrick Hammer | 8b1e823991 | |
Derrick Hammer | 67e432c345 | |
Derrick Hammer | c6b136c214 | |
Derrick Hammer | eedd6e7da5 | |
Derrick Hammer | 8ea5a38b83 | |
Derrick Hammer | 9e480f1cfb | |
Derrick Hammer | bbc9020f66 | |
Derrick Hammer | cd7b12e8b3 | |
Derrick Hammer | e35b602133 | |
Derrick Hammer | 0e6c84c566 | |
Derrick Hammer | 4121e23fd9 | |
Derrick Hammer | a87660b678 | |
Derrick Hammer | 7dff9a1ab4 | |
Derrick Hammer | 9393ffc4c1 | |
Derrick Hammer | c8c19b77a6 | |
Derrick Hammer | d7897af137 | |
Derrick Hammer | 64611618de | |
Derrick Hammer | 616b74a820 | |
Derrick Hammer | 4bb0636a8d | |
Derrick Hammer | 69fd9a14ef | |
Derrick Hammer | 0387316e4f | |
Derrick Hammer | 9b15c738e9 | |
Derrick Hammer | cb2299f9e8 | |
Derrick Hammer | 77d72a6666 | |
Derrick Hammer | 029aab6901 | |
Derrick Hammer | 1c37f7809c | |
Derrick Hammer | 2f9a0c7356 | |
Derrick Hammer | 91642ea729 | |
Derrick Hammer | c5febf06d8 | |
Derrick Hammer | 8895f557e5 | |
Derrick Hammer | 364e628c7a | |
Derrick Hammer | 86ce21a4b4 | |
Derrick Hammer | 9ce66b15a3 | |
Derrick Hammer | 83b62bfdcb | |
Derrick Hammer | 5c02356595 | |
Derrick Hammer | ec33e40c74 | |
Derrick Hammer | ebd09f9a52 | |
Derrick Hammer | 0d5aa24b74 |
|
@ -0,0 +1,37 @@
|
||||||
|
version: 2.1
|
||||||
|
|
||||||
|
orbs:
|
||||||
|
node: circleci/node@5.1.0
|
||||||
|
ssh: credijusto/ssh@0.5.2
|
||||||
|
workflows:
|
||||||
|
release:
|
||||||
|
jobs:
|
||||||
|
- node/run:
|
||||||
|
name: build
|
||||||
|
npm-run: build
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only:
|
||||||
|
- master
|
||||||
|
- develop
|
||||||
|
- /^develop-.*$/
|
||||||
|
- node/run:
|
||||||
|
name: release
|
||||||
|
npm-run: semantic-release
|
||||||
|
requires:
|
||||||
|
- build
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only:
|
||||||
|
- master
|
||||||
|
- develop
|
||||||
|
- /^develop-.*$/
|
||||||
|
|
||||||
|
context:
|
||||||
|
- publish
|
||||||
|
setup:
|
||||||
|
- add_ssh_keys:
|
||||||
|
fingerprints:
|
||||||
|
- "47:cf:a1:17:d9:81:8e:c5:51:e5:53:c8:33:e4:33:b9"
|
||||||
|
- ssh/ssh-add-host:
|
||||||
|
host_url: GITEA_HOST
|
|
@ -1 +1,8 @@
|
||||||
node_modules
|
node_modules
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/cache
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/sdks
|
||||||
|
!.yarn/versions
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
public-hoist-pattern[]=udx-native
|
||||||
|
public-hoist-pattern[]=sodium-native
|
||||||
|
public-hoist-pattern[]=@hyperswarm/dht
|
||||||
|
public-hoist-pattern[]=hypercore-crypto
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"plugins": [
|
||||||
|
"@semantic-release/commit-analyzer",
|
||||||
|
"@semantic-release/release-notes-generator",
|
||||||
|
"@semantic-release/changelog",
|
||||||
|
[
|
||||||
|
"@semantic-release/exec",
|
||||||
|
{
|
||||||
|
"publishCmd": "./ci/publish.sh \"${nextRelease.version}\""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@semantic-release/npm",
|
||||||
|
{
|
||||||
|
"npmPublish": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@semantic-release/git",
|
||||||
|
{
|
||||||
|
"assets": [
|
||||||
|
"package.json"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"branches": [
|
||||||
|
"master",
|
||||||
|
{
|
||||||
|
name: "develop",
|
||||||
|
prerelease: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "develop-*",
|
||||||
|
prerelease: true
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
|
@ -1,34 +0,0 @@
|
||||||
pipeline:
|
|
||||||
build:
|
|
||||||
image: git.lumeweb.com/lumeweb/ci-node
|
|
||||||
commands:
|
|
||||||
- yarn
|
|
||||||
- yarn build
|
|
||||||
package:
|
|
||||||
image: ghcr.io/goreleaser/nfpm
|
|
||||||
commands:
|
|
||||||
- nfpm pkg --packager deb
|
|
||||||
publish_focal:
|
|
||||||
image: git.lumeweb.com/lumeweb/aptly-publisher
|
|
||||||
settings:
|
|
||||||
apt_username:
|
|
||||||
from_secret: apt_username
|
|
||||||
apt_password:
|
|
||||||
from_secret: apt_password
|
|
||||||
repo: apt.web3relay.io
|
|
||||||
folder: ubuntu
|
|
||||||
distro: focal
|
|
||||||
gpg_password:
|
|
||||||
from_secret: gpg_password
|
|
||||||
publish_jammy:
|
|
||||||
image: git.lumeweb.com/lumeweb/aptly-publisher
|
|
||||||
settings:
|
|
||||||
apt_username:
|
|
||||||
from_secret: apt_username
|
|
||||||
apt_password:
|
|
||||||
from_secret: apt_password
|
|
||||||
repo: apt.web3relay.io
|
|
||||||
folder: ubuntu
|
|
||||||
distro: jammy
|
|
||||||
gpg_password:
|
|
||||||
from_secret: gpg_password
|
|
|
@ -0,0 +1 @@
|
||||||
|
This project is supported by a [Sia Foundation](https://sia.tech) grant.
|
|
@ -0,0 +1,18 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if ! command -v go &>/dev/null; then
|
||||||
|
sudo apt-get update && sudo apt-get install -y golang
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo go install github.com/goreleaser/nfpm/v2/cmd/nfpm@latest
|
||||||
|
sudo chmod +x /root/go/bin/nfpm
|
||||||
|
|
||||||
|
yq -i ".version=\"${1}\"" nfpm.yaml
|
||||||
|
sudo /root/go/bin/nfpm package -p deb
|
||||||
|
|
||||||
|
if ! command -v pip &>/dev/null; then
|
||||||
|
sudo apt-get update && sudo apt-get install -y python-pip
|
||||||
|
fi
|
||||||
|
|
||||||
|
pip2 install --upgrade cloudsmith-cli
|
||||||
|
cloudsmith push deb lumeweb/lume-web-relay *.deb
|
File diff suppressed because it is too large
Load Diff
91
package.json
91
package.json
|
@ -1,76 +1,75 @@
|
||||||
{
|
{
|
||||||
"name": "@lumeweb/relay",
|
"name": "@lumeweb/relay",
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0-develop.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "build/index.js",
|
"main": "build/index.js",
|
||||||
"types": "src/types.ts",
|
"repository": {
|
||||||
|
"url": "gitea@git.lumeweb.com:LumeWeb/relay.git"
|
||||||
|
},
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Derrick Hammer",
|
"name": "Derrick Hammer",
|
||||||
"email": "contact@lumeweb.com"
|
"email": "contact@lumeweb.com"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"semantic-release": "semantic-release",
|
||||||
"compile": "tsc",
|
"compile": "tsc",
|
||||||
"prebuild": "bash prebuild.sh",
|
"prebuild": "bash prebuild.sh",
|
||||||
"package": "pkg -c pkg.json build/index.js -t linux --public --no-native-build -C gzip",
|
"package": "pkg -c pkg.json build/index.js -t linux --public --no-native-build -C gzip",
|
||||||
"package-debug": "pkg -c pkg.json build/index.js -b -t linux --no-bytecode --public",
|
|
||||||
"build": "npm run compile && npm run prebuild && npm run package",
|
"build": "npm run compile && npm run prebuild && npm run package",
|
||||||
"barebuild": "npm run compile && npm run package"
|
"postinstall": "patch-package"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hyperswarm/dht": "^6.0.1",
|
"@fastify/websocket": "^7.2.0",
|
||||||
"@hyperswarm/dht-relay": "^0.3.0",
|
"@hyperswarm/dht-relay": "^0.4.0",
|
||||||
"@lumeweb/cfg": "https://github.com/LumeWeb/bcfg.git",
|
"@lumeweb/cfg": "git+https://git.lumeweb.com/LumeWeb/cfg.git",
|
||||||
"@lumeweb/dht-cache": "https://git.lumeweb.com/LumeWeb/dht-cache.git",
|
"@lumeweb/interface-relay": "git+https://git.lumeweb.com/LumeWeb/interface-relay",
|
||||||
"@lumeweb/kernel-utils": "https://github.com/LumeWeb/kernel-utils.git",
|
"@scure/bip39": "^1.2.0",
|
||||||
"@lumeweb/pokt-rpc-endpoints": "https://github.com/LumeWeb/pokt-rpc-endpoints.git",
|
"@types/node": "^18.15.11",
|
||||||
"@skynetlabs/skynet-nodejs": "^2.6.0",
|
"@types/ws": "^8.5.4",
|
||||||
"@solana/web3.js": "^1.47.3",
|
|
||||||
"@types/acme-client": "^3.3.0",
|
|
||||||
"@types/node": "^18.0.0",
|
|
||||||
"@types/node-cron": "^3.0.2",
|
|
||||||
"@types/ws": "^8.5.3",
|
|
||||||
"ajv": "^8.11.0",
|
|
||||||
"async-mutex": "^0.3.2",
|
"async-mutex": "^0.3.2",
|
||||||
"b4a": "^1.6.1",
|
"b4a": "^1.6.3",
|
||||||
"compact-encoding": "^2.11.0",
|
"compact-encoding": "^2.11.0",
|
||||||
"date-fns": "^2.28.0",
|
"dotenv": "^16.0.3",
|
||||||
"dotenv": "^16.0.1",
|
"ed25519-keygen": "github:LumeWeb/ed25519-keygen",
|
||||||
"ethers": "^5.6.9",
|
"ethers": "^5.7.2",
|
||||||
"express": "^4.18.1",
|
"eventemitter2": "^6.4.9",
|
||||||
"fetch-blob": "https://github.com/LumeWeb/fetch-blob.git",
|
"fastify": "^4.15.0",
|
||||||
"hyperswarm": "^3.0.4",
|
"fetch-blob": "github:LumeWeb/fetch-blob",
|
||||||
"json-stable-stringify": "^1.0.1",
|
"hyperswarm": "^4.4.0",
|
||||||
"libskynet": "https://github.com/LumeWeb/libskynet.git",
|
"json-stable-stringify": "^1.0.2",
|
||||||
"libskynetnode": "https://github.com/LumeWeb/libskynetnode.git",
|
"json-stringify-deterministic": "^1.0.8",
|
||||||
"loady": "https://github.com/LumeWeb/loady.git",
|
"loady": "github:LumeWeb/loady",
|
||||||
"loglevel": "^1.8.0",
|
"msgpackr": "^1.8.5",
|
||||||
"msgpackr": "^1.6.1",
|
|
||||||
"node-cache": "^5.1.2",
|
"node-cache": "^5.1.2",
|
||||||
"node-cron": "^3.0.1",
|
"node-fetch": "^2.6.9",
|
||||||
"node-fetch": "2",
|
"p-defer": "git+https://git.lumeweb.com/LumeWeb/p-defer.git",
|
||||||
"ordered-json": "^0.1.1",
|
"p-timeout": "git+https://git.lumeweb.com/LumeWeb/p-timeout.git",
|
||||||
|
"pino": "^8.11.0",
|
||||||
|
"pino-pretty": "^9.4.0",
|
||||||
"promise-retry": "^2.0.1",
|
"promise-retry": "^2.0.1",
|
||||||
"protomux": "^3.4.0",
|
"protomux": "^3.4.1",
|
||||||
"protomux-rpc": "^1.3.0",
|
"protomux-rpc": "^1.3.0",
|
||||||
"random-access-memory": "^4.1.0",
|
|
||||||
"random-key": "^0.3.2",
|
"random-key": "^0.3.2",
|
||||||
"slugify": "^1.6.5",
|
"slugify": "^1.6.6",
|
||||||
"sodium-universal": "^3.1.0"
|
"sodium-universal": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lumeweb/relay-types": "https://git.lumeweb.com/LumeWeb/relay-types.git",
|
"@semantic-release/changelog": "^6.0.3",
|
||||||
|
"@semantic-release/exec": "^6.0.3",
|
||||||
|
"@semantic-release/git": "^10.0.1",
|
||||||
"@types/b4a": "^1.6.0",
|
"@types/b4a": "^1.6.0",
|
||||||
"@types/express": "^4.17.13",
|
|
||||||
"@types/minimatch": "^3.0.5",
|
"@types/minimatch": "^3.0.5",
|
||||||
"@types/node-fetch": "^2.6.2",
|
"@types/node-fetch": "^2.6.3",
|
||||||
|
"cli-progress": "^3.12.0",
|
||||||
"hyper-typings": "^1.0.0",
|
"hyper-typings": "^1.0.0",
|
||||||
"node-gyp": "^9.1.0",
|
"node-gyp": "^9.3.1",
|
||||||
"pkg": "^5.8.0",
|
"patch-package": "^6.5.1",
|
||||||
|
"pkg": "^5.8.1",
|
||||||
"prebuildify": "^5.0.1",
|
"prebuildify": "^5.0.1",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.8.7",
|
||||||
"rollup": "^2.77.0",
|
"semantic-release": "21",
|
||||||
"supports-color": "https://github.com/LumeWeb/supports-color.git",
|
"supports-color": "github:LumeWeb/supports-color",
|
||||||
"typescript": "^4.7.4"
|
"typescript": "^4.9.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
diff --git a/node_modules/sodium-native/package.json b/node_modules/sodium-native/package.json
|
||||||
|
index bda9dd4..3a5541a 100644
|
||||||
|
--- a/node_modules/sodium-native/package.json
|
||||||
|
+++ b/node_modules/sodium-native/package.json
|
||||||
|
@@ -3,15 +3,6 @@
|
||||||
|
"version": "4.0.1",
|
||||||
|
"description": "Low level bindings for libsodium",
|
||||||
|
"main": "index.js",
|
||||||
|
- "files": [
|
||||||
|
- "index.js",
|
||||||
|
- "deps/**",
|
||||||
|
- "modules/**",
|
||||||
|
- "binding.c",
|
||||||
|
- "binding.gyp",
|
||||||
|
- "macros.h",
|
||||||
|
- "prebuilds/**"
|
||||||
|
- ],
|
||||||
|
"dependencies": {
|
||||||
|
"node-gyp-build": "^4.3.0"
|
||||||
|
},
|
4
pkg.json
4
pkg.json
|
@ -1,9 +1,5 @@
|
||||||
{
|
{
|
||||||
"assets": [
|
"assets": [
|
||||||
"node_modules/*/build/Release/*.node",
|
|
||||||
"node_modules/libskynet",
|
|
||||||
"node_modules/libskynetnode",
|
|
||||||
"node_modules/@lumeweb"
|
|
||||||
],
|
],
|
||||||
"outputPath": "dist"
|
"outputPath": "dist"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
systemctl enable lumeweb-relay.service
|
systemctl enable lumeweb-relay.service
|
||||||
|
systemctl start lumeweb-relay.service
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
systemctl stop lumeweb-relay.service
|
||||||
systemctl disable lumeweb-relay.service
|
systemctl disable lumeweb-relay.service
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
rimraf node_modules/libskynetnode/node_modules/node-fetch
|
|
||||||
|
|
||||||
for pkg in udx-native sodium-native; do
|
for pkg in udx-native sodium-native; do
|
||||||
(
|
(
|
||||||
cd "node_modules/${pkg}" || return
|
cd "node_modules/${pkg}" || return
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
//const require = createRequire(import.meta.url);
|
|
||||||
//import { createRequire } from "module";
|
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import Config from "@lumeweb/cfg";
|
import Config from "@lumeweb/cfg";
|
||||||
import * as os from "os";
|
import * as os from "os";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { errorExit } from "./lib/error.js";
|
import log from "./log.js";
|
||||||
|
|
||||||
const config = new Config("lumeweb-relay");
|
const config = new Config("lumeweb-relay", "core.confDir");
|
||||||
|
|
||||||
let configDir;
|
let configDir;
|
||||||
|
|
||||||
|
@ -22,25 +19,21 @@ switch (os.platform()) {
|
||||||
|
|
||||||
case "linux":
|
case "linux":
|
||||||
default:
|
default:
|
||||||
configDir = "/etc/lumeweb/relay/config.d";
|
configDir = "/etc/lumeweb/relay/conf.d";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
config.inject({
|
config.inject({
|
||||||
configDir,
|
"core.confDir": configDir,
|
||||||
port: 8080,
|
"core.port": 8080,
|
||||||
logLevel: "info",
|
"core.appPort": 80,
|
||||||
pluginDir: path.resolve(configDir, "..", "plugins"),
|
"core.logLevel": "info",
|
||||||
plugins: ["core"],
|
"core.pluginDir": path.resolve(configDir, "..", "plugins"),
|
||||||
ssl: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
config.load({
|
config.load();
|
||||||
env: true,
|
|
||||||
argv: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
configDir = config.str("configdir");
|
configDir = config.str("core.confDir");
|
||||||
|
|
||||||
if (fs.existsSync(configDir)) {
|
if (fs.existsSync(configDir)) {
|
||||||
try {
|
try {
|
||||||
|
@ -50,15 +43,8 @@ if (fs.existsSync(configDir)) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
config.load({
|
config.load();
|
||||||
env: true,
|
|
||||||
argv: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const setting of ["domain"]) {
|
log.level = config.get("core.loglevel");
|
||||||
if (!config.get(setting)) {
|
|
||||||
errorExit(`Required config option ${setting} not set`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|
38
src/index.ts
38
src/index.ts
|
@ -1,30 +1,25 @@
|
||||||
import { start as startRpc } from "./modules/rpc.js";
|
import { start as startRpc } from "./modules/rpc.js";
|
||||||
import { start as startRelay } from "./modules/relay.js";
|
import { start as startRelay } from "./modules/relay.js";
|
||||||
import { start as startApp } from "./modules/app";
|
import { start as startApp } from "./modules/app";
|
||||||
import log from "loglevel";
|
|
||||||
import config from "./config.js";
|
import config from "./config.js";
|
||||||
import { loadPlugins } from "./modules/plugin.js";
|
import { getPluginAPI, loadPlugins } from "./modules/plugin.js";
|
||||||
import { start as startDns } from "./modules/dns.js";
|
import { start as startSwarm, get as getSwarm } from "./modules/swarm.js";
|
||||||
import { start as startSSl } from "./modules/ssl.js";
|
import * as bip39 from "@scure/bip39";
|
||||||
import { generateSeedPhraseDeterministic } from "libskynet";
|
import { wordlist } from "@scure/bip39/wordlists/english";
|
||||||
import * as crypto from "crypto";
|
|
||||||
|
|
||||||
log.setDefaultLevel(config.str("log-level"));
|
if (!config.str("core.seed")) {
|
||||||
|
config.save("account", {
|
||||||
if (!config.str("seed")) {
|
core: {
|
||||||
config.saveConfigJson("account.json", {
|
seed: bip39.generateMnemonic(wordlist),
|
||||||
seed: generateSeedPhraseDeterministic(
|
},
|
||||||
crypto.randomBytes(100).toString("hex")
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function boot() {
|
async function boot() {
|
||||||
|
await startSwarm();
|
||||||
await loadPlugins();
|
await loadPlugins();
|
||||||
await startApp();
|
await startApp();
|
||||||
await startRpc();
|
await startRpc();
|
||||||
await startDns();
|
|
||||||
await startSSl();
|
|
||||||
await startRelay();
|
await startRelay();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,9 +28,12 @@ boot();
|
||||||
process.on("uncaughtException", function (err) {
|
process.on("uncaughtException", function (err) {
|
||||||
console.log(`Caught exception: ${err.message} ${err.stack}`);
|
console.log(`Caught exception: ${err.message} ${err.stack}`);
|
||||||
});
|
});
|
||||||
process.on("SIGINT", function () {
|
|
||||||
|
async function shutdown() {
|
||||||
|
await getPluginAPI().emitAsync("core.shutdown");
|
||||||
|
await getSwarm().destroy();
|
||||||
process.exit();
|
process.exit();
|
||||||
});
|
}
|
||||||
process.on("SIGTERM", function () {
|
|
||||||
process.exit();
|
process.on("SIGINT", shutdown);
|
||||||
});
|
process.on("SIGTERM", shutdown);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import log from "loglevel";
|
import log from "../log.js";
|
||||||
|
|
||||||
export function errorExit(msg: string): void {
|
export function errorExit(msg: string): void {
|
||||||
log.error(msg);
|
log.error(msg);
|
||||||
|
|
445
src/lib/file.ts
445
src/lib/file.ts
|
@ -1,445 +0,0 @@
|
||||||
import type { Err, progressiveFetchResult } from "libskynet";
|
|
||||||
// @ts-ignore
|
|
||||||
import { SkynetClient } from "@skynetlabs/skynet-nodejs";
|
|
||||||
import type {
|
|
||||||
IndependentFileSmall,
|
|
||||||
IndependentFileSmallMetadata,
|
|
||||||
} from "@lumeweb/relay-types";
|
|
||||||
import {
|
|
||||||
addContextToErr,
|
|
||||||
blake2b,
|
|
||||||
bufToHex,
|
|
||||||
ed25519Sign,
|
|
||||||
encodePrefixedBytes,
|
|
||||||
encodeU64,
|
|
||||||
defaultPortalList,
|
|
||||||
skylinkToResolverEntryData,
|
|
||||||
encryptFileSmall,
|
|
||||||
decryptFileSmall,
|
|
||||||
entryIDToSkylink,
|
|
||||||
deriveRegistryEntryID,
|
|
||||||
taggedRegistryEntryKeys,
|
|
||||||
namespaceInode,
|
|
||||||
deriveChildSeed,
|
|
||||||
bufToB64,
|
|
||||||
} from "libskynet";
|
|
||||||
|
|
||||||
import { readRegistryEntry, progressiveFetch, upload } from "libskynetnode";
|
|
||||||
|
|
||||||
const ERR_NOT_EXISTS = "DNE";
|
|
||||||
const STD_FILENAME = "file";
|
|
||||||
|
|
||||||
async function overwriteRegistryEntry(
|
|
||||||
keypair: any,
|
|
||||||
datakey: Uint8Array,
|
|
||||||
data: Uint8Array,
|
|
||||||
revision: bigint
|
|
||||||
): Promise<null> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (data.length > 86) {
|
|
||||||
reject("provided data is too large to fit in a registry entry");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let [encodedRevision, errU64] = encodeU64(revision);
|
|
||||||
if (errU64 !== null) {
|
|
||||||
reject(addContextToErr(errU64, "unable to encode revision number"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let datakeyHex = bufToHex(datakey);
|
|
||||||
let [encodedData, errEPB] = encodePrefixedBytes(data);
|
|
||||||
if (errEPB !== null) {
|
|
||||||
reject(addContextToErr(errEPB, "unable to encode the registry data"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let dataToSign = new Uint8Array(32 + 8 + data.length + 8);
|
|
||||||
dataToSign.set(datakey, 0);
|
|
||||||
dataToSign.set(encodedData, 32);
|
|
||||||
dataToSign.set(encodedRevision, 32 + 8 + data.length);
|
|
||||||
let sigHash = blake2b(dataToSign);
|
|
||||||
let [sig, errS] = ed25519Sign(sigHash, keypair.secretKey);
|
|
||||||
if (errS !== null) {
|
|
||||||
reject(addContextToErr(errS, "unable to produce signature"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let postBody = {
|
|
||||||
publickey: {
|
|
||||||
algorithm: "ed25519",
|
|
||||||
key: Array.from(keypair.publicKey),
|
|
||||||
},
|
|
||||||
datakey: datakeyHex,
|
|
||||||
revision: Number(revision),
|
|
||||||
data: Array.from(data),
|
|
||||||
signature: Array.from(sig),
|
|
||||||
};
|
|
||||||
let fetchOpts = {
|
|
||||||
method: "post",
|
|
||||||
body: JSON.stringify(postBody),
|
|
||||||
};
|
|
||||||
let endpoint = "/skynet/registry";
|
|
||||||
|
|
||||||
progressiveFetch(
|
|
||||||
endpoint,
|
|
||||||
fetchOpts,
|
|
||||||
defaultPortalList,
|
|
||||||
verifyRegistryWrite
|
|
||||||
).then((result: progressiveFetchResult) => {
|
|
||||||
if (result.success === true) {
|
|
||||||
resolve(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
reject("unable to write registry entry\n" + JSON.stringify(result));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
async function verifyRegistryWrite(response: Response): Promise<Err> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
if (!("status" in response)) {
|
|
||||||
resolve("response did not contain a status");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (response.status === 204) {
|
|
||||||
resolve(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolve("unrecognized status");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createIndependentFileSmall(
|
|
||||||
seed: Uint8Array,
|
|
||||||
userInode: string,
|
|
||||||
fileData: Uint8Array
|
|
||||||
): Promise<[IndependentFileSmall, Err]> {
|
|
||||||
return new Promise(async (resolve) => {
|
|
||||||
let [inode, errNI] = namespaceInode("IndependentFileSmall", userInode);
|
|
||||||
if (errNI !== null) {
|
|
||||||
resolve([{} as any, addContextToErr(errNI, "unable to namespace inode")]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let [keypair, dataKey, errTREK] = taggedRegistryEntryKeys(
|
|
||||||
seed,
|
|
||||||
inode,
|
|
||||||
inode
|
|
||||||
);
|
|
||||||
if (errTREK !== null) {
|
|
||||||
resolve([
|
|
||||||
{} as any,
|
|
||||||
addContextToErr(
|
|
||||||
errTREK,
|
|
||||||
"unable to get registry entry for provided inode"
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let result;
|
|
||||||
try {
|
|
||||||
result = await readRegistryEntry(keypair.publicKey, dataKey);
|
|
||||||
} catch (e) {
|
|
||||||
result = { exists: false };
|
|
||||||
}
|
|
||||||
if (result.exists === true) {
|
|
||||||
resolve([{} as any, "exists"]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let encryptionKey = deriveChildSeed(seed, inode);
|
|
||||||
let metadata: IndependentFileSmallMetadata = {
|
|
||||||
largestHistoricSize: BigInt(fileData.length),
|
|
||||||
};
|
|
||||||
|
|
||||||
let revisionSeed = new Uint8Array(seed.length + 8);
|
|
||||||
revisionSeed.set(seed, 0);
|
|
||||||
let revisionKey = deriveChildSeed(revisionSeed, inode);
|
|
||||||
let revision = BigInt(revisionKey[0]) * 256n + BigInt(revisionKey[1]);
|
|
||||||
let [encryptedData, errEF] = encryptFileSmall(
|
|
||||||
encryptionKey,
|
|
||||||
inode,
|
|
||||||
revision,
|
|
||||||
metadata,
|
|
||||||
fileData,
|
|
||||||
metadata.largestHistoricSize
|
|
||||||
);
|
|
||||||
if (errEF !== null) {
|
|
||||||
resolve([{} as any, addContextToErr(errEF, "unable to encrypt file")]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let immutableSkylink;
|
|
||||||
|
|
||||||
try {
|
|
||||||
immutableSkylink = await upload(encryptedData, {
|
|
||||||
Filename: STD_FILENAME,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
resolve([{} as any, addContextToErr(e, "upload failed")]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let [entryData, errSTRED] = skylinkToResolverEntryData(immutableSkylink);
|
|
||||||
if (errSTRED !== null) {
|
|
||||||
resolve([
|
|
||||||
{} as any,
|
|
||||||
addContextToErr(
|
|
||||||
errSTRED,
|
|
||||||
"couldn't create resovler link from upload skylink"
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await overwriteRegistryEntry(keypair, dataKey, entryData, revision);
|
|
||||||
} catch (e: any) {
|
|
||||||
resolve([
|
|
||||||
{} as any,
|
|
||||||
addContextToErr(e, "could not write to registry entry"),
|
|
||||||
]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let [entryID, errDREID] = deriveRegistryEntryID(keypair.publicKey, dataKey);
|
|
||||||
if (errDREID !== null) {
|
|
||||||
resolve([
|
|
||||||
{} as any,
|
|
||||||
addContextToErr(errDREID, "could not compute entry id"),
|
|
||||||
]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let skylink = entryIDToSkylink(entryID);
|
|
||||||
|
|
||||||
let encStr = bufToB64(encryptionKey);
|
|
||||||
let viewKey = encStr + inode;
|
|
||||||
|
|
||||||
let ifile: IndependentFileSmall = {
|
|
||||||
dataKey,
|
|
||||||
fileData,
|
|
||||||
inode,
|
|
||||||
keypair,
|
|
||||||
metadata,
|
|
||||||
revision,
|
|
||||||
seed,
|
|
||||||
|
|
||||||
skylink,
|
|
||||||
viewKey,
|
|
||||||
|
|
||||||
overwriteData: function (newData: Uint8Array): Promise<Err> {
|
|
||||||
return overwriteIndependentFileSmall(ifile, newData);
|
|
||||||
},
|
|
||||||
readData: function (): Promise<[Uint8Array, Err]> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
let data = new Uint8Array(ifile.fileData.length);
|
|
||||||
data.set(ifile.fileData, 0);
|
|
||||||
resolve([data, null]);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
resolve([ifile, null]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openIndependentFileSmall(
|
|
||||||
seed: Uint8Array,
|
|
||||||
userInode: string
|
|
||||||
): Promise<[IndependentFileSmall, Err]> {
|
|
||||||
return new Promise(async (resolve) => {
|
|
||||||
let [inode, errNI] = namespaceInode("IndependentFileSmall", userInode);
|
|
||||||
if (errNI !== null) {
|
|
||||||
resolve([{} as any, addContextToErr(errNI, "unable to namespace inode")]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let [keypair, dataKey, errTREK] = taggedRegistryEntryKeys(
|
|
||||||
seed,
|
|
||||||
inode,
|
|
||||||
inode
|
|
||||||
);
|
|
||||||
if (errTREK !== null) {
|
|
||||||
resolve([
|
|
||||||
{} as any,
|
|
||||||
addContextToErr(
|
|
||||||
errTREK,
|
|
||||||
"unable to get registry entry for provided inode"
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let result;
|
|
||||||
try {
|
|
||||||
result = await readRegistryEntry(keypair.publicKey, dataKey);
|
|
||||||
} catch (e: any) {
|
|
||||||
resolve([
|
|
||||||
{} as any,
|
|
||||||
addContextToErr(e, "unable to read registry entry for file"),
|
|
||||||
]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (result.exists !== true) {
|
|
||||||
resolve([{} as any, ERR_NOT_EXISTS]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let [entryID, errDREID] = deriveRegistryEntryID(keypair.publicKey, dataKey);
|
|
||||||
if (errDREID !== null) {
|
|
||||||
resolve([
|
|
||||||
{} as any,
|
|
||||||
addContextToErr(errDREID, "unable to derive registry entry id"),
|
|
||||||
]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let skylink = entryIDToSkylink(entryID);
|
|
||||||
|
|
||||||
const client = new SkynetClient("https://web3portal.com");
|
|
||||||
let encryptedData;
|
|
||||||
try {
|
|
||||||
encryptedData = await client.downloadData(skylink);
|
|
||||||
} catch (e: any) {
|
|
||||||
resolve([{} as any, addContextToErr(e, "unable to download file")]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let encryptionKey = deriveChildSeed(seed, inode);
|
|
||||||
let [metadata, fileData, errDF] = decryptFileSmall(
|
|
||||||
encryptionKey,
|
|
||||||
inode,
|
|
||||||
encryptedData
|
|
||||||
);
|
|
||||||
if (errDF !== null) {
|
|
||||||
resolve([{} as any, addContextToErr(errDF, "unable to decrypt file")]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let encStr = bufToB64(encryptionKey);
|
|
||||||
let viewKey = encStr + inode;
|
|
||||||
|
|
||||||
let ifile: IndependentFileSmall = {
|
|
||||||
dataKey,
|
|
||||||
fileData,
|
|
||||||
inode,
|
|
||||||
keypair,
|
|
||||||
metadata,
|
|
||||||
revision: result.revision!,
|
|
||||||
seed,
|
|
||||||
|
|
||||||
skylink,
|
|
||||||
viewKey,
|
|
||||||
|
|
||||||
overwriteData: function (newData: Uint8Array): Promise<Err> {
|
|
||||||
return overwriteIndependentFileSmall(ifile, newData);
|
|
||||||
},
|
|
||||||
|
|
||||||
readData: function (): Promise<[Uint8Array, Err]> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
let data = new Uint8Array(ifile.fileData.length);
|
|
||||||
data.set(ifile.fileData, 0);
|
|
||||||
resolve([data, null]);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
resolve([ifile, null]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
async function overwriteIndependentFileSmall(
|
|
||||||
file: IndependentFileSmall,
|
|
||||||
newData: Uint8Array
|
|
||||||
): Promise<Err> {
|
|
||||||
return new Promise(async (resolve) => {
|
|
||||||
// Create a new metadata for the file based on the current file
|
|
||||||
// metadata. Need to update the largest historic size.
|
|
||||||
let newMetadata: IndependentFileSmallMetadata = {
|
|
||||||
largestHistoricSize: BigInt(file.metadata.largestHistoricSize),
|
|
||||||
};
|
|
||||||
if (BigInt(newData.length) > newMetadata.largestHistoricSize) {
|
|
||||||
newMetadata.largestHistoricSize = BigInt(newData.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute the new revision number for the file. This is done
|
|
||||||
// deterministically using the seed and the current revision number, so
|
|
||||||
// that multiple concurrent updates will end up with the same revision.
|
|
||||||
// We use a random number between 1 and 256 for our increment.
|
|
||||||
let [encodedRevision, errEU64] = encodeU64(file.revision);
|
|
||||||
if (errEU64 !== null) {
|
|
||||||
resolve(addContextToErr(errEU64, "unable to encode revision"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let revisionSeed = new Uint8Array(
|
|
||||||
file.seed.length + encodedRevision.length
|
|
||||||
);
|
|
||||||
revisionSeed.set(file.seed, 0);
|
|
||||||
revisionSeed.set(encodedRevision, file.seed.length);
|
|
||||||
let revisionKey = deriveChildSeed(revisionSeed, file.inode);
|
|
||||||
let newRevision = file.revision + BigInt(revisionKey[0]) + 1n;
|
|
||||||
|
|
||||||
// Get the encryption key.
|
|
||||||
let encryptionKey = deriveChildSeed(file.seed, file.inode);
|
|
||||||
|
|
||||||
// Create a new encrypted blob for the data.
|
|
||||||
//
|
|
||||||
// NOTE: Need to supply the data that would be in place after a
|
|
||||||
// successful update, which means using the new metadata and revision
|
|
||||||
// number.
|
|
||||||
let [encryptedData, errEFS] = encryptFileSmall(
|
|
||||||
encryptionKey,
|
|
||||||
file.inode,
|
|
||||||
newRevision,
|
|
||||||
newMetadata,
|
|
||||||
newData,
|
|
||||||
newMetadata.largestHistoricSize
|
|
||||||
);
|
|
||||||
if (errEFS !== null) {
|
|
||||||
resolve(addContextToErr(errEFS, "unable to encrypt updated file"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upload the data to get the immutable link.
|
|
||||||
let skylink;
|
|
||||||
try {
|
|
||||||
skylink = await upload(encryptedData, {
|
|
||||||
Filename: STD_FILENAME,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
resolve(addContextToErr(e, "new data upload failed"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write to the registry entry.
|
|
||||||
let [entryData, errSTRED] = skylinkToResolverEntryData(skylink);
|
|
||||||
if (errSTRED !== null) {
|
|
||||||
resolve(
|
|
||||||
addContextToErr(
|
|
||||||
errSTRED,
|
|
||||||
"could not create resolver link from upload skylink"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await overwriteRegistryEntry(
|
|
||||||
file.keypair,
|
|
||||||
file.dataKey,
|
|
||||||
entryData,
|
|
||||||
newRevision
|
|
||||||
);
|
|
||||||
} catch (e: any) {
|
|
||||||
resolve(addContextToErr(e, "could not write to registry entry"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// File update was successful, update the file metadata.
|
|
||||||
file.revision = newRevision;
|
|
||||||
file.metadata = newMetadata;
|
|
||||||
file.fileData = newData;
|
|
||||||
resolve(null);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
createIndependentFileSmall,
|
|
||||||
openIndependentFileSmall,
|
|
||||||
overwriteIndependentFileSmall,
|
|
||||||
};
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { HDKey } from "ed25519-keygen/dist/hdkey";
|
||||||
|
import config from "../config";
|
||||||
|
import * as bip39 from "@scure/bip39";
|
||||||
|
import { wordlist } from "@scure/bip39/wordlists/english";
|
||||||
|
import { errorExit } from "./error.js";
|
||||||
|
import b4a from "b4a";
|
||||||
|
|
||||||
|
const BIP44_PATH = "m/44'/1627'/0'/0'/0'";
|
||||||
|
|
||||||
|
export function getSeed() {
|
||||||
|
const seed = config.str("core.seed");
|
||||||
|
|
||||||
|
let valid = bip39.validateMnemonic(seed, wordlist);
|
||||||
|
if (!valid) {
|
||||||
|
errorExit("LUME_WEB_RELAY_SEED is invalid. Aborting.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return bip39.mnemonicToSeedSync(seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHDKey(): HDKey {
|
||||||
|
return HDKey.fromMasterSeed(getSeed()).derive(BIP44_PATH);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getKeyPair(): { publicKey: Uint8Array; secretKey: Uint8Array } {
|
||||||
|
const key = getHDKey();
|
||||||
|
|
||||||
|
return {
|
||||||
|
publicKey: key.publicKeyRaw,
|
||||||
|
secretKey: b4a.concat([key.privateKey, key.publicKeyRaw]),
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,17 +0,0 @@
|
||||||
import config from "../config";
|
|
||||||
import { seedPhraseToSeed } from "libskynet";
|
|
||||||
|
|
||||||
export function dynImport(module: string) {
|
|
||||||
return Function(`return import("${module}")`)() as Promise<any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSeed(): Uint8Array {
|
|
||||||
let [seed, err] = seedPhraseToSeed(config.str("seed"));
|
|
||||||
|
|
||||||
if (err) {
|
|
||||||
console.error(err);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return seed;
|
|
||||||
}
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
import pino from "pino";
|
||||||
|
import pretty from "pino-pretty";
|
||||||
|
|
||||||
|
const stream = pretty({
|
||||||
|
colorize: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const log = pino(stream);
|
||||||
|
export default log;
|
|
@ -1,60 +1,30 @@
|
||||||
import express, { Express } from "express";
|
|
||||||
import http from "http";
|
|
||||||
import { AddressInfo } from "net";
|
import { AddressInfo } from "net";
|
||||||
import log from "loglevel";
|
import log from "../log.js";
|
||||||
import { getKeyPair } from "./swarm.js";
|
import fastify from "fastify";
|
||||||
|
import type { FastifyInstance } from "fastify";
|
||||||
|
import { getKeyPair } from "../lib/seed.js";
|
||||||
|
import config from "../config";
|
||||||
|
import { getPluginAPI } from "./plugin";
|
||||||
|
|
||||||
let app: Express;
|
let app: FastifyInstance;
|
||||||
let router = express.Router();
|
|
||||||
let server: http.Server;
|
|
||||||
|
|
||||||
export function getRouter(): express.Router {
|
|
||||||
return router;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setRouter(newRouter: express.Router): void {
|
|
||||||
router = newRouter;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function start() {
|
export async function start() {
|
||||||
app = express();
|
const keyPair = getKeyPair();
|
||||||
server = http.createServer(app);
|
app = fastify({
|
||||||
resetRouter();
|
logger: log.child({ module: "app-server" }),
|
||||||
await new Promise((resolve) => {
|
|
||||||
server.listen(80, "0.0.0.0", function () {
|
|
||||||
const address = server.address() as AddressInfo;
|
|
||||||
log.info(
|
|
||||||
"HTTP/App Server started on ",
|
|
||||||
`${address.address}:${address.port}`
|
|
||||||
);
|
|
||||||
resolve(null);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use(function (req, res, next) {
|
app.get("/", (req, res) => {
|
||||||
router(req, res, next);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getApp(): Express {
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
export function getServer(): http.Server {
|
|
||||||
return server;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resetRouter(): void {
|
|
||||||
setRouter(newRouter());
|
|
||||||
}
|
|
||||||
|
|
||||||
function newRouter(): express.Router {
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
let keyPair = getKeyPair();
|
|
||||||
|
|
||||||
router.get("/", (req, res) => {
|
|
||||||
res.send(Buffer.from(keyPair.publicKey).toString("hex"));
|
res.send(Buffer.from(keyPair.publicKey).toString("hex"));
|
||||||
});
|
});
|
||||||
|
|
||||||
return router;
|
await getPluginAPI().emitAsync("core.appServer.buildRoutes");
|
||||||
|
|
||||||
|
await app.listen({ port: config.uint("core.appPort"), host: "0.0.0.0" });
|
||||||
|
|
||||||
|
getPluginAPI().emit("core.appServer.started");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function get(): FastifyInstance {
|
||||||
|
return app;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,59 +0,0 @@
|
||||||
import cron from "node-cron";
|
|
||||||
import { get as getSwarm } from "./swarm.js";
|
|
||||||
import { Buffer } from "buffer";
|
|
||||||
import { pack } from "msgpackr";
|
|
||||||
import config from "../config.js";
|
|
||||||
import log from "loglevel";
|
|
||||||
import fetch from "node-fetch";
|
|
||||||
import { overwriteRegistryEntry } from "libskynetnode";
|
|
||||||
import type { DnsProvider } from "@lumeweb/relay-types";
|
|
||||||
// @ts-ignore
|
|
||||||
import { hashDataKey } from "@lumeweb/kernel-utils";
|
|
||||||
|
|
||||||
let activeIp: string;
|
|
||||||
const REGISTRY_NODE_KEY = "lumeweb-dht-node";
|
|
||||||
|
|
||||||
let dnsProvider: DnsProvider = async (ip) => {};
|
|
||||||
|
|
||||||
export function setDnsProvider(provider: DnsProvider) {
|
|
||||||
dnsProvider = provider;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function ipUpdate() {
|
|
||||||
let currentIp = await getCurrentIp();
|
|
||||||
|
|
||||||
if (activeIp && currentIp === activeIp) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const domain = config.str("domain");
|
|
||||||
|
|
||||||
await dnsProvider(currentIp, domain);
|
|
||||||
|
|
||||||
activeIp = currentIp;
|
|
||||||
|
|
||||||
log.info(`Updated DynDNS hostname ${domain} to ${activeIp}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function start() {
|
|
||||||
const swarm = (await getSwarm()) as any;
|
|
||||||
|
|
||||||
await ipUpdate();
|
|
||||||
|
|
||||||
await overwriteRegistryEntry(
|
|
||||||
swarm.dht.defaultKeyPair,
|
|
||||||
hashDataKey(REGISTRY_NODE_KEY),
|
|
||||||
pack(`${config.str("domain")}:${config.uint("port")}`)
|
|
||||||
);
|
|
||||||
|
|
||||||
log.info(
|
|
||||||
"Relay Identity is",
|
|
||||||
Buffer.from(swarm.dht.defaultKeyPair.publicKey).toString("hex")
|
|
||||||
);
|
|
||||||
|
|
||||||
cron.schedule("0 * * * *", ipUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getCurrentIp(): Promise<string> {
|
|
||||||
return await (await fetch("http://ip1.dynupdate.no-ip.com/")).text();
|
|
||||||
}
|
|
|
@ -1,36 +1,124 @@
|
||||||
import config from "../config.js";
|
import config from "../config.js";
|
||||||
|
import type { RPCServer } from "./rpc/server.js";
|
||||||
import { getRpcServer } from "./rpc/server.js";
|
import { getRpcServer } from "./rpc/server.js";
|
||||||
import type { PluginAPI, RPCMethod, Plugin } from "@lumeweb/relay-types";
|
import type { Plugin, RPCMethod } from "@lumeweb/interface-relay";
|
||||||
import slugify from "slugify";
|
import slugify from "slugify";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import {
|
import type { Logger } from "pino";
|
||||||
getSavedSsl,
|
|
||||||
getSsl,
|
|
||||||
getSslContext,
|
|
||||||
saveSSl,
|
|
||||||
setSsl,
|
|
||||||
setSSlCheck,
|
|
||||||
setSslContext,
|
|
||||||
} from "./ssl.js";
|
|
||||||
import log from "loglevel";
|
|
||||||
import { getSeed } from "../lib/util.js";
|
|
||||||
import { getRouter, resetRouter, setRouter } from "./app.js";
|
|
||||||
import {
|
|
||||||
createIndependentFileSmall,
|
|
||||||
openIndependentFileSmall,
|
|
||||||
overwriteIndependentFileSmall,
|
|
||||||
} from "../lib/file";
|
|
||||||
import { setDnsProvider } from "./dns";
|
|
||||||
import pluginRpc from "./plugins/rpc";
|
|
||||||
import pluginCore from "./plugins/core";
|
|
||||||
|
|
||||||
let pluginApi: PluginApiManager;
|
import { getHDKey, getSeed } from "../lib/seed.js";
|
||||||
|
import type Config from "@lumeweb/cfg";
|
||||||
|
import EventEmitter2 from "eventemitter2";
|
||||||
|
import log from "../log.js";
|
||||||
|
import {
|
||||||
|
get as getSwarm,
|
||||||
|
getProtocolManager,
|
||||||
|
ProtocolManager,
|
||||||
|
} from "./swarm.js";
|
||||||
|
import { get as getApp } from "./app.js";
|
||||||
|
import type { HDKey } from "ed25519-keygen/dist/hdkey";
|
||||||
|
import corePlugins from "../plugins";
|
||||||
|
import Util from "./plugin/util";
|
||||||
|
|
||||||
|
let pluginAPIManager: PluginAPIManager;
|
||||||
|
let pluginAPI: PluginAPI;
|
||||||
|
|
||||||
const sanitizeName = (name: string) =>
|
const sanitizeName = (name: string) =>
|
||||||
slugify(name, { lower: true, strict: true });
|
slugify(name, { lower: true, strict: true });
|
||||||
|
|
||||||
export class PluginApiManager {
|
class PluginAPI extends EventEmitter2 {
|
||||||
|
private _server: RPCServer;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
config,
|
||||||
|
server,
|
||||||
|
swarm,
|
||||||
|
}: {
|
||||||
|
config: Config;
|
||||||
|
server: RPCServer;
|
||||||
|
swarm: any;
|
||||||
|
}) {
|
||||||
|
super({
|
||||||
|
wildcard: true,
|
||||||
|
verboseMemoryLeak: true,
|
||||||
|
maxListeners: 0,
|
||||||
|
});
|
||||||
|
this._config = config;
|
||||||
|
this._server = server;
|
||||||
|
this._swarm = swarm;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _util: Util = new Util();
|
||||||
|
|
||||||
|
get util(): Util {
|
||||||
|
return this._util;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _swarm: any;
|
||||||
|
|
||||||
|
get swarm(): any {
|
||||||
|
return this._swarm;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _config: Config;
|
||||||
|
|
||||||
|
get config(): Config {
|
||||||
|
return this._config;
|
||||||
|
}
|
||||||
|
|
||||||
|
get pluginConfig(): Config {
|
||||||
|
throw new Error("not implemented and should not be called");
|
||||||
|
}
|
||||||
|
|
||||||
|
get logger(): Logger {
|
||||||
|
throw new Error("not implemented and should not be called");
|
||||||
|
}
|
||||||
|
|
||||||
|
get rpcServer(): RPCServer {
|
||||||
|
return this._server;
|
||||||
|
}
|
||||||
|
|
||||||
|
get seed(): Uint8Array {
|
||||||
|
return getSeed();
|
||||||
|
}
|
||||||
|
|
||||||
|
get identity(): HDKey {
|
||||||
|
return getHDKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
get protocols(): ProtocolManager {
|
||||||
|
return getProtocolManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
get app() {
|
||||||
|
return getApp();
|
||||||
|
}
|
||||||
|
|
||||||
|
public loadPlugin(
|
||||||
|
moduleName: string
|
||||||
|
): (moduleName: string) => Promise<Plugin> {
|
||||||
|
return getPluginAPIManager().loadPlugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
registerMethod(methodName: string, method: RPCMethod): void {
|
||||||
|
throw new Error("not implemented and should not be called");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPluginAPI(): PluginAPI {
|
||||||
|
if (!pluginAPI) {
|
||||||
|
pluginAPI = new PluginAPI({
|
||||||
|
config,
|
||||||
|
server: getRpcServer(),
|
||||||
|
swarm: getSwarm(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return pluginAPI as PluginAPI;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PluginAPIManager {
|
||||||
private registeredPlugins: Map<string, Plugin> = new Map<string, Plugin>();
|
private registeredPlugins: Map<string, Plugin> = new Map<string, Plugin>();
|
||||||
|
|
||||||
public async loadPlugin(moduleName: string): Promise<Plugin> {
|
public async loadPlugin(moduleName: string): Promise<Plugin> {
|
||||||
|
@ -42,7 +130,7 @@ export class PluginApiManager {
|
||||||
|
|
||||||
const paths = [];
|
const paths = [];
|
||||||
for (const modulePath of [`${moduleName}.js`, `${moduleName}.mjs`]) {
|
for (const modulePath of [`${moduleName}.js`, `${moduleName}.mjs`]) {
|
||||||
const fullPath = path.join(config.get("plugindir"), modulePath);
|
const fullPath = path.join(config.get("core.plugindir"), modulePath);
|
||||||
if (fs.existsSync(fullPath)) {
|
if (fs.existsSync(fullPath)) {
|
||||||
paths.push(fullPath);
|
paths.push(fullPath);
|
||||||
break;
|
break;
|
||||||
|
@ -54,84 +142,117 @@ export class PluginApiManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
let plugin: Plugin;
|
let plugin: Plugin;
|
||||||
|
let pluginPath = paths.shift();
|
||||||
try {
|
try {
|
||||||
plugin = (await import(paths.shift() as string)) as Plugin;
|
plugin = require(pluginPath as string) as Plugin;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.loadPluginInstance(plugin);
|
log.debug("Loaded plugin %s", moduleName);
|
||||||
|
|
||||||
|
const instance = await this.loadPluginInstance(plugin);
|
||||||
|
|
||||||
|
if (!instance) {
|
||||||
|
throw new Error(`Corrupt plugin found at ${pluginPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance as Plugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async loadPluginInstance(plugin: Plugin): Promise<Plugin> {
|
public async loadPluginInstance(plugin: Plugin): Promise<Plugin | boolean> {
|
||||||
if ("default" in plugin) {
|
if ("default" in plugin) {
|
||||||
plugin = plugin?.default as Plugin;
|
plugin = plugin?.default as Plugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!("name" in plugin)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
plugin.name = sanitizeName(plugin.name);
|
plugin.name = sanitizeName(plugin.name);
|
||||||
|
|
||||||
this.registeredPlugins.set(plugin.name, plugin);
|
this.registeredPlugins.set(plugin.name, plugin);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
plugin.plugin(this.getPluginAPI(plugin.name));
|
await plugin.plugin(
|
||||||
|
// @ts-ignore
|
||||||
|
new Proxy<PluginAPI>(getPluginAPI(), {
|
||||||
|
get(target: PluginAPI, prop: string): any {
|
||||||
|
if (prop === "registerMethod") {
|
||||||
|
return (methodName: string, method: RPCMethod): void => {
|
||||||
|
return getRpcServer().registerMethod(
|
||||||
|
plugin.name,
|
||||||
|
methodName,
|
||||||
|
method
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prop === "pluginConfig") {
|
||||||
|
return new Proxy<Config>(config, {
|
||||||
|
get(target: Config, prop: string): any {
|
||||||
|
if (prop === "set") {
|
||||||
|
return (key: string, value: any): void => {
|
||||||
|
target.set(`plugin.${plugin.name}.${key}`, value);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prop === "get") {
|
||||||
|
return (key: string, fallback = null): any => {
|
||||||
|
return target.get(
|
||||||
|
`plugin.${plugin.name}.${key}`,
|
||||||
|
fallback
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prop === "has") {
|
||||||
|
return (key: string): any => {
|
||||||
|
return target.has(`plugin.${plugin.name}.${key}`);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return (target as any)[prop];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prop === "logger") {
|
||||||
|
return log.child({ plugin: plugin.name });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (target as any)[prop];
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
return plugin;
|
log.debug("Initialized plugin %s", plugin.name);
|
||||||
}
|
|
||||||
|
|
||||||
private getPluginAPI(pluginName: string): PluginAPI {
|
return plugin;
|
||||||
return {
|
|
||||||
config,
|
|
||||||
registerMethod: (methodName: string, method: RPCMethod): void => {
|
|
||||||
getRpcServer().registerMethod(pluginName, methodName, method);
|
|
||||||
},
|
|
||||||
loadPlugin: getPluginAPI().loadPlugin.bind(getPluginAPI()),
|
|
||||||
getRpcServer,
|
|
||||||
ssl: {
|
|
||||||
setContext: setSslContext,
|
|
||||||
getContext: getSslContext,
|
|
||||||
getSaved: getSavedSsl,
|
|
||||||
set: setSsl,
|
|
||||||
get: getSsl,
|
|
||||||
save: saveSSl,
|
|
||||||
setCheck: setSSlCheck,
|
|
||||||
},
|
|
||||||
files: {
|
|
||||||
createIndependentFileSmall,
|
|
||||||
openIndependentFileSmall,
|
|
||||||
overwriteIndependentFileSmall,
|
|
||||||
},
|
|
||||||
dns: {
|
|
||||||
setProvider: setDnsProvider,
|
|
||||||
},
|
|
||||||
logger: log,
|
|
||||||
getSeed,
|
|
||||||
appRouter: {
|
|
||||||
get: getRouter,
|
|
||||||
set: setRouter,
|
|
||||||
reset: resetRouter,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPluginAPI(): PluginApiManager {
|
export function getPluginAPIManager(): PluginAPIManager {
|
||||||
if (!pluginApi) {
|
if (!pluginAPIManager) {
|
||||||
pluginApi = new PluginApiManager();
|
pluginAPIManager = new PluginAPIManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
return pluginApi as PluginApiManager;
|
return pluginAPIManager as PluginAPIManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadPlugins() {
|
export async function loadPlugins() {
|
||||||
const api = await getPluginAPI();
|
const apiManager = getPluginAPIManager();
|
||||||
|
|
||||||
api.loadPluginInstance(pluginCore);
|
for (const plugin of corePlugins) {
|
||||||
api.loadPluginInstance(pluginRpc);
|
await apiManager.loadPluginInstance(plugin);
|
||||||
|
|
||||||
for (const plugin of [...new Set(config.array("plugins", []))] as []) {
|
|
||||||
api.loadPlugin(plugin);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const plugin of [...new Set(config.array("core.plugins", []))] as []) {
|
||||||
|
await apiManager.loadPlugin(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPluginAPI().emit("core.pluginsLoaded");
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
import Crypto from "./util/crypto";
|
||||||
|
import b4a from "b4a";
|
||||||
|
// @ts-ignore
|
||||||
|
import c from "compact-encoding";
|
||||||
|
|
||||||
|
export default class Util {
|
||||||
|
private _crypto: Crypto = new Crypto();
|
||||||
|
|
||||||
|
get crypto(): Crypto {
|
||||||
|
return this._crypto;
|
||||||
|
}
|
||||||
|
get bufferEncoding(): typeof b4a {
|
||||||
|
return b4a;
|
||||||
|
}
|
||||||
|
|
||||||
|
get binaryEncoding(): typeof c {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
// @ts-ignore
|
||||||
|
import sodium from "sodium-universal";
|
||||||
|
import { getPluginAPI } from "../../plugin";
|
||||||
|
|
||||||
|
export default class Crypto {
|
||||||
|
createHash(data: string): Buffer {
|
||||||
|
const b4a = getPluginAPI().util.bufferEncoding;
|
||||||
|
const buffer = b4a.from(data);
|
||||||
|
let hash = b4a.allocUnsafe(32) as Buffer;
|
||||||
|
sodium.crypto_generichash(hash, buffer);
|
||||||
|
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,132 +0,0 @@
|
||||||
import { getRpcServer } from "../rpc/server";
|
|
||||||
import {
|
|
||||||
Plugin,
|
|
||||||
PluginAPI,
|
|
||||||
RPCBroadcastRequest,
|
|
||||||
RPCBroadcastResponse,
|
|
||||||
RPCClearCacheRequest,
|
|
||||||
RPCClearCacheResponse,
|
|
||||||
RPCClearCacheResponseRelayList,
|
|
||||||
RPCRequest,
|
|
||||||
RPCResponse,
|
|
||||||
} from "@lumeweb/relay-types";
|
|
||||||
import { getRpcByPeer } from "../rpc";
|
|
||||||
|
|
||||||
async function broadcastRequest(
|
|
||||||
request: RPCRequest,
|
|
||||||
relays: string[]
|
|
||||||
): Promise<Map<string, Promise<any>>> {
|
|
||||||
const makeRequest = async (relay: string) => {
|
|
||||||
const rpc = await getRpcByPeer(relay);
|
|
||||||
return rpc.request(`${request.module}.${request.method}`, request.data);
|
|
||||||
};
|
|
||||||
|
|
||||||
let relayMap = new Map<string, Promise<any>>();
|
|
||||||
|
|
||||||
for (const relay of relays) {
|
|
||||||
relayMap.set(relay, makeRequest(relay));
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.allSettled([...relays.values()]);
|
|
||||||
return relayMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
const plugin: Plugin = {
|
|
||||||
name: "rpc",
|
|
||||||
async plugin(api: PluginAPI): Promise<void> {
|
|
||||||
api.registerMethod("get_cached_item", {
|
|
||||||
cacheable: false,
|
|
||||||
async handler(req: string): Promise<RPCResponse> {
|
|
||||||
if (typeof req !== "string") {
|
|
||||||
throw new Error("item must be a string");
|
|
||||||
}
|
|
||||||
|
|
||||||
const cache = getRpcServer().cache.data;
|
|
||||||
|
|
||||||
if (!Object.keys(cache).includes(req)) {
|
|
||||||
throw new Error("item does not exist");
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: true,
|
|
||||||
...cache[req]?.value,
|
|
||||||
signature: cache[req]?.signature,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
api.registerMethod("clear_cached_item", {
|
|
||||||
cacheable: false,
|
|
||||||
async handler(req: RPCClearCacheRequest): Promise<RPCClearCacheResponse> {
|
|
||||||
if (req?.relays?.length) {
|
|
||||||
let resp = await broadcastRequest(
|
|
||||||
{
|
|
||||||
module: "rpc",
|
|
||||||
method: "clear_cached_item",
|
|
||||||
data: req.request,
|
|
||||||
},
|
|
||||||
req?.relays
|
|
||||||
);
|
|
||||||
let results: RPCClearCacheResponse = {
|
|
||||||
relays: {},
|
|
||||||
data: true,
|
|
||||||
signedField: "relays",
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const relay in resp) {
|
|
||||||
let ret: RPCClearCacheResponse;
|
|
||||||
try {
|
|
||||||
ret = await resp.get(relay);
|
|
||||||
} catch (e: any) {
|
|
||||||
(results.relays as RPCClearCacheResponseRelayList)[relay] = {
|
|
||||||
error: e.message,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
api.getRpcServer().cache.deleteItem(req.request);
|
|
||||||
} catch (e: any) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: true,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
api.registerMethod("broadcast_request", {
|
|
||||||
cacheable: false,
|
|
||||||
async handler(req: RPCBroadcastRequest): Promise<RPCBroadcastResponse> {
|
|
||||||
if (!req?.request) {
|
|
||||||
throw new Error("request required");
|
|
||||||
}
|
|
||||||
if (!req?.relays?.length) {
|
|
||||||
throw new Error("relays required");
|
|
||||||
}
|
|
||||||
|
|
||||||
let resp = await broadcastRequest(req.request, req.relays);
|
|
||||||
|
|
||||||
const result: RPCBroadcastResponse = {
|
|
||||||
relays: {},
|
|
||||||
data: true,
|
|
||||||
signedField: "relays",
|
|
||||||
};
|
|
||||||
for (const relay in resp) {
|
|
||||||
let ret: RPCClearCacheResponse;
|
|
||||||
try {
|
|
||||||
ret = await resp.get(relay);
|
|
||||||
} catch (e: any) {
|
|
||||||
result.relays[relay] = { error: e.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default plugin;
|
|
|
@ -4,69 +4,30 @@ import DHT from "@hyperswarm/dht";
|
||||||
import { relay } from "@hyperswarm/dht-relay";
|
import { relay } from "@hyperswarm/dht-relay";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import Stream from "@hyperswarm/dht-relay/ws";
|
import Stream from "@hyperswarm/dht-relay/ws";
|
||||||
import express, { Express } from "express";
|
|
||||||
import config from "../config.js";
|
import config from "../config.js";
|
||||||
import * as http from "http";
|
|
||||||
import * as https from "https";
|
|
||||||
import { get as getSwarm } from "./swarm.js";
|
import { get as getSwarm } from "./swarm.js";
|
||||||
import WS from "ws";
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import log from "loglevel";
|
import log from "../log.js";
|
||||||
import { AddressInfo } from "net";
|
import { AddressInfo } from "net";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import promiseRetry from "promise-retry";
|
import promiseRetry from "promise-retry";
|
||||||
import { getSslContext } from "./ssl.js";
|
import fastify from "fastify";
|
||||||
|
import * as http2 from "http2";
|
||||||
|
import websocket from "@fastify/websocket";
|
||||||
|
|
||||||
export async function start() {
|
export async function start() {
|
||||||
const relayPort = config.uint("port");
|
const dht = getSwarm();
|
||||||
|
|
||||||
const dht = await getSwarm();
|
let relayServer = fastify({
|
||||||
|
http2: true,
|
||||||
const statusCodeServer = http.createServer(function (req, res) {
|
logger: log.child({ module: "relay-server" }),
|
||||||
// @ts-ignore
|
|
||||||
res.writeHead(req.headers["x-status"] ?? 200, {
|
|
||||||
"Content-Type": "text/plain",
|
|
||||||
});
|
|
||||||
res.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await new Promise((resolve) => {
|
relayServer.register(websocket);
|
||||||
statusCodeServer.listen(25252, "0.0.0.0", function () {
|
|
||||||
const address = statusCodeServer.address() as AddressInfo;
|
relayServer.get("/", { websocket: true }, (connection) => {
|
||||||
log.info(
|
relay(dht, new Stream(false, connection.socket));
|
||||||
"Status Code Server started on ",
|
|
||||||
`${address.address}:${address.port}`
|
|
||||||
);
|
|
||||||
resolve(null);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let relayServer: https.Server | http.Server;
|
await relayServer.listen({ port: config.uint("core.port"), host: "0.0.0.0" });
|
||||||
|
|
||||||
if (config.bool("ssl")) {
|
|
||||||
relayServer = https.createServer({
|
|
||||||
SNICallback(servername, cb) {
|
|
||||||
cb(null, getSslContext());
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
relayServer = http.createServer();
|
|
||||||
}
|
|
||||||
|
|
||||||
let wsServer = new WS.Server({ server: relayServer });
|
|
||||||
|
|
||||||
wsServer.on("connection", (socket: any) => {
|
|
||||||
relay(dht, new Stream(false, socket));
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise((resolve) => {
|
|
||||||
relayServer.listen(relayPort, "0.0.0.0", function () {
|
|
||||||
const address = relayServer.address() as AddressInfo;
|
|
||||||
log.info(
|
|
||||||
"DHT Relay Server started on ",
|
|
||||||
`${address.address}:${address.port}`
|
|
||||||
);
|
|
||||||
resolve(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,35 +5,50 @@ import config from "../config.js";
|
||||||
import { errorExit } from "../lib/error.js";
|
import { errorExit } from "../lib/error.js";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import stringify from "json-stable-stringify";
|
import stringify from "json-stable-stringify";
|
||||||
import { getRpcServer, RPC_PROTOCOL_SYMBOL } from "./rpc/server.js";
|
import {
|
||||||
|
getRpcServer,
|
||||||
|
RPC_PROTOCOL_ID,
|
||||||
|
RPC_PROTOCOL_SYMBOL,
|
||||||
|
setupStream,
|
||||||
|
} from "./rpc/server.js";
|
||||||
import { get as getSwarm, SecretStream } from "./swarm.js";
|
import { get as getSwarm, SecretStream } from "./swarm.js";
|
||||||
|
import b4a from "b4a";
|
||||||
|
// @ts-ignore
|
||||||
|
import Protomux from "protomux";
|
||||||
|
|
||||||
export async function start() {
|
export async function start() {
|
||||||
if (!config.str("pocket-app-id") || !config.str("pocket-app-key")) {
|
getSwarm().on("connection", (stream: SecretStream) => {
|
||||||
errorExit("Please set pocket-app-id and pocket-app-key config options.");
|
Protomux.from(stream).pair(
|
||||||
}
|
{ protocol: "protomux-rpc", id: RPC_PROTOCOL_ID },
|
||||||
|
async () => {
|
||||||
(await getSwarm()).on("connection", (stream: SecretStream) =>
|
getRpcServer().setup(stream);
|
||||||
getRpcServer().setup(stream)
|
}
|
||||||
);
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getRpcByPeer(peer: string) {
|
export async function getRpcByPeer(peer: Buffer | string) {
|
||||||
const swarm = await getSwarm();
|
const swarm = getSwarm();
|
||||||
|
if (!b4a.isBuffer(peer)) {
|
||||||
|
peer = b4a.from(peer, "hex") as Buffer;
|
||||||
|
}
|
||||||
|
|
||||||
if (swarm._allConnections.has(peer)) {
|
if (swarm._allConnections.has(peer)) {
|
||||||
return swarm._allConnections.get(peer)[RPC_PROTOCOL_SYMBOL];
|
return swarm._allConnections.get(peer)[RPC_PROTOCOL_SYMBOL];
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const listener = () => {};
|
const listener = (peer: any, info: any) => {
|
||||||
swarm.on("connection", (peer: any, info: any) => {
|
if (info.publicKey.toString("hex") !== peer.toString("hex")) {
|
||||||
if (info.publicKey.toString("hex") !== peer) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
swarm.removeListener("connection", listener);
|
swarm.removeListener("connection", listener);
|
||||||
|
|
||||||
resolve(peer[RPC_PROTOCOL_SYMBOL]);
|
resolve(setupStream(peer));
|
||||||
});
|
};
|
||||||
|
|
||||||
|
swarm.on("connection", listener);
|
||||||
|
|
||||||
|
swarm.joinPeer(peer);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,135 +0,0 @@
|
||||||
import EventEmitter from "events";
|
|
||||||
import DHTCache from "@lumeweb/dht-cache";
|
|
||||||
import {
|
|
||||||
RPCCacheData,
|
|
||||||
RPCCacheItem,
|
|
||||||
RPCRequest,
|
|
||||||
RPCResponse,
|
|
||||||
} from "@lumeweb/relay-types";
|
|
||||||
import { getRpcByPeer } from "../rpc";
|
|
||||||
import b4a from "b4a";
|
|
||||||
import { get as getSwarm } from "../swarm";
|
|
||||||
import { RPCServer } from "./server";
|
|
||||||
// @ts-ignore
|
|
||||||
import orderedJSON from "ordered-json";
|
|
||||||
// @ts-ignore
|
|
||||||
import crypto from "hypercore-crypto";
|
|
||||||
|
|
||||||
export class RPCCache extends EventEmitter {
|
|
||||||
private dhtCache?: DHTCache;
|
|
||||||
private server: RPCServer;
|
|
||||||
|
|
||||||
private _swarm?: any;
|
|
||||||
|
|
||||||
get swarm(): any {
|
|
||||||
return this._swarm;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _data: RPCCacheData = {};
|
|
||||||
|
|
||||||
get data(): RPCCacheData {
|
|
||||||
return this._data;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(server: RPCServer) {
|
|
||||||
super();
|
|
||||||
this.server = server;
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getNodeQuery(
|
|
||||||
node: string,
|
|
||||||
queryHash: string
|
|
||||||
): Promise<boolean | RPCResponse> {
|
|
||||||
if (!this.dhtCache?.peerHasItem(node, queryHash)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rpc = await getRpcByPeer(node);
|
|
||||||
|
|
||||||
let response;
|
|
||||||
|
|
||||||
try {
|
|
||||||
response = rpc.request("rpc.get_cached_item", queryHash) as RPCCacheItem;
|
|
||||||
} catch (e: any) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.verifyResponse(b4a.from(node, "hex") as Buffer, response)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ...response?.value };
|
|
||||||
}
|
|
||||||
|
|
||||||
public signResponse(item: RPCCacheItem): string {
|
|
||||||
const field = item.value.signedField || "data";
|
|
||||||
const updated = item.value.updated;
|
|
||||||
// @ts-ignore
|
|
||||||
const data = item.value[field];
|
|
||||||
const json = orderedJSON.stringify(data);
|
|
||||||
|
|
||||||
return this.server.signData(`${updated}${json}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
public verifyResponse(pubkey: Buffer, item: RPCCacheItem): boolean | Buffer {
|
|
||||||
const field = item.value.signedField || "data";
|
|
||||||
const updated = item.value.updated;
|
|
||||||
// @ts-ignore
|
|
||||||
const data = item.value[field];
|
|
||||||
const json = orderedJSON.stringify(data);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (
|
|
||||||
!crypto.verify(
|
|
||||||
Buffer.from(`${updated}${json}`),
|
|
||||||
Buffer.from(item?.signature as string, "hex"),
|
|
||||||
pubkey
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public addItem(query: RPCRequest, response: RPCResponse) {
|
|
||||||
const queryHash = RPCServer.hashQuery(query);
|
|
||||||
|
|
||||||
const clonedResponse = { ...response };
|
|
||||||
|
|
||||||
clonedResponse.updated = Date.now();
|
|
||||||
|
|
||||||
const item = {
|
|
||||||
value: clonedResponse,
|
|
||||||
signature: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
item.signature = this.signResponse(item);
|
|
||||||
|
|
||||||
this._data[queryHash] = item;
|
|
||||||
}
|
|
||||||
|
|
||||||
public deleteItem(queryHash: string): boolean {
|
|
||||||
const cache = this.dhtCache?.cache;
|
|
||||||
|
|
||||||
if (!cache?.includes(queryHash)) {
|
|
||||||
throw Error("item does not exist");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dhtCache?.removeItem(queryHash);
|
|
||||||
delete this._data[queryHash];
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async init() {
|
|
||||||
this.dhtCache = new DHTCache(await getSwarm(), {
|
|
||||||
protocol: "lumeweb.rpccache",
|
|
||||||
});
|
|
||||||
this._swarm = await getSwarm();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +1,9 @@
|
||||||
import {
|
import {
|
||||||
RPCCacheData,
|
|
||||||
RPCCacheItem,
|
RPCCacheItem,
|
||||||
RPCMethod,
|
RPCMethod,
|
||||||
RPCRequest,
|
RPCRequest,
|
||||||
RPCResponse,
|
RPCResponse,
|
||||||
} from "@lumeweb/relay-types";
|
} from "@lumeweb/interface-relay";
|
||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import ProtomuxRPC from "protomux-rpc";
|
import ProtomuxRPC from "protomux-rpc";
|
||||||
|
@ -12,18 +11,17 @@ import b4a from "b4a";
|
||||||
import { get as getSwarm, SecretStream } from "../swarm";
|
import { get as getSwarm, SecretStream } from "../swarm";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import c from "compact-encoding";
|
import c from "compact-encoding";
|
||||||
import DHTCache from "@lumeweb/dht-cache";
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import crypto from "hypercore-crypto";
|
import crypto from "hypercore-crypto";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import orderedJSON from "ordered-json";
|
|
||||||
import { Mutex } from "async-mutex";
|
import { Mutex } from "async-mutex";
|
||||||
import { RPCCache } from "./cache";
|
// @ts-ignore
|
||||||
|
import jsonStringify from "json-stringify-deterministic";
|
||||||
|
|
||||||
const sodium = require("sodium-universal");
|
const sodium = require("sodium-universal");
|
||||||
let server: RPCServer;
|
let server: RPCServer;
|
||||||
|
|
||||||
const RPC_PROTOCOL_ID = b4a.from("lumeweb");
|
export const RPC_PROTOCOL_ID = b4a.from("lumeweb");
|
||||||
export const RPC_PROTOCOL_SYMBOL = Symbol.for(RPC_PROTOCOL_ID.toString());
|
export const RPC_PROTOCOL_SYMBOL = Symbol.for(RPC_PROTOCOL_ID.toString());
|
||||||
|
|
||||||
export function getRpcServer(): RPCServer {
|
export function getRpcServer(): RPCServer {
|
||||||
|
@ -34,6 +32,20 @@ export function getRpcServer(): RPCServer {
|
||||||
return server as RPCServer;
|
return server as RPCServer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setupStream(stream: SecretStream) {
|
||||||
|
const existing = stream[RPC_PROTOCOL_SYMBOL];
|
||||||
|
if (existing) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
stream[RPC_PROTOCOL_SYMBOL] = new ProtomuxRPC(stream, {
|
||||||
|
id: RPC_PROTOCOL_ID,
|
||||||
|
valueEncoding: c.json,
|
||||||
|
});
|
||||||
|
|
||||||
|
return stream[RPC_PROTOCOL_SYMBOL];
|
||||||
|
}
|
||||||
|
|
||||||
export class RPCServer extends EventEmitter {
|
export class RPCServer extends EventEmitter {
|
||||||
private _modules: Map<string, Map<string, RPCMethod>> = new Map<
|
private _modules: Map<string, Map<string, RPCMethod>> = new Map<
|
||||||
string,
|
string,
|
||||||
|
@ -41,12 +53,6 @@ export class RPCServer extends EventEmitter {
|
||||||
>();
|
>();
|
||||||
private pendingRequests: Map<string, Mutex> = new Map<string, Mutex>();
|
private pendingRequests: Map<string, Mutex> = new Map<string, Mutex>();
|
||||||
|
|
||||||
private _cache: RPCCache = new RPCCache(this);
|
|
||||||
|
|
||||||
get cache(): RPCCache {
|
|
||||||
return this._cache;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static hashQuery(query: RPCRequest): string {
|
public static hashQuery(query: RPCRequest): string {
|
||||||
const clonedQuery: RPCRequest = {
|
const clonedQuery: RPCRequest = {
|
||||||
module: query.module,
|
module: query.module,
|
||||||
|
@ -56,7 +62,7 @@ export class RPCServer extends EventEmitter {
|
||||||
const queryHash = Buffer.allocUnsafe(32);
|
const queryHash = Buffer.allocUnsafe(32);
|
||||||
sodium.crypto_generichash(
|
sodium.crypto_generichash(
|
||||||
queryHash,
|
queryHash,
|
||||||
Buffer.from(orderedJSON.stringify(clonedQuery))
|
Buffer.from(jsonStringify(clonedQuery))
|
||||||
);
|
);
|
||||||
return queryHash.toString("hex");
|
return queryHash.toString("hex");
|
||||||
}
|
}
|
||||||
|
@ -102,16 +108,7 @@ export class RPCServer extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
public setup(stream: SecretStream) {
|
public setup(stream: SecretStream) {
|
||||||
const existing = stream[RPC_PROTOCOL_SYMBOL];
|
const rpc = setupStream(stream);
|
||||||
if (existing) return existing;
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
id: RPC_PROTOCOL_ID,
|
|
||||||
valueEncoding: c.json,
|
|
||||||
};
|
|
||||||
const rpc = new ProtomuxRPC(stream, options);
|
|
||||||
|
|
||||||
stream[RPC_PROTOCOL_SYMBOL] = rpc;
|
|
||||||
|
|
||||||
for (const module of this._modules.keys()) {
|
for (const module of this._modules.keys()) {
|
||||||
for (const method of (
|
for (const method of (
|
||||||
|
@ -129,44 +126,52 @@ export class RPCServer extends EventEmitter {
|
||||||
public signData(data: any): string {
|
public signData(data: any): string {
|
||||||
let raw = data;
|
let raw = data;
|
||||||
if (typeof data !== "string") {
|
if (typeof data !== "string") {
|
||||||
raw = orderedJSON.stringify(data);
|
raw = jsonStringify(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
return crypto
|
return crypto
|
||||||
.sign(Buffer.from(raw, this._cache.swarm.keyPair.privateKey))
|
.sign(Buffer.from(raw), getSwarm().keyPair.secretKey)
|
||||||
.toString("hex");
|
.toString("hex");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleRequest(request: RPCRequest) {
|
public async handleRequest(request: RPCRequest) {
|
||||||
let lockedRequest = await this.waitOnRequestLock(request);
|
let lockedRequest = await this.waitOnRequestLock(request);
|
||||||
|
|
||||||
if (lockedRequest) {
|
if (lockedRequest) {
|
||||||
return lockedRequest;
|
return lockedRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
let cachedRequest = this.getCachedRequest(request) as RPCCacheItem;
|
let method = this.getMethodByRequest(request);
|
||||||
|
|
||||||
if (cachedRequest) {
|
|
||||||
return cachedRequest.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
let method = this.getMethodByRequest(request) as RPCMethod;
|
|
||||||
|
|
||||||
let ret;
|
let ret;
|
||||||
let error;
|
let error;
|
||||||
|
|
||||||
try {
|
if (method instanceof Error) {
|
||||||
ret = (await method.handler(request.data)) as RPCResponse | any;
|
error = method;
|
||||||
} catch (e) {
|
}
|
||||||
error = e;
|
|
||||||
|
if (!error) {
|
||||||
|
method = method as RPCMethod;
|
||||||
|
try {
|
||||||
|
ret = (await method.handler(request.data)) as RPCResponse | any;
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
this.getRequestLock(request)?.release();
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
let rpcResult: RPCResponse = {};
|
let rpcResult: RPCResponse = {};
|
||||||
|
|
||||||
|
if (ret === undefined) {
|
||||||
|
ret = {
|
||||||
|
data: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (ret?.data) {
|
if (ret?.data) {
|
||||||
rpcResult = { ...ret };
|
rpcResult = { ...ret };
|
||||||
|
|
||||||
|
@ -181,22 +186,11 @@ export class RPCServer extends EventEmitter {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (method.cacheable) {
|
this.getRequestLock(request)?.release();
|
||||||
this.cache.addItem(request, rpcResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rpcResult;
|
return rpcResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCachedRequest(request: RPCRequest): RPCCacheItem | boolean {
|
|
||||||
const req = RPCServer.hashQuery(request);
|
|
||||||
if (RPCServer.hashQuery(request) in this._cache.data) {
|
|
||||||
this._cache.data[req];
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getMethodByRequest(request: RPCRequest): Error | RPCMethod {
|
private getMethodByRequest(request: RPCRequest): Error | RPCMethod {
|
||||||
return this.getMethod(request.module, request.method);
|
return this.getMethod(request.module, request.method);
|
||||||
}
|
}
|
||||||
|
@ -225,23 +219,35 @@ export class RPCServer extends EventEmitter {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const reqId = RPCServer.hashQuery(request);
|
if (!this.getRequestLock(request)) {
|
||||||
|
this.createRequestLock(request);
|
||||||
let lock: Mutex = this.pendingRequests.get(reqId) as Mutex;
|
|
||||||
const lockExists = !!lock;
|
|
||||||
|
|
||||||
if (!lockExists) {
|
|
||||||
lock = new Mutex();
|
|
||||||
this.pendingRequests.set(reqId, lock);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const reqId = RPCServer.hashQuery(request);
|
||||||
|
const lock: Mutex = this.getRequestLock(request) as Mutex;
|
||||||
|
|
||||||
if (lock.isLocked()) {
|
if (lock.isLocked()) {
|
||||||
await lock.waitForUnlock();
|
await lock.waitForUnlock();
|
||||||
if (reqId in this._cache.data) {
|
|
||||||
return this._cache.data[reqId] as RPCCacheItem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await lock.acquire();
|
await lock.acquire();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getRequestLock(request: RPCRequest): Mutex | null {
|
||||||
|
const reqId = RPCServer.hashQuery(request);
|
||||||
|
|
||||||
|
let lock: Mutex = this.pendingRequests.get(reqId) as Mutex;
|
||||||
|
|
||||||
|
if (!lock) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lock;
|
||||||
|
}
|
||||||
|
|
||||||
|
private createRequestLock(request: RPCRequest) {
|
||||||
|
const reqId = RPCServer.hashQuery(request);
|
||||||
|
|
||||||
|
this.pendingRequests.set(reqId, new Mutex());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,151 +0,0 @@
|
||||||
import tls from "tls";
|
|
||||||
import {
|
|
||||||
createIndependentFileSmall,
|
|
||||||
openIndependentFileSmall,
|
|
||||||
overwriteIndependentFileSmall,
|
|
||||||
} from "../lib/file.js";
|
|
||||||
// @ts-ignore
|
|
||||||
import promiseRetry from "promise-retry";
|
|
||||||
import config from "../config.js";
|
|
||||||
import log from "loglevel";
|
|
||||||
import { getSeed } from "../lib/util.js";
|
|
||||||
import type {
|
|
||||||
IndependentFileSmall,
|
|
||||||
SavedSslData,
|
|
||||||
SslData,
|
|
||||||
} from "@lumeweb/relay-types";
|
|
||||||
|
|
||||||
let sslCtx: tls.SecureContext = tls.createSecureContext();
|
|
||||||
let sslObject: SslData = {};
|
|
||||||
let sslChecker: () => Promise<void>;
|
|
||||||
|
|
||||||
const FILE_CERT_NAME = "/lumeweb/relay/ssl.crt";
|
|
||||||
const FILE_KEY_NAME = "/lumeweb/relay/ssl.key";
|
|
||||||
|
|
||||||
export function setSslContext(context: tls.SecureContext) {
|
|
||||||
sslCtx = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSslContext(): tls.SecureContext {
|
|
||||||
return sslCtx;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setSsl(
|
|
||||||
cert: IndependentFileSmall | Uint8Array,
|
|
||||||
key: IndependentFileSmall | Uint8Array
|
|
||||||
): void {
|
|
||||||
cert = (cert as IndependentFileSmall)?.fileData || cert;
|
|
||||||
key = (key as IndependentFileSmall)?.fileData || key;
|
|
||||||
sslObject.cert = cert as Uint8Array;
|
|
||||||
sslObject.key = key as Uint8Array;
|
|
||||||
setSslContext(
|
|
||||||
tls.createSecureContext({
|
|
||||||
cert: Buffer.from(cert),
|
|
||||||
key: Buffer.from(key),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSsl(): SslData {
|
|
||||||
return sslObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function saveSSl(): Promise<void> {
|
|
||||||
const seed = getSeed();
|
|
||||||
|
|
||||||
log.info(`Saving SSL Certificate for ${config.str("domain")}`);
|
|
||||||
|
|
||||||
let oldCert = await getSslCert();
|
|
||||||
let cert: any = getSsl()?.cert;
|
|
||||||
if (oldCert) {
|
|
||||||
await overwriteIndependentFileSmall(
|
|
||||||
oldCert as IndependentFileSmall,
|
|
||||||
Buffer.from(cert)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await createIndependentFileSmall(seed, FILE_CERT_NAME, Buffer.from(cert));
|
|
||||||
}
|
|
||||||
|
|
||||||
let oldKey = await getSslKey();
|
|
||||||
let key: any = getSsl()?.key;
|
|
||||||
|
|
||||||
if (oldKey) {
|
|
||||||
await overwriteIndependentFileSmall(
|
|
||||||
oldKey as IndependentFileSmall,
|
|
||||||
Buffer.from(key)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await createIndependentFileSmall(seed, FILE_KEY_NAME, Buffer.from(key));
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info(`Saved SSL Certificate for ${config.str("domain")}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getSavedSsl(
|
|
||||||
retry = true
|
|
||||||
): Promise<boolean | SavedSslData> {
|
|
||||||
let retryOptions = retry ? {} : { retries: 0 };
|
|
||||||
let sslCert: IndependentFileSmall | boolean = false;
|
|
||||||
let sslKey: IndependentFileSmall | boolean = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await promiseRetry(async (retry: any) => {
|
|
||||||
sslCert = await getSslCert();
|
|
||||||
if (!sslCert) {
|
|
||||||
retry();
|
|
||||||
}
|
|
||||||
}, retryOptions);
|
|
||||||
|
|
||||||
await promiseRetry(async (retry: any) => {
|
|
||||||
sslKey = await getSslKey();
|
|
||||||
if (!sslKey) {
|
|
||||||
retry();
|
|
||||||
}
|
|
||||||
}, retryOptions);
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
if (!sslCert || !sslKey) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
cert: sslCert as IndependentFileSmall,
|
|
||||||
key: sslKey as IndependentFileSmall,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getSslCert(): Promise<IndependentFileSmall | boolean> {
|
|
||||||
return getSslFile(FILE_CERT_NAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getSslKey(): Promise<IndependentFileSmall | boolean> {
|
|
||||||
return getSslFile(FILE_KEY_NAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getSslFile(
|
|
||||||
name: string
|
|
||||||
): Promise<IndependentFileSmall | boolean> {
|
|
||||||
let seed = getSeed();
|
|
||||||
|
|
||||||
let [file, err] = await openIndependentFileSmall(seed, name);
|
|
||||||
|
|
||||||
if (err) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return file;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setSSlCheck(checker: () => Promise<void>): void {
|
|
||||||
sslChecker = checker;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSslCheck(): () => Promise<void> {
|
|
||||||
return sslChecker;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function start() {
|
|
||||||
if (config.bool("ssl") && getSslCheck()) {
|
|
||||||
await getSslCheck()();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -5,54 +5,102 @@
|
||||||
import Hyperswarm from "hyperswarm";
|
import Hyperswarm from "hyperswarm";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import DHT from "@hyperswarm/dht";
|
import DHT from "@hyperswarm/dht";
|
||||||
import config from "../config.js";
|
// @ts-ignore
|
||||||
import { errorExit } from "../lib/error.js";
|
import Protomux from "protomux";
|
||||||
import {
|
|
||||||
deriveMyskyRootKeypair,
|
|
||||||
seedPhraseToSeed,
|
|
||||||
validSeedPhrase,
|
|
||||||
} from "libskynet";
|
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import sodium from "sodium-universal";
|
import sodium from "sodium-universal";
|
||||||
import b4a from "b4a";
|
import b4a from "b4a";
|
||||||
|
import log from "../log.js";
|
||||||
|
import { getKeyPair } from "../lib/seed.js";
|
||||||
|
import { getPluginAPI } from "./plugin";
|
||||||
|
|
||||||
const LUMEWEB = b4a.from("lumeweb");
|
const LUMEWEB = b4a.from("lumeweb");
|
||||||
|
export const LUMEWEB_TOPIC_HASH = b4a.allocUnsafe(32);
|
||||||
|
sodium.crypto_generichash(LUMEWEB_TOPIC_HASH, LUMEWEB);
|
||||||
|
|
||||||
export type SecretStream = any;
|
export type SecretStream = any;
|
||||||
|
|
||||||
let node: Hyperswarm;
|
let node: Hyperswarm;
|
||||||
|
let protocolManager: ProtocolManager;
|
||||||
|
|
||||||
export function getKeyPair() {
|
export async function start() {
|
||||||
const seed = config.str("seed");
|
|
||||||
|
|
||||||
let err = validSeedPhrase(seed);
|
|
||||||
if (err !== null) {
|
|
||||||
errorExit("LUME_WEB_RELAY_SEED is invalid. Aborting.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return deriveMyskyRootKeypair(seedPhraseToSeed(seed)[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function start() {
|
|
||||||
const keyPair = getKeyPair();
|
const keyPair = getKeyPair();
|
||||||
|
const bootstrap = DHT.bootstrapper(49737, "0.0.0.0");
|
||||||
|
await bootstrap.ready();
|
||||||
|
|
||||||
node = new Hyperswarm({ keyPair, dht: new DHT({ keyPair }) });
|
const address = bootstrap.address();
|
||||||
const topic = b4a.allocUnsafe(32);
|
node = new Hyperswarm({
|
||||||
sodium.crypto_generichash(topic, LUMEWEB);
|
keyPair,
|
||||||
|
dht: new DHT({
|
||||||
|
keyPair,
|
||||||
|
bootstrap: [{ host: address.host, port: address.port }].concat(
|
||||||
|
require("@hyperswarm/dht/lib/constants").BOOTSTRAP_NODES
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
await node.dht.ready();
|
await node.dht.ready();
|
||||||
await node.listen();
|
await node.listen();
|
||||||
node.join(topic);
|
node.join(LUMEWEB_TOPIC_HASH);
|
||||||
|
|
||||||
|
getPluginAPI().on("core.shutdown", async () => {
|
||||||
|
return bootstrap.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"Relay Identity is %s",
|
||||||
|
b4a.from(getKeyPair().publicKey).toString("hex")
|
||||||
|
);
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get(): Promise<Hyperswarm> {
|
export function get(): Hyperswarm {
|
||||||
if (!node) {
|
return node;
|
||||||
await start();
|
}
|
||||||
|
|
||||||
|
export class ProtocolManager {
|
||||||
|
private _protocols: Map<string, Function> = new Map<string, Function>();
|
||||||
|
private _swarm;
|
||||||
|
|
||||||
|
constructor(swarm: any) {
|
||||||
|
this._swarm = swarm;
|
||||||
|
|
||||||
|
this._swarm.on("connection", (peer: any) => {
|
||||||
|
if (!peer.userData) {
|
||||||
|
peer.userData = null;
|
||||||
|
}
|
||||||
|
for (const protocol of this._protocols) {
|
||||||
|
Protomux.from(peer).pair(
|
||||||
|
{ protocol: protocol[0] },
|
||||||
|
this.handler.bind(this, protocol[0], peer)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return node;
|
private handler(protocol: string, peer: any) {
|
||||||
|
if (this._protocols.has(protocol)) {
|
||||||
|
this._protocols.get(protocol)?.(peer, Protomux.from(peer));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public register(name: string, handler: Function): boolean {
|
||||||
|
if (this._protocols.has(name)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._protocols.set(name, handler);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProtocolManager(): ProtocolManager {
|
||||||
|
if (!protocolManager) {
|
||||||
|
protocolManager = new ProtocolManager(get());
|
||||||
|
}
|
||||||
|
|
||||||
|
return protocolManager;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
import { Plugin, PluginAPI } from "@lumeweb/relay-types";
|
import { Plugin, PluginAPI } from "@lumeweb/interface-relay";
|
||||||
import { getRpcServer } from "../rpc/server";
|
|
||||||
|
import defer from "p-defer";
|
||||||
|
|
||||||
const plugin: Plugin = {
|
const plugin: Plugin = {
|
||||||
name: "core",
|
name: "core",
|
||||||
async plugin(api: PluginAPI): Promise<void> {
|
async plugin(api: PluginAPI): Promise<void> {
|
||||||
|
const pluginsLoaded = defer();
|
||||||
|
api.once("core.pluginsLoaded", () => {
|
||||||
|
pluginsLoaded.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
api.registerMethod("ping", {
|
api.registerMethod("ping", {
|
||||||
cacheable: false,
|
cacheable: false,
|
||||||
async handler(): Promise<any> {
|
async handler(): Promise<any> {
|
||||||
|
@ -14,7 +20,9 @@ const plugin: Plugin = {
|
||||||
api.registerMethod("get_methods", {
|
api.registerMethod("get_methods", {
|
||||||
cacheable: false,
|
cacheable: false,
|
||||||
async handler(): Promise<any> {
|
async handler(): Promise<any> {
|
||||||
return api.getRpcServer().getMethods();
|
await pluginsLoaded.promise;
|
||||||
|
|
||||||
|
return api.rpcServer.getMethods();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { Plugin, PluginAPI } from "@lumeweb/interface-relay";
|
||||||
|
import b4a from "b4a";
|
||||||
|
|
||||||
|
const plugin: Plugin = {
|
||||||
|
name: "dht",
|
||||||
|
async plugin(api: PluginAPI): Promise<void> {
|
||||||
|
api.registerMethod("join_topic", {
|
||||||
|
cacheable: false,
|
||||||
|
async handler(topic: string): Promise<void> {
|
||||||
|
if (!api.swarm._discovery.has(topic)) {
|
||||||
|
api.swarm.join(topic);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
api.registerMethod("get_topic_peers", {
|
||||||
|
cacheable: false,
|
||||||
|
async handler(topic: string): Promise<string[]> {
|
||||||
|
return [...api.swarm.peers.values()]
|
||||||
|
.filter((peerInfo) => peerInfo._seenTopics.has(topic))
|
||||||
|
.map((peerInfo) => b4a.from(peerInfo.publicKey).toString());
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
api.registerMethod("get_topics", {
|
||||||
|
cacheable: false,
|
||||||
|
async handler(): Promise<string[]> {
|
||||||
|
return [...api.swarm.peers.keys()];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default plugin;
|
|
@ -0,0 +1,7 @@
|
||||||
|
import core from "./core";
|
||||||
|
import rpc from "./rpc";
|
||||||
|
import dht from "./dht";
|
||||||
|
|
||||||
|
const corePlugins = [core, dht, rpc];
|
||||||
|
|
||||||
|
export default corePlugins;
|
|
@ -0,0 +1,102 @@
|
||||||
|
import {
|
||||||
|
Plugin,
|
||||||
|
PluginAPI,
|
||||||
|
RPCBroadcastRequest,
|
||||||
|
RPCBroadcastResponse,
|
||||||
|
RPCRequest,
|
||||||
|
RPCResponse,
|
||||||
|
} from "@lumeweb/interface-relay";
|
||||||
|
import { getRpcByPeer } from "../modules/rpc";
|
||||||
|
import { get as getSwarm } from "../modules/swarm";
|
||||||
|
import b4a from "b4a";
|
||||||
|
import pTimeout, { ClearablePromise } from "p-timeout";
|
||||||
|
|
||||||
|
let api: PluginAPI;
|
||||||
|
|
||||||
|
async function broadcastRequest(
|
||||||
|
request: RPCRequest,
|
||||||
|
relays: string[],
|
||||||
|
timeout = 5000
|
||||||
|
): Promise<Map<string, Promise<any>>> {
|
||||||
|
const makeRequest = async (relay: string) => {
|
||||||
|
const rpc = await getRpcByPeer(relay);
|
||||||
|
return rpc.request(`${request.module}.${request.method}`, request.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
let relayMap = new Map<string, ClearablePromise<any>>();
|
||||||
|
|
||||||
|
for (const relay of relays) {
|
||||||
|
let req;
|
||||||
|
if (b4a.equals(b4a.from(relay, "hex"), getSwarm().keyPair.publicKey)) {
|
||||||
|
req = api.rpcServer.handleRequest(request);
|
||||||
|
} else {
|
||||||
|
req = makeRequest(relay);
|
||||||
|
}
|
||||||
|
|
||||||
|
let timeoutPromise = pTimeout(req, {
|
||||||
|
milliseconds: timeout,
|
||||||
|
message: `relay timed out after ${timeout} milliseconds`,
|
||||||
|
});
|
||||||
|
|
||||||
|
relayMap.set(relay, timeoutPromise);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.allSettled([...relays.values()]);
|
||||||
|
return relayMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
const plugin: Plugin = {
|
||||||
|
name: "rpc",
|
||||||
|
async plugin(_api: PluginAPI): Promise<void> {
|
||||||
|
api = _api;
|
||||||
|
api.registerMethod("broadcast_request", {
|
||||||
|
cacheable: false,
|
||||||
|
async handler(req: RPCBroadcastRequest): Promise<RPCBroadcastResponse> {
|
||||||
|
if (!req?.request) {
|
||||||
|
throw new Error("request required");
|
||||||
|
}
|
||||||
|
if (!req?.request?.module) {
|
||||||
|
throw new Error("request.module required");
|
||||||
|
}
|
||||||
|
if (!req?.request?.method) {
|
||||||
|
throw new Error("request.method required");
|
||||||
|
}
|
||||||
|
if (!req?.relays?.length) {
|
||||||
|
throw new Error("relays required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
req?.request?.module === "rpc" &&
|
||||||
|
req?.request?.method === "broadcast_request"
|
||||||
|
) {
|
||||||
|
throw new Error("recursive broadcast_request calls are not allowed");
|
||||||
|
}
|
||||||
|
|
||||||
|
let resp = await broadcastRequest(req.request, req.relays, req.timeout);
|
||||||
|
|
||||||
|
const result: RPCBroadcastResponse = {
|
||||||
|
relays: {},
|
||||||
|
data: true,
|
||||||
|
signedField: "relays",
|
||||||
|
};
|
||||||
|
for (const relay of resp.keys()) {
|
||||||
|
let ret: RPCResponse | Error;
|
||||||
|
try {
|
||||||
|
ret = await resp.get(relay);
|
||||||
|
if (ret instanceof Error) {
|
||||||
|
result.relays[relay] = { error: ret.message };
|
||||||
|
} else {
|
||||||
|
result.relays[relay] = ret as RPCResponse;
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
result.relays[relay] = { error: e.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default plugin;
|
Loading…
Reference in New Issue