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
|
||||
.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",
|
||||
"type": "commonjs",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.0-develop.1",
|
||||
"description": "",
|
||||
"main": "build/index.js",
|
||||
"types": "src/types.ts",
|
||||
"repository": {
|
||||
"url": "gitea@git.lumeweb.com:LumeWeb/relay.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Derrick Hammer",
|
||||
"email": "contact@lumeweb.com"
|
||||
},
|
||||
"scripts": {
|
||||
"semantic-release": "semantic-release",
|
||||
"compile": "tsc",
|
||||
"prebuild": "bash prebuild.sh",
|
||||
"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",
|
||||
"barebuild": "npm run compile && npm run package"
|
||||
"postinstall": "patch-package"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hyperswarm/dht": "^6.0.1",
|
||||
"@hyperswarm/dht-relay": "^0.3.0",
|
||||
"@lumeweb/cfg": "https://github.com/LumeWeb/bcfg.git",
|
||||
"@lumeweb/dht-cache": "https://git.lumeweb.com/LumeWeb/dht-cache.git",
|
||||
"@lumeweb/kernel-utils": "https://github.com/LumeWeb/kernel-utils.git",
|
||||
"@lumeweb/pokt-rpc-endpoints": "https://github.com/LumeWeb/pokt-rpc-endpoints.git",
|
||||
"@skynetlabs/skynet-nodejs": "^2.6.0",
|
||||
"@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",
|
||||
"@fastify/websocket": "^7.2.0",
|
||||
"@hyperswarm/dht-relay": "^0.4.0",
|
||||
"@lumeweb/cfg": "git+https://git.lumeweb.com/LumeWeb/cfg.git",
|
||||
"@lumeweb/interface-relay": "git+https://git.lumeweb.com/LumeWeb/interface-relay",
|
||||
"@scure/bip39": "^1.2.0",
|
||||
"@types/node": "^18.15.11",
|
||||
"@types/ws": "^8.5.4",
|
||||
"async-mutex": "^0.3.2",
|
||||
"b4a": "^1.6.1",
|
||||
"b4a": "^1.6.3",
|
||||
"compact-encoding": "^2.11.0",
|
||||
"date-fns": "^2.28.0",
|
||||
"dotenv": "^16.0.1",
|
||||
"ethers": "^5.6.9",
|
||||
"express": "^4.18.1",
|
||||
"fetch-blob": "https://github.com/LumeWeb/fetch-blob.git",
|
||||
"hyperswarm": "^3.0.4",
|
||||
"json-stable-stringify": "^1.0.1",
|
||||
"libskynet": "https://github.com/LumeWeb/libskynet.git",
|
||||
"libskynetnode": "https://github.com/LumeWeb/libskynetnode.git",
|
||||
"loady": "https://github.com/LumeWeb/loady.git",
|
||||
"loglevel": "^1.8.0",
|
||||
"msgpackr": "^1.6.1",
|
||||
"dotenv": "^16.0.3",
|
||||
"ed25519-keygen": "github:LumeWeb/ed25519-keygen",
|
||||
"ethers": "^5.7.2",
|
||||
"eventemitter2": "^6.4.9",
|
||||
"fastify": "^4.15.0",
|
||||
"fetch-blob": "github:LumeWeb/fetch-blob",
|
||||
"hyperswarm": "^4.4.0",
|
||||
"json-stable-stringify": "^1.0.2",
|
||||
"json-stringify-deterministic": "^1.0.8",
|
||||
"loady": "github:LumeWeb/loady",
|
||||
"msgpackr": "^1.8.5",
|
||||
"node-cache": "^5.1.2",
|
||||
"node-cron": "^3.0.1",
|
||||
"node-fetch": "2",
|
||||
"ordered-json": "^0.1.1",
|
||||
"node-fetch": "^2.6.9",
|
||||
"p-defer": "git+https://git.lumeweb.com/LumeWeb/p-defer.git",
|
||||
"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",
|
||||
"protomux": "^3.4.0",
|
||||
"protomux": "^3.4.1",
|
||||
"protomux-rpc": "^1.3.0",
|
||||
"random-access-memory": "^4.1.0",
|
||||
"random-key": "^0.3.2",
|
||||
"slugify": "^1.6.5",
|
||||
"sodium-universal": "^3.1.0"
|
||||
"slugify": "^1.6.6",
|
||||
"sodium-universal": "^4.0.0"
|
||||
},
|
||||
"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/express": "^4.17.13",
|
||||
"@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",
|
||||
"node-gyp": "^9.1.0",
|
||||
"pkg": "^5.8.0",
|
||||
"node-gyp": "^9.3.1",
|
||||
"patch-package": "^6.5.1",
|
||||
"pkg": "^5.8.1",
|
||||
"prebuildify": "^5.0.1",
|
||||
"prettier": "^2.7.1",
|
||||
"rollup": "^2.77.0",
|
||||
"supports-color": "https://github.com/LumeWeb/supports-color.git",
|
||||
"typescript": "^4.7.4"
|
||||
"prettier": "^2.8.7",
|
||||
"semantic-release": "21",
|
||||
"supports-color": "github:LumeWeb/supports-color",
|
||||
"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": [
|
||||
"node_modules/*/build/Release/*.node",
|
||||
"node_modules/libskynet",
|
||||
"node_modules/libskynetnode",
|
||||
"node_modules/@lumeweb"
|
||||
],
|
||||
"outputPath": "dist"
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
systemctl enable lumeweb-relay.service
|
||||
systemctl start lumeweb-relay.service
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
systemctl stop lumeweb-relay.service
|
||||
systemctl disable lumeweb-relay.service
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
#!/bin/bash
|
||||
|
||||
rimraf node_modules/libskynetnode/node_modules/node-fetch
|
||||
|
||||
for pkg in udx-native sodium-native; do
|
||||
(
|
||||
cd "node_modules/${pkg}" || return
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
//const require = createRequire(import.meta.url);
|
||||
//import { createRequire } from "module";
|
||||
|
||||
// @ts-ignore
|
||||
import Config from "@lumeweb/cfg";
|
||||
import * as os from "os";
|
||||
import * as fs from "fs";
|
||||
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;
|
||||
|
||||
|
@ -22,25 +19,21 @@ switch (os.platform()) {
|
|||
|
||||
case "linux":
|
||||
default:
|
||||
configDir = "/etc/lumeweb/relay/config.d";
|
||||
configDir = "/etc/lumeweb/relay/conf.d";
|
||||
break;
|
||||
}
|
||||
|
||||
config.inject({
|
||||
configDir,
|
||||
port: 8080,
|
||||
logLevel: "info",
|
||||
pluginDir: path.resolve(configDir, "..", "plugins"),
|
||||
plugins: ["core"],
|
||||
ssl: false,
|
||||
"core.confDir": configDir,
|
||||
"core.port": 8080,
|
||||
"core.appPort": 80,
|
||||
"core.logLevel": "info",
|
||||
"core.pluginDir": path.resolve(configDir, "..", "plugins"),
|
||||
});
|
||||
|
||||
config.load({
|
||||
env: true,
|
||||
argv: true,
|
||||
});
|
||||
config.load();
|
||||
|
||||
configDir = config.str("configdir");
|
||||
configDir = config.str("core.confDir");
|
||||
|
||||
if (fs.existsSync(configDir)) {
|
||||
try {
|
||||
|
@ -50,15 +43,8 @@ if (fs.existsSync(configDir)) {
|
|||
}
|
||||
}
|
||||
|
||||
config.load({
|
||||
env: true,
|
||||
argv: true,
|
||||
});
|
||||
config.load();
|
||||
|
||||
for (const setting of ["domain"]) {
|
||||
if (!config.get(setting)) {
|
||||
errorExit(`Required config option ${setting} not set`);
|
||||
}
|
||||
}
|
||||
log.level = config.get("core.loglevel");
|
||||
|
||||
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 startRelay } from "./modules/relay.js";
|
||||
import { start as startApp } from "./modules/app";
|
||||
import log from "loglevel";
|
||||
import config from "./config.js";
|
||||
import { loadPlugins } from "./modules/plugin.js";
|
||||
import { start as startDns } from "./modules/dns.js";
|
||||
import { start as startSSl } from "./modules/ssl.js";
|
||||
import { generateSeedPhraseDeterministic } from "libskynet";
|
||||
import * as crypto from "crypto";
|
||||
import { getPluginAPI, loadPlugins } from "./modules/plugin.js";
|
||||
import { start as startSwarm, get as getSwarm } from "./modules/swarm.js";
|
||||
import * as bip39 from "@scure/bip39";
|
||||
import { wordlist } from "@scure/bip39/wordlists/english";
|
||||
|
||||
log.setDefaultLevel(config.str("log-level"));
|
||||
|
||||
if (!config.str("seed")) {
|
||||
config.saveConfigJson("account.json", {
|
||||
seed: generateSeedPhraseDeterministic(
|
||||
crypto.randomBytes(100).toString("hex")
|
||||
),
|
||||
if (!config.str("core.seed")) {
|
||||
config.save("account", {
|
||||
core: {
|
||||
seed: bip39.generateMnemonic(wordlist),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function boot() {
|
||||
await startSwarm();
|
||||
await loadPlugins();
|
||||
await startApp();
|
||||
await startRpc();
|
||||
await startDns();
|
||||
await startSSl();
|
||||
await startRelay();
|
||||
}
|
||||
|
||||
|
@ -33,9 +28,12 @@ boot();
|
|||
process.on("uncaughtException", function (err) {
|
||||
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.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 {
|
||||
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 log from "loglevel";
|
||||
import { getKeyPair } from "./swarm.js";
|
||||
import log from "../log.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 router = express.Router();
|
||||
let server: http.Server;
|
||||
|
||||
export function getRouter(): express.Router {
|
||||
return router;
|
||||
}
|
||||
|
||||
export function setRouter(newRouter: express.Router): void {
|
||||
router = newRouter;
|
||||
}
|
||||
let app: FastifyInstance;
|
||||
|
||||
export async function start() {
|
||||
app = express();
|
||||
server = http.createServer(app);
|
||||
resetRouter();
|
||||
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);
|
||||
});
|
||||
const keyPair = getKeyPair();
|
||||
app = fastify({
|
||||
logger: log.child({ module: "app-server" }),
|
||||
});
|
||||
|
||||
app.use(function (req, res, next) {
|
||||
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) => {
|
||||
app.get("/", (req, res) => {
|
||||
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 type { RPCServer } 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 * as fs from "fs";
|
||||
import path from "path";
|
||||
import {
|
||||
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";
|
||||
import type { Logger } from "pino";
|
||||
|
||||
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) =>
|
||||
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>();
|
||||
|
||||
public async loadPlugin(moduleName: string): Promise<Plugin> {
|
||||
|
@ -42,7 +130,7 @@ export class PluginApiManager {
|
|||
|
||||
const paths = [];
|
||||
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)) {
|
||||
paths.push(fullPath);
|
||||
break;
|
||||
|
@ -54,84 +142,117 @@ export class PluginApiManager {
|
|||
}
|
||||
|
||||
let plugin: Plugin;
|
||||
let pluginPath = paths.shift();
|
||||
try {
|
||||
plugin = (await import(paths.shift() as string)) as Plugin;
|
||||
plugin = require(pluginPath as string) as Plugin;
|
||||
} catch (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) {
|
||||
plugin = plugin?.default as Plugin;
|
||||
}
|
||||
|
||||
if (!("name" in plugin)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
plugin.name = sanitizeName(plugin.name);
|
||||
|
||||
this.registeredPlugins.set(plugin.name, plugin);
|
||||
|
||||
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) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
return plugin;
|
||||
}
|
||||
log.debug("Initialized plugin %s", plugin.name);
|
||||
|
||||
private getPluginAPI(pluginName: string): PluginAPI {
|
||||
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,
|
||||
},
|
||||
};
|
||||
return plugin;
|
||||
}
|
||||
}
|
||||
|
||||
export function getPluginAPI(): PluginApiManager {
|
||||
if (!pluginApi) {
|
||||
pluginApi = new PluginApiManager();
|
||||
export function getPluginAPIManager(): PluginAPIManager {
|
||||
if (!pluginAPIManager) {
|
||||
pluginAPIManager = new PluginAPIManager();
|
||||
}
|
||||
|
||||
return pluginApi as PluginApiManager;
|
||||
return pluginAPIManager as PluginAPIManager;
|
||||
}
|
||||
|
||||
export async function loadPlugins() {
|
||||
const api = await getPluginAPI();
|
||||
const apiManager = getPluginAPIManager();
|
||||
|
||||
api.loadPluginInstance(pluginCore);
|
||||
api.loadPluginInstance(pluginRpc);
|
||||
|
||||
for (const plugin of [...new Set(config.array("plugins", []))] as []) {
|
||||
api.loadPlugin(plugin);
|
||||
for (const plugin of corePlugins) {
|
||||
await apiManager.loadPluginInstance(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";
|
||||
// @ts-ignore
|
||||
import Stream from "@hyperswarm/dht-relay/ws";
|
||||
import express, { Express } from "express";
|
||||
import config from "../config.js";
|
||||
import * as http from "http";
|
||||
import * as https from "https";
|
||||
import { get as getSwarm } from "./swarm.js";
|
||||
import WS from "ws";
|
||||
// @ts-ignore
|
||||
import log from "loglevel";
|
||||
import log from "../log.js";
|
||||
import { AddressInfo } from "net";
|
||||
// @ts-ignore
|
||||
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() {
|
||||
const relayPort = config.uint("port");
|
||||
const dht = getSwarm();
|
||||
|
||||
const dht = await getSwarm();
|
||||
|
||||
const statusCodeServer = http.createServer(function (req, res) {
|
||||
// @ts-ignore
|
||||
res.writeHead(req.headers["x-status"] ?? 200, {
|
||||
"Content-Type": "text/plain",
|
||||
});
|
||||
res.end();
|
||||
let relayServer = fastify({
|
||||
http2: true,
|
||||
logger: log.child({ module: "relay-server" }),
|
||||
});
|
||||
|
||||
await new Promise((resolve) => {
|
||||
statusCodeServer.listen(25252, "0.0.0.0", function () {
|
||||
const address = statusCodeServer.address() as AddressInfo;
|
||||
log.info(
|
||||
"Status Code Server started on ",
|
||||
`${address.address}:${address.port}`
|
||||
);
|
||||
resolve(null);
|
||||
});
|
||||
relayServer.register(websocket);
|
||||
|
||||
relayServer.get("/", { websocket: true }, (connection) => {
|
||||
relay(dht, new Stream(false, connection.socket));
|
||||
});
|
||||
|
||||
let relayServer: https.Server | http.Server;
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
await relayServer.listen({ port: config.uint("core.port"), host: "0.0.0.0" });
|
||||
}
|
||||
|
|
|
@ -5,35 +5,50 @@ import config from "../config.js";
|
|||
import { errorExit } from "../lib/error.js";
|
||||
// @ts-ignore
|
||||
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 b4a from "b4a";
|
||||
// @ts-ignore
|
||||
import Protomux from "protomux";
|
||||
|
||||
export async function start() {
|
||||
if (!config.str("pocket-app-id") || !config.str("pocket-app-key")) {
|
||||
errorExit("Please set pocket-app-id and pocket-app-key config options.");
|
||||
}
|
||||
|
||||
(await getSwarm()).on("connection", (stream: SecretStream) =>
|
||||
getRpcServer().setup(stream)
|
||||
);
|
||||
getSwarm().on("connection", (stream: SecretStream) => {
|
||||
Protomux.from(stream).pair(
|
||||
{ protocol: "protomux-rpc", id: RPC_PROTOCOL_ID },
|
||||
async () => {
|
||||
getRpcServer().setup(stream);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export async function getRpcByPeer(peer: string) {
|
||||
const swarm = await getSwarm();
|
||||
export async function getRpcByPeer(peer: Buffer | string) {
|
||||
const swarm = getSwarm();
|
||||
if (!b4a.isBuffer(peer)) {
|
||||
peer = b4a.from(peer, "hex") as Buffer;
|
||||
}
|
||||
|
||||
if (swarm._allConnections.has(peer)) {
|
||||
return swarm._allConnections.get(peer)[RPC_PROTOCOL_SYMBOL];
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const listener = () => {};
|
||||
swarm.on("connection", (peer: any, info: any) => {
|
||||
if (info.publicKey.toString("hex") !== peer) {
|
||||
const listener = (peer: any, info: any) => {
|
||||
if (info.publicKey.toString("hex") !== peer.toString("hex")) {
|
||||
return;
|
||||
}
|
||||
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 {
|
||||
RPCCacheData,
|
||||
RPCCacheItem,
|
||||
RPCMethod,
|
||||
RPCRequest,
|
||||
RPCResponse,
|
||||
} from "@lumeweb/relay-types";
|
||||
} from "@lumeweb/interface-relay";
|
||||
import EventEmitter from "events";
|
||||
// @ts-ignore
|
||||
import ProtomuxRPC from "protomux-rpc";
|
||||
|
@ -12,18 +11,17 @@ import b4a from "b4a";
|
|||
import { get as getSwarm, SecretStream } from "../swarm";
|
||||
// @ts-ignore
|
||||
import c from "compact-encoding";
|
||||
import DHTCache from "@lumeweb/dht-cache";
|
||||
// @ts-ignore
|
||||
import crypto from "hypercore-crypto";
|
||||
// @ts-ignore
|
||||
import orderedJSON from "ordered-json";
|
||||
import { Mutex } from "async-mutex";
|
||||
import { RPCCache } from "./cache";
|
||||
// @ts-ignore
|
||||
import jsonStringify from "json-stringify-deterministic";
|
||||
|
||||
const sodium = require("sodium-universal");
|
||||
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 function getRpcServer(): RPCServer {
|
||||
|
@ -34,6 +32,20 @@ export function getRpcServer(): 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 {
|
||||
private _modules: Map<string, Map<string, RPCMethod>> = new Map<
|
||||
string,
|
||||
|
@ -41,12 +53,6 @@ export class RPCServer extends EventEmitter {
|
|||
>();
|
||||
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 {
|
||||
const clonedQuery: RPCRequest = {
|
||||
module: query.module,
|
||||
|
@ -56,7 +62,7 @@ export class RPCServer extends EventEmitter {
|
|||
const queryHash = Buffer.allocUnsafe(32);
|
||||
sodium.crypto_generichash(
|
||||
queryHash,
|
||||
Buffer.from(orderedJSON.stringify(clonedQuery))
|
||||
Buffer.from(jsonStringify(clonedQuery))
|
||||
);
|
||||
return queryHash.toString("hex");
|
||||
}
|
||||
|
@ -102,16 +108,7 @@ export class RPCServer extends EventEmitter {
|
|||
}
|
||||
|
||||
public setup(stream: SecretStream) {
|
||||
const existing = stream[RPC_PROTOCOL_SYMBOL];
|
||||
if (existing) return existing;
|
||||
|
||||
const options = {
|
||||
id: RPC_PROTOCOL_ID,
|
||||
valueEncoding: c.json,
|
||||
};
|
||||
const rpc = new ProtomuxRPC(stream, options);
|
||||
|
||||
stream[RPC_PROTOCOL_SYMBOL] = rpc;
|
||||
const rpc = setupStream(stream);
|
||||
|
||||
for (const module of this._modules.keys()) {
|
||||
for (const method of (
|
||||
|
@ -129,44 +126,52 @@ export class RPCServer extends EventEmitter {
|
|||
public signData(data: any): string {
|
||||
let raw = data;
|
||||
if (typeof data !== "string") {
|
||||
raw = orderedJSON.stringify(data);
|
||||
raw = jsonStringify(data);
|
||||
}
|
||||
|
||||
return crypto
|
||||
.sign(Buffer.from(raw, this._cache.swarm.keyPair.privateKey))
|
||||
.sign(Buffer.from(raw), getSwarm().keyPair.secretKey)
|
||||
.toString("hex");
|
||||
}
|
||||
|
||||
private async handleRequest(request: RPCRequest) {
|
||||
public async handleRequest(request: RPCRequest) {
|
||||
let lockedRequest = await this.waitOnRequestLock(request);
|
||||
|
||||
if (lockedRequest) {
|
||||
return lockedRequest;
|
||||
}
|
||||
|
||||
let cachedRequest = this.getCachedRequest(request) as RPCCacheItem;
|
||||
|
||||
if (cachedRequest) {
|
||||
return cachedRequest.value;
|
||||
}
|
||||
|
||||
let method = this.getMethodByRequest(request) as RPCMethod;
|
||||
let method = this.getMethodByRequest(request);
|
||||
|
||||
let ret;
|
||||
let error;
|
||||
|
||||
try {
|
||||
ret = (await method.handler(request.data)) as RPCResponse | any;
|
||||
} catch (e) {
|
||||
error = e;
|
||||
if (method instanceof Error) {
|
||||
error = method;
|
||||
}
|
||||
|
||||
if (!error) {
|
||||
method = method as RPCMethod;
|
||||
try {
|
||||
ret = (await method.handler(request.data)) as RPCResponse | any;
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
}
|
||||
|
||||
if (error) {
|
||||
this.getRequestLock(request)?.release();
|
||||
throw error;
|
||||
}
|
||||
|
||||
let rpcResult: RPCResponse = {};
|
||||
|
||||
if (ret === undefined) {
|
||||
ret = {
|
||||
data: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (ret?.data) {
|
||||
rpcResult = { ...ret };
|
||||
|
||||
|
@ -181,22 +186,11 @@ export class RPCServer extends EventEmitter {
|
|||
};
|
||||
}
|
||||
|
||||
if (method.cacheable) {
|
||||
this.cache.addItem(request, rpcResult);
|
||||
}
|
||||
this.getRequestLock(request)?.release();
|
||||
|
||||
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 {
|
||||
return this.getMethod(request.module, request.method);
|
||||
}
|
||||
|
@ -225,23 +219,35 @@ export class RPCServer extends EventEmitter {
|
|||
return;
|
||||
}
|
||||
|
||||
const reqId = RPCServer.hashQuery(request);
|
||||
|
||||
let lock: Mutex = this.pendingRequests.get(reqId) as Mutex;
|
||||
const lockExists = !!lock;
|
||||
|
||||
if (!lockExists) {
|
||||
lock = new Mutex();
|
||||
this.pendingRequests.set(reqId, lock);
|
||||
if (!this.getRequestLock(request)) {
|
||||
this.createRequestLock(request);
|
||||
}
|
||||
|
||||
const reqId = RPCServer.hashQuery(request);
|
||||
const lock: Mutex = this.getRequestLock(request) as Mutex;
|
||||
|
||||
if (lock.isLocked()) {
|
||||
await lock.waitForUnlock();
|
||||
if (reqId in this._cache.data) {
|
||||
return this._cache.data[reqId] as RPCCacheItem;
|
||||
}
|
||||
}
|
||||
|
||||
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";
|
||||
// @ts-ignore
|
||||
import DHT from "@hyperswarm/dht";
|
||||
import config from "../config.js";
|
||||
import { errorExit } from "../lib/error.js";
|
||||
import {
|
||||
deriveMyskyRootKeypair,
|
||||
seedPhraseToSeed,
|
||||
validSeedPhrase,
|
||||
} from "libskynet";
|
||||
// @ts-ignore
|
||||
import Protomux from "protomux";
|
||||
|
||||
// @ts-ignore
|
||||
import sodium from "sodium-universal";
|
||||
import b4a from "b4a";
|
||||
import log from "../log.js";
|
||||
import { getKeyPair } from "../lib/seed.js";
|
||||
import { getPluginAPI } from "./plugin";
|
||||
|
||||
const LUMEWEB = b4a.from("lumeweb");
|
||||
export const LUMEWEB_TOPIC_HASH = b4a.allocUnsafe(32);
|
||||
sodium.crypto_generichash(LUMEWEB_TOPIC_HASH, LUMEWEB);
|
||||
|
||||
export type SecretStream = any;
|
||||
|
||||
let node: Hyperswarm;
|
||||
let protocolManager: ProtocolManager;
|
||||
|
||||
export function getKeyPair() {
|
||||
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() {
|
||||
export async function start() {
|
||||
const keyPair = getKeyPair();
|
||||
const bootstrap = DHT.bootstrapper(49737, "0.0.0.0");
|
||||
await bootstrap.ready();
|
||||
|
||||
node = new Hyperswarm({ keyPair, dht: new DHT({ keyPair }) });
|
||||
const topic = b4a.allocUnsafe(32);
|
||||
sodium.crypto_generichash(topic, LUMEWEB);
|
||||
const address = bootstrap.address();
|
||||
node = new Hyperswarm({
|
||||
keyPair,
|
||||
dht: new DHT({
|
||||
keyPair,
|
||||
bootstrap: [{ host: address.host, port: address.port }].concat(
|
||||
require("@hyperswarm/dht/lib/constants").BOOTSTRAP_NODES
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
await node.dht.ready();
|
||||
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;
|
||||
}
|
||||
|
||||
export async function get(): Promise<Hyperswarm> {
|
||||
if (!node) {
|
||||
await start();
|
||||
export function get(): Hyperswarm {
|
||||
return node;
|
||||
}
|
||||
|
||||
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 { getRpcServer } from "../rpc/server";
|
||||
import { Plugin, PluginAPI } from "@lumeweb/interface-relay";
|
||||
|
||||
import defer from "p-defer";
|
||||
|
||||
const plugin: Plugin = {
|
||||
name: "core",
|
||||
async plugin(api: PluginAPI): Promise<void> {
|
||||
const pluginsLoaded = defer();
|
||||
api.once("core.pluginsLoaded", () => {
|
||||
pluginsLoaded.resolve();
|
||||
});
|
||||
|
||||
api.registerMethod("ping", {
|
||||
cacheable: false,
|
||||
async handler(): Promise<any> {
|
||||
|
@ -14,7 +20,9 @@ const plugin: Plugin = {
|
|||
api.registerMethod("get_methods", {
|
||||
cacheable: false,
|
||||
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