version 0.1.1

refactor
Niko PLP 1 day ago
commit 6755978c03
  1. 17
      .gitignore
  2. 14
      .prettierrc.json
  3. 199
      CHANGELOG.md
  4. 4918
      Cargo.lock
  5. 44
      Cargo.toml
  6. 240
      DEV.md
  7. 176
      LICENSE-APACHE2
  8. 22
      LICENSE-MIT
  9. 71
      README.md
  10. 40
      RELEASE-NOTE.md
  11. 2
      nextgraph/.gitignore
  12. BIN
      nextgraph/.static/header.png
  13. 55
      nextgraph/Cargo.toml
  14. 83
      nextgraph/README.md
  15. 17
      nextgraph/examples/README.md
  16. 13
      nextgraph/examples/in_memory.md
  17. 178
      nextgraph/examples/in_memory.rs
  18. 17
      nextgraph/examples/open.md
  19. 76
      nextgraph/examples/open.rs
  20. 13
      nextgraph/examples/persistent.md
  21. 161
      nextgraph/examples/persistent.rs
  22. 103
      nextgraph/examples/sparql_update.rs
  23. BIN
      nextgraph/examples/wallet-security-image-demo.png
  24. BIN
      nextgraph/examples/wallet-security-image-white.png
  25. 137
      nextgraph/src/lib.rs
  26. 3209
      nextgraph/src/local_broker.rs
  27. 1
      nextgraph/src/local_broker_dev_env.rs
  28. 45
      ng-broker/Cargo.toml
  29. 56
      ng-broker/README.md
  30. 5
      ng-broker/build.rs
  31. 1
      ng-broker/src/actors/mod.rs
  32. 110
      ng-broker/src/interfaces.rs
  33. 15
      ng-broker/src/lib.rs
  34. BIN
      ng-broker/src/public/favicon.ico
  35. 776
      ng-broker/src/rocksdb_server_storage.rs
  36. 905
      ng-broker/src/server_broker.rs
  37. 355
      ng-broker/src/server_storage/admin/account.rs
  38. 184
      ng-broker/src/server_storage/admin/invitation.rs
  39. 5
      ng-broker/src/server_storage/admin/mod.rs
  40. 123
      ng-broker/src/server_storage/admin/wallet.rs
  41. 95
      ng-broker/src/server_storage/core/account.rs
  42. 158
      ng-broker/src/server_storage/core/commit.rs
  43. 120
      ng-broker/src/server_storage/core/inbox.rs
  44. 20
      ng-broker/src/server_storage/core/mod.rs
  45. 142
      ng-broker/src/server_storage/core/overlay.rs
  46. 187
      ng-broker/src/server_storage/core/peer.rs
  47. 123
      ng-broker/src/server_storage/core/repo.rs
  48. 182
      ng-broker/src/server_storage/core/topic.rs
  49. 3
      ng-broker/src/server_storage/mod.rs
  50. 950
      ng-broker/src/server_ws.rs
  51. 35
      ng-broker/src/types.rs
  52. 31
      ng-broker/src/utils.rs
  53. 38
      ng-client-ws/Cargo.toml
  54. 56
      ng-client-ws/README.md
  55. 13
      ng-client-ws/src/lib.rs
  56. 394
      ng-client-ws/src/remote_ws.rs
  57. 216
      ng-client-ws/src/remote_ws_wasm.rs
  58. 54
      ng-net/Cargo.toml
  59. 56
      ng-net/README.md
  60. 238
      ng-net/src/actor.rs
  61. 145
      ng-net/src/actors/admin/add_invitation.rs
  62. 121
      ng-net/src/actors/admin/add_user.rs
  63. 111
      ng-net/src/actors/admin/create_user.rs
  64. 94
      ng-net/src/actors/admin/del_user.rs
  65. 135
      ng-net/src/actors/admin/list_invitations.rs
  66. 95
      ng-net/src/actors/admin/list_users.rs
  67. 17
      ng-net/src/actors/admin/mod.rs
  68. 3
      ng-net/src/actors/app/mod.rs
  69. 134
      ng-net/src/actors/app/request.rs
  70. 197
      ng-net/src/actors/app/session.rs
  71. 104
      ng-net/src/actors/client/blocks_exist.rs
  72. 132
      ng-net/src/actors/client/blocks_get.rs
  73. 81
      ng-net/src/actors/client/blocks_put.rs
  74. 94
      ng-net/src/actors/client/client_event.rs
  75. 125
      ng-net/src/actors/client/commit_get.rs
  76. 121
      ng-net/src/actors/client/event.rs
  77. 74
      ng-net/src/actors/client/inbox_post.rs
  78. 91
      ng-net/src/actors/client/inbox_register.rs
  79. 25
      ng-net/src/actors/client/mod.rs
  80. 230
      ng-net/src/actors/client/pin_repo.rs
  81. 96
      ng-net/src/actors/client/repo_pin_status.rs
  82. 139
      ng-net/src/actors/client/topic_sub.rs
  83. 151
      ng-net/src/actors/client/topic_sync_req.rs
  84. 89
      ng-net/src/actors/client/wallet_put_export.rs
  85. 43
      ng-net/src/actors/connecting.rs
  86. 103
      ng-net/src/actors/ext/get.rs
  87. 3
      ng-net/src/actors/ext/mod.rs
  88. 109
      ng-net/src/actors/ext/wallet_get_export.rs
  89. 25
      ng-net/src/actors/mod.rs
  90. 69
      ng-net/src/actors/noise.rs
  91. 73
      ng-net/src/actors/probe.rs
  92. 326
      ng-net/src/actors/start.rs
  93. 1311
      ng-net/src/app_protocol.rs
  94. 1319
      ng-net/src/broker.rs
  95. 53
      ng-net/src/bsps.rs
  96. 1621
      ng-net/src/connection.rs
  97. 54
      ng-net/src/lib.rs
  98. 157
      ng-net/src/server_broker.rs
  99. 1435
      ng-net/src/tests/file.rs
  100. 2
      ng-net/src/tests/mod.rs
  101. Some files were not shown because too many files have changed in this diff Show More

17
.gitignore vendored

@ -0,0 +1,17 @@
*~
*.tar.gz
.ng
.direnv
!.github
\#*
/target
/result*
.DS_Store
node_modules
*/tests/*.ng
*/tests/*.ngw
*/tests/*.pazzle
*/tests/*.mnemonic
*/ng-example/*
.vscode/settings.json
.env.local

@ -0,0 +1,14 @@
{
"plugins": ["prettier-plugin-svelte"],
"overrides": [
{
"files": "*",
"excludeFiles": ["*.svelte", "*.html", "*.json"],
"options": {
"tabWidth": 4
}
},
{ "files": "*.svelte", "options": { "parser": "svelte" } }
],
"trailingComma": "es5"
}

@ -0,0 +1,199 @@
# Changelog
Access the sub-sections directly :
[App](#app) - [SDK](#sdk) - [Broker](#broker) - [CLI](#cli)
## App
### App [0.1.1-alpha] - 2024-09-02
#### Added
- edit title and intro
#### Fixed
- bug doc not saved when back navigation
### App [0.1.0-preview.8] - 2024-08-21
#### Added
- signature tool: signs HEADS or a snapshot
#### Fixed
- bug in synchronization of stores content (container) on tauri native apps
- removed dark theme (that wasn't implemented properly)
- on web-app, detects jshelter and ask user to deactivate it
### App [0.1.0-preview.7] - 2024-08-15
#### Added
- Wallet Creation : Download Recovery PDF
- Wallet Creation : Download wallet file
- Wallet Login : with pazzle
- Wallet Login : correct errors while entering pazzle
- Wallet Login : with mnemonic
- Wallet Login : in-memory session (save nothing locally)
- Wallet Import : from file
- Wallet Import : from QR code
- Wallet Import : from TextCode
- User Panel : Online / Offline status
- User Panel : Toggle Personal Connection
- User Panel : Logout
- User Panel / Wallet : Export by scanning QRCode
- User Panel / Wallet : Export by generating QRCode
- User Panel / Wallet : Export by generating TextCode
- User Panel / Wallet : Download file
- User Panel / Accounts Info : basic info (not accurate)
- Document Menu : switch Viewer / Editor
- Document Menu : switch Graph / Document
- Document Menu : Live editing
- Document Menu : Upload binary file + Attachements and Files pane
- Document Menu : History pane
- Add Document : Save in current Store
- Document class: Source Code: Rust, JS, TS, Svelte, React
- Document class: Data : Graph, Container, JSON, Array, Object
- Document class: Post (rich text)
- Document class: Markdown (rich text)
- Document class: Plain Text
- A11Y : limited ARIA and tabulation navigation on all pages. not tested with screen-reader.
- I18N : english
- I18N : german (partial)
- Native app: macOS
- Native app: android
- Native app: linux and Ubuntu
- Native app: Windows
## SDK
### SDK [0.1.1-alpha.7] - 2025-04-03
#### Changed
- js : doc_create : parameters are session_id, crdt, class_name, destination, store_repo (defaults to Private Store)
- nodejs & python : doc_create : parameters are session_id, crdt, class_name, destination, store_type (string), store_repo (string) if 2 last params omitted, defaults to Private Store.
- all : sparql_update : returns list of Nuri of new commits, in the form `did:ng:o:c`
#### Added
- python : wallet_open_with_mnemonic_words
- python : disconnect_and_close
- python : doc_create
- python : doc_sparql_update
- js & nodejs : fetch_header
- js & nodejs : update_header
- js & nodejs : signature_status
- js & nodejs : signed_snapshot_request
- js & nodejs : signature_request
- rust : app_request: Fetch : SignatureStatus , SignatureRequest SignedSnapshotRequest
### SDK [0.1.0-preview.6] - 2024-08-15
#### Added
- js : session_start
- js : session_start_remote
- js : session_stop
- js : user_connect
- js : user_disconnect
- js : discrete_update
- js : sparql_update
- js : sparql_query (returns SPARQL Query Results JSON Format, a list of turtle triples, or a boolean )
- js : branch_history
- js : app_request_stream (fetch and subscribe)
- js : app_request
- js : doc_create
- js : file_get
- js : upload_start
- js : upload_done
- js : upload_chunk
- nodejs : init_headless
- nodejs : session_headless_start
- nodejs : session_headless_stop
- nodejs : sparql_query (returns SPARQL Query Results JSON Format, RDF-JS data model, or a boolean)
- nodejs : discrete_update
- nodejs : sparql_update
- nodejs : rdf_dump
- nodejs : admin_create_user
- nodejs : doc_create
- nodejs : file_get
- nodejs : file_put
- rust : session_start
- rust : session_stop
- rust : app_request_stream, gives access to:
- fetch and subscribe
- file_get
- rust : app_request, gives access to:
- create_doc
- sparql_query
- sparql_update
- discrete_update
- rdf_dump
- history
- file_put
## Broker
### Broker [0.1.1-alpha] - 2024-09-02
### Broker [0.1.0-preview.8] - 2024-08-21
#### Added
- ExtProtocol : ObjectGet
### Broker [0.1.0-preview.7] - 2024-08-15
#### Added
- listen on localhost
- listen on domain
- listen on private LAN
- listen on public IP
- invite-admin
- broker service provider : add invitation for user
- serve web app
- ExtProtocol : WalletGetExport
- ClientProtocol : BlocksExist
- ClientProtocol : BlocksGet
- ClientProtocol : BlocksPut
- ClientProtocol : CommitGet
- ClientProtocol : Event
- ClientProtocol : PinRepo
- ClientProtocol : RepoPinStatus
- ClientProtocol : TopicSub
- ClientProtocol : TopicSyncReq
- ClientProtocol : WalletPutExport
- AppProtocol : AppRequest
- AppProtocol : AppSessionStart
- AppProtocol : AppSessionStop
- AdminProtocol : AddInvitation
- AdminProtocol : AddUser
- AdminProtocol : CreateUser
- AdminProtocol : DelUser
- AdminProtocol : ListInvitations
- AdminProtocol : ListUsers
## CLI
### CLI [0.1.1-alpha] - 2024-09-02
### CLI [0.1.0-preview.8] - 2024-08-21
#### Added
- get : download binary files, snapshots, and head commits, and verify signature
### CLI [0.1.0-preview.7] - 2024-08-15
#### Added
- gen-key
- admin : add/remove admin user
- admin : add invitation
- admin : list users
- admin : list invitations

4918
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,44 @@
[workspace]
resolver = "2"
members = [
"nextgraph",
"ng-repo",
"ng-net",
"ng-broker",
"ng-client-ws",
"ng-verifier",
"ng-wallet",
"ng-storage-rocksdb",
"sdk/ng-sdk-js",
"ng-oxigraph",
]
default-members = [ "nextgraph" ]
[workspace.package]
version = "0.1.2"
edition = "2021"
rust-version = "1.81.0"
license = "MIT/Apache-2.0"
authors = ["Niko PLP <niko@nextgraph.org>"]
repository = "https://git.nextgraph.org/NextGraph/nextgraph-rs"
homepage = "https://nextgraph.org"
keywords = [
"crdt","dapp","decentralized","e2ee","local-first","p2p","semantic-web","eventual-consistency","json-ld","markdown",
"ocap","vc","offline-first","p2p-network","collaboration","privacy-protection","rdf","rich-text-editor","self-hosted",
"sparql","byzantine-fault-tolerance",
"web3", "graph-database", "database","triplestore"
]
documentation = "https://docs.nextgraph.org/"
[profile.release]
lto = true
opt-level = 's'
[profile.dev]
opt-level = 2
[patch.crates-io]
# tauri = { git = "https://github.com/simonhyll/tauri.git", branch="fix/ipc-mixup"}
# tauri = { git = "https://git.nextgraph.org/NextGraph/tauri.git", branch="alpha.11-nextgraph", features = ["no-ipc-custom-protocol"] }
[workspace.dependencies]

240
DEV.md

@ -0,0 +1,240 @@
# Contributors or compilation guide
- [Install Rust](https://www.rust-lang.org/tools/install) minimum required MSRV 1.81.0
- [Install Nodejs](https://nodejs.org/en/download/)
- [Install LLVM](https://rust-lang.github.io/rust-bindgen/requirements.html)
On OpenBSD, for LLVM you need to choose llvm-17.
Until this [PR](https://github.com/rustwasm/wasm-pack/pull/1271) is accepted, will have to install wasm-pack this way:
```
cargo install wasm-pack --git https://git.nextgraph.org/NextGraph/wasm-pack.git --branch master
```
On Debian distros
```
sudo apt install pkg-config gcc build-essential libglib2.0-dev libgtk-3-dev libwebkit2gtk-4.1-dev
```
```
cargo install cargo-watch
cargo install cargo-run-script
// optionally, if you want a Rust REPL: cargo install evcxr_repl
git clone git@git.nextgraph.org:NextGraph/nextgraph-rs.git
// or if you don't have a git account with us: git clone https://git.nextgraph.org/NextGraph/nextgraph-rs.git
cd nextgraph-rs
npm install -g pnpm
cd sdk/ng-sdk-js
cargo run-script app
cd ../..
cd helpers/wasm-tools
cargo run-script app
cd ../..
pnpm -C ./ng-app install
pnpm -C ./ng-app webfilebuild
pnpm -C ./helpers/app-auth install
pnpm -C ./helpers/app-auth build
```
For building the native apps, see the [ng-app/README](ng-app/README.md)
### First run
The current directory will be used to save all the config, keys and storage data.
If you prefer to change the base directory, use the argument `--base [PATH]` when using `ngd` and/or `ngcli`.
```
// runs the daemon in one terminal
cargo run -p ngd -- -vv --save-key -l 14400
```
If you are developing also the front-end, you should run it with this command in a separate terminal:
```
cd ng-app
pnpm -C ../helpers/net-auth builddev
pnpm -C ../helpers/app-auth builddev
pnpm -C ../helpers/net-bootstrap builddev
pnpm webdev
```
In the logs/output of ngd, you will see an invitation link that you should open in your web browser. If there are many links, choose the one that starts with `http://localhost:`, and if you run a local front-end, replace the prefix `http://localhost:14400/` with `http://localhost:1421/` before you open the link in your browser.
The computer you use to open the link should have direct access to the ngd server on localhost. In most of the cases, it will work, as you are running ngd on localhost. If you are running ngd in a docker container, then you need to give access to the container to the local network of the host by using `docker run --network="host"`. see more here https://docs.docker.com/network/drivers/host/
Follow the steps on the screen to create your wallet :)
Once your ngd server will run in your dev env, replace the string in `nextgraph/src/local_broker_dev_env.rs` with the actual PEER ID of your ngd server that is displayed when you first start `ngd`, with a line starting with `INFO ngd] PeerId of node:`.
### Using ngcli with the account you just created
The current directory will be used to save all the config, keys and storage data.
If you prefer to change the base directory, use the argument `--base [PATH]` when using `ngd` and/or `ngcli`.
`PEER_ID_OF_SERVER` is displayed when you first start `ngd`, with a line starting with `INFO ngd] PeerId of node:`.
`THE_PRIVATE_KEY_OF_THE_USER_YOU_JUST_CREATED` can be found in the app, after you opened your wallet, click on the logo of NextGraph, and you will see the User Panel. Click on `Accounts` and you will find the User Private Key.
By example, to list all the admin users :
```
cargo run -p ngcli -- --save-key --save-config -s 127.0.0.1,14400,<PEER_ID_OF_SERVER> -u <THE_PRIVATE_KEY_OF_THE_USER_YOU_JUST_CREATED> admin list-users -a
```
### Adding more accounts and wallets
In your dev env, if you want to create more wallets and accounts, you have 2 options:
- creating an invitation link from the admin account
```
cargo run -p ngcli -- -s 127.0.0.1,14400,<PEER_ID_OF_SERVER> -u <THE_PRIVATE_KEY_OF_THE_USER_YOU_JUST_CREATED> admin add-invitation --notos
```
and then open the link after replacing the port number from `14400` to `1421` (if you are running the front-end in development mode).
- run a local instance of `ngaccount`. this is useful if you want to test or develop the ngaccount part of the flow..
See the [README of ngaccount here](ngaccount/README.md).
Then you need to stop your ngd and start it again with the additional option :
```
--registration-url="http://127.0.0.1:5173/#/create"
```
### Packages
The crates are organized as follow :
- [nextgraph](nextgraph/README.md) : Client library. Use this crate to embed NextGraph client in your Rust application
- [ngcli](ngcli/README.md) : CLI tool to manipulate the local documents and repos and administrate the server
- [ngd](ngd/README.md) : binary executable of the daemon (that can run a broker, verifier and/or Rust services)
- [ng-app](ng-app/README.md) : all the native apps, based on Tauri, and the official web app.
- [ng-sdk-js](ng-sdk-js/DEV.md) : contains the JS SDK, with example for: web app, react app, or node service.
- [ng-sdk-python](ng-sdk-python/README.md) : contains the Python SDK.
- ng-repo : Repositories common library
- ng-net : Network common library
- ng-oxigraph : Fork of OxiGraph. contains our CRDT of RDF
- ng-verifier : Verifier library, that exposes the document API to the app
- ng-wallet : keeps the secret keys of all identities of the user in a safe wallet
- ng-broker : Core and Server Broker library
- ng-client-ws : Websocket client library
- ng-storage-rocksdb : RocksDB backed stores. see also dependency [repo here](https://git.nextgraph.org/NextGraph/rust-rocksdb)
- helpers : all kind of servers and front end code needed for our infrastructure.
### Test
Please test by following this order (as we need to generate some files locally)
```
cargo test --package nextgraph -r --lib -- local_broker::test::gen_wallet_for_test --show-output --nocapture
cargo test -r
cargo test --package nextgraph -r --lib -- local_broker::test::import_session_for_test_to_disk --show-output --nocapture --ignored
```
Test a single crate:
```
cargo test --package ng-repo --lib -- --show-output --nocapture
cargo test --package ng-wallet --lib -- --show-output --nocapture
cargo test --package ng-verifier --lib -- --show-output --nocapture
cargo test --package ng-sdk-js --lib -- --show-output --nocapture
cargo test --package ng-broker --lib -- --show-output --nocapture
cargo test --package ng-client-ws --lib -- --show-output --nocapture
```
Test WASM websocket
First you need to install the `chromedriver` that matches your version of Chrome
https://googlechromelabs.github.io/chrome-for-testing/
then:
```
cd ng-sdk-js
wasm-pack test --chrome --headless
```
Test Rust websocket
```
cargo test --package ng-client-ws --lib -- remote_ws::test::test_ws --show-output --nocapture
```
### Build release binaries
First you will need to have the production build of the frontend.
You need to freshly built it from source, following those instructions:
```
cargo install cargo-run-script
npm install -g pnpm
cd ng-sdk-js
cargo run-script app
cd ..
pnpm -C ./ng-app install
pnpm -C ./ng-app webfilebuild
pnpm -C ./helpers/app-auth install
pnpm -C ./helpers/app-auth build
```
then build the ngd daemon
```
cargo build -r -p ngd
```
you can then find the binary `ngd` in `target/release`
The CLI tool can be obtained with :
```
cargo build -r -p ngcli
```
you can then use the binary `target/release/ngcli`
For usage, see the documentation [here](ngd/README.md).
For building the apps, see this [documentation](ng-app/README.md).
#### OpenBSD
On OpenBSD, a conflict between the installed LibreSSL library and the reqwest crate, needs a bit of attention.
Before compiling the daemon for OpenBSD, please comment out lines 41-42 of `ng-net/Cargo.toml`. This will be solved soon by using `resolver = "2"`.
```
#[target.'cfg(target_arch = "wasm32")'.dependencies]
#reqwest = { version = "0.11.18", features = ["json","native-tls-vendored"] }
```
to use the app on OpenBSD, you need to run the daemon locally.
```
ngd -l 14400 --save-key
```
then open chrome (previously installed with `doas pkg_add chrome`)
```
env ENABLE_WASM=1 chrome --enable-wasm --process-per-site --new-window --app=http://localhost:14400
```
### Generate documentation
Generate documentation for all packages without their dependencies:
```
cargo doc --no-deps
```
The generated documentation can be found in `target/doc/nextgraph`.
### Contributions license
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in the work by you shall be dual licensed as below, without any
additional terms or conditions.

@ -0,0 +1,176 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

@ -0,0 +1,22 @@
MIT License
Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,71 @@
<p align="center">
<img src=".github/header.png" alt="nextgraph-header" />
</p>
# nextgraph-rs
![MSRV][rustc-image]
[![Apache 2.0 Licensed][license-image]][license-link]
[![MIT Licensed][license-image2]][license-link2]
[![project chat](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg)](https://forum.nextgraph.org)
[![Crates.io Version](https://img.shields.io/crates/v/nextgraph)](https://crates.io/crates/nextgraph)
[![docs.rs](https://img.shields.io/docsrs/nextgraph)](https://docs.rs/nextgraph)
[node:![NPM Version node](https://img.shields.io/npm/v/nextgraph)](https://www.npmjs.com/package/nextgraph)
[web:![NPM Version web](https://img.shields.io/npm/v/nextgraphweb)](https://www.npmjs.com/package/nextgraphweb)
[![PyPI - Version](https://img.shields.io/pypi/v/nextgraphpy)](https://pypi.org/project/nextgraphpy/)
Rust implementation of NextGraph
This repository is in active development at [https://git.nextgraph.org/NextGraph/nextgraph-rs](https://git.nextgraph.org/NextGraph/nextgraph-rs), a Gitea instance. For bug reports, issues, merge requests, and in order to join the dev team, please visit the link above and create an account (you can do so with a github account). The [github repo](https://github.com/nextgraph-org/nextgraph-rs) is just a read-only mirror that does not accept issues.
## NextGraph
> NextGraph brings about the convergence of P2P and Semantic Web technologies, towards a decentralized, secure and privacy-preserving cloud, based on CRDTs.
>
> This open source ecosystem provides solutions for end-users (a platform) and software developers (a framework), wishing to use or create **decentralized** apps featuring: **live collaboration** on rich-text documents, peer to peer communication with **end-to-end encryption**, offline-first, **local-first**, portable and interoperable data, total ownership of data and software, security and privacy. Centered on repositories containing **semantic data** (RDF), **rich text**, and structured data formats like **JSON**, synced between peers belonging to permissioned groups of users, it offers strong eventual consistency, thanks to the use of **CRDTs**. Documents can be linked together, signed, shared securely, queried using the **SPARQL** language and organized into sites and containers.
>
> More info here [https://nextgraph.org](https://nextgraph.org)
## Support
Documentation can be found here [https://docs.nextgraph.org](https://docs.nextgraph.org)
And our community forum where you can ask questions is here [https://forum.nextgraph.org](https://forum.nextgraph.org)
[![Mastodon](https://img.shields.io/badge/-MASTODON-%232B90D9?style=for-the-badge&logo=mastodon&logoColor=white)](https://fosstodon.org/@nextgraph)
## How to use NextGraph App & Platform
NextGraph is in alpha release!
You can try it online or by installing the apps. Please follow our [Getting started](https://docs.nextgraph.org/en/getting-started/) guide .
You can also subscribe to [our newsletter](https://list.nextgraph.org/subscription/form) to get updates, and support us with a [donation](https://nextgraph.org/donate/).
## NextGraph is also a Framework for App developers
Read our [getting started guide for developers](https://docs.nextgraph.org/en/framework/getting-started/).
## For contributors or self compilation
See our [contributor's guide](DEV.md)
## License
Licensed under either of
- Apache License, Version 2.0 ([LICENSE-APACHE2](LICENSE-APACHE2) or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
at your option.
`SPDX-License-Identifier: Apache-2.0 OR MIT`
---
NextGraph received funding through the [NGI Assure Fund](https://nlnet.nl/assure) and the [NGI Zero Commons Fund](https://nlnet.nl/commonsfund/), both funds established by [NLnet](https://nlnet.nl/) Foundation with financial support from the European Commission's [Next Generation Internet](https://ngi.eu/) programme, under the aegis of DG Communications Networks, Content and Technology under grant agreements No 957073 and No 101092990, respectively.
[rustc-image]: https://img.shields.io/badge/rustc-1.81+-blue.svg
[license-image]: https://img.shields.io/badge/license-Apache2.0-blue.svg
[license-link]: https://git.nextgraph.org/NextGraph/nextgraph-rs/raw/branch/master/LICENSE-APACHE2
[license-image2]: https://img.shields.io/badge/license-MIT-blue.svg
[license-link2]: https://git.nextgraph.org/NextGraph/nextgraph-rs/src/branch/master/LICENSE-MIT

@ -0,0 +1,40 @@
# Release 0.1.1-alpha
_02 September 2024_
This release is not stable and should not be used for any productive work or to store personal documents. This release is meant as a **preview** of what NextGraph can do as of today and hints at its future potential.
**Please note: The binary format of the Documents or Wallet might change, that might result in a complete loss of data. We will not provide migration scripts as the APIs and formats aren't stable yet.**
If you previously installed any NextGraph app on your device, please uninstall it first, by following the normal uninstall procedure specific to your OS. If you have previously created a Wallet, it will not work with this new release. Please create a new one now.
## App
Please read the [Getting started](https://docs.nextgraph.org/en/getting-started) guide.
[changelog](CHANGELOG.md#app-0-1-1-alpha-2024-09-02)
## SDK
The SDK for is not documented yet.
[changelog](CHANGELOG.md#sdk-0-1-0-preview-6-2024-08-15)
## Broker
The `ngd` daemon is release with the basic features listed in `ngd --help`. More documentation will come soon
[changelog](CHANGELOG.md#broker-0-1-1-alpha-2024-09-02)
## CLI
The `ngcli` daemon is release with the basic features listed in `ngcli --help`. More documentation will come soon.
[changelog](CHANGELOG.md#cli-0-1-1-alpha-2024-09-02)
## Limitations of this release
- you cannot share documents with other users. Everything is ready for this internally, but there is still some wiring to do that will take some more time.
- the Rich text editors (both for normal Post/Article and in Markdown) do not let you insert images nor links to other documents.
- The webapp has some limitation for now when it is offline, because it doesn't have a UserStorage. it works differently than the native apps, as it has to replay all the commits at every load. This will stay like that for now, as the feature "Web UserStorage" based on IndexedDB will take some time to be coded.
- JSON-LD isn't ready yet as we need the "Context branch" feature in order to enter the list of ontologies each document is based on.

@ -0,0 +1,2 @@
tests
local_broker_dev_env_peer_id.rs

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

@ -0,0 +1,55 @@
[package]
name = "nextgraph"
description = "NextGraph client library. Nextgraph is a decentralized, secure and local-first web 3.0 ecosystem based on Semantic Web and CRDTs"
categories = ["asynchronous","text-editors","web-programming","development-tools","database-implementations"]
version = "0.1.2"
edition.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
homepage.workspace = true
keywords = [ "crdt","e2ee","local-first","p2p","semantic-web" ]
documentation = "https://docs.rs/nextgraph"
rust-version.workspace = true
[badges]
maintenance = { status = "actively-developed" }
[dependencies]
serde_bare = "0.5.0"
serde_json = "1.0"
serde_bytes = "0.11.7"
base64-url = "2.0.0"
once_cell = "1.17.1"
zeroize = { version = "1.7.0", features = ["zeroize_derive"] }
futures = "0.3.24"
async-std = { version = "1.12.0", features = [ "attributes", "unstable" ] }
async-trait = "0.1.64"
async-once-cell = "0.5.3"
lazy_static = "1.4.0"
web-time = "0.2.0"
whoami = "1.5.1"
qrcode = { version = "0.14.1", default-features = false, features = ["svg"] }
svg2pdf = { version = "0.11.0", default-features = false }
pdf-writer = "0.10.0"
ng-repo = { path = "../ng-repo", version = "0.1.2" }
ng-net = { path = "../ng-net", version = "0.1.2" }
ng-wallet = { path = "../ng-wallet", version = "0.1.2" }
ng-client-ws = { path = "../ng-client-ws", version = "0.1.2" }
ng-verifier = { path = "../ng-verifier", version = "0.1.2" }
[target.'cfg(all(not(target_family = "wasm"),not(docsrs)))'.dependencies]
ng-storage-rocksdb = { path = "../ng-storage-rocksdb", version = "0.1.2" }
[[example]]
name = "in_memory"
required-features = []
[[example]]
name = "persistent"
required-features = []
[[example]]
name = "open"
required-features = []

@ -0,0 +1,83 @@
<p align="center">
<img src="https://git.nextgraph.org/NextGraph/nextgraph-rs/raw/branch/master/nextgraph/.static/header.png" alt="nextgraph-header" />
</p>
# nextgraph
![MSRV][rustc-image]
[![Apache 2.0 Licensed][license-image]][license-link]
[![MIT Licensed][license-image2]][license-link2]
[![project chat](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg)](https://forum.nextgraph.org)
[![Crates.io Version](https://img.shields.io/crates/v/nextgraph)](https://crates.io/crates/nextgraph)
[![docs.rs](https://img.shields.io/docsrs/nextgraph)](https://docs.rs/nextgraph)
[node:![NPM Version node](https://img.shields.io/npm/v/nextgraph)](https://www.npmjs.com/package/nextgraph)
[web:![NPM Version web](https://img.shields.io/npm/v/nextgraphweb)](https://www.npmjs.com/package/nextgraphweb)
Rust client library for NextGraph framework
This library is in active development at [https://git.nextgraph.org/NextGraph/nextgraph-rs](https://git.nextgraph.org/NextGraph/nextgraph-rs), a Gitea instance. For bug reports, issues, merge requests, and in order to join the dev team, please visit the link above and create an account (you can do so with a github account). The [github repo](https://github.com/nextgraph-org/nextgraph-rs) is just a read-only mirror that does not accept issues.
## NextGraph
> NextGraph brings about the convergence of P2P and Semantic Web technologies, towards a decentralized, secure and privacy-preserving cloud, based on CRDTs.
>
> This open source ecosystem provides solutions for end-users (a platform) and software developers (a framework), wishing to use or create **decentralized** apps featuring: **live collaboration** on rich-text documents, peer to peer communication with **end-to-end encryption**, offline-first, **local-first**, portable and interoperable data, total ownership of data and software, security and privacy. Centered on repositories containing **semantic data** (RDF), **rich text**, and structured data formats like **JSON**, synced between peers belonging to permissioned groups of users, it offers strong eventual consistency, thanks to the use of **CRDTs**. Documents can be linked together, signed, shared securely, queried using the **SPARQL** language and organized into sites and containers.
>
> More info here [https://nextgraph.org](https://nextgraph.org)
## Support
This crate has official documentation at [docs.rs](https://docs.rs/nextgraph/0.1.0/nextgraph/)
Documentation can be found here [https://docs.nextgraph.org](https://docs.nextgraph.org)
And our community forum where you can ask questions is here [https://forum.nextgraph.org](https://forum.nextgraph.org)
## Status
NextGraph is not ready yet. You can subscribe to [our newsletter](https://list.nextgraph.org/subscription/form) to get updates, and support us with a [donation](https://nextgraph.org/donate/).
## Dependencies
Nextgraph library is dependent on [async-std](https://async.rs/). You must include it in your `Cargo.toml`.
A tokio-based version (as a feature) might be available in the future.
```toml
[dependencies]
nextgraph = "0.1.1-alpha.2"
async-std = "1.12.0"
```
## Examples
You can find some examples on how to use the library:
- [in_memory](https://git.nextgraph.org/NextGraph/nextgraph-rs/src/branch/master/nextgraph/examples)
- [persistent](https://git.nextgraph.org/NextGraph/nextgraph-rs/src/branch/master/nextgraph/examples)
## License
Licensed under either of
- Apache License, Version 2.0 ([LICENSE-APACHE2](LICENSE-APACHE2) or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
at your option.
`SPDX-License-Identifier: Apache-2.0 OR MIT`
### Contributions license
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in the work by you shall be dual licensed as below, without any
additional terms or conditions.
---
NextGraph received funding through the [NGI Assure Fund](https://nlnet.nl/assure) and the [NGI Zero Commons Fund](https://nlnet.nl/commonsfund/), both funds established by [NLnet](https://nlnet.nl/) Foundation with financial support from the European Commission's [Next Generation Internet](https://ngi.eu/) programme, under the aegis of DG Communications Networks, Content and Technology under grant agreements No 957073 and No 101092990, respectively.
[rustc-image]: https://img.shields.io/badge/rustc-1.81+-blue.svg
[license-image]: https://img.shields.io/badge/license-Apache2.0-blue.svg
[license-link]: https://git.nextgraph.org/NextGraph/nextgraph-rs/raw/branch/master/LICENSE-APACHE2
[license-image2]: https://img.shields.io/badge/license-MIT-blue.svg
[license-link2]: https://git.nextgraph.org/NextGraph/nextgraph-rs/src/branch/master/LICENSE-MIT

@ -0,0 +1,17 @@
# Examples
Some examples of using `nextgraph` client library
run them with:
```
cargo run -p nextgraph --example in_memory
cargo run -p nextgraph --example persistent
cargo run -p nextgraph --example open
```
See the code:
- [in_memory](in_memory.md)
- [persistent](persistent.md)
- [open](open.md)

@ -0,0 +1,13 @@
# in-memory LocalBroker
Example of LocalBroker configured with in-memory (no persistence).
run with:
```
cargo run -p nextgraph -r --example in_memory
```
we assume that you run this command from the root of the git repo (nextgraph-rs)
the `-r` for release version is important, without it, the creation and opening of the wallet will take ages.

@ -0,0 +1,178 @@
// Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
// All rights reserved.
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
use std::fs::read;
#[allow(unused_imports)]
use nextgraph::local_broker::{
app_request, app_request_stream, init_local_broker, session_start, session_stop, user_connect,
user_disconnect, wallet_close, wallet_create_v0, wallet_get, wallet_get_file, wallet_import,
wallet_open_with_pazzle_words, wallet_read_file, wallet_was_opened, LocalBrokerConfig,
SessionConfig,
};
use nextgraph::net::types::BootstrapContentV0;
use nextgraph::repo::errors::NgError;
use nextgraph::repo::types::PubKey;
use nextgraph::wallet::types::CreateWalletV0;
use nextgraph::wallet::{display_mnemonic, emojis::display_pazzle};
#[async_std::main]
async fn main() -> std::io::Result<()> {
// initialize the local_broker with in-memory config.
// all sessions will be lost when the program exits
init_local_broker(Box::new(|| LocalBrokerConfig::InMemory)).await;
// load some image that will be used as security_img
// we assume here for the sake of this example,
// that the current directory contains this demo image file
let security_img = read("nextgraph/examples/wallet-security-image-demo.png")?;
// the peer_id should come from somewhere else.
// this is just given for the sake of an example
#[allow(deprecated)]
let peer_id_of_server_broker = PubKey::nil();
// Create your wallet
// this will take some time !
println!("Creating the wallet. this will take some time...");
let wallet_result = wallet_create_v0(CreateWalletV0 {
security_img,
security_txt: "know yourself".to_string(),
pin: [1, 2, 1, 2],
pazzle_length: 9,
send_bootstrap: false,
send_wallet: false,
result_with_wallet_file: true,
local_save: false,
// we default to localhost:14400. this is just for the sake of an example
core_bootstrap: BootstrapContentV0::new_localhost(peer_id_of_server_broker),
core_registration: None,
additional_bootstrap: None,
pdf: false,
device_name: "test".to_string(),
})
.await?;
println!("Your wallet name is : {}", wallet_result.wallet_name);
let pazzle = display_pazzle(&wallet_result.pazzle);
let mut pazzle_words = vec![];
println!("Your pazzle is: {:?}", wallet_result.pazzle);
for emoji in pazzle {
println!(
"\t{}:\t{}{}",
emoji.0,
if emoji.0.len() > 12 { "" } else { "\t" },
emoji.1
);
pazzle_words.push(emoji.1.to_string());
}
println!("Your mnemonic is:");
display_mnemonic(&wallet_result.mnemonic)
.iter()
.for_each(|word| print!("{} ", word.as_str()));
println!("");
// A session has been opened for you and you can directly use it without the need to call [wallet_was_opened] nor [session_start].
let user_id = wallet_result.personal_identity();
// if the user has internet access, they can now decide to connect to its Server Broker, in order to sync data
let status = user_connect(&user_id).await?;
// The connection cannot succeed because we miss-configured the core_bootstrap of the wallet. its Peer ID is invalid.
let error_reason = status[0].3.as_ref().unwrap();
assert!(error_reason == "NoiseHandshakeFailed" || error_reason == "ConnectionError");
// a session ID has been assigned to you in `wallet_result.session_id` you can use it to fetch a document
//let _ = doc_fetch(wallet_result.session_id, "ng:example".to_string(), None).await?;
// Then we should disconnect
user_disconnect(&user_id).await?;
// if you need the Wallet File again (if you didn't select `result_with_wallet_file` by example), you can retrieve it with:
let wallet_file = wallet_get_file(&wallet_result.wallet_name).await?;
// if you did ask for `result_with_wallet_file`, as we did above, then the 2 vectors should be identical
assert_eq!(wallet_file, wallet_result.wallet_file);
// stop the session
session_stop(&user_id).await?;
// closes the wallet
wallet_close(&wallet_result.wallet_name).await?;
// if you have saved the wallet locally (which we haven't done in the example above, see `local_save: false`), next time you want to connect,
// you can retrieve the wallet, display the security phrase and image to the user, ask for the pazzle or mnemonic, and then open the wallet
// if you haven't saved the wallet, the next line will not work once you restart the LocalBroker.
let _wallet = wallet_get(&wallet_result.wallet_name).await?;
// at this point, the wallet is kept in the internal memory of the LocalBroker
// and it hasn't been opened yet, so it is not usable right away.
// now let's open the wallet, by providing the pazzle and PIN code
let opened_wallet =
wallet_open_with_pazzle_words(&wallet_result.wallet, &pazzle_words, [1, 2, 1, 2])?;
// once the wallet is opened, we notify the LocalBroker that we have opened it.
let _client = wallet_was_opened(opened_wallet).await?;
// if instead of saving the wallet locally, you want to provide the Wallet File for every login,
// then you have to import the wallet. here is an example:
{
// this part should happen on another device or on the same machine if you haven't saved the wallet locally
// you could use the Wallet File and import it there so it could be used for login.
// first you would read and decode the Wallet File
// this fails here because we already added this wallet in the LocalBroker (when we created it).
// But on another device or after a restart of LocalBroker, it would work.
let wallet = wallet_read_file(wallet_file).await;
assert_eq!(wallet.unwrap_err(), NgError::WalletAlreadyAdded);
// we would then open the wallet
// (here we take the Wallet as we received it from wallet_create_v0, but in real case you would use `wallet`)
let opened_wallet2 =
wallet_open_with_pazzle_words(&wallet_result.wallet, &pazzle_words, [1, 2, 1, 2])?;
// once it has been opened, the Wallet can be imported into the LocalBroker
// if you try to import the same wallet in a LocalBroker where it is already opened, it will fail.
// So here it fails. But on another device, it would work.
let client_fail = wallet_import(wallet_result.wallet.clone(), opened_wallet2, true).await;
assert_eq!(client_fail.unwrap_err(), NgError::WalletAlreadyAdded);
}
// now that the wallet is opened or imported, let's start a session.
// we pass the user_id and the wallet_name
let _session = session_start(SessionConfig::new_in_memory(
&user_id,
&wallet_result.wallet_name,
))
.await?;
// if the user has internet access, they can now decide to connect to its Server Broker, in order to sync data
let status = user_connect(&user_id).await?;
// The connection cannot succeed because we miss-configured the core_bootstrap of the wallet. its Peer ID is invalid.
let error_reason = status[0].3.as_ref().unwrap();
assert!(error_reason == "NoiseHandshakeFailed" || error_reason == "ConnectionError");
// then you can make some calls to the APP protocol
// with app_request or app_request_stream
// more to be detailed soon.
// Then we should disconnect
user_disconnect(&user_id).await?;
// stop the session
session_stop(&user_id).await?;
// closes the wallet
wallet_close(&wallet_result.wallet_name).await?;
Ok(())
}

@ -0,0 +1,17 @@
# open LocalBroker
Example of LocalBroker configured with persistence to disk, and opening of a previsouly saved wallet
You need to replace `wallet_name` on line 35 with the name that was given to you when you ran the example [persistent], in `Your wallet name is : `
You need to replace the argument `pazzle` in the function call `wallet_open_with_pazzle` with the array that you received in `Your pazzle is:`
then, run with:
```
cargo run -p nextgraph -r --example open
```
we assume that you run this command from the root of the git repo (nextgraph-rs).
the `-r` for release version is important, without it, the creation and opening of the wallet will take ages.

@ -0,0 +1,76 @@
// Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
// All rights reserved.
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
use std::env::current_dir;
use std::fs::create_dir_all;
#[allow(unused_imports)]
use nextgraph::local_broker::{
app_request, app_request_stream, init_local_broker, session_start, session_stop, user_connect,
user_disconnect, wallet_close, wallet_create_v0, wallet_get, wallet_get_file, wallet_import,
wallet_open_with_pazzle, wallet_open_with_pazzle_words, wallet_read_file, wallet_was_opened,
LocalBrokerConfig, SessionConfig,
};
#[async_std::main]
async fn main() -> std::io::Result<()> {
// get the current working directory
let mut current_path = current_dir()?;
current_path.push(".ng");
current_path.push("example");
create_dir_all(current_path.clone())?;
// initialize the local_broker with config to save to disk in a folder called `.ng/example` in the current directory
init_local_broker(Box::new(move || {
LocalBrokerConfig::BasePath(current_path.clone())
}))
.await;
let wallet_name = "9ivXl3TpgcQlDKTmR9NOipjhPWxQw6Yg5jkWBTlJuXw".to_string();
// as we have previously saved the wallet,
// we can retrieve it, display the security phrase and image to the user, ask for the pazzle or mnemonic, and then open the wallet
let wallet = wallet_get(&wallet_name).await?;
// at this point, the wallet is kept in the internal memory of the LocalBroker
// and it hasn't been opened yet, so it is not usable right away.
// now let's open the wallet, by providing the pazzle and PIN code
let opened_wallet = wallet_open_with_pazzle(
&wallet,
vec![110, 139, 115, 94, 9, 40, 74, 25, 52],
[2, 3, 2, 3],
)?;
let user_id = opened_wallet.personal_identity();
// once the wallet is opened, we notify the LocalBroker that we have opened it.
let _client = wallet_was_opened(opened_wallet).await?;
// now that the wallet is opened, let's start a session.
// we pass the user_id and the wallet_name
let _session = session_start(SessionConfig::new_save(&user_id, &wallet_name)).await?;
// if the user has internet access, they can now decide to connect to its Server Broker, in order to sync data
let status = user_connect(&user_id).await?;
// The connection cannot succeed because we miss-configured the core_bootstrap of the wallet. its Peer ID is invalid.
println!("Connection was : {:?}", status[0]);
//assert!(error_reason == "NoiseHandshakeFailed" || error_reason == "ConnectionError");
// Then we should disconnect
user_disconnect(&user_id).await?;
// stop the session
session_stop(&user_id).await?;
// closes the wallet
wallet_close(&wallet_name).await?;
Ok(())
}

@ -0,0 +1,13 @@
# persistent LocalBroker
Example of LocalBroker configured with persistence to disk
run with:
```
cargo run -p nextgraph -r --example persistent
```
we assume that you run this command from the root of the git repo (nextgraph-rs).
the `-r` for release version is important, without it, the creation and opening of the wallet will take ages.

@ -0,0 +1,161 @@
// Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
// All rights reserved.
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
use std::env::current_dir;
use std::fs::create_dir_all;
use std::fs::read;
#[allow(unused_imports)]
use nextgraph::local_broker::{
app_request, app_request_stream, init_local_broker, session_start, session_stop, user_connect,
user_disconnect, wallet_close, wallet_create_v0, wallet_get, wallet_get_file, wallet_import,
wallet_open_with_pazzle_words, wallet_read_file, wallet_was_opened, LocalBrokerConfig,
SessionConfig,
};
use nextgraph::net::types::BootstrapContentV0;
use nextgraph::repo::types::PubKey;
use nextgraph::wallet::types::CreateWalletV0;
use nextgraph::wallet::{display_mnemonic, emojis::display_pazzle};
#[async_std::main]
async fn main() -> std::io::Result<()> {
// get the current working directory
let mut current_path = current_dir()?;
current_path.push(".ng");
current_path.push("example");
create_dir_all(current_path.clone())?;
// initialize the local_broker with config to save to disk in a folder called `.ng/example` in the current directory
init_local_broker(Box::new(move || {
LocalBrokerConfig::BasePath(current_path.clone())
}))
.await;
// load some image that will be used as security_img
// we assume here for the sake of this example,
// that the current directory contains this demo image file
let security_img = read("nextgraph/examples/wallet-security-image-demo.png")?;
// the peer_id should come from somewhere else.
// this is just given for the sake of an example
let peer_id_of_server_broker = PubKey::nil();
// Create your wallet
// this will take some time !
println!("Creating the wallet. this will take some time...");
let wallet_result = wallet_create_v0(CreateWalletV0 {
security_img,
security_txt: "know yourself".to_string(),
pin: [1, 2, 1, 2],
pazzle_length: 9,
send_bootstrap: false,
send_wallet: false,
result_with_wallet_file: true,
local_save: true,
// we default to localhost:14400. this is just for the sake of an example
core_bootstrap: BootstrapContentV0::new_localhost(peer_id_of_server_broker),
core_registration: None,
additional_bootstrap: None,
pdf: false,
device_name: "test".to_string(),
})
.await?;
println!("Your wallet name is : {}", wallet_result.wallet_name);
let pazzle = display_pazzle(&wallet_result.pazzle);
let mut pazzle_words = vec![];
println!("Your pazzle is: {:?}", wallet_result.pazzle);
for emoji in pazzle {
println!(
"\t{}:\t{}{}",
emoji.0,
if emoji.0.len() > 12 { "" } else { "\t" },
emoji.1
);
pazzle_words.push(emoji.1.to_string());
}
println!("Your mnemonic is:");
display_mnemonic(&wallet_result.mnemonic)
.iter()
.for_each(|word| print!("{} ", word.as_str()));
println!("");
// A session has been opened for you and you can directly use it without the need to call [wallet_was_opened] nor [session_start].
let user_id = wallet_result.personal_identity();
// if the user has internet access, they can now decide to connect to its Server Broker, in order to sync data
let status = user_connect(&user_id).await?;
// The connection cannot succeed because we miss-configured the core_bootstrap of the wallet. its Peer ID is invalid.
let error_reason = status[0].3.as_ref().unwrap();
assert!(error_reason == "NoiseHandshakeFailed" || error_reason == "ConnectionError");
// a session ID has been assigned to you in `wallet_result.session_id` you can use it to fetch a document
//let _ = doc_fetch(wallet_result.session_id, "ng:example".to_string(), None).await?;
// Then we should disconnect
user_disconnect(&user_id).await?;
// if you need the Wallet File again (if you didn't select `result_with_wallet_file` by example), you can retrieve it with:
let wallet_file = wallet_get_file(&wallet_result.wallet_name).await?;
// if you did ask for `result_with_wallet_file`, as we did above, then the 2 vectors should be identical
assert_eq!(wallet_file, wallet_result.wallet_file);
// stop the session
session_stop(&user_id).await?;
// closes the wallet
wallet_close(&wallet_result.wallet_name).await?;
// as we have saved the wallet, the next time we want to connect,
// we can retrieve the wallet, display the security phrase and image to the user, ask for the pazzle or mnemonic, and then open the wallet
let _wallet = wallet_get(&wallet_result.wallet_name).await?;
// at this point, the wallet is kept in the internal memory of the LocalBroker
// and it hasn't been opened yet, so it is not usable right away.
// now let's open the wallet, by providing the pazzle and PIN code
let opened_wallet =
wallet_open_with_pazzle_words(&wallet_result.wallet, &pazzle_words, [1, 2, 1, 2])?;
// once the wallet is opened, we notify the LocalBroker that we have opened it.
let _client = wallet_was_opened(opened_wallet).await?;
// now that the wallet is opened, let's start a session.
// we pass the user_id and the wallet_name
let _session = session_start(SessionConfig::new_save(
&user_id,
&wallet_result.wallet_name,
))
.await?;
// if the user has internet access, they can now decide to connect to its Server Broker, in order to sync data
let status = user_connect(&user_id).await?;
// The connection cannot succeed because we miss-configured the core_bootstrap of the wallet. its Peer ID is invalid.
let error_reason = status[0].3.as_ref().unwrap();
assert!(error_reason == "NoiseHandshakeFailed" || error_reason == "ConnectionError");
// then you can make some calls to the APP protocol
// with app_request or app_request_stream
// more to be detailed soon.
// Then we should disconnect
user_disconnect(&user_id).await?;
// stop the session
session_stop(&user_id).await?;
// closes the wallet
wallet_close(&wallet_result.wallet_name).await?;
Ok(())
}

@ -0,0 +1,103 @@
// Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
// All rights reserved.
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
use std::fs::read;
use async_std::stream::StreamExt;
#[allow(unused_imports)]
use nextgraph::local_broker::{
app_request, app_request_stream, doc_fetch_repo_subscribe, doc_sparql_update,
init_local_broker, session_start, session_stop, user_connect, user_disconnect, wallet_close,
wallet_create_v0, wallet_get, wallet_get_file, wallet_import, wallet_open_with_mnemonic_words,
wallet_read_file, wallet_was_opened, LocalBrokerConfig, SessionConfig,
};
use nextgraph::net::types::BootstrapContentV0;
use nextgraph::repo::errors::NgError;
use nextgraph::repo::log::*;
use nextgraph::repo::types::PubKey;
use nextgraph::wallet::types::CreateWalletV0;
use nextgraph::wallet::{display_mnemonic, emojis::display_pazzle};
#[async_std::main]
async fn main() -> std::io::Result<()> {
// initialize the local_broker with in-memory config.
// all sessions will be lost when the program exits
init_local_broker(Box::new(|| LocalBrokerConfig::InMemory)).await;
let wallet_file =
read("/Users/nl/Downloads/wallet-Hr-UITwGtjE1k6lXBoVGzD4FQMiDkM3T6bSeAi9PXt4A.ngw")
.expect("read wallet file");
let wallet = wallet_read_file(wallet_file).await?;
let mnemonic_words = vec![
"jealous".to_string(),
"during".to_string(),
"elevator".to_string(),
"swallow".to_string(),
"pen".to_string(),
"phone".to_string(),
"like".to_string(),
"employ".to_string(),
"myth".to_string(),
"remember".to_string(),
"question".to_string(),
"lemon".to_string(),
];
let opened_wallet = wallet_open_with_mnemonic_words(&wallet, &mnemonic_words, [2, 3, 2, 3])?;
let user_id = opened_wallet.personal_identity();
let wallet_name = opened_wallet.name();
let client = wallet_import(wallet.clone(), opened_wallet, true).await?;
let session = session_start(SessionConfig::new_in_memory(&user_id, &wallet_name)).await?;
// let session = session_start(SessionConfig::new_remote(&user_id, &wallet_name, None)).await?;
// if the user has internet access, they can now decide to connect to its Server Broker, in order to sync data
let status = user_connect(&user_id).await?;
let result = doc_sparql_update(
session.session_id,
"INSERT DATA { <did:ng:_> <example:predicate> \"An example value10\". }".to_string(),
Some("did:ng:o:Dn0QpE9_4jhta1mUWRl_LZh1SbXUkXfOB5eu38PNIk4A:v:Z4ihjV3KMVIqBxzjP6hogVLyjkZunLsb7MMsCR0kizQA".to_string()),
)
.await;
log_debug!("{:?}", result);
// // a session ID has been assigned to you in `session.session_id` you can use it to fetch a document
// let (mut receiver, cancel) = doc_fetch_repo_subscribe(
// session.session_id,
// "did:ng:o:Dn0QpE9_4jhta1mUWRl_LZh1SbXUkXfOB5eu38PNIk4A".to_string(),
// )
// .await?;
// cancel();
// while let Some(app_response) = receiver.next().await {
// let (inserts, removes) =
// nextgraph::verifier::read_triples_in_app_response_from_rust(app_response)?;
// log_debug!("inserts {:?}", inserts);
// log_debug!("removes {:?}", removes);
// }
// Then we should disconnect
user_disconnect(&user_id).await?;
// stop the session
session_stop(&user_id).await?;
// closes the wallet
wallet_close(&wallet_name).await?;
Ok(())
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 B

@ -0,0 +1,137 @@
#![doc(html_logo_url = "https://nextgraph.org/nextgraph-logo-192.png")]
#![doc(issue_tracker_base_url = "https://git.nextgraph.org/NextGraph/nextgraph-rs/issues")]
#![doc(html_favicon_url = "https://nextgraph.org/favicon.svg")]
//! # NextGraph framework client library
//!
//! NextGraph brings about the convergence of P2P and Semantic Web technologies, towards a decentralized, secure and privacy-preserving cloud, based on CRDTs.
//!
//! This open source ecosystem provides solutions for end-users (a platform) and software developers (a framework), wishing to use or create **decentralized** apps featuring: **live collaboration** on rich-text documents, peer to peer communication with **end-to-end encryption**, offline-first, **local-first**, portable and interoperable data, total ownership of data and software, security and privacy. Centered on repositories containing **semantic data** (RDF), **rich text**, and structured data formats like **JSON**, synced between peers belonging to permissioned groups of users, it offers strong eventual consistency, thanks to the use of **CRDTs**. Documents can be linked together, signed, shared securely, queried using the **SPARQL** language and organized into sites and containers.
//!
//! More info here [https://nextgraph.org](https://nextgraph.org). Documentation available here [https://docs.nextgraph.org](https://docs.nextgraph.org).
//!
//! ## LocalBroker, the entrypoint to NextGraph network
//!
//! `local_broker` contains the API for controlling the Local Broker, which is a reduced instance of the network Broker.
//! This is your entrypoint to NextGraph network.
//! It runs embedded in your client program, and once configured (by opening a Session), it can keep for you (on disk or in memory):
//! - the blocks of the repos,
//! - the connection(s) to your Server Broker
//! - the events that you send to the Overlay, if there is no connectivity (Outbox)
//! - A reference to the verifier
//!
//! In addition, the API for creating and managing your wallet is provided here.
//!
//! The Rust API is used internally in the CLI, and for all the Tauri-based Apps.
//!
//! The same API is also made available in Javascript for the browser (and is used by our webapp) and for nodejs. See the npm package [ng-sdk-js](https://www.npmjs.com/package/ng-sdk-js) or [nextgraph](https://www.npmjs.com/package/nextgraph)
//!
//! The library requires `async-std` minimal version 1.12.0
//!
//! See [examples](https://git.nextgraph.org/NextGraph/nextgraph-rs/src/branch/master/nextgraph/examples) for a quick start.
//!
//! ## In-memory
//!
//! With this config, no data will be persisted to disk.
//!
//! ```
//! use nextgraph::local_broker::{init_local_broker, LocalBrokerConfig};
//!
//! #[async_std::main]
//! async fn main() -> std::io::Result<()> {
//! // initialize the local_broker with in-memory config.
//! // all sessions will be lost when the program exits
//! init_local_broker(Box::new(|| LocalBrokerConfig::InMemory)).await;
//!
//! // see https://git.nextgraph.org/NextGraph/nextgraph-rs/src/branch/master/nextgraph/examples/in_memory.md
//! // for a full example of what the Rust API gives you
//!
//! Ok(())
//! }
//! ```
//!
//! ## Persistent
//!
//! With this config, the encrypted wallet, session information, outbox, and all user data will be saved locally, with encryption at rest.
//!
//! ```
//! use nextgraph::local_broker::{init_local_broker, LocalBrokerConfig};
//!
//! #[async_std::main]
//! async fn main() -> std::io::Result<()> {
//! // initialize the local_broker with in-memory config.
//! // all sessions will be lost when the program exits
//! let mut current_path = current_dir()?;
//! current_path.push(".ng");
//! current_path.push("example");
//! create_dir_all(current_path.clone())?;
//!
//! // initialize the local_broker with config to save to disk in a folder called `.ng/example` in the current directory
//! init_local_broker(Box::new(move || {
//! LocalBrokerConfig::BasePath(current_path.clone())
//! })).await;
//!
//! // see https://git.nextgraph.org/NextGraph/nextgraph-rs/src/branch/master/nextgraph/examples/persistent.md
//! // for a full example of what the Rust API gives you
//!
//! Ok(())
//! }
//! ```
pub mod local_broker;
pub mod repo {
pub use ng_repo::*;
}
pub mod net {
pub use ng_net::*;
}
pub mod verifier {
pub use ng_verifier::site::*;
pub use ng_verifier::types::*;
pub mod protocol {
pub use ng_net::app_protocol::*;
}
pub use ng_verifier::prepare_app_response_for_js;
pub use ng_verifier::read_triples_in_app_response_from_rust;
pub use ng_verifier::triples_ser_to_json_string;
}
pub mod wallet {
pub use ng_wallet::*;
}
pub fn get_device_name() -> String {
let mut list: Vec<String> = Vec::with_capacity(3);
#[cfg(not(target_arch = "wasm32"))]
if let Ok(realname) = whoami::fallible::realname() {
list.push(realname);
} else {
#[cfg(not(target_arch = "wasm32"))]
if let Ok(username) = whoami::fallible::username() {
list.push(username);
}
}
if let Ok(devicename) = whoami::fallible::devicename() {
list.push(devicename);
} else {
#[cfg(not(target_arch = "wasm32"))]
if let Ok(hostname) = whoami::fallible::hostname() {
list.push(hostname);
} else {
if let Ok(distro) = whoami::fallible::distro() {
list.push(distro);
}
}
}
#[cfg(target_arch = "wasm32")]
if let Ok(distro) = whoami::fallible::distro() {
list.push(distro.replace("Unknown ",""));
}
list.join(" ")
}
#[cfg(debug_assertions)]
mod local_broker_dev_env;

File diff suppressed because it is too large Load Diff

@ -0,0 +1 @@
pub const PEER_ID: &str = "FtdzuDYGewfXWdoPuXIPb0wnd0SAg1WoA2B14S7jW3MA";

@ -0,0 +1,45 @@
[package]
name = "ng-broker"
version = "0.1.2"
description = "Broker library of NextGraph, a decentralized, secure and local-first web 3.0 ecosystem based on Semantic Web and CRDTs"
edition.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
homepage.workspace = true
keywords = ["crdt","e2ee","local-first","p2p","pubsub"]
documentation.workspace = true
rust-version.workspace = true
[badges]
maintenance = { status = "actively-developed" }
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_bare = "0.5.0"
serde_json = "1.0.96"
futures = "0.3.24"
once_cell = "1.17.1"
either = { version = "1.8.1", features=["serde"] }
async-std = { version = "1.12.0", features = ["attributes"] }
async-trait = "0.1.64"
rust-embed= { version = "6.7.0", features=["include-exclude"] }
urlencoding = "2.1.3"
blake3 = "1.3.1"
ng-async-tungstenite = { version = "0.22.2", git = "https://git.nextgraph.org/NextGraph/async-tungstenite.git", branch = "nextgraph", features = ["async-std-runtime"] }
ng-repo = { path = "../ng-repo", version = "0.1.2" }
ng-net = { path = "../ng-net", version = "0.1.2" }
ng-client-ws = { path = "../ng-client-ws", version = "0.1.2" }
ng-verifier = { path = "../ng-verifier", version = "0.1.2" }
ng-storage-rocksdb = { path = "../ng-storage-rocksdb", version = "0.1.2" }
[target.'cfg(target_arch = "wasm32")'.dependencies.getrandom]
version = "0.3.3"
features = ["wasm_js"]
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
getrandom = "0.3.3"
netdev = "0.26"
[dev-dependencies]
tempfile = "3"

@ -0,0 +1,56 @@
# ng-broker
![MSRV][rustc-image]
[![Apache 2.0 Licensed][license-image]][license-link]
[![MIT Licensed][license-image2]][license-link2]
Broker library of NextGraph
This repository is in active development at [https://git.nextgraph.org/NextGraph/nextgraph-rs](https://git.nextgraph.org/NextGraph/nextgraph-rs), a Gitea instance. For bug reports, issues, merge requests, and in order to join the dev team, please visit the link above and create an account (you can do so with a github account). The [github repo](https://github.com/nextgraph-org/nextgraph-rs) is just a read-only mirror that does not accept issues.
## NextGraph
> NextGraph brings about the convergence of P2P and Semantic Web technologies, towards a decentralized, secure and privacy-preserving cloud, based on CRDTs.
>
> This open source ecosystem provides solutions for end-users (a platform) and software developers (a framework), wishing to use or create **decentralized** apps featuring: **live collaboration** on rich-text documents, peer to peer communication with **end-to-end encryption**, offline-first, **local-first**, portable and interoperable data, total ownership of data and software, security and privacy. Centered on repositories containing **semantic data** (RDF), **rich text**, and structured data formats like **JSON**, synced between peers belonging to permissioned groups of users, it offers strong eventual consistency, thanks to the use of **CRDTs**. Documents can be linked together, signed, shared securely, queried using the **SPARQL** language and organized into sites and containers.
>
> More info here [https://nextgraph.org](https://nextgraph.org)
## Support
Documentation can be found here [https://docs.nextgraph.org](https://docs.nextgraph.org)
And our community forum where you can ask questions is here [https://forum.nextgraph.org](https://forum.nextgraph.org)
## How to use the library
NextGraph is not ready yet. You can subscribe to [our newsletter](https://list.nextgraph.org/subscription/form) to get updates, and support us with a [donation](https://nextgraph.org/donate/).
This library is used internally by [ngd](../ngd/README.md) the daemon/server of NextGraph. It could potentially be used too by external projects that want to embed the NextGraph daemon in their own program.
## License
Licensed under either of
- Apache License, Version 2.0 ([LICENSE-APACHE2](LICENSE-APACHE2) or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
at your option.
`SPDX-License-Identifier: Apache-2.0 OR MIT`
### Contributions license
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in the work by you shall be dual licensed as below, without any
additional terms or conditions.
---
NextGraph received funding through the [NGI Assure Fund](https://nlnet.nl/assure) and the [NGI Zero Commons Fund](https://nlnet.nl/commonsfund/), both funds established by [NLnet](https://nlnet.nl/) Foundation with financial support from the European Commission's [Next Generation Internet](https://ngi.eu/) programme, under the aegis of DG Communications Networks, Content and Technology under grant agreements No 957073 and No 101092990, respectively.
[rustc-image]: https://img.shields.io/badge/rustc-1.81+-blue.svg
[license-image]: https://img.shields.io/badge/license-Apache2.0-blue.svg
[license-link]: https://git.nextgraph.org/NextGraph/nextgraph-rs/raw/branch/master/LICENSE-APACHE2
[license-image2]: https://img.shields.io/badge/license-MIT-blue.svg
[license-link2]: https://git.nextgraph.org/NextGraph/nextgraph-rs/src/branch/master/LICENSE-MIT

@ -0,0 +1,5 @@
fn main() {
if std::env::var("DOCS_RS").is_ok() {
println!("cargo:rustc-cfg=docsrs");
}
}

@ -0,0 +1,110 @@
/*
* Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
use ng_net::types::{Interface, InterfaceType};
use ng_net::utils::{is_ipv4_private, is_public_ipv4};
#[cfg(not(target_arch = "wasm32"))]
pub fn print_ipv4(ip: &netdev::ip::Ipv4Net) -> String {
format!("{}/{}", ip.addr, ip.prefix_len)
}
#[cfg(not(target_arch = "wasm32"))]
pub fn print_ipv6(ip: &netdev::ip::Ipv6Net) -> String {
format!("{}/{}", ip.addr, ip.prefix_len)
}
pub fn find_first(list: &Vec<Interface>, iftype: InterfaceType) -> Option<Interface> {
for inf in list {
if inf.if_type == iftype {
return Some(inf.clone());
}
}
None
}
pub fn find_first_or_name(
list: &Vec<Interface>,
iftype: InterfaceType,
name: &String,
) -> Option<Interface> {
for inf in list {
if (name == "default" || *name == inf.name) && inf.if_type == iftype {
return Some(inf.clone());
}
}
None
}
pub fn find_name(list: &Vec<Interface>, name: &String) -> Option<Interface> {
for inf in list {
if *name == inf.name {
return Some(inf.clone());
}
}
None
}
#[cfg(not(target_arch = "wasm32"))]
pub fn get_interface() -> Vec<Interface> {
let mut res: Vec<Interface> = vec![];
let interfaces = netdev::get_interfaces();
for interface in interfaces {
if interface.ipv4.len() > 0 {
let first_v4 = interface.ipv4[0].addr;
let if_type = if first_v4.is_loopback() {
InterfaceType::Loopback
} else if is_ipv4_private(&first_v4) {
InterfaceType::Private
} else if is_public_ipv4(&first_v4) {
InterfaceType::Public
} else {
continue;
};
let interf = Interface {
if_type,
name: interface.name,
mac_addr: interface.mac_addr,
ipv4: interface.ipv4,
ipv6: interface.ipv6,
};
res.push(interf);
}
}
res
}
pub fn print_interfaces() {
let interfaces = get_interface();
for interface in interfaces {
println!("{} \t{:?}", interface.name, interface.if_type);
println!(
"\tIPv4: {}",
interface
.ipv4
.iter()
.map(|ip| print_ipv4(ip))
.collect::<Vec<String>>()
.join(" ")
);
println!(
"\tIPv6: {}",
interface
.ipv6
.iter()
.map(|ip| print_ipv6(ip))
.collect::<Vec<String>>()
.join(" ")
);
if let Some(mac_addr) = interface.mac_addr {
println!("\tMAC: {}", mac_addr);
}
}
}

@ -0,0 +1,15 @@
pub mod types;
pub mod utils;
pub mod interfaces;
pub mod server_broker;
pub mod server_storage;
pub mod rocksdb_server_storage;
pub mod server_ws;
pub mod actors;

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

@ -0,0 +1,776 @@
/*
* Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
use std::collections::{HashMap, HashSet};
use std::fs::{read, File, OpenOptions};
use std::io::Write;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use ng_repo::block_storage::{BlockStorage, HashMapBlockStorage};
use ng_repo::errors::{ProtocolError, ServerError, StorageError};
use ng_repo::log::*;
use ng_repo::object::Object;
use ng_repo::store::Store;
use ng_repo::types::*;
use ng_net::types::*;
use ng_storage_rocksdb::block_storage::RocksDbBlockStorage;
use ng_storage_rocksdb::kcv_storage::RocksDbKCVStorage;
use crate::server_broker::*;
use crate::server_storage::admin::{account::Account, invitation::Invitation, wallet::Wallet};
use crate::server_storage::core::*;
pub(crate) struct RocksDbServerStorage {
#[allow(dead_code)]
wallet_storage: RocksDbKCVStorage,
accounts_storage: RocksDbKCVStorage,
//peers_storage: RocksDbKCVStorage,
peers_last_seq_path: PathBuf,
peers_last_seq: Mutex<HashMap<PeerId, u64>>,
block_storage: Arc<std::sync::RwLock<dyn BlockStorage + Send + Sync>>,
core_storage: RocksDbKCVStorage,
}
impl RocksDbServerStorage {
pub(crate) fn open(
path: &mut PathBuf,
master_key: SymKey,
admin_invite: Option<BootstrapContentV0>,
) -> Result<Self, StorageError> {
// create/open the WALLET
let mut wallet_path = path.clone();
wallet_path.push("wallet");
std::fs::create_dir_all(wallet_path.clone()).unwrap();
log_debug!("opening wallet DB");
//TODO redo the whole key passing mechanism in RKV so it uses zeroize all the way
let wallet_storage = RocksDbKCVStorage::open(&wallet_path, master_key.slice().clone())?;
let wallet = Wallet::open(&wallet_storage);
// create/open the ACCOUNTS storage
let mut accounts_path = path.clone();
let accounts_key;
accounts_path.push("accounts");
if admin_invite.is_some() && !accounts_path.exists() && !wallet.exists_accounts_key() {
accounts_key = wallet.create_accounts_key()?;
std::fs::create_dir_all(accounts_path.clone()).unwrap();
let accounts_storage =
RocksDbKCVStorage::open(&accounts_path, accounts_key.slice().clone())?;
let symkey = SymKey::random();
let invite_code = InvitationCode::Setup(symkey.clone());
let _ = Invitation::create(
&invite_code,
0,
&Some("admin user automatically invited at first startup".to_string()),
&accounts_storage,
)?;
let invitation = ng_net::types::Invitation::V0(InvitationV0 {
code: Some(symkey),
name: Some("your Broker, as admin".into()),
url: None,
bootstrap: admin_invite.unwrap(),
});
for link in invitation.get_urls() {
println!("The admin invitation link is: {}", link)
}
} else {
if admin_invite.is_some() {
log_warn!("Cannot add an admin invitation anymore, as it is not the first start of the server.");
}
accounts_key = wallet.get_or_create_accounts_key()?;
}
log_debug!("opening accounts DB");
std::fs::create_dir_all(accounts_path.clone()).unwrap();
//TODO redo the whole key passing mechanism in RKV so it uses zeroize all the way
let accounts_storage =
RocksDbKCVStorage::open(&accounts_path, accounts_key.slice().clone())?;
// create/open the PEERS storage
// log_debug!("opening peers DB");
// let peers_key = wallet.get_or_create_peers_key()?;
// let mut peers_path = path.clone();
// peers_path.push("peers");
// std::fs::create_dir_all(peers_path.clone()).unwrap();
// //TODO redo the whole key passing mechanism in RKV so it uses zeroize all the way
// let peers_storage = RocksDbKCVStorage::open(&peers_path, peers_key.slice().clone())?;
// creates the path for peers_last_seq
let mut peers_last_seq_path = path.clone();
peers_last_seq_path.push("peers_last_seq");
std::fs::create_dir_all(peers_last_seq_path.clone()).unwrap();
// opening block_storage
let mut blocks_path = path.clone();
blocks_path.push("blocks");
std::fs::create_dir_all(blocks_path.clone()).unwrap();
let blocks_key = wallet.get_or_create_blocks_key()?;
let block_storage = Arc::new(std::sync::RwLock::new(RocksDbBlockStorage::open(
&blocks_path,
*blocks_key.slice(),
)?));
// create/open the PEERS storage
log_debug!("opening core DB");
let core_key = wallet.get_or_create_core_key()?;
let mut core_path = path.clone();
core_path.push("core");
std::fs::create_dir_all(core_path.clone()).unwrap();
//TODO redo the whole key passing mechanism in RKV so it uses zeroize all the way
#[cfg(debug_assertions)]
let mut core_storage = RocksDbKCVStorage::open(&core_path, core_key.slice().clone())?;
#[cfg(not(debug_assertions))]
let core_storage = RocksDbKCVStorage::open(&core_path, core_key.slice().clone())?;
// check unicity of class prefixes, by storage
#[cfg(debug_assertions)]
{
// TODO: refactor the wallet and accounts with Class and the new OKM mechanism, then include them uncomment the following lines
//log_debug!("CHECKING...");
// wallet_storage.add_class(&Wallet::CLASS);
// wallet_storage.check_prefixes();
// accounts_storage.add_class(&Account::CLASS);
// accounts_storage.add_class(&Invitation::CLASS);
// accounts_storage.check_prefixes();
core_storage.add_class(&TopicStorage::CLASS);
core_storage.add_class(&RepoHashStorage::CLASS);
core_storage.add_class(&OverlayStorage::CLASS);
core_storage.add_class(&CommitStorage::CLASS);
core_storage.add_class(&InboxStorage::CLASS);
core_storage.add_class(&AccountStorage::CLASS);
core_storage.check_prefixes();
}
Ok(RocksDbServerStorage {
wallet_storage,
accounts_storage,
//peers_storage,
peers_last_seq_path,
peers_last_seq: Mutex::new(HashMap::new()),
block_storage,
core_storage,
})
}
pub(crate) fn get_block_storage(
&self,
) -> Arc<std::sync::RwLock<dyn BlockStorage + Send + Sync>> {
Arc::clone(&self.block_storage)
}
pub(crate) fn next_seq_for_peer(&self, peer: &PeerId, seq: u64) -> Result<(), ServerError> {
// for now we don't use the hashmap.
// TODO: let's see if the lock is even needed
let _peers_last_seq = self.peers_last_seq.lock();
let mut filename = self.peers_last_seq_path.clone();
filename.push(format!("{}", peer));
let file = read(filename.clone());
let mut file_save = match file {
Ok(ser) => {
let last: u64 = serde_bare::from_slice(&ser).map_err(|_| ServerError::FileError)?;
if last >= seq {
return Err(ServerError::SequenceMismatch);
}
OpenOptions::new()
.write(true)
.open(filename)
.map_err(|_| ServerError::FileError)?
}
Err(_) => File::create(filename).map_err(|_| ServerError::FileError)?,
};
let ser = serde_bare::to_vec(&seq).unwrap();
file_save
.write_all(&ser)
.map_err(|_| ServerError::FileError)?;
file_save.sync_data().map_err(|_| ServerError::FileError)?;
Ok(())
}
pub(crate) fn get_user(&self, user_id: PubKey) -> Result<bool, ProtocolError> {
log_debug!("get_user {user_id}");
Ok(Account::open(&user_id, &self.accounts_storage)?.is_admin()?)
}
pub(crate) fn has_no_user(&self) -> Result<bool, ProtocolError> {
Ok(!Account::has_users(&self.accounts_storage)?)
}
/// returns the credentials, storage_master_key, and peer_priv_key
pub(crate) fn get_user_credentials(
&self,
user_id: &PubKey,
) -> Result<Credentials, ProtocolError> {
log_debug!("get_user_credentials {user_id}");
let acc = Account::open(user_id, &self.accounts_storage)?;
Ok(acc.get_credentials()?)
}
pub(crate) fn add_user(&self, user_id: PubKey, is_admin: bool) -> Result<(), ProtocolError> {
log_debug!("add_user {user_id} is admin {is_admin}");
Account::create(&user_id, is_admin, &self.accounts_storage)?;
Ok(())
}
pub(crate) fn add_user_credentials(
&self,
user_id: &PubKey,
credentials: &Credentials,
) -> Result<(), ProtocolError> {
log_debug!("add_user_credentials {user_id}");
let acc = Account::create(&user_id, false, &self.accounts_storage)?;
acc.add_credentials(credentials)?;
//let storage_key = SymKey::random();
//let peer_priv_key = PrivKey::random_ed();
//acc.add_user_keys(&storage_key, &peer_priv_key)?;
Ok(())
}
pub(crate) fn del_user(&self, user_id: PubKey) -> Result<(), ProtocolError> {
log_debug!("del_user {user_id}");
let acc = Account::open(&user_id, &self.accounts_storage)?;
acc.del()?;
// TODO: stop the verifier, if any
Ok(())
}
pub(crate) fn list_users(&self, admins: bool) -> Result<Vec<PubKey>, ProtocolError> {
log_debug!("list_users that are admin == {admins}");
Ok(Account::get_all_users(admins, &self.accounts_storage)?)
}
pub(crate) fn list_invitations(
&self,
admin: bool,
unique: bool,
multi: bool,
) -> Result<Vec<(InvitationCode, u32, Option<String>)>, ProtocolError> {
log_debug!("list_invitations admin={admin} unique={unique} multi={multi}");
Ok(Invitation::get_all_invitations(
&self.accounts_storage,
admin,
unique,
multi,
)?)
}
pub(crate) fn add_invitation(
&self,
invite_code: &InvitationCode,
expiry: u32,
memo: &Option<String>,
) -> Result<(), ProtocolError> {
log_debug!("add_invitation {invite_code} expiry {expiry}");
Invitation::create(invite_code, expiry, memo, &self.accounts_storage)?;
Ok(())
}
pub(crate) fn get_invitation_type(&self, invite_code: [u8; 32]) -> Result<u8, ProtocolError> {
log_debug!("get_invitation_type {:?}", invite_code);
let inv = Invitation::open(&invite_code, &self.accounts_storage)?;
inv.get_type()
}
pub(crate) fn remove_invitation(&self, invite_code: [u8; 32]) -> Result<(), ProtocolError> {
log_debug!("remove_invitation {:?}", invite_code);
let inv = Invitation::open(&invite_code, &self.accounts_storage)?;
inv.del()?;
Ok(())
}
pub(crate) fn get_inboxes_for_readers(&self, user: &UserId) -> Result<HashSet<(PubKey, OverlayId)>,StorageError> {
AccountStorage::load_inboxes(user, &self.core_storage)
}
pub(crate) fn take_first_msg_from_inbox(
&self,
inbox: &PubKey,
overlay: &OverlayId
) -> Result<InboxMsg, StorageError> {
InboxStorage::take_first_msg(inbox, overlay, &self.core_storage)
}
pub(crate) fn get_readers_for_inbox(
&self,
inbox: &PubKey,
overlay: &OverlayId
) -> Result<HashSet<UserId>, StorageError> {
InboxStorage::load_readers(inbox, overlay, &self.core_storage)
}
pub(crate) fn register_inbox_reader(&self, user_id: UserId, inbox_id: PubKey, overlay: OverlayId) -> Result<(), StorageError> {
InboxStorage::register_reader(&inbox_id, &overlay, &user_id, &self.core_storage)?;
AccountStorage::add_inbox(&user_id, inbox_id, overlay, &self.core_storage)
}
pub(crate) fn enqueue_inbox_msg(
&self,
msg: &InboxMsg
) -> Result<(), StorageError> {
InboxStorage::open(&msg.body.to_inbox, &msg.body.to_overlay, &self.core_storage)?.enqueue_msg(msg)
}
pub(crate) fn get_repo_pin_status(
&self,
overlay: &OverlayId,
repo: &RepoHash,
user: &UserId,
) -> Result<RepoPinStatus, ServerError> {
let repo_info = RepoHashStorage::load_for_user(user, repo, overlay, &self.core_storage)?;
let mut topics = vec![];
for topic in repo_info.topics {
if let Ok(mut model) = TopicStorage::open(&topic, overlay, &self.core_storage) {
match TopicStorage::USERS.get(&mut model, user) {
Err(_) => {}
Ok(publisher) => topics.push(TopicSubRes::new_from_heads(
TopicStorage::get_all_heads(&mut model)?,
publisher,
topic,
TopicStorage::COMMITS_NBR.get(&mut model)?,
)),
}
}
}
if topics.is_empty() {
return Err(ServerError::False);
}
Ok(RepoPinStatus::V0(RepoPinStatusV0 {
hash: repo.clone(),
expose_outer: repo_info.expose_outer.len() > 0,
topics,
}))
}
pub(crate) fn pin_repo_write(
&self,
overlay_access: &OverlayAccess,
repo: &RepoHash,
user_id: &UserId,
ro_topics: &Vec<TopicId>,
rw_topics: &Vec<PublisherAdvert>,
overlay_root_topic: &Option<TopicId>,
expose_outer: bool,
) -> Result<RepoOpened, ServerError> {
assert!(!overlay_access.is_read_only());
// TODO: all the below DB operations should be done inside a single transaction. need refactor of Object-KCV-Mapping to take an optional transaction.
let inner_overlay = overlay_access.overlay_id_for_client_protocol_purpose();
let mut inner_overlay_storage =
match OverlayStorage::open(inner_overlay, &self.core_storage) {
Err(StorageError::NotFound) => {
// inner overlay doesn't exist, we need to create it
OverlayStorage::create(
inner_overlay,
&(*overlay_access).into(),
expose_outer,
&self.core_storage,
)?
}
Err(e) => return Err(e.into()),
Ok(os) => os,
};
// the overlay we use to store all the info is: the outer for a RW access, and the inner for a WO access.
let overlay = match inner_overlay_storage.overlay_type() {
OverlayType::Outer(_) | OverlayType::OuterOnly => {
panic!("shouldnt happen: we are pinning to an inner overlay. why is it outer type?")
}
OverlayType::Inner(outer) => outer,
OverlayType::InnerOnly => inner_overlay,
}
.clone();
// if an overlay_root_topic was provided, we update it in the DB:
// this information is stored on the inner overlay record, contrary to the rest of the info below, that is stored on the outer (except for WO)
if overlay_root_topic.is_some() {
OverlayStorage::TOPIC.set(
&mut inner_overlay_storage,
overlay_root_topic.as_ref().unwrap(),
)?;
}
// we now do the pinning :
let mut result: RepoOpened = vec![];
let mut repo_info = RepoHashStorage::open(repo, &overlay, &self.core_storage)?;
if expose_outer {
RepoHashStorage::EXPOSE_OUTER.add(&mut repo_info, user_id)?;
}
let mut rw_topics_added: HashMap<TopicId, TopicSubRes> =
HashMap::with_capacity(rw_topics.len());
for topic in rw_topics {
let topic_id = topic.topic_id();
let mut topic_storage =
TopicStorage::create(topic_id, &overlay, repo, &self.core_storage, true)?;
RepoHashStorage::TOPICS.add_lazy(&mut repo_info, topic_id)?;
let _ = TopicStorage::ADVERT.get_or_set(&mut topic_storage, topic)?;
TopicStorage::USERS.add_or_change(&mut topic_storage, user_id, &true)?;
rw_topics_added.insert(
*topic_id,
TopicSubRes::new_from_heads(
TopicStorage::get_all_heads(&mut topic_storage)?,
true,
*topic_id,
TopicStorage::COMMITS_NBR.get(&mut topic_storage)?,
),
);
}
for topic in ro_topics {
if rw_topics_added.contains_key(topic) {
continue;
//we do not want to add again as read_only, a topic that was just opened as RW (publisher)
}
let mut topic_storage =
TopicStorage::create(topic, &overlay, repo, &self.core_storage, true)?;
RepoHashStorage::TOPICS.add_lazy(&mut repo_info, topic)?;
let _ = TopicStorage::USERS.get_or_add(&mut topic_storage, user_id, &false)?;
result.push(TopicSubRes::new_from_heads(
TopicStorage::get_all_heads(&mut topic_storage)?,
false,
*topic,
TopicStorage::COMMITS_NBR.get(&mut topic_storage)?,
));
}
result.extend(rw_topics_added.into_values());
Ok(result)
}
pub(crate) fn pin_repo_read(
&self,
overlay: &OverlayId,
repo: &RepoHash,
user_id: &UserId,
ro_topics: &Vec<TopicId>,
) -> Result<RepoOpened, ServerError> {
let mut overlay_storage = OverlayStorage::open(overlay, &self.core_storage)?;
match overlay_storage.overlay_type() {
OverlayType::Outer(_) => {
let mut result: RepoOpened = vec![];
let repo_info = RepoHashStorage::load_topics(repo, overlay, &self.core_storage)?;
for topic in ro_topics {
if repo_info.topics.contains(topic) {
let mut topic_storage =
TopicStorage::open(topic, overlay, &self.core_storage)?;
let _ =
TopicStorage::USERS.get_or_add(&mut topic_storage, user_id, &false)?;
result.push(TopicSubRes::new_from_heads(
TopicStorage::get_all_heads(&mut topic_storage)?,
false,
*topic,
TopicStorage::COMMITS_NBR.get(&mut topic_storage)?,
));
}
}
Ok(result)
}
_ => return Err(ServerError::NotFound),
}
}
fn check_overlay(&self, overlay: &OverlayId) -> Result<OverlayId, ServerError> {
let mut overlay_storage =
OverlayStorage::open(overlay, &self.core_storage).map_err(|e| match e {
StorageError::NotFound => ServerError::OverlayNotFound,
_ => e.into(),
})?;
Ok(match overlay_storage.overlay_type() {
OverlayType::OuterOnly => {
if overlay.is_outer() {
*overlay
} else {
return Err(ServerError::OverlayMismatch);
}
}
OverlayType::Outer(_) => {
if overlay.is_outer() {
*overlay
} else {
return Err(ServerError::OverlayMismatch);
}
}
OverlayType::Inner(outer) => {
if outer.is_outer() {
*outer
} else {
return Err(ServerError::OverlayMismatch);
}
}
OverlayType::InnerOnly => {
if overlay.is_inner() {
*overlay
} else {
return Err(ServerError::OverlayMismatch);
}
}
})
}
pub(crate) fn topic_sub(
&self,
overlay: &OverlayId,
repo: &RepoHash,
topic: &TopicId,
user_id: &UserId,
publisher: Option<&PublisherAdvert>,
) -> Result<TopicSubRes, ServerError> {
let overlay = self.check_overlay(overlay)?;
// now we check that the repo was previously pinned.
// if it was opened but not pinned, then this should be dealt with in the ServerBroker, in memory, not here)
let is_publisher = publisher.is_some();
// (we already checked that the advert is valid)
let mut topic_storage =
TopicStorage::create(topic, &overlay, repo, &self.core_storage, true)?;
let _ = TopicStorage::USERS.get_or_add(&mut topic_storage, user_id, &is_publisher)?;
if is_publisher {
let _ = TopicStorage::ADVERT.get_or_set(&mut topic_storage, publisher.unwrap())?;
}
let mut repo_info = RepoHashStorage::open(repo, &overlay, &self.core_storage)?;
RepoHashStorage::TOPICS.add_lazy(&mut repo_info, topic)?;
Ok(TopicSubRes::new_from_heads(
TopicStorage::get_all_heads(&mut topic_storage)?,
is_publisher,
*topic,
TopicStorage::COMMITS_NBR.get(&mut topic_storage)?,
))
}
pub(crate) fn get_commit(
&self,
overlay: &OverlayId,
id: &ObjectId,
) -> Result<Vec<Block>, ServerError> {
let overlay = self.check_overlay(overlay)?;
let mut commit_storage = CommitStorage::open(id, &overlay, &self.core_storage)?;
let event_info = commit_storage
.event()
.as_ref()
.left()
.ok_or(ServerError::NotFound)?; // TODO: for now we do not deal with events that have been removed from storage
let mut blocks = Vec::with_capacity(event_info.blocks.len());
for block_id in event_info.blocks.iter() {
let block = self.block_storage.read().unwrap().get(&overlay, block_id)?;
blocks.push(block);
}
Ok(blocks)
}
pub(crate) fn has_block(
&self,
overlay: &OverlayId,
block_id: &BlockId,
) -> Result<(), ServerError> {
let overlay = self.check_overlay(overlay)?;
let overlay = &overlay;
Ok(self.block_storage.read().unwrap().has(overlay, block_id)?)
}
pub(crate) fn get_block(
&self,
overlay: &OverlayId,
block_id: &BlockId,
) -> Result<Block, ServerError> {
let overlay = self.check_overlay(overlay)?;
let overlay = &overlay;
Ok(self.block_storage.read().unwrap().get(overlay, block_id)?)
}
pub(crate) fn add_block(
&self,
overlay: &OverlayId,
block: Block,
) -> Result<BlockId, ServerError> {
if overlay.is_outer() {
// we don't publish events on the outer overlay!
return Err(ServerError::OverlayMismatch);
}
let overlay = self.check_overlay(overlay)?;
let overlay = &overlay;
let mut overlay_storage = OverlayStorage::new(overlay, &self.core_storage);
Ok(self.add_block_(overlay, &mut overlay_storage, block)?)
}
fn add_block_(
&self,
overlay_id: &OverlayId,
overlay_storage: &mut OverlayStorage,
block: Block,
) -> Result<BlockId, StorageError> {
let block_id = self
.block_storage
.write()
.unwrap()
.put(overlay_id, &block, true)?;
OverlayStorage::BLOCKS.increment(overlay_storage, &block_id)?;
Ok(block_id)
}
pub(crate) fn save_event(
&self,
overlay: &OverlayId,
event: Event,
user_id: &UserId,
) -> Result<TopicId, ServerError> {
if overlay.is_outer() {
// we don't publish events on the outer overlay!
return Err(ServerError::OverlayMismatch);
}
let overlay = self.check_overlay(overlay)?;
let overlay = &overlay;
// TODO: check that the sequence number is correct
let topic = *event.topic_id();
// check that the topic exists and that this user has pinned it as publisher
let mut topic_storage =
TopicStorage::open(&topic, overlay, &self.core_storage).map_err(|e| match e {
StorageError::NotFound => ServerError::TopicNotFound,
_ => e.into(),
})?;
let is_publisher = TopicStorage::USERS
.get(&mut topic_storage, user_id)
.map_err(|e| match e {
StorageError::NotFound => ServerError::AccessDenied,
_ => e.into(),
})?;
if !is_publisher {
return Err(ServerError::AccessDenied);
}
//log_info!("SAVED EVENT in overlay {:?} : {}", overlay, event);
// remove the blocks from inside the event, and save the "dehydrated" event and each block separately.
match event {
Event::V0(mut v0) => {
let mut overlay_storage = OverlayStorage::new(overlay, &self.core_storage);
let mut extracted_blocks_ids = Vec::with_capacity(v0.content.blocks.len());
let first_block_copy = v0.content.blocks[0].clone();
let temp_mini_block_storage = HashMapBlockStorage::new();
for block in v0.content.blocks {
let _ = temp_mini_block_storage.put(overlay, &block, false)?;
extracted_blocks_ids.push(self.add_block_(
overlay,
&mut overlay_storage,
block,
)?);
}
// creating a temporary store to access the blocks
let temp_store = Store::new_from_overlay_id(
overlay,
Arc::new(std::sync::RwLock::new(temp_mini_block_storage)),
);
let commit_id = extracted_blocks_ids[0];
let header = Object::load_header(&first_block_copy, &temp_store).map_err(|_e| {
//log_err!("err : {:?}", e);
ServerError::InvalidHeader
})?;
v0.content.blocks = vec![];
let event_info = EventInfo {
event: Event::V0(v0),
blocks: extracted_blocks_ids,
};
CommitStorage::create(
&commit_id,
overlay,
event_info,
&header,
true,
&self.core_storage,
)?;
let past = if header.is_some() {
HashSet::from_iter(header.unwrap().acks_and_nacks())
} else {
HashSet::new()
};
let head = HashSet::from([commit_id]);
//TODO: current_heads in TopicInfo in ServerBroker is not updated (but it isn't used so far)
TopicStorage::HEADS.remove_from_set_and_add(&mut topic_storage, past, head)?;
TopicStorage::COMMITS_NBR.increment(&mut topic_storage)?;
}
}
Ok(topic)
}
pub(crate) fn topic_sync_req(
&self,
overlay: &OverlayId,
topic: &TopicId,
known_heads: &Vec<ObjectId>,
target_heads: &Vec<ObjectId>,
known_commits: &Option<BloomFilter>,
) -> Result<Vec<TopicSyncRes>, ServerError> {
let overlay = self.check_overlay(overlay)?;
// quick solution for now using the Branch::sync_req. TODO: use the saved references (ACKS,DEPS) in the server_storage, to have much quicker responses
let target_heads = if target_heads.is_empty() {
// get the current_heads
let mut topic_storage = TopicStorage::new(topic, &overlay, &self.core_storage);
let heads = TopicStorage::get_all_heads(&mut topic_storage)?;
if heads.is_empty() {
return Err(ServerError::TopicNotFound);
}
Box::new(heads.into_iter()) as Box<dyn Iterator<Item = ObjectId>>
} else {
Box::new(target_heads.iter().cloned()) as Box<dyn Iterator<Item = ObjectId>>
};
let store = Store::new_from_overlay_id(&overlay, Arc::clone(&self.block_storage));
let commits = Branch::sync_req(target_heads, known_heads, known_commits, &store)
.map_err(|_| ServerError::MalformedBranch)?;
let mut result = Vec::with_capacity(commits.len());
for commit_id in commits {
let commit_storage = CommitStorage::open(&commit_id, &overlay, &self.core_storage)?;
let mut event_info = commit_storage
.take_event()
.left()
.ok_or(ServerError::NotFound)?; // TODO: for now we do not deal with events that have been removed from storage
// rehydrate the event :
let mut blocks = Vec::with_capacity(event_info.blocks.len());
for block_id in event_info.blocks {
let block = store.get(&block_id)?;
blocks.push(block);
}
match event_info.event {
Event::V0(ref mut v0) => {
v0.content.blocks = blocks;
}
}
result.push(TopicSyncRes::V0(TopicSyncResV0::Event(event_info.event)));
}
Ok(result)
}
}

@ -0,0 +1,905 @@
/*
* Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
//! Implementation of the Server Broker
use std::{
collections::{BTreeMap, HashMap, HashSet},
path::PathBuf,
sync::Arc,
time::{Duration, SystemTime},
};
use async_std::sync::{Mutex, RwLock};
use either::Either;
use futures::{channel::mpsc, SinkExt, StreamExt};
use serde::{Deserialize, Serialize};
use ng_repo::{
block_storage::BlockStorage,
errors::{NgError, ProtocolError, ServerError},
log::*,
types::*,
};
use ng_net::{
app_protocol::*,
broker::{ClientPeerId, BROKER},
connection::NoiseFSM,
server_broker::IServerBroker,
types::*,
utils::{spawn_and_log_error, Receiver, ResultSend, Sender},
};
use ng_verifier::{
site::SiteV0,
types::{BrokerPeerId, VerifierConfig, VerifierConfigType},
verifier::Verifier,
};
use crate::rocksdb_server_storage::RocksDbServerStorage;
pub struct TopicInfo {
pub repo: RepoHash,
pub publisher_advert: Option<PublisherAdvert>,
pub current_heads: HashSet<ObjectId>,
pub root_commit: Option<ObjectId>,
/// indicates which users have opened the topic (boolean says if as publisher or not)
pub users: HashMap<UserId, bool>,
}
pub struct RepoInfo {
/// set of users that requested the repo to be exposed on the outer overlay
/// only possible if the user is a publisher
pub expose_outer: HashSet<UserId>,
/// set of topics of this repo
pub topics: HashSet<TopicId>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct EventInfo {
pub event: Event,
pub blocks: Vec<BlockId>,
}
pub struct CommitInfo {
pub event: Either<EventInfo, TopicId>,
pub home_pinned: bool,
pub acks: HashSet<ObjectId>,
pub deps: HashSet<ObjectId>,
pub futures: HashSet<ObjectId>,
pub files: HashSet<ObjectId>,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum OverlayType {
OuterOnly,
Outer(OverlayId), // the ID of the inner overlay corresponding to this outer.
Inner(OverlayId), // the ID of the outer overlay corresponding to the inner
InnerOnly,
}
impl OverlayType {
pub fn is_inner_get_outer(&self) -> Option<&OverlayId> {
match self {
Self::Inner(outer) => Some(outer),
_ => None,
}
}
pub fn is_outer_to_inner(&self) -> bool {
match self {
Self::Outer(_) => true,
_ => false,
}
}
pub fn is_outer_only(&self) -> bool {
match self {
Self::OuterOnly => true,
_ => false,
}
}
}
impl From<OverlayAccess> for OverlayType {
fn from(oa: OverlayAccess) -> OverlayType {
match oa {
OverlayAccess::ReadOnly(_) => {
panic!("cannot create an OverlayType from a ReadOnly OverlayAccess")
}
OverlayAccess::ReadWrite((_inner, outer)) => OverlayType::Inner(outer),
OverlayAccess::WriteOnly(_inner) => OverlayType::InnerOnly,
}
}
}
#[allow(dead_code)]
pub(crate) struct OverlayInfo {
pub overlay_type: OverlayType,
pub overlay_topic: Option<TopicId>,
pub topics: HashMap<TopicId, TopicInfo>,
pub repos: HashMap<RepoHash, RepoInfo>,
}
struct DetachableVerifier {
detach: bool,
attached: Option<(DirectPeerId, u64)>,
verifier: Verifier,
}
pub struct ServerBrokerState {
#[allow(dead_code)]
overlays: HashMap<OverlayId, OverlayInfo>,
#[allow(dead_code)]
inner_overlays: HashMap<OverlayId, Option<OverlayId>>,
local_subscriptions: HashMap<(OverlayId, TopicId), HashMap<PubKey, Option<UserId>>>,
verifiers: HashMap<UserId, Arc<RwLock<DetachableVerifier>>>,
remote_apps: HashMap<(DirectPeerId, u64), UserId>,
wallet_rendezvous: HashMap<SymKey, Sender<ExportedWallet>>,
wallet_exports: HashMap<SymKey, ExportedWallet>,
wallet_exports_timestamp: BTreeMap<SystemTime, SymKey>,
}
pub struct ServerBroker {
storage: RocksDbServerStorage,
state: RwLock<ServerBrokerState>,
path_users: PathBuf,
master_key: Option<SymKey>,
}
impl ServerBroker {
pub(crate) fn new(
storage: RocksDbServerStorage,
path_users: PathBuf,
master_key: Option<SymKey>,
) -> Self {
ServerBroker {
storage: storage,
state: RwLock::new(ServerBrokerState {
overlays: HashMap::new(),
inner_overlays: HashMap::new(),
local_subscriptions: HashMap::new(),
verifiers: HashMap::new(),
remote_apps: HashMap::new(),
wallet_rendezvous: HashMap::new(),
wallet_exports: HashMap::new(),
wallet_exports_timestamp: BTreeMap::new(),
}),
master_key,
path_users,
}
}
pub fn load(&mut self) -> Result<(), NgError> {
Ok(())
}
async fn add_subscription(
&self,
overlay: OverlayId,
topic: TopicId,
peer: ClientPeerId,
) -> Result<(), ServerError> {
let mut lock = self.state.write().await;
let peers_map = lock
.local_subscriptions
.entry((overlay, topic))
.or_insert(HashMap::with_capacity(1));
log_debug!(
"SUBSCRIBING PEER {:?} TOPIC {} OVERLAY {}",
peer,
topic,
overlay
);
if peers_map.insert(*peer.key(), peer.value()).is_some() {
//return Err(ServerError::PeerAlreadySubscribed);
}
Ok(())
}
#[allow(dead_code)]
async fn remove_subscription(
&self,
overlay: &OverlayId,
topic: &TopicId,
peer: &PubKey,
) -> Result<(), ServerError> {
let mut lock = self.state.write().await;
let peers_set = lock
.local_subscriptions
.get_mut(&(*overlay, *topic))
.ok_or(ServerError::SubscriptionNotFound)?;
if peers_set.remove(peer).is_none() {
return Err(ServerError::SubscriptionNotFound);
}
Ok(())
}
async fn new_verifier_from_credentials(
&self,
user_id: &UserId,
credentials: Credentials,
local_peer_id: DirectPeerId,
partial_credentials: bool,
) -> Result<Verifier, NgError> {
let block_storage = self.get_block_storage();
let mut path = self.get_path_users();
let user_hash: Digest = user_id.into();
path.push(user_hash.to_string());
std::fs::create_dir_all(path.clone()).unwrap();
let peer_id_dh = credentials.peer_priv_key.to_pub().to_dh_from_ed();
let mut verifier = Verifier::new(
VerifierConfig {
config_type: VerifierConfigType::RocksDb(path),
user_master_key: *credentials.user_master_key.slice(),
peer_priv_key: credentials.peer_priv_key,
user_priv_key: credentials.user_key,
private_store_read_cap: if partial_credentials {
None
} else {
Some(credentials.read_cap)
},
private_store_id: if partial_credentials {
None
} else {
Some(credentials.private_store)
},
protected_store_id: if partial_credentials {
None
} else {
Some(credentials.protected_store)
},
public_store_id: if partial_credentials {
None
} else {
Some(credentials.public_store)
},
locator: Locator::empty(),
},
block_storage,
)?;
if !partial_credentials {
verifier.connected_broker = BrokerPeerId::Local(local_peer_id);
// start the local transport connection
let mut lock = BROKER.write().await;
lock.connect_local(peer_id_dh, *user_id)?;
}
Ok(verifier)
}
}
use async_std::future::timeout;
async fn wait_for_wallet(
mut internal_receiver: Receiver<ExportedWallet>,
mut sender: Sender<Result<ExportedWallet, ServerError>>,
rendezvous: SymKey,
) -> ResultSend<()> {
let wallet_future = internal_receiver.next();
let _ = sender
.send(
match timeout(Duration::from_millis(5 * 60_000), wallet_future).await {
Err(_) => Err(ServerError::ExportWalletTimeOut),
Ok(Some(w)) => Ok(w),
Ok(None) => Err(ServerError::BrokerError),
},
)
.await;
BROKER
.read()
.await
.get_server_broker()?
.read()
.await
.remove_rendezvous(&rendezvous)
.await;
Ok(())
}
//TODO: the purpose of this trait is to have a level of indirection so we can keep some data in memory (cache) and avoid hitting the storage backend (rocksdb) at every call.
//for now this cache is not implemented, but the structs are ready (see above), and it would just require to change slightly the implementation of the trait functions here below.
#[async_trait::async_trait]
impl IServerBroker for ServerBroker {
fn take_master_key(&mut self) -> Result<SymKey, ProtocolError> {
match self.master_key.take() {
None => Err(ProtocolError::AccessDenied),
Some(key) => Ok(key),
}
}
async fn remove_rendezvous(&self, rendezvous: &SymKey) {
let mut lock = self.state.write().await;
let _ = lock.wallet_rendezvous.remove(&rendezvous);
}
async fn wait_for_wallet_at_rendezvous(
&self,
rendezvous: SymKey,
) -> Receiver<Result<ExportedWallet, ServerError>> {
let (internal_sender, internal_receiver) = mpsc::unbounded();
let (mut sender, receiver) = mpsc::unbounded();
{
let mut state = self.state.write().await;
if state.wallet_rendezvous.contains_key(&rendezvous) {
let _ = sender.send(Err(ServerError::BrokerError)).await;
sender.close_channel();
return receiver;
} else {
let _ = state
.wallet_rendezvous
.insert(rendezvous.clone(), internal_sender);
}
}
spawn_and_log_error(wait_for_wallet(internal_receiver, sender, rendezvous));
receiver
}
async fn get_wallet_export(&self, rendezvous: SymKey) -> Result<ExportedWallet, ServerError> {
let mut state = self.state.write().await;
match state.wallet_exports.remove(&rendezvous) {
Some(wallet) => Ok(wallet),
None => Err(ServerError::NotFound),
}
}
async fn put_wallet_export(&self, rendezvous: SymKey, export: ExportedWallet) {
let mut state = self.state.write().await;
let _ = state.wallet_exports.insert(rendezvous.clone(), export);
let _ = state
.wallet_exports_timestamp
.insert(SystemTime::now(), rendezvous);
}
// TODO: periodically (every 5 min) remove entries in wallet_exports_timestamp and wallet_exports
async fn put_wallet_at_rendezvous(
&self,
rendezvous: SymKey,
export: ExportedWallet,
) -> Result<(), ServerError> {
let mut state = self.state.write().await;
match state.wallet_rendezvous.remove(&rendezvous) {
None => Err(ServerError::NotFound),
Some(mut sender) => {
let _ = sender.send(export).await;
Ok(())
}
}
}
fn get_block_storage(
&self,
) -> std::sync::Arc<std::sync::RwLock<dyn BlockStorage + Send + Sync>> {
self.storage.get_block_storage()
}
fn get_path_users(&self) -> PathBuf {
self.path_users.clone()
}
fn has_block(&self, overlay_id: &OverlayId, block_id: &BlockId) -> Result<(), ServerError> {
self.storage.has_block(overlay_id, block_id)
}
fn get_block(&self, overlay_id: &OverlayId, block_id: &BlockId) -> Result<Block, ServerError> {
self.storage.get_block(overlay_id, block_id)
}
fn next_seq_for_peer(&self, peer: &PeerId, seq: u64) -> Result<(), ServerError> {
self.storage.next_seq_for_peer(peer, seq)
}
fn put_block(&self, overlay_id: &OverlayId, block: Block) -> Result<(), ServerError> {
self.storage.add_block(overlay_id, block)?;
Ok(())
}
async fn create_user(&self, broker_id: &DirectPeerId) -> Result<UserId, ProtocolError> {
let user_privkey = PrivKey::random_ed();
let user_id = user_privkey.to_pub();
let mut creds = Credentials::new_partial(&user_privkey);
let mut verifier = self
.new_verifier_from_credentials(&user_id, creds.clone(), *broker_id, true)
.await?;
let _site = SiteV0::create_personal(user_privkey.clone(), &mut verifier)
.await
.map_err(|e| {
log_err!("create_personal failed with {e}");
ProtocolError::BrokerError
})?;
// update credentials from config of verifier.
verifier.complement_credentials(&mut creds);
//verifier.close().await;
// save credentials and user
self.add_user_credentials(&user_id, &creds)?;
verifier.connected_broker = BrokerPeerId::Local(*broker_id);
// start the local transport connection
{
let mut lock = BROKER.write().await;
let peer_id_dh = creds.peer_priv_key.to_pub().to_dh_from_ed();
lock.connect_local(peer_id_dh, user_id)?;
}
let _res = verifier.send_outbox().await;
if _res.is_err() {
log_err!("{:?}", _res);
}
Ok(user_id)
}
fn get_user(&self, user_id: PubKey) -> Result<bool, ProtocolError> {
self.storage.get_user(user_id)
}
fn has_no_user(&self) -> Result<bool, ProtocolError> {
self.storage.has_no_user()
}
fn add_user_credentials(
&self,
user_id: &PubKey,
credentials: &Credentials,
) -> Result<(), ProtocolError> {
self.storage.add_user_credentials(user_id, credentials)
}
fn get_user_credentials(&self, user_id: &PubKey) -> Result<Credentials, ProtocolError> {
self.storage.get_user_credentials(user_id)
}
fn add_user(&self, user_id: PubKey, is_admin: bool) -> Result<(), ProtocolError> {
self.storage.add_user(user_id, is_admin)
}
fn del_user(&self, user_id: PubKey) -> Result<(), ProtocolError> {
self.storage.del_user(user_id)
}
fn list_users(&self, admins: bool) -> Result<Vec<PubKey>, ProtocolError> {
self.storage.list_users(admins)
}
fn list_invitations(
&self,
admin: bool,
unique: bool,
multi: bool,
) -> Result<Vec<(InvitationCode, u32, Option<String>)>, ProtocolError> {
self.storage.list_invitations(admin, unique, multi)
}
fn add_invitation(
&self,
invite_code: &InvitationCode,
expiry: u32,
memo: &Option<String>,
) -> Result<(), ProtocolError> {
self.storage.add_invitation(invite_code, expiry, memo)
}
fn get_invitation_type(&self, invite_code: [u8; 32]) -> Result<u8, ProtocolError> {
self.storage.get_invitation_type(invite_code)
}
fn remove_invitation(&self, invite_code: [u8; 32]) -> Result<(), ProtocolError> {
self.storage.remove_invitation(invite_code)
}
async fn app_process_request(
&self,
req: AppRequest,
request_id: i64,
fsm: &Mutex<NoiseFSM>,
) -> Result<(), ServerError> {
// get the session
let remote = {
fsm.lock()
.await
.remote_peer()
.ok_or(ServerError::SessionNotFound)?
};
let session_id = (remote, req.session_id());
let session_lock = {
let lock = self.state.read().await;
let user_id = lock
.remote_apps
.get(&session_id)
.ok_or(ServerError::SessionNotFound)?
.to_owned();
Arc::clone(
lock.verifiers
.get(&user_id)
.ok_or(ServerError::SessionNotFound)?,
)
};
let mut session = session_lock.write().await;
if session.attached.is_none() || session.attached.unwrap() != session_id {
return Err(ServerError::SessionDetached);
}
if req.command().is_stream() {
let res = session.verifier.app_request_stream(req).await;
match res {
Err(e) => {
let error: ServerError = e.into();
let error_res: AppMessage = error.into();
fsm.lock()
.await
.send_in_reply_to(error_res.into(), request_id)
.await?;
}
Ok((mut receiver, _cancel)) => {
//TODO: implement cancel
let mut some_sent = false;
while let Some(response) = receiver.next().await {
some_sent = true;
let mut msg: AppMessage = response.into();
msg.set_result(ServerError::PartialContent.into());
fsm.lock()
.await
.send_in_reply_to(msg.into(), request_id)
.await?;
}
let end: Result<EmptyAppResponse, ServerError> = if some_sent {
Err(ServerError::EndOfStream)
} else {
Err(ServerError::EmptyStream)
};
fsm.lock()
.await
.send_in_reply_to(end.into(), request_id)
.await?;
}
}
} else {
let res = session.verifier.app_request(req).await;
//log_debug!("GOT RES {:?}", res);
let app_message: AppMessage = match res {
Err(e) => {
log_debug!("AppRequest error NgError {e}");
let server_err: ServerError = e.into();
server_err.into()
}
Ok(app_res) => app_res.into(),
};
fsm.lock()
.await
.send_in_reply_to(app_message.into(), request_id)
.await?;
}
Ok(())
}
async fn app_session_start(
&self,
req: AppSessionStart,
remote: DirectPeerId,
local_peer_id: DirectPeerId,
) -> Result<AppSessionStartResponse, ServerError> {
let user_id = req.user_id();
let id = (remote, req.session_id());
let verifier_lock_res = {
let lock = self.state.read().await;
lock.verifiers.get(user_id).map(|l| Arc::clone(l))
};
let verifier_lock = match verifier_lock_res {
Some(session_lock) => {
let mut session = session_lock.write().await;
if let Some((peer_id, session_id)) = session.attached {
if peer_id != remote || session_id == req.session_id() {
// remove the previous session
let mut write_lock = self.state.write().await;
let _ = write_lock.remote_apps.remove(&(peer_id, session_id));
}
}
session.attached = Some(id);
Arc::clone(&session_lock)
}
None => {
// we create and load a new verifier
let credentials = if req.credentials().is_none() {
// headless do not have credentials. we fetch them from server_storage
self.storage.get_user_credentials(user_id)?
} else {
req.credentials().clone().unwrap()
};
if *user_id != credentials.user_key.to_pub() {
log_debug!("InvalidRequest");
return Err(ServerError::InvalidRequest);
}
let verifier = self
.new_verifier_from_credentials(user_id, credentials, local_peer_id, false)
.await;
if verifier.is_err() {
log_err!(
"new_verifier failed with: {:?}",
verifier.as_ref().unwrap_err()
);
}
let mut verifier = verifier?;
// TODO : key.zeroize();
//load verifier from local_storage
let _ = verifier.load();
//TODO: save opened_branches in user_storage, so that when we open again the verifier, the syncing can work
verifier.sync().await;
let session = DetachableVerifier {
detach: true,
attached: Some(id),
verifier,
};
let mut write_lock = self.state.write().await;
Arc::clone(
write_lock
.verifiers
.entry(*user_id)
.or_insert(Arc::new(RwLock::new(session))),
)
}
};
let verifier = &verifier_lock.read().await.verifier;
let res = AppSessionStartResponse::V0(AppSessionStartResponseV0 {
private_store: *verifier.private_store_id(),
protected_store: *verifier.protected_store_id(),
public_store: *verifier.public_store_id(),
});
let mut write_lock = self.state.write().await;
if let Some(previous_user) = write_lock.remote_apps.insert(id, *user_id) {
// weird. another session was opened for this id.
// we have to stop it otherwise it would be dangling.
if previous_user != *user_id {
if let Some(previous_session_lock) = write_lock
.verifiers
.get(&previous_user)
.map(|v| Arc::clone(v))
{
let mut previous_session = previous_session_lock.write().await;
if previous_session.detach {
previous_session.attached = None;
} else {
// we stop it and drop it
let verifier = write_lock.verifiers.remove(&previous_user);
verifier.unwrap().read().await.verifier.close().await;
}
}
}
}
Ok(res)
}
async fn app_session_stop(
&self,
req: AppSessionStop,
remote_peer_id: &DirectPeerId,
) -> Result<EmptyAppResponse, ServerError> {
let id = (*remote_peer_id, req.session_id());
let mut write_lock = self.state.write().await;
let must_be_destroyed = {
let session_user = write_lock
.remote_apps
.remove(&id)
.ok_or(ServerError::SessionNotFound)?;
let session = Arc::clone(
write_lock
.verifiers
.get(&session_user)
.ok_or(ServerError::SessionNotFound)?,
);
let mut verifier_lock = session.write().await;
if !req.is_force_close() && verifier_lock.detach {
verifier_lock.attached = None;
None
} else {
Some(session_user)
}
};
if let Some(user) = must_be_destroyed {
let verifier = write_lock.verifiers.remove(&user);
verifier.unwrap().read().await.verifier.close().await;
}
Ok(EmptyAppResponse(()))
}
fn get_repo_pin_status(
&self,
overlay: &OverlayId,
repo: &RepoHash,
user: &UserId,
) -> Result<RepoPinStatus, ServerError> {
self.storage.get_repo_pin_status(overlay, repo, user)
}
async fn pin_repo_write(
&self,
overlay: &OverlayAccess,
repo: &RepoHash,
user_id: &UserId,
ro_topics: &Vec<TopicId>,
rw_topics: &Vec<PublisherAdvert>,
overlay_root_topic: &Option<TopicId>,
expose_outer: bool,
peer: &ClientPeerId,
) -> Result<RepoOpened, ServerError> {
let res = self.storage.pin_repo_write(
overlay,
repo,
user_id,
ro_topics,
rw_topics,
overlay_root_topic,
expose_outer,
)?;
for topic in res.iter() {
self.add_subscription(
*overlay.overlay_id_for_client_protocol_purpose(),
*topic.topic_id(),
peer.clone(),
)
.await?;
}
Ok(res)
}
async fn pin_repo_read(
&self,
overlay: &OverlayId,
repo: &RepoHash,
user_id: &UserId,
ro_topics: &Vec<TopicId>,
peer: &ClientPeerId,
) -> Result<RepoOpened, ServerError> {
let res = self
.storage
.pin_repo_read(overlay, repo, user_id, ro_topics)?;
for topic in res.iter() {
// TODO: those outer subscriptions are not handled yet. they will not emit events.
self.add_subscription(*overlay, *topic.topic_id(), peer.clone())
.await?;
}
Ok(res)
}
async fn topic_sub(
&self,
overlay: &OverlayId,
repo: &RepoHash,
topic: &TopicId,
user: &UserId,
publisher: Option<&PublisherAdvert>,
peer: &ClientPeerId,
) -> Result<TopicSubRes, ServerError> {
let res = self
.storage
.topic_sub(overlay, repo, topic, user, publisher)?;
self.add_subscription(*overlay, *topic, peer.clone())
.await?;
Ok(res)
}
fn get_commit(&self, overlay: &OverlayId, id: &ObjectId) -> Result<Vec<Block>, ServerError> {
self.storage.get_commit(overlay, id)
}
async fn remove_all_subscriptions_of_client(&self, client: &ClientPeerId) {
let remote_peer = client.key();
let mut lock = self.state.write().await;
for ((overlay, topic), peers) in lock.local_subscriptions.iter_mut() {
if peers.remove(remote_peer).is_some() {
log_debug!(
"subscription of peer {} to topic {} in overlay {} removed",
remote_peer,
topic,
overlay
);
}
}
}
async fn inbox_post(&self, post: InboxPost) -> Result<(), ServerError> {
// TODO: deal with Inbox that is not local to the broker (use Core protocol to dispatch it)
let users = self.storage.get_readers_for_inbox(&post.msg.body.to_inbox, &post.msg.body.to_overlay)?;
if users.is_empty() {
self.storage.enqueue_inbox_msg(&post.msg)?;
return Ok(())
}
let broker = BROKER.read().await;
let not_dispatched = broker
.dispatch_inbox_msg(&users, post.msg)
.await?;
if let Some(msg) = not_dispatched {
self.storage.enqueue_inbox_msg(&msg)?;
}
Ok(())
}
fn inbox_register(&self, user_id: UserId, registration: InboxRegister) -> Result<(), ServerError> {
self.storage.register_inbox_reader(user_id, registration.inbox_id, registration.overlay)?;
Ok(())
}
async fn inbox_pop_for_user(&self, user: UserId ) -> Result<InboxMsg, ServerError> {
let inboxes = self.storage.get_inboxes_for_readers(&user)?;
for (inbox,overlay) in inboxes {
match self.storage.take_first_msg_from_inbox(&inbox, &overlay) {
Ok(msg) => {
return Ok(msg)
},
Err(_) => {}
}
}
Err(ServerError::NotFound)
}
async fn dispatch_event(
&self,
overlay: &OverlayId,
event: Event,
user_id: &UserId,
remote_peer: &PubKey,
) -> Result<Vec<ClientPeerId>, ServerError> {
let topic = self.storage.save_event(overlay, event, user_id)?;
// log_debug!(
// "DISPATCH EVENT {} {} {:?}",
// overlay,
// topic,
// self.local_subscriptions
// );
let lock = self.state.read().await;
let mut map = lock
.local_subscriptions
.get(&(*overlay, topic))
.map(|map| map.iter().collect())
.unwrap_or(HashMap::new());
map.remove(remote_peer);
Ok(map
.iter()
.map(|(k, v)| ClientPeerId::new_from(k, v))
.collect())
}
fn topic_sync_req(
&self,
overlay: &OverlayId,
topic: &TopicId,
known_heads: &Vec<ObjectId>,
target_heads: &Vec<ObjectId>,
known_commits: &Option<BloomFilter>,
) -> Result<Vec<TopicSyncRes>, ServerError> {
self.storage
.topic_sync_req(overlay, topic, known_heads, target_heads, known_commits)
}
}

@ -0,0 +1,355 @@
// Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
// All rights reserved.
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
//! User account Storage (Object Key/Col/Value Mapping)
use std::collections::hash_map::DefaultHasher;
use std::fmt;
use std::hash::Hash;
use std::hash::Hasher;
use std::time::SystemTime;
use serde_bare::{from_slice, to_vec};
use ng_repo::errors::StorageError;
use ng_repo::kcv_storage::KCVStorage;
#[allow(unused_imports)]
use ng_repo::log::*;
use ng_repo::types::UserId;
use ng_net::types::*;
pub struct Account<'a> {
/// User ID
id: UserId,
storage: &'a dyn KCVStorage,
}
impl<'a> fmt::Debug for Account<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Account {}", self.id)
}
}
impl<'a> Account<'a> {
const PREFIX_ACCOUNT: u8 = b'a';
const PREFIX_CLIENT: u8 = b'c';
const PREFIX_CLIENT_PROPERTY: u8 = b'd';
// propertie's client suffixes
const INFO: u8 = b'i';
const LAST_SEEN: u8 = b'l';
const CREDENTIALS: u8 = b'c';
//const USER_KEYS: u8 = b'k';
const ALL_CLIENT_PROPERTIES: [u8; 3] = [
Self::INFO,
Self::LAST_SEEN,
Self::CREDENTIALS,
//Self::USER_KEYS,
];
pub fn open(id: &UserId, storage: &'a dyn KCVStorage) -> Result<Account<'a>, StorageError> {
let opening = Account {
id: id.clone(),
storage,
};
if !opening.exists() {
return Err(StorageError::NotFound);
}
Ok(opening)
}
pub fn create(
id: &UserId,
admin: bool,
storage: &'a dyn KCVStorage,
) -> Result<Account<'a>, StorageError> {
let acc = Account {
id: id.clone(),
storage,
};
if acc.exists() {
return Err(StorageError::AlreadyExists);
}
storage.put(
Self::PREFIX_ACCOUNT,
&to_vec(&id)?,
None,
&to_vec(&admin)?,
&None,
)?;
Ok(acc)
}
#[allow(deprecated)]
pub fn get_all_users(
admins: bool,
storage: &'a dyn KCVStorage,
) -> Result<Vec<UserId>, StorageError> {
let size = to_vec(&UserId::nil())?.len();
let mut res: Vec<UserId> = vec![];
for user in
storage.get_all_keys_and_values(Self::PREFIX_ACCOUNT, size, vec![], None, &None)?
{
let admin: bool = from_slice(&user.1)?;
if admin == admins {
let id: UserId = from_slice(&user.0[1..user.0.len()])?;
res.push(id);
}
}
Ok(res)
}
pub fn has_users(storage: &'a dyn KCVStorage) -> Result<bool, StorageError> {
let size = to_vec(&UserId::nil())?.len();
let mut res: Vec<UserId> = vec![];
//TODO: fix this. we shouldn't have to fetch all the users to know if there is at least one user. highly inefficient. need to add a storage.has_one_key_value method
Ok(!storage
.get_all_keys_and_values(Self::PREFIX_ACCOUNT, size, vec![], None, &None)?
.is_empty())
}
pub fn exists(&self) -> bool {
self.storage
.get(
Self::PREFIX_ACCOUNT,
&to_vec(&self.id).unwrap(),
None,
&None,
)
.is_ok()
}
pub fn id(&self) -> UserId {
self.id
}
pub fn add_client(&self, client: &ClientId, info: &ClientInfo) -> Result<(), StorageError> {
if !self.exists() {
return Err(StorageError::BackendError);
}
let mut s = DefaultHasher::new();
info.hash(&mut s);
let hash = s.finish();
let client_key = (client.clone(), hash);
let mut client_key_ser = to_vec(&client_key)?;
let info_ser = to_vec(info)?;
self.storage.write_transaction(&mut |tx| {
let mut id_and_client = to_vec(&self.id)?;
id_and_client.append(&mut client_key_ser);
if tx
.has_property_value(Self::PREFIX_CLIENT, &id_and_client, None, &vec![], &None)
.is_err()
{
tx.put(Self::PREFIX_CLIENT, &id_and_client, None, &vec![], &None)?;
}
if tx
.has_property_value(
Self::PREFIX_CLIENT_PROPERTY,
&id_and_client,
Some(Self::INFO),
&info_ser,
&None,
)
.is_err()
{
tx.put(
Self::PREFIX_CLIENT_PROPERTY,
&id_and_client,
Some(Self::INFO),
&info_ser,
&None,
)?;
}
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs();
tx.replace(
Self::PREFIX_CLIENT_PROPERTY,
&id_and_client,
Some(Self::LAST_SEEN),
&to_vec(&now)?,
&None,
)?;
Ok(())
})
}
pub fn add_credentials(&self, credentials: &Credentials) -> Result<(), StorageError> {
if !self.exists() {
return Err(StorageError::BackendError);
}
self.storage.put(
Self::PREFIX_ACCOUNT,
&to_vec(&self.id)?,
Some(Self::CREDENTIALS),
&to_vec(credentials)?,
&None,
)
}
pub fn remove_credentials(&self) -> Result<(), StorageError> {
self.storage.del(
Self::PREFIX_ACCOUNT,
&to_vec(&self.id)?,
Some(Self::CREDENTIALS),
&None,
)
}
pub fn get_credentials(&self) -> Result<Credentials, StorageError> {
Ok(from_slice(&self.storage.get(
Self::PREFIX_ACCOUNT,
&to_vec(&self.id)?,
Some(Self::CREDENTIALS),
&None,
)?)?)
}
// pub fn add_user_keys(
// &self,
// storage_key: &SymKey,
// peer_priv_key: &PrivKey,
// ) -> Result<(), StorageError> {
// if !self.exists() {
// return Err(StorageError::BackendError);
// }
// self.storage.put(
// Self::PREFIX_ACCOUNT,
// &to_vec(&self.id)?,
// Some(Self::USER_KEYS),
// &to_vec(&(storage_key.clone(), peer_priv_key.clone()))?,
// &None,
// )
// }
// pub fn remove_user_keys(&self) -> Result<(), StorageError> {
// self.storage.del(
// Self::PREFIX_ACCOUNT,
// &to_vec(&self.id)?,
// Some(Self::USER_KEYS),
// &None,
// )
// }
// pub fn get_user_keys(&self) -> Result<(SymKey, PrivKey), StorageError> {
// Ok(from_slice(&self.storage.get(
// Self::PREFIX_ACCOUNT,
// &to_vec(&self.id)?,
// Some(Self::USER_KEYS),
// &None,
// )?)?)
// }
// pub fn remove_overlay(&self, overlay: &OverlayId) -> Result<(), StorageError> {
// self.storage.del_property_value(
// Self::PREFIX,
// &to_vec(&self.id)?,
// Some(Self::OVERLAY),
// to_vec(overlay)?,
// )
// }
// pub fn has_overlay(&self, overlay: &OverlayId) -> Result<(), StorageError> {
// self.storage.has_property_value(
// Self::PREFIX,
// &to_vec(&self.id)?,
// Some(Self::OVERLAY),
// to_vec(overlay)?,
// )
// }
pub fn is_admin(&self) -> Result<bool, StorageError> {
if self
.storage
.has_property_value(
Self::PREFIX_ACCOUNT,
&to_vec(&self.id)?,
None,
&to_vec(&true)?,
&None,
)
.is_ok()
{
return Ok(true);
}
Ok(false)
}
pub fn del(&self) -> Result<(), StorageError> {
self.storage.write_transaction(&mut |tx| {
let id = to_vec(&self.id)?;
// let mut id_and_client = to_vec(&self.id)?;
// let client_key = (client.clone(), hash);
// let mut client_key_ser = to_vec(&client_key)?;
#[allow(deprecated)]
let client_key = (ClientId::nil(), 0u64);
let client_key_ser = to_vec(&client_key)?;
let size = client_key_ser.len() + id.len();
if let Ok(clients) =
tx.get_all_keys_and_values(Self::PREFIX_CLIENT, size, id, None, &None)
{
for client in clients {
tx.del(Self::PREFIX_CLIENT, &client.0, None, &None)?;
tx.del_all(
Self::PREFIX_CLIENT_PROPERTY,
&client.0,
&Self::ALL_CLIENT_PROPERTIES,
&None,
)?;
}
}
tx.del(Self::PREFIX_ACCOUNT, &to_vec(&self.id)?, None, &None)?;
Ok(())
})
}
}
#[cfg(test)]
mod test {
use ng_repo::types::*;
use ng_storage_rocksdb::kcv_storage::RocksDbKCVStorage;
use std::fs;
use tempfile::Builder;
use crate::server_storage::admin::account::Account;
#[test]
pub fn test_account() {
let path_str = "test-env";
let root = Builder::new().prefix(path_str).tempdir().unwrap();
let key: [u8; 32] = [0; 32];
fs::create_dir_all(root.path()).unwrap();
println!("{}", root.path().to_str().unwrap());
let storage = RocksDbKCVStorage::open(root.path(), key).unwrap();
let user_id = PubKey::Ed25519PubKey([1; 32]);
let account = Account::create(&user_id, true, &storage).unwrap();
println!("account created {}", account.id());
let account2 = Account::open(&user_id, &storage).unwrap();
println!("account opened {}", account2.id());
// let client_id = PubKey::Ed25519PubKey([56; 32]);
// let client_id_not_added = PubKey::Ed25519PubKey([57; 32]);
// account2.add_client(&client_id).unwrap();
// assert!(account2.is_admin().unwrap());
// account.has_client(&client_id).unwrap();
// assert!(account.has_client(&client_id_not_added).is_err());
}
}

@ -0,0 +1,184 @@
// Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
// All rights reserved.
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
//! User account Storage (Object Key/Col/Value Mapping)
use serde_bare::from_slice;
use serde_bare::to_vec;
use ng_repo::errors::ProtocolError;
use ng_repo::errors::StorageError;
use ng_repo::kcv_storage::KCVStorage;
use ng_repo::types::SymKey;
use ng_repo::utils::now_timestamp;
use ng_net::types::*;
pub struct Invitation<'a> {
/// code
id: [u8; 32],
storage: &'a dyn KCVStorage,
}
impl<'a> Invitation<'a> {
const PREFIX: u8 = b'i';
// propertie's invitation suffixes
const TYPE: u8 = b't';
//const EXPIRE: u8 = b'e';
const ALL_PROPERTIES: [u8; 1] = [Self::TYPE];
const SUFFIX_FOR_EXIST_CHECK: u8 = Self::TYPE;
pub fn open(
id: &[u8; 32],
storage: &'a dyn KCVStorage,
) -> Result<Invitation<'a>, StorageError> {
let opening = Invitation {
id: id.clone(),
storage,
};
if !opening.exists() {
return Err(StorageError::NotFound);
}
Ok(opening)
}
pub fn create(
id: &InvitationCode,
expiry: u32,
memo: &Option<String>,
storage: &'a dyn KCVStorage,
) -> Result<Invitation<'a>, StorageError> {
let (code_type, code) = match id {
InvitationCode::Unique(c) => (0u8, c.slice()),
InvitationCode::Multi(c) => (1u8, c.slice()),
InvitationCode::Admin(c) => (2u8, c.slice()),
InvitationCode::Setup(c) => (3u8, c.slice()),
};
let acc = Invitation {
id: code.clone(),
storage,
};
if acc.exists() {
return Err(StorageError::BackendError);
}
let value = to_vec(&(code_type, expiry, memo.clone()))?;
storage.write_transaction(&mut |tx| {
tx.put(
Self::PREFIX,
&to_vec(code)?,
Some(Self::TYPE),
&value,
&None,
)?;
Ok(())
})?;
Ok(acc)
}
pub fn get_all_invitations(
storage: &'a dyn KCVStorage,
mut admin: bool,
mut unique: bool,
mut multi: bool,
) -> Result<Vec<(InvitationCode, u32, Option<String>)>, StorageError> {
let size = to_vec(&[0u8; 32])?.len();
let mut res: Vec<(InvitationCode, u32, Option<String>)> = vec![];
if !admin && !unique && !multi {
admin = true;
unique = true;
multi = true;
}
for invite in storage.get_all_keys_and_values(Self::PREFIX, size, vec![], None, &None)? {
if invite.0.len() == size + 2 {
let code: [u8; 32] = from_slice(&invite.0[1..invite.0.len() - 1])?;
if invite.0[size + 1] == Self::TYPE {
let code_type: (u8, u32, Option<String>) = from_slice(&invite.1)?;
let inv_code = match code_type {
(0, ex, memo) => {
if unique {
Some((InvitationCode::Unique(SymKey::ChaCha20Key(code)), ex, memo))
} else {
None
}
}
(1, ex, memo) => {
if multi {
Some((InvitationCode::Multi(SymKey::ChaCha20Key(code)), ex, memo))
} else {
None
}
}
(2, ex, memo) => {
if admin {
Some((InvitationCode::Admin(SymKey::ChaCha20Key(code)), ex, memo))
} else {
None
}
}
_ => panic!("invalid code type value"),
};
if inv_code.is_some() {
res.push(inv_code.unwrap());
}
}
}
}
Ok(res)
}
pub fn exists(&self) -> bool {
self.storage
.get(
Self::PREFIX,
&to_vec(&self.id).unwrap(),
Some(Self::SUFFIX_FOR_EXIST_CHECK),
&None,
)
.is_ok()
}
pub fn id(&self) -> [u8; 32] {
self.id
}
pub fn get_type(&self) -> Result<u8, ProtocolError> {
let type_ser =
self.storage
.get(Self::PREFIX, &to_vec(&self.id)?, Some(Self::TYPE), &None)?;
let t: (u8, u32, Option<String>) = from_slice(&type_ser)?;
// if t.1 < now_timestamp() {
// return Err(ProtocolError::Expired);
// }
Ok(t.0)
}
pub fn is_expired(&self) -> Result<bool, StorageError> {
let expire_ser =
self.storage
.get(Self::PREFIX, &to_vec(&self.id)?, Some(Self::TYPE), &None)?;
let expire: (u8, u32, Option<String>) = from_slice(&expire_ser)?;
if expire.1 < now_timestamp() {
return Ok(true);
}
Ok(false)
}
pub fn del(&self) -> Result<(), StorageError> {
self.storage.write_transaction(&mut |tx| {
tx.del_all(
Self::PREFIX,
&to_vec(&self.id)?,
&Self::ALL_PROPERTIES,
&None,
)?;
Ok(())
})
}
}

@ -0,0 +1,5 @@
pub mod invitation;
pub mod wallet;
pub mod account;

@ -0,0 +1,123 @@
// Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
// All rights reserved.
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
//! Broker Wallet Storage (Object Key/Col/Value Mapping), persists to storage all the SymKeys needed to open other storages
use serde_bare::to_vec;
use ng_repo::errors::StorageError;
use ng_repo::kcv_storage::KCVStorage;
use ng_repo::kcv_storage::WriteTransaction;
use ng_repo::log::*;
use ng_repo::types::*;
pub struct Wallet<'a> {
storage: &'a dyn KCVStorage,
}
impl<'a> Wallet<'a> {
const PREFIX: u8 = b'w';
const PREFIX_OVERLAY: u8 = b'o';
const PREFIX_USER: u8 = b'u';
const KEY_ACCOUNTS: [u8; 8] = *b"accounts";
//const KEY_PEERS: [u8; 5] = *b"peers";
const KEY_CORE: [u8; 4] = *b"core";
const KEY_BLOCKS: [u8; 6] = *b"blocks";
// propertie's suffixes
const SYM_KEY: u8 = b"s"[0];
//const ALL_PROPERTIES: [u8; 1] = [Self::SYM_KEY];
const SUFFIX_FOR_EXIST_CHECK: u8 = Self::SYM_KEY;
pub fn open(storage: &'a dyn KCVStorage) -> Wallet<'a> {
Wallet { storage }
}
pub fn get_or_create_single_key(
&self,
prefix: u8,
key: &Vec<u8>,
) -> Result<SymKey, StorageError> {
let mut result: Option<SymKey> = None;
self.storage.write_transaction(&mut |tx| {
let got = tx.get(prefix, key, Some(Self::SUFFIX_FOR_EXIST_CHECK), &None);
match got {
Err(e) => {
if e == StorageError::NotFound {
let res = Self::create_single_key(tx, prefix, key)?;
result = Some(res);
} else {
log_debug!("Error while creating single key {}", e);
return Err(StorageError::BackendError);
}
}
Ok(p) => {
let k: SymKey = p
.as_slice()
.try_into()
.map_err(|_| StorageError::BackendError)?;
result = Some(k);
}
}
Ok(())
})?;
Ok(result.unwrap())
}
pub fn get_or_create_user_key(&self, user: &UserId) -> Result<SymKey, StorageError> {
self.get_or_create_single_key(Self::PREFIX_USER, &to_vec(user)?)
}
pub fn get_or_create_overlay_key(&self, overlay: &OverlayId) -> Result<SymKey, StorageError> {
self.get_or_create_single_key(Self::PREFIX_OVERLAY, &to_vec(overlay)?)
}
pub fn create_single_key(
tx: &mut dyn WriteTransaction,
prefix: u8,
key: &Vec<u8>,
) -> Result<SymKey, StorageError> {
let symkey = SymKey::random();
let vec = symkey.slice().to_vec();
tx.put(prefix, key, Some(Self::SYM_KEY), &vec, &None)?;
Ok(symkey)
}
pub fn exists_single_key(&self, prefix: u8, key: &Vec<u8>) -> bool {
self.storage
.get(prefix, key, Some(Self::SUFFIX_FOR_EXIST_CHECK), &None)
.is_ok()
}
pub fn exists_accounts_key(&self) -> bool {
self.exists_single_key(Self::PREFIX, &Self::KEY_ACCOUNTS.to_vec())
}
pub fn create_accounts_key(&self) -> Result<SymKey, StorageError> {
let mut result: Option<SymKey> = None;
self.storage.write_transaction(&mut |tx| {
let res = Self::create_single_key(tx, Self::PREFIX, &Self::KEY_ACCOUNTS.to_vec())?;
result = Some(res);
Ok(())
})?;
Ok(result.unwrap())
}
// pub fn get_or_create_peers_key(&self) -> Result<SymKey, StorageError> {
// self.get_or_create_single_key(Self::PREFIX, &Self::KEY_PEERS.to_vec())
// }
pub fn get_or_create_blocks_key(&self) -> Result<SymKey, StorageError> {
self.get_or_create_single_key(Self::PREFIX, &Self::KEY_BLOCKS.to_vec())
}
pub fn get_or_create_core_key(&self) -> Result<SymKey, StorageError> {
self.get_or_create_single_key(Self::PREFIX, &Self::KEY_CORE.to_vec())
}
pub fn get_or_create_accounts_key(&self) -> Result<SymKey, StorageError> {
self.get_or_create_single_key(Self::PREFIX, &Self::KEY_ACCOUNTS.to_vec())
}
}

@ -0,0 +1,95 @@
// Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
// All rights reserved.
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
//! Account Storage (Object Key/Col/Value Mapping)
use std::collections::HashSet;
use std::hash::{DefaultHasher, Hash, Hasher};
use ng_net::types::InboxMsg;
use ng_repo::utils::now_precise_timestamp;
use serde_bare::to_vec;
use ng_repo::errors::StorageError;
use ng_repo::kcv_storage::*;
use ng_repo::types::*;
pub struct AccountStorage<'a> {
key: Vec<u8>,
storage: &'a dyn KCVStorage,
}
impl<'a> IModel for AccountStorage<'a> {
fn key(&self) -> &Vec<u8> {
&self.key
}
fn storage(&self) -> &dyn KCVStorage {
self.storage
}
fn class(&self) -> &Class {
&Self::CLASS
}
fn existential(&mut self) -> Option<&mut dyn IExistentialValue> {
None
}
}
impl<'a> AccountStorage<'a> {
// User <-> Inboxes : list of inboxes a user has registered as reader.
// FIXME: this should be in accounts storage, but because it doesn't implement the ORM yet, it is quicker to implement it here.
pub const INBOXES: MultiValueColumn<Self, (PubKey, OverlayId)> = MultiValueColumn::new(b'k');
pub const CLASS: Class<'a> = Class::new(
"Account",
None,
None,
&[],
&[&Self::INBOXES as &dyn IMultiValueColumn],
);
pub fn load_inboxes(
user: &UserId,
storage: &'a dyn KCVStorage,
) -> Result<HashSet<(PubKey, OverlayId)>, StorageError> {
let mut opening = Self::new(user, storage);
Self::INBOXES.get_all(&mut opening)
}
pub fn new(user: &UserId, storage: &'a dyn KCVStorage) -> Self {
let mut key: Vec<u8> = Vec::with_capacity(33);
key.append(&mut to_vec(user).unwrap());
Self { key, storage }
}
pub fn open(
user: &UserId,
storage: &'a dyn KCVStorage,
) -> Result<AccountStorage<'a>, StorageError> {
let opening = Self::new(user, storage);
Ok(opening)
}
pub fn add_inbox(
user: &UserId,
inbox: PubKey,
overlay: OverlayId,
storage: &'a dyn KCVStorage,
) -> Result<(), StorageError> {
let mut opening = Self::new(user, storage);
Self::INBOXES.add(&mut opening, &(inbox,overlay))
}
pub fn create(
user: &UserId,
storage: &'a dyn KCVStorage,
) -> Result<AccountStorage<'a>, StorageError> {
let creating = Self::new(user, storage);
Ok(creating)
}
}

@ -0,0 +1,158 @@
// Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
// All rights reserved.
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
//! Commit Storage (Object Key/Col/Value Mapping)
use either::Either;
use serde_bare::to_vec;
use ng_repo::errors::StorageError;
use ng_repo::kcv_storage::*;
use ng_repo::types::*;
use super::OverlayStorage;
use crate::server_broker::CommitInfo;
use crate::server_broker::EventInfo;
pub struct CommitStorage<'a> {
key: Vec<u8>,
event: ExistentialValue<Either<EventInfo, TopicId>>,
storage: &'a dyn KCVStorage,
}
impl<'a> IModel for CommitStorage<'a> {
fn key(&self) -> &Vec<u8> {
&self.key
}
fn storage(&self) -> &dyn KCVStorage {
self.storage
}
fn class(&self) -> &Class {
&Self::CLASS
}
fn existential(&mut self) -> Option<&mut dyn IExistentialValue> {
Some(&mut self.event)
}
}
impl<'a> CommitStorage<'a> {
const PREFIX: u8 = b'e';
// Topic properties
pub const EVENT: ExistentialValueColumn = ExistentialValueColumn::new(b'e');
pub const HOME_PINNED: SingleValueColumn<Self, bool> = SingleValueColumn::new(b'p');
// Commit -> Acks
pub const ACKS: MultiValueColumn<Self, ObjectId> = MultiValueColumn::new(b'a');
// Commit -> Deps
pub const DEPS: MultiValueColumn<Self, ObjectId> = MultiValueColumn::new(b'd');
// Commit -> Files
pub const FILES: MultiValueColumn<Self, ObjectId> = MultiValueColumn::new(b'f');
// Commit -> Causal future commits
pub const FUTURES: MultiValueColumn<Self, ObjectId> = MultiValueColumn::new(b'c');
pub const CLASS: Class<'a> = Class::new(
"Commit",
Some(Self::PREFIX),
Some(&Self::EVENT),
&[&Self::HOME_PINNED as &dyn ISingleValueColumn],
&[
&Self::ACKS as &dyn IMultiValueColumn,
&Self::DEPS,
&Self::FILES,
&Self::FUTURES,
],
);
pub fn new(id: &ObjectId, overlay: &OverlayId, storage: &'a dyn KCVStorage) -> Self {
let mut key: Vec<u8> = Vec::with_capacity(33 + 33);
key.append(&mut to_vec(overlay).unwrap());
key.append(&mut to_vec(id).unwrap());
CommitStorage {
key,
event: ExistentialValue::<Either<EventInfo, TopicId>>::new(),
storage,
}
}
pub fn load(
id: &ObjectId,
overlay: &OverlayId,
storage: &'a dyn KCVStorage,
) -> Result<CommitInfo, StorageError> {
let mut opening = CommitStorage::new(id, overlay, storage);
let props = opening.load_props()?;
let existential = col(&Self::EVENT, &props)?;
opening.event.set(&existential)?;
Ok(CommitInfo {
event: existential,
home_pinned: col(&Self::HOME_PINNED, &props).unwrap_or(false),
acks: Self::ACKS.get_all(&mut opening)?,
deps: Self::DEPS.get_all(&mut opening)?,
files: Self::FILES.get_all(&mut opening)?,
futures: Self::FUTURES.get_all(&mut opening)?,
})
}
pub fn open(
id: &ObjectId,
overlay: &OverlayId,
storage: &'a dyn KCVStorage,
) -> Result<CommitStorage<'a>, StorageError> {
let mut opening = CommitStorage::new(id, overlay, storage);
opening.check_exists()?;
Ok(opening)
}
pub fn create(
id: &ObjectId,
overlay: &OverlayId,
event: EventInfo,
header: &Option<CommitHeader>,
home_pinned: bool,
storage: &'a dyn KCVStorage,
) -> Result<CommitStorage<'a>, StorageError> {
let mut creating = CommitStorage::new(id, overlay, storage);
if creating.exists() {
return Err(StorageError::AlreadyExists);
}
let event_either = Either::Left(event);
creating.event.set(&event_either)?;
ExistentialValue::save(&creating, &event_either)?;
if home_pinned {
Self::HOME_PINNED.set(&mut creating, &true)?;
}
if let Some(header) = header {
let mut overlay_storage = OverlayStorage::new(overlay, storage);
// adding all the references
for ack in header.acks() {
Self::ACKS.add(&mut creating, &ack)?;
OverlayStorage::OBJECTS.increment(&mut overlay_storage, &ack)?;
}
for dep in header.deps() {
Self::DEPS.add(&mut creating, &dep)?;
OverlayStorage::OBJECTS.increment(&mut overlay_storage, &dep)?;
}
for file in header.files() {
Self::FILES.add(&mut creating, file)?;
OverlayStorage::OBJECTS.increment(&mut overlay_storage, &file)?;
}
}
Ok(creating)
}
pub fn event(&mut self) -> &Either<EventInfo, TopicId> {
self.event.get().unwrap()
}
pub fn take_event(self) -> Either<EventInfo, TopicId> {
self.event.take().unwrap()
}
}

@ -0,0 +1,120 @@
// Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
// All rights reserved.
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
//! Inbox Storage (Object Key/Col/Value Mapping)
use std::collections::HashSet;
use std::hash::{DefaultHasher, Hash, Hasher};
use ng_net::types::InboxMsg;
use ng_repo::utils::now_precise_timestamp;
use serde_bare::to_vec;
use ng_repo::errors::StorageError;
use ng_repo::kcv_storage::*;
use ng_repo::types::*;
pub struct InboxStorage<'a> {
key: Vec<u8>,
storage: &'a dyn KCVStorage,
}
impl<'a> IModel for InboxStorage<'a> {
fn key(&self) -> &Vec<u8> {
&self.key
}
fn storage(&self) -> &dyn KCVStorage {
self.storage
}
fn class(&self) -> &Class {
&Self::CLASS
}
fn existential(&mut self) -> Option<&mut dyn IExistentialValue> {
None
}
}
// seconds, nanosecs, hash of InboxMsgBody
type MsgKeySuffix = (u64, u32, u64);
impl<'a> InboxStorage<'a> {
// Inbox <-> Msg : list of incoming messages that will be delivered once a user is online
pub const MSGS: MultiMapColumn<Self, MsgKeySuffix, InboxMsg> = MultiMapColumn::new(b'm');
// Inbox <-> User : list of users who registered as readers of an inbox
pub const READERS: MultiValueColumn<Self, UserId> = MultiValueColumn::new(b'i');
pub const CLASS: Class<'a> = Class::new(
"Inbox",
None,
None,
&[],
&[&Self::MSGS as &dyn IMultiValueColumn, &Self::READERS],
);
pub fn take_first_msg(
inbox: &PubKey,
overlay: &OverlayId,
storage: &'a dyn KCVStorage,
) -> Result<InboxMsg, StorageError> {
let mut opening = Self::new(inbox, overlay, storage);
Self::MSGS.take_first_value(&mut opening)
}
pub fn load_readers(
inbox: &PubKey,
overlay: &OverlayId,
storage: &'a dyn KCVStorage,
) -> Result<HashSet<UserId>, StorageError> {
let mut opening = Self::new(inbox, overlay, storage);
Self::READERS.get_all(&mut opening)
}
pub fn new(inbox: &PubKey, overlay: &OverlayId, storage: &'a dyn KCVStorage) -> Self {
let mut key: Vec<u8> = Vec::with_capacity(33 + 33);
key.append(&mut to_vec(overlay).unwrap());
key.append(&mut to_vec(inbox).unwrap());
Self { key, storage }
}
pub fn open(
inbox: &PubKey,
overlay: &OverlayId,
storage: &'a dyn KCVStorage,
) -> Result<InboxStorage<'a>, StorageError> {
let opening = Self::new(inbox, overlay, storage);
Ok(opening)
}
pub fn register_reader(
inbox: &PubKey,
overlay: &OverlayId,
reader: &UserId,
storage: &'a dyn KCVStorage,
) -> Result<(), StorageError> {
let mut opening = Self::new(inbox, overlay, storage);
Self::READERS.add(&mut opening, reader)
}
pub fn enqueue_msg(&mut self, msg: &InboxMsg) -> Result<(), StorageError> {
let (sec,nano) = now_precise_timestamp();
let mut hasher = DefaultHasher::new();
msg.body.hash(&mut hasher);
let key = (sec,nano, hasher.finish());
Self::MSGS.add(self, &key,msg)
}
pub fn create(
inbox: &PubKey,
overlay: &OverlayId,
storage: &'a dyn KCVStorage,
) -> Result<InboxStorage<'a>, StorageError> {
let creating = Self::new(inbox, overlay, storage);
Ok(creating)
}
}

@ -0,0 +1,20 @@
pub mod overlay;
pub use overlay::*;
pub mod peer;
pub use peer::*;
pub mod topic;
pub use topic::*;
pub mod repo;
pub use repo::*;
pub mod commit;
pub use commit::*;
pub mod inbox;
pub use inbox::*;
pub mod account;
pub use account::*;

@ -0,0 +1,142 @@
// Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
// All rights reserved.
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
//! Overlay Storage (Object Key/Col/Value Mapping)
use std::collections::HashMap;
use serde_bare::to_vec;
use ng_repo::errors::StorageError;
use ng_repo::kcv_storage::*;
use ng_repo::types::*;
use crate::server_broker::OverlayInfo;
use crate::server_broker::OverlayType;
pub struct OverlayStorage<'a> {
key: Vec<u8>,
overlay_type: ExistentialValue<OverlayType>,
storage: &'a dyn KCVStorage,
}
impl<'a> IModel for OverlayStorage<'a> {
fn key(&self) -> &Vec<u8> {
&self.key
}
fn storage(&self) -> &dyn KCVStorage {
self.storage
}
fn class(&self) -> &Class {
&Self::CLASS
}
fn existential(&mut self) -> Option<&mut dyn IExistentialValue> {
Some(&mut self.overlay_type)
}
}
impl<'a> OverlayStorage<'a> {
const PREFIX: u8 = b'o';
// Overlay properties
pub const TYPE: ExistentialValueColumn = ExistentialValueColumn::new(b'y');
/// BE CAREFUL: this property is exceptionally stored on the InnerOverlay
pub const TOPIC: SingleValueColumn<Self, TopicId> = SingleValueColumn::new(b't');
// Overlay <-> Block refcount
pub const BLOCKS: MultiCounterColumn<Self, BlockId> = MultiCounterColumn::new(b'b');
// Overlay <-> Object refcount
pub const OBJECTS: MultiCounterColumn<Self, ObjectId> = MultiCounterColumn::new(b'j');
pub const CLASS: Class<'a> = Class::new(
"Overlay",
Some(Self::PREFIX),
Some(&Self::TYPE),
&[&Self::TOPIC as &dyn ISingleValueColumn],
&[&Self::BLOCKS as &dyn IMultiValueColumn, &Self::OBJECTS],
);
pub fn new(id: &OverlayId, storage: &'a dyn KCVStorage) -> Self {
OverlayStorage {
key: to_vec(id).unwrap(),
overlay_type: ExistentialValue::<OverlayType>::new(),
storage,
}
}
#[allow(dead_code)]
pub(crate) fn load(
id: &OverlayId,
storage: &'a dyn KCVStorage,
) -> Result<OverlayInfo, StorageError> {
let mut opening = OverlayStorage::new(id, storage);
let props = opening.load_props()?;
let existential = col(&Self::TYPE, &props)?;
opening.overlay_type.set(&existential)?;
let loading = OverlayInfo {
overlay_type: existential,
overlay_topic: col(&Self::TOPIC, &props).ok(),
topics: HashMap::new(),
repos: HashMap::new(),
};
Ok(loading)
}
pub fn open(
id: &OverlayId,
storage: &'a dyn KCVStorage,
) -> Result<OverlayStorage<'a>, StorageError> {
let mut opening = OverlayStorage::new(id, storage);
opening.check_exists()?;
Ok(opening)
}
pub fn create(
id: &OverlayId,
overlay_type: &OverlayType,
expose_outer: bool,
storage: &'a dyn KCVStorage,
) -> Result<OverlayStorage<'a>, StorageError> {
let mut overlay = OverlayStorage::new(id, storage);
if overlay.exists() {
if !expose_outer
&& overlay_type.is_outer_to_inner()
&& overlay.overlay_type().is_outer_only()
{
// we are asked to upgrade an OuterOnly to an Outer().
// let's do it
ExistentialValue::save(&overlay, overlay_type)?;
}
return Err(StorageError::AlreadyExists);
}
overlay.overlay_type.set(overlay_type)?;
ExistentialValue::save(&overlay, overlay_type)?;
if id.is_inner() {
if let Some(outer) = overlay_type.is_inner_get_outer() {
if expose_outer {
match OverlayStorage::create(outer, &OverlayType::Outer(*id), false, storage) {
Err(StorageError::AlreadyExists) => {
//it is ok if the Outer overlay already exists. someone else had pinned it before, in read_only, and the broker had subscribed to it from another broker
// or some other user pinned it before as expose_outer.
}
Err(e) => return Err(e), //TODO: in case of error, remove the existentialvalue that was previously saved (or use a transaction)
Ok(_) => {}
}
}
}
}
Ok(overlay)
}
pub fn overlay_type(&mut self) -> &OverlayType {
self.overlay_type.get().unwrap()
}
}

@ -0,0 +1,187 @@
// Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
// All rights reserved.
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
//! Peer
use serde_bare::{from_slice, to_vec};
use ng_repo::errors::StorageError;
use ng_repo::kcv_storage::KCVStorage;
use ng_repo::types::*;
use ng_net::types::*;
pub struct Peer<'a> {
/// Topic ID
id: PeerId,
storage: &'a dyn KCVStorage,
}
impl<'a> Peer<'a> {
const PREFIX: u8 = b"p"[0];
// propertie's suffixes
const VERSION: u8 = b"v"[0];
const ADVERT: u8 = b"a"[0];
const ALL_PROPERTIES: [u8; 2] = [Self::VERSION, Self::ADVERT];
const SUFFIX_FOR_EXIST_CHECK: u8 = Self::VERSION;
pub fn open(id: &PeerId, storage: &'a dyn KCVStorage) -> Result<Peer<'a>, StorageError> {
let opening = Peer {
id: id.clone(),
storage,
};
if !opening.exists() {
return Err(StorageError::NotFound);
}
Ok(opening)
}
pub fn update_or_create(
advert: &PeerAdvert,
storage: &'a dyn KCVStorage,
) -> Result<Peer<'a>, StorageError> {
let id = advert.peer();
match Self::open(id, storage) {
Err(e) => {
if e == StorageError::NotFound {
Self::create(advert, storage)
} else {
Err(StorageError::BackendError)
}
}
Ok(p) => {
p.update_advert(advert)?;
Ok(p)
}
}
}
pub fn create(
advert: &PeerAdvert,
storage: &'a dyn KCVStorage,
) -> Result<Peer<'a>, StorageError> {
let id = advert.peer();
let acc = Peer {
id: id.clone(),
storage,
};
if acc.exists() {
return Err(StorageError::BackendError);
}
storage.write_transaction(&mut |tx| {
tx.put(
Self::PREFIX,
&to_vec(&id)?,
Some(Self::VERSION),
&to_vec(&advert.version())?,
&None,
)?;
tx.put(
Self::PREFIX,
&to_vec(&id)?,
Some(Self::ADVERT),
&to_vec(&advert)?,
&None,
)?;
Ok(())
})?;
Ok(acc)
}
pub fn exists(&self) -> bool {
self.storage
.get(
Self::PREFIX,
&to_vec(&self.id).unwrap(),
Some(Self::SUFFIX_FOR_EXIST_CHECK),
&None,
)
.is_ok()
}
pub fn id(&self) -> PeerId {
self.id
}
pub fn version(&self) -> Result<u32, StorageError> {
match self
.storage
.get(Self::PREFIX, &to_vec(&self.id)?, Some(Self::VERSION), &None)
{
Ok(ver) => Ok(from_slice::<u32>(&ver)?),
Err(e) => Err(e),
}
}
pub fn set_version(&self, version: u32) -> Result<(), StorageError> {
if !self.exists() {
return Err(StorageError::BackendError);
}
self.storage.replace(
Self::PREFIX,
&to_vec(&self.id)?,
Some(Self::VERSION),
&to_vec(&version)?,
&None,
)
}
pub fn update_advert(&self, advert: &PeerAdvert) -> Result<(), StorageError> {
if advert.peer() != &self.id {
return Err(StorageError::InvalidValue);
}
let current_advert = self.advert().map_err(|_| StorageError::BackendError)?;
if current_advert.version() >= advert.version() {
return Ok(());
}
self.storage.write_transaction(&mut |tx| {
tx.replace(
Self::PREFIX,
&to_vec(&self.id)?,
Some(Self::VERSION),
&to_vec(&advert.version())?,
&None,
)?;
tx.replace(
Self::PREFIX,
&to_vec(&self.id)?,
Some(Self::ADVERT),
&to_vec(&advert)?,
&None,
)?;
Ok(())
})
}
pub fn advert(&self) -> Result<PeerAdvert, StorageError> {
match self
.storage
.get(Self::PREFIX, &to_vec(&self.id)?, Some(Self::ADVERT), &None)
{
Ok(advert) => Ok(from_slice::<PeerAdvert>(&advert)?),
Err(e) => Err(e),
}
}
pub fn set_advert(&self, advert: &PeerAdvert) -> Result<(), StorageError> {
if !self.exists() {
return Err(StorageError::BackendError);
}
self.storage.replace(
Self::PREFIX,
&to_vec(&self.id)?,
Some(Self::ADVERT),
&to_vec(advert)?,
&None,
)
}
pub fn del(&self) -> Result<(), StorageError> {
self.storage.del_all(
Self::PREFIX,
&to_vec(&self.id)?,
&Self::ALL_PROPERTIES,
&None,
)
}
}

@ -0,0 +1,123 @@
// Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
// All rights reserved.
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
//! Repo Storage (Object Key/Col/Value Mapping)
use std::collections::HashSet;
use serde_bare::to_vec;
use ng_repo::errors::StorageError;
use ng_repo::kcv_storage::*;
use ng_repo::types::*;
use crate::server_broker::RepoInfo;
pub struct RepoHashStorage<'a> {
key: Vec<u8>,
storage: &'a dyn KCVStorage,
}
impl<'a> IModel for RepoHashStorage<'a> {
fn key(&self) -> &Vec<u8> {
&self.key
}
fn storage(&self) -> &dyn KCVStorage {
self.storage
}
fn class(&self) -> &Class {
&Self::CLASS
}
fn existential(&mut self) -> Option<&mut dyn IExistentialValue> {
None
}
}
impl<'a> RepoHashStorage<'a> {
// RepoHash <-> Topic : list of topics of a repo that was pinned on the broker
pub const TOPICS: MultiValueColumn<Self, TopicId> = MultiValueColumn::new(b'r');
// RepoHash <-> User : list of users who asked to expose the repo to the outer overlay
pub const EXPOSE_OUTER: MultiValueColumn<Self, UserId> = MultiValueColumn::new(b'x');
pub const CLASS: Class<'a> = Class::new(
"Repo",
None,
None,
&[],
&[&Self::TOPICS as &dyn IMultiValueColumn, &Self::EXPOSE_OUTER],
);
pub fn load(
repo: &RepoHash,
overlay: &OverlayId,
storage: &'a dyn KCVStorage,
) -> Result<RepoInfo, StorageError> {
let mut opening = Self::new(repo, overlay, storage);
let info = RepoInfo {
topics: Self::TOPICS.get_all(&mut opening)?,
expose_outer: Self::EXPOSE_OUTER.get_all(&mut opening)?,
};
Ok(info)
}
pub fn load_topics(
repo: &RepoHash,
overlay: &OverlayId,
storage: &'a dyn KCVStorage,
) -> Result<RepoInfo, StorageError> {
let mut opening = Self::new(repo, overlay, storage);
let info = RepoInfo {
topics: Self::TOPICS.get_all(&mut opening)?,
expose_outer: HashSet::new(),
};
Ok(info)
}
pub fn load_for_user(
user: &UserId,
repo: &RepoHash,
overlay: &OverlayId,
storage: &'a dyn KCVStorage,
) -> Result<RepoInfo, StorageError> {
let mut opening = Self::new(repo, overlay, storage);
let mut expose_outer = HashSet::new();
if let Ok(()) = Self::EXPOSE_OUTER.has(&mut opening, user) {
expose_outer.insert(*user);
}
let info = RepoInfo {
topics: Self::TOPICS.get_all(&mut opening)?,
expose_outer,
};
Ok(info)
}
pub fn new(repo: &RepoHash, overlay: &OverlayId, storage: &'a dyn KCVStorage) -> Self {
let mut key: Vec<u8> = Vec::with_capacity(33 + 33);
key.append(&mut to_vec(overlay).unwrap());
key.append(&mut to_vec(repo).unwrap());
Self { key, storage }
}
pub fn open(
repo: &RepoHash,
overlay: &OverlayId,
storage: &'a dyn KCVStorage,
) -> Result<RepoHashStorage<'a>, StorageError> {
let opening = Self::new(repo, overlay, storage);
Ok(opening)
}
pub fn create(
repo: &RepoHash,
overlay: &OverlayId,
storage: &'a dyn KCVStorage,
) -> Result<RepoHashStorage<'a>, StorageError> {
let creating = Self::new(repo, overlay, storage);
Ok(creating)
}
}

@ -0,0 +1,182 @@
// Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
// All rights reserved.
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
//! Topic Storage (Object Key/Col/Value Mapping)
use std::collections::HashMap;
use std::collections::HashSet;
use serde_bare::to_vec;
use ng_repo::errors::StorageError;
use ng_repo::kcv_storage::*;
use ng_repo::types::*;
use ng_net::types::*;
use crate::server_broker::TopicInfo;
pub struct TopicStorage<'a> {
key: Vec<u8>,
repo: ExistentialValue<RepoHash>,
storage: &'a dyn KCVStorage,
}
impl<'a> IModel for TopicStorage<'a> {
fn key(&self) -> &Vec<u8> {
&self.key
}
fn storage(&self) -> &dyn KCVStorage {
self.storage
}
fn class(&self) -> &Class {
&Self::CLASS
}
fn existential(&mut self) -> Option<&mut dyn IExistentialValue> {
Some(&mut self.repo)
}
// fn name(&self) -> String {
// format_type_of(self)
// }
}
impl<'a> TopicStorage<'a> {
const PREFIX: u8 = b't';
// Topic properties
pub const ADVERT: SingleValueColumn<Self, PublisherAdvert> = SingleValueColumn::new(b'a');
pub const REPO: ExistentialValueColumn = ExistentialValueColumn::new(b'r');
pub const ROOT_COMMIT: SingleValueColumn<Self, ObjectId> = SingleValueColumn::new(b'o');
pub const COMMITS_NBR: CounterValue<Self> = CounterValue::new(b'n');
// Topic <-> Users who pinned it (with boolean: R or W)
pub const USERS: MultiMapColumn<Self, UserId, bool> = MultiMapColumn::new(b'u');
// Topic <-> heads
pub const HEADS: MultiValueColumn<Self, ObjectId> = MultiValueColumn::new(b'h');
pub const CLASS: Class<'a> = Class::new(
"Topic",
Some(Self::PREFIX),
Some(&Self::REPO),
&[
&Self::ADVERT as &dyn ISingleValueColumn,
&Self::ROOT_COMMIT,
&Self::COMMITS_NBR,
],
&[&Self::USERS as &dyn IMultiValueColumn, &Self::HEADS],
);
pub fn new(id: &TopicId, overlay: &OverlayId, storage: &'a dyn KCVStorage) -> Self {
let mut key: Vec<u8> = Vec::with_capacity(33 + 33);
key.append(&mut to_vec(overlay).unwrap());
key.append(&mut to_vec(id).unwrap());
TopicStorage {
key,
repo: ExistentialValue::<RepoHash>::new(),
storage,
}
}
pub fn load(
id: &TopicId,
overlay: &OverlayId,
storage: &'a dyn KCVStorage,
) -> Result<TopicInfo, StorageError> {
let mut opening = TopicStorage::new(id, overlay, storage);
let props = opening.load_props()?;
let existential = col(&Self::REPO, &props)?;
opening.repo.set(&existential)?;
let ti = TopicInfo {
repo: existential,
publisher_advert: col(&Self::ADVERT, &props).ok(),
root_commit: col(&Self::ROOT_COMMIT, &props).ok(),
users: Self::USERS.get_all(&mut opening)?,
current_heads: Self::HEADS.get_all(&mut opening)?,
};
Ok(ti)
}
pub fn open(
id: &TopicId,
overlay: &OverlayId,
storage: &'a dyn KCVStorage,
) -> Result<TopicStorage<'a>, StorageError> {
let mut opening = TopicStorage::new(id, overlay, storage);
opening.check_exists()?;
Ok(opening)
}
pub fn create(
id: &TopicId,
overlay: &OverlayId,
repo: &RepoHash,
storage: &'a dyn KCVStorage,
or_open: bool,
) -> Result<TopicStorage<'a>, StorageError> {
let mut topic = TopicStorage::new(id, overlay, storage);
if topic.exists() {
if or_open {
return Ok(topic);
} else {
return Err(StorageError::AlreadyExists);
}
}
topic.repo.set(repo)?;
ExistentialValue::save(&topic, repo)?;
Ok(topic)
}
pub fn repo_hash(&mut self) -> &RepoHash {
self.repo.get().unwrap()
}
pub fn root_commit(&mut self) -> Result<ObjectId, StorageError> {
Self::ROOT_COMMIT.get(self)
}
pub fn set_root_commit(&mut self, commit: &ObjectId) -> Result<(), StorageError> {
Self::ROOT_COMMIT.set(self, commit)
}
pub fn publisher_advert(&mut self) -> Result<PublisherAdvert, StorageError> {
Self::ADVERT.get(self)
}
pub fn set_publisher_advert(&mut self, advert: &PublisherAdvert) -> Result<(), StorageError> {
Self::ADVERT.set(self, advert)
}
pub fn add_head(&mut self, head: &ObjectId) -> Result<(), StorageError> {
Self::HEADS.add(self, head)
}
pub fn remove_head(&mut self, head: &ObjectId) -> Result<(), StorageError> {
Self::HEADS.remove(self, head)
}
pub fn has_head(&mut self, head: &ObjectId) -> Result<(), StorageError> {
Self::HEADS.has(self, head)
}
pub fn get_all_heads(&mut self) -> Result<HashSet<ObjectId>, StorageError> {
Self::HEADS.get_all(self)
}
pub fn add_user(&mut self, user: &UserId, publisher: bool) -> Result<(), StorageError> {
Self::USERS.add(self, user, &publisher)
}
pub fn remove_user(&mut self, user: &UserId, publisher: bool) -> Result<(), StorageError> {
Self::USERS.remove(self, user, &publisher)
}
pub fn has_user(&mut self, user: &UserId, publisher: bool) -> Result<(), StorageError> {
Self::USERS.has(self, user, &publisher)
}
pub fn get_all_users(&mut self) -> Result<HashMap<UserId, bool>, StorageError> {
Self::USERS.get_all(self)
}
}

@ -0,0 +1,3 @@
pub mod admin;
pub mod core;

@ -0,0 +1,950 @@
/*
* Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
//! WebSocket implementation of the Broker
use std::collections::HashMap;
use std::collections::HashSet;
use std::net::IpAddr;
use std::net::SocketAddr;
use std::path::PathBuf;
use futures::StreamExt;
use ng_async_tungstenite::tungstenite::http::header::REFERER;
use once_cell::sync::OnceCell;
use rust_embed::RustEmbed;
use serde_json::json;
use urlencoding::decode;
use async_std::net::{TcpListener, TcpStream};
use ng_async_tungstenite::accept_hdr_async;
use ng_async_tungstenite::tungstenite::handshake::server::{
Callback, ErrorResponse, Request, Response,
};
use ng_async_tungstenite::tungstenite::http::{
header::{CONNECTION, HOST, ORIGIN},
HeaderValue, Method, StatusCode, Uri, Version,
};
use ng_repo::errors::NgError;
use ng_repo::log::*;
use ng_repo::types::{PrivKey, PubKey, SymKey};
use ng_net::broker::*;
use ng_net::connection::IAccept;
use ng_net::types::*;
use ng_net::utils::{is_private_ip, is_public_ip};
use ng_net::NG_BOOTSTRAP_LOCAL_PATH;
use ng_client_ws::remote_ws::ConnectionWebSocket;
use crate::interfaces::*;
use crate::rocksdb_server_storage::RocksDbServerStorage;
use crate::server_broker::ServerBroker;
use crate::types::*;
static LISTENERS_INFO: OnceCell<(HashMap<String, ListenerInfo>, HashMap<BindAddress, String>)> =
OnceCell::new();
static BOOTSTRAP_STRING: OnceCell<String> = OnceCell::new();
struct SecurityCallback {
remote_bind_address: BindAddress,
local_bind_address: BindAddress,
}
impl SecurityCallback {
fn new(remote_bind_address: BindAddress, local_bind_address: BindAddress) -> Self {
Self {
remote_bind_address,
local_bind_address,
}
}
}
fn make_error(code: StatusCode) -> ErrorResponse {
Response::builder().status(code).body(None).unwrap()
}
fn check_no_origin(origin: Option<&HeaderValue>) -> Result<(), ErrorResponse> {
match origin {
Some(_) => Err(make_error(StatusCode::FORBIDDEN)),
None => Ok(()),
}
}
fn check_origin_is_url(
origin: Option<&HeaderValue>,
domains: &Vec<String>,
) -> Result<(), ErrorResponse> {
match origin {
None => Ok(()),
Some(val) => {
for domain in domains {
if val.to_str().unwrap().starts_with(domain.as_str()) {
return Ok(());
}
}
Err(make_error(StatusCode::FORBIDDEN))
}
}
}
fn check_xff_is_public_or_private(
xff: Option<&HeaderValue>,
none_is_ok: bool,
public: bool,
) -> Result<(), ErrorResponse> {
match xff {
None => {
if none_is_ok {
Ok(())
} else {
Err(make_error(StatusCode::FORBIDDEN))
}
}
Some(val) => {
let mut ip_str = val
.to_str()
.map_err(|_| make_error(StatusCode::FORBIDDEN))?;
if ip_str.starts_with("::ffff:") {
ip_str = ip_str.strip_prefix("::ffff:").unwrap();
}
let ip: IpAddr = ip_str
.parse()
.map_err(|_| make_error(StatusCode::FORBIDDEN))?;
if public && !is_public_ip(&ip) || !public && !is_private_ip(&ip) {
Err(make_error(StatusCode::FORBIDDEN))
} else {
Ok(())
}
}
}
}
fn check_no_xff(xff: Option<&HeaderValue>) -> Result<(), ErrorResponse> {
match xff {
None => Ok(()),
Some(_) => Err(make_error(StatusCode::FORBIDDEN)),
}
}
fn check_host(host: Option<&HeaderValue>, hosts: Vec<String>) -> Result<(), ErrorResponse> {
match host {
None => Err(make_error(StatusCode::FORBIDDEN)),
Some(val) => {
for hos in hosts {
if val.to_str().unwrap().starts_with(&hos) {
return Ok(());
}
}
Err(make_error(StatusCode::FORBIDDEN))
}
}
}
fn check_host_in_addrs(
host: Option<&HeaderValue>,
addrs: &Vec<BindAddress>,
) -> Result<(), ErrorResponse> {
match host {
None => Err(make_error(StatusCode::FORBIDDEN)),
Some(val) => {
for ba in addrs {
if val.to_str().unwrap().starts_with(&ba.ip.to_string()) {
return Ok(());
}
}
Err(make_error(StatusCode::FORBIDDEN))
}
}
}
fn prepare_domain_url_and_host(
accept_forward_for: &AcceptForwardForV0,
) -> (Vec<String>, Vec<String>) {
let domain_str = accept_forward_for.get_domain();
let url = ["https://", domain_str].concat();
let hosts_str = vec![domain_str.to_string()];
let urls_str = vec![url];
(hosts_str, urls_str)
}
fn prepare_urls_from_private_addrs(addrs: &Vec<BindAddress>, port: u16) -> Vec<String> {
let port_str = if port != 80 {
[":", &port.to_string()].concat()
} else {
"".to_string()
};
let mut res: Vec<String> = vec![];
for addr in addrs {
let url = ["http://", &addr.ip.to_string(), &port_str].concat();
res.push(url);
}
res
}
#[derive(RustEmbed)]
#[folder = "../ng-app/dist-file/"]
#[include = "*.sha256"]
#[include = "*.gzip"]
struct App;
#[derive(RustEmbed)]
#[folder = "../helpers/app-auth/dist/"]
#[include = "*.sha256"]
#[include = "*.gzip"]
struct AppAuth;
// #[derive(RustEmbed)]
// #[folder = "./static/app/"]
// #[include = "*.sha256"]
// #[include = "*.gzip"]
// struct App;
// #[derive(RustEmbed)]
// #[folder = "./static/app-auth/"]
// #[include = "*.sha256"]
// #[include = "*.gzip"]
// struct AppAuth;
#[derive(RustEmbed)]
#[folder = "src/public/"]
struct AppPublic;
static ROBOTS: &str = "User-agent: *\r\nDisallow: /";
fn upgrade_ws_or_serve_app(
connection: Option<&HeaderValue>,
remote: IP,
serve_app: bool,
uri: &Uri,
last_etag: Option<&HeaderValue>,
cors: Option<&str>,
referer: Option<&HeaderValue>,
) -> Result<(), ErrorResponse> {
if connection.is_some()
&& connection
.unwrap()
.to_str()
.unwrap()
.split(|c| c == ' ' || c == ',')
.any(|p| p.eq_ignore_ascii_case("Upgrade"))
{
return Ok(());
}
if serve_app && (remote.is_private() || remote.is_loopback()) {
if uri == "/" {
log_debug!("Serving the app");
let sha_file = App::get("index.sha256").unwrap();
let sha = format!(
"\"{}\"",
std::str::from_utf8(sha_file.data.as_ref()).unwrap()
);
if last_etag.is_some() && last_etag.unwrap().to_str().unwrap() == sha {
// return 304
let res = Response::builder()
.status(StatusCode::NOT_MODIFIED)
.header("Cache-Control", "max-age=31536000, must-revalidate")
.header("ETag", sha)
.body(None)
.unwrap();
return Err(res);
}
let file = App::get("index.gzip").unwrap();
let res = Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/html")
.header("Cache-Control", "max-age=31536000, must-revalidate")
.header("Content-Encoding", "gzip")
.header("ETag", sha)
.body(Some(file.data.to_vec()))
.unwrap();
return Err(res);
} else if uri.path() == "/auth/" {
log_debug!("Serving auth app");
// if referer.is_none() || referer.unwrap().to_str().is_err() || referer.unwrap().to_str().unwrap() != "https://nextgraph.net/" {
// return Err(make_error(StatusCode::FORBIDDEN));
// }
let webapp_origin = match uri.query() {
Some(query) => {
if query.starts_with("o=") {
match decode(&query.chars().skip(2).collect::<String>()) {
Err(_) => return Err(make_error(StatusCode::BAD_REQUEST)),
Ok(cow) => {
cow.into_owned()
}
}
} else {
return Err(make_error(StatusCode::BAD_REQUEST))
}
},
None => {return Err(make_error(StatusCode::BAD_REQUEST))}
};
let sha_file = AppAuth::get("index.sha256").unwrap();
let sha = format!(
"\"{}\"",
std::str::from_utf8(sha_file.data.as_ref()).unwrap()
);
if last_etag.is_some() && last_etag.unwrap().to_str().unwrap() == sha {
// return 304
let res = Response::builder()
.status(StatusCode::NOT_MODIFIED)
.header("Cache-Control", "max-age=31536000, must-revalidate")
.header("ETag", sha)
.header("Content-Security-Policy", format!("frame-ancestors 'self' https://nextgraph.net {webapp_origin};"))
.header("X-Frame-Options", format!("ALLOW-FROM {webapp_origin}"))
.body(None)
.unwrap();
return Err(res);
}
let file = AppAuth::get("index.gzip").unwrap();
let res = Response::builder().status(StatusCode::OK)
.header("Content-Security-Policy", format!("frame-ancestors 'self' https://nextgraph.net {webapp_origin};"))
.header("X-Frame-Options", format!("ALLOW-FROM {webapp_origin}"))
.header("Content-Type", "text/html")
.header("Cache-Control", "max-age=31536000, must-revalidate")
.header("Content-Encoding", "gzip")
.header("ETag", sha)
.body(Some(file.data.to_vec()))
.unwrap();
return Err(res);
} else if uri == NG_BOOTSTRAP_LOCAL_PATH {
log_debug!("Serving bootstrap");
let mut builder = Response::builder().status(StatusCode::OK);
if cors.is_some() {
builder = builder.header("Access-Control-Allow-Origin", cors.unwrap());
}
let res = builder
.header("Content-Type", "text/json")
.header("Cache-Control", "max-age=0, must-revalidate")
.body(Some(BOOTSTRAP_STRING.get().unwrap().as_bytes().to_vec()))
.unwrap();
return Err(res);
} else if uri == "/favicon.ico" {
let file = AppPublic::get("favicon.ico").unwrap();
let res = Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "image/x-icon")
.header("Cache-Control", "max-age=432000, must-revalidate")
.body(Some(file.data.to_vec()))
.unwrap();
return Err(res);
} else if uri == "/robots.txt" {
let res = Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/plain")
.header("Cache-Control", "max-age=3600, must-revalidate")
.body(Some(ROBOTS.as_bytes().to_vec()))
.unwrap();
return Err(res);
}
}
Err(make_error(StatusCode::FORBIDDEN))
}
impl Callback for SecurityCallback {
fn on_request(self, request: &Request) -> Result<(), ErrorResponse> {
let local_urls = LOCAL_URLS
.to_vec()
.iter()
.map(ToString::to_string)
.collect();
let local_hosts = LOCAL_HOSTS
.to_vec()
.iter()
.map(ToString::to_string)
.collect();
let (listeners, bind_addresses) = LISTENERS_INFO.get().ok_or(
Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(None)
.unwrap(),
)?;
// check that the remote address is allowed to connect on the listener
let listener_id = bind_addresses
.get(&self.local_bind_address)
.ok_or(make_error(StatusCode::FORBIDDEN))?;
let listener = listeners
.get(listener_id)
.ok_or(make_error(StatusCode::FORBIDDEN))?;
if request.method() != Method::GET {
return Err(make_error(StatusCode::METHOD_NOT_ALLOWED));
}
if request.version() != Version::HTTP_11 {
return Err(make_error(StatusCode::HTTP_VERSION_NOT_SUPPORTED));
}
let xff = request.headers().get("X-Forwarded-For");
let connection = request.headers().get(CONNECTION);
let host = request.headers().get(HOST);
let origin = request.headers().get(ORIGIN);
let referer = request.headers().get(REFERER);
let remote = self.remote_bind_address.ip;
let last_etag = request.headers().get("If-None-Match");
let uri = request.uri();
log_debug!(
"connection:{:?} origin:{:?} host:{:?} xff:{:?} remote:{:?} local:{:?} uri:{:?}",
connection,
origin,
host,
xff,
remote,
self.local_bind_address,
uri
);
match listener.config.if_type {
InterfaceType::Public => {
if !remote.is_public() {
return Err(make_error(StatusCode::FORBIDDEN));
}
check_no_xff(xff)?;
check_no_origin(origin)?;
// let mut urls_str = vec![];
// if !listener.config.refuse_clients {
// urls_str.push(NG_APP_URL.to_string());
// }
// check_origin_is_url(origin, urls_str)?;
check_host_in_addrs(host, &listener.addrs)?;
log_debug!(
"accepted core with refuse_clients {}",
listener.config.refuse_clients
);
return upgrade_ws_or_serve_app(
connection,
remote,
listener.config.serve_app && !listener.config.refuse_clients,
uri,
last_etag,
None,
referer
);
}
InterfaceType::Loopback => {
if !remote.is_loopback() {
return Err(make_error(StatusCode::FORBIDDEN));
}
if listener.config.accept_forward_for.is_public_domain() {
let (mut hosts_str, mut urls_str) =
prepare_domain_url_and_host(&listener.config.accept_forward_for);
if listener.config.accept_direct {
hosts_str = [hosts_str, local_hosts].concat();
// TODO local_urls might need a trailing :port, but it is ok for now as we do starts_with
urls_str = [urls_str, local_urls].concat();
}
check_origin_is_url(origin, &urls_str)?;
check_host(host, hosts_str)?;
check_xff_is_public_or_private(xff, listener.config.accept_direct, true)?;
log_debug!(
"accepted loopback PUBLIC_DOMAIN with direct {}",
listener.config.accept_direct
);
return upgrade_ws_or_serve_app(
connection,
remote,
listener.config.serve_app,
uri,
last_etag,
origin.map(|or| or.to_str().unwrap()).and_then(|val| {
if listener.config.refuse_clients {
None
} else {
Some(val)
}
}),
referer
);
} else if listener.config.accept_forward_for.is_private_domain() {
let (hosts_str, urls_str) =
prepare_domain_url_and_host(&listener.config.accept_forward_for);
check_origin_is_url(origin, &urls_str)?;
check_host(host, hosts_str)?;
check_xff_is_public_or_private(xff, false, false)?;
log_debug!("accepted loopback PRIVATE_DOMAIN");
return upgrade_ws_or_serve_app(
connection,
remote,
listener.config.serve_app,
uri,
last_etag,
origin.map(|or| or.to_str().unwrap()),
referer
);
} else if listener.config.accept_forward_for == AcceptForwardForV0::No {
check_host(host, local_hosts)?;
check_no_xff(xff)?;
// TODO local_urls might need a trailing :port, but it is ok for now as we do starts_with
check_origin_is_url(origin, &local_urls)?;
log_debug!("accepted loopback DIRECT");
return upgrade_ws_or_serve_app(
connection,
remote,
listener.config.serve_app,
uri,
last_etag,
origin.map(|or| or.to_str().unwrap()),
referer
);
}
}
InterfaceType::Private => {
if listener.config.accept_forward_for.is_public_static()
|| listener.config.accept_forward_for.is_public_dyn()
{
if !listener.config.accept_direct && !remote.is_public()
|| listener.config.accept_direct
&& !remote.is_private()
&& !remote.is_public()
{
return Err(make_error(StatusCode::FORBIDDEN));
}
check_no_xff(xff)?;
let mut addrs = listener
.config
.accept_forward_for
.get_public_bind_addresses();
let mut urls_str = vec![];
// if !listener.config.refuse_clients {
// urls_str.push(NG_APP_URL.to_string());
// }
if listener.config.accept_direct {
addrs.extend(&listener.addrs);
urls_str = [
urls_str,
prepare_urls_from_private_addrs(&listener.addrs, listener.config.port),
]
.concat();
}
check_origin_is_url(origin, &urls_str)?;
check_host_in_addrs(host, &addrs)?;
log_debug!("accepted private PUBLIC_STATIC or PUBLIC_DYN with direct {} with refuse_clients {}",listener.config.accept_direct, listener.config.refuse_clients);
return upgrade_ws_or_serve_app(
connection,
remote,
listener.config.serve_app,
uri,
last_etag,
origin.map(|or| or.to_str().unwrap()),
referer
);
} else if listener.config.accept_forward_for.is_public_domain() {
if !remote.is_private() {
return Err(make_error(StatusCode::FORBIDDEN));
}
check_xff_is_public_or_private(xff, listener.config.accept_direct, true)?;
let (mut hosts_str, mut urls_str) =
prepare_domain_url_and_host(&listener.config.accept_forward_for);
if listener.config.accept_direct {
for addr in listener.addrs.iter() {
let str = addr.ip.to_string();
hosts_str.push(str);
}
urls_str = [
urls_str,
prepare_urls_from_private_addrs(&listener.addrs, listener.config.port),
]
.concat();
}
check_origin_is_url(origin, &urls_str)?;
check_host(host, hosts_str)?;
log_debug!(
"accepted private PUBLIC_DOMAIN with direct {}",
listener.config.accept_direct
);
return upgrade_ws_or_serve_app(
connection,
remote,
listener.config.serve_app,
uri,
last_etag,
origin.map(|or| or.to_str().unwrap()).and_then(|val| {
if listener.config.refuse_clients {
None
} else {
Some(val)
}
}),
referer
);
} else if listener.config.accept_forward_for == AcceptForwardForV0::No {
if !remote.is_private() {
return Err(make_error(StatusCode::FORBIDDEN));
}
check_no_xff(xff)?;
check_host_in_addrs(host, &listener.addrs)?;
let urls_str =
prepare_urls_from_private_addrs(&listener.addrs, listener.config.port);
check_origin_is_url(origin, &urls_str)?;
log_debug!("accepted private DIRECT");
return upgrade_ws_or_serve_app(
connection,
remote,
listener.config.serve_app,
uri,
last_etag,
origin.map(|or| or.to_str().unwrap()),
referer
);
}
}
_ => {}
}
Err(make_error(StatusCode::FORBIDDEN))
}
}
pub async fn accept(tcp: TcpStream, peer_priv_key: PrivKey) {
let remote_addr = tcp.peer_addr().unwrap();
let remote_bind_address: BindAddress = (&remote_addr).into();
let local_addr = tcp.local_addr().unwrap();
let local_bind_address: BindAddress = (&local_addr).into();
let ws = accept_hdr_async(
tcp,
SecurityCallback::new(remote_bind_address, local_bind_address),
)
.await;
if ws.is_err() {
log_debug!("websocket rejected");
return;
}
log_debug!("websocket accepted");
let cws = ConnectionWebSocket {};
let base = cws
.accept(
remote_bind_address,
local_bind_address,
peer_priv_key,
ws.unwrap(),
)
.await
.unwrap();
let res = BROKER
.write()
.await
.accept(base, remote_bind_address, local_bind_address)
.await;
if res.is_err() {
log_warn!("Accept error: {:?}", res.unwrap_err());
}
}
#[cfg(test)]
pub async fn run_server_accept_one(
addr: &str,
port: u16,
peer_priv_key: PrivKey,
_peer_pub_key: PubKey,
) -> std::io::Result<()> {
let addrs = format!("{}:{}", addr, port);
let _root = tempfile::Builder::new().prefix("ngd").tempdir().unwrap();
// let master_key: [u8; 32] = [0; 32];
// std::fs::create_dir_all(root.path()).unwrap();
// log_debug!("data directory: {}", root.path().to_str().unwrap());
// let store = RocksDbKCVStorage::open(root.path(), master_key);
let socket = TcpListener::bind(addrs.as_str()).await?;
log_debug!("Listening on {}", addrs.as_str());
let mut connections = socket.incoming();
let tcp = connections.next().await.unwrap()?;
{
//BROKER.write().await.set_my_peer_id(peer_pub_key);
}
accept(tcp, peer_priv_key).await;
Ok(())
}
pub async fn run_server_v0(
peer_priv_key: PrivKey,
peer_id: PubKey,
wallet_master_key: SymKey,
config: DaemonConfigV0,
mut path: PathBuf,
admin_invite: bool,
) -> Result<(), NgError> {
// check config
let mut run_core = false;
let mut run_server = false;
for overlay_conf in config.overlays_configs.iter() {
if overlay_conf.core != BrokerOverlayPermission::Nobody {
run_core = true;
}
if overlay_conf.server != BrokerOverlayPermission::Nobody {
run_server = true;
}
}
if !run_core && !run_server {
return Err(NgError::BrokerConfigErrorStr(
"There isn't any overlay_config that should run as core or server. Check your config.",
));
}
if run_core && !run_server {
log_warn!("There isn't any overlay_config that should run as server. This is a misconfiguration as a core server that cannot receive client connections is useless");
}
let mut listeners: HashSet<String> = HashSet::new();
for listener in &config.listeners {
let id: String = listener.to_string();
if !listeners.insert(id.clone()) {
return Err(NgError::BrokerConfigError(format!(
"The listener {} is defined twice. Check your config file.",
id
)));
}
}
let interfaces = get_interface();
log_debug!("interfaces {:?}", interfaces);
let mut listener_infos: HashMap<String, ListenerInfo> = HashMap::new();
let mut listeners_addrs: Vec<(Vec<SocketAddr>, String)> = vec![];
let mut listeners: Vec<TcpListener> = vec![];
let mut accept_clients = false;
//let mut serve_app = false;
// TODO: check that there is only one PublicDyn or one PublicStatic or one Core
let mut servers: Vec<BrokerServerV0> = vec![];
let registration_url = config.registration_url;
// Preparing the listeners addrs and infos
for listener in config.listeners {
if !listener.accept_direct && listener.accept_forward_for == AcceptForwardForV0::No {
log_warn!(
"The interface {} does not accept direct connections nor is configured to forward. it is therefor disabled",
listener.interface_name
);
continue;
}
match find_name(&interfaces, &listener.interface_name) {
None => {
return Err(NgError::BrokerConfigError(format!(
"The interface {} does not exist on your host. Check your config file.",
listener.interface_name
)));
}
Some(interface) => {
let mut addrs: Vec<SocketAddr> = interface
.ipv4
.iter()
.filter_map(|ip| {
if interface.if_type.is_ipv4_valid_for_type(&ip.addr) {
Some(SocketAddr::new(IpAddr::V4(ip.addr), listener.port))
} else {
None
}
})
.collect();
if addrs.is_empty() {
return Err(NgError::BrokerConfigError(format!(
"The interface {} does not have any IPv4 address.",
listener.interface_name
)));
}
if listener.ipv6 {
let mut ipv6s: Vec<SocketAddr> = interface
.ipv6
.iter()
.filter_map(|ip| {
if interface.if_type.is_ipv6_valid_for_type(&ip.addr)
|| listener.should_bind_public_ipv6_to_private_interface(ip.addr)
{
Some(SocketAddr::new(IpAddr::V6(ip.addr), listener.port))
} else {
None
}
})
.collect();
addrs.append(&mut ipv6s);
}
if !listener.refuse_clients {
accept_clients = true;
}
if listener.refuse_clients && listener.accept_forward_for.is_public_domain() {
log_warn!(
"You have disabled accepting connections from clients on {}. This is unusual as --domain and --domain-private listeners are meant to answer to clients only. This will activate the relay_websocket on this listener. Is it really intended?",
listener.interface_name
);
}
// if listener.serve_app {
// serve_app = true;
// }
let bind_addresses: Vec<BindAddress> =
addrs.iter().map(|addr| addr.into()).collect();
let server_types = listener.get_bootstraps(bind_addresses.clone());
let common_peer_id = listener.accept_forward_for.domain_with_common_peer_id();
for server_type in server_types {
servers.push(BrokerServerV0 {
peer_id: common_peer_id.unwrap_or(peer_id),
can_verify: false,
can_forward: !run_core,
server_type,
})
}
let listener_id: String = listener.to_string();
let listener_info = ListenerInfo {
config: listener,
addrs: bind_addresses,
};
listener_infos.insert(listener_id, listener_info);
listeners_addrs.push((addrs, interface.name));
}
}
}
if listeners_addrs.is_empty() {
return Err(NgError::BrokerConfigErrorStr("No listener configured."));
}
if !accept_clients {
log_warn!("There isn't any listener that accept clients. This is a misconfiguration as a core server that cannot receive client connections is useless");
}
let bootstrap_v0 = BootstrapContentV0 { servers };
let local_bootstrap_info = LocalBootstrapInfo::V0(LocalBootstrapInfoV0 {
bootstrap: bootstrap_v0.clone(),
registration_url: registration_url.clone(),
});
BOOTSTRAP_STRING
.set(json!(local_bootstrap_info).to_string())
.unwrap();
// saving the infos in the broker. This needs to happen before we start listening, as new incoming connections can happen anytime after that.
// and we need those infos for permission checking.
{
//let root = tempfile::Builder::new().prefix("ngd").tempdir().unwrap();
let mut path_users = path.clone();
path_users.push("users");
path.push("storage");
std::fs::create_dir_all(path.clone()).unwrap();
std::fs::create_dir_all(path_users.clone()).unwrap();
// opening the server storage (that contains the encryption keys for each store/overlay )
let server_storage = RocksDbServerStorage::open(
&mut path,
wallet_master_key.clone(),
if admin_invite {
Some(bootstrap_v0.clone())
} else {
None
},
)
.map_err(|e| {
NgError::BrokerConfigError(format!("Error while opening server storage: {}", e))
})?;
let server_broker = ServerBroker::new(
server_storage,
path_users,
if admin_invite {
Some(wallet_master_key)
} else {
None
},
);
let mut broker = BROKER.write().await;
broker.set_server_broker(server_broker);
LISTENERS_INFO
.set(broker.set_listeners(listener_infos))
.unwrap();
let server_config = ServerConfig {
overlays_configs: config.overlays_configs,
registration: config.registration,
admin_user: config.admin_user,
registration_url,
peer_id,
bootstrap: BootstrapContent::V0(bootstrap_v0),
};
broker.set_server_config(server_config);
}
// Actually starting the listeners
for addrs in listeners_addrs {
let addrs_string = addrs
.0
.iter()
.map(SocketAddr::to_string)
.collect::<Vec<String>>()
.join(", ");
for addr in addrs.0 {
let tcp_listener = TcpListener::bind(addr).await.map_err(|e| {
NgError::BrokerConfigError(format!(
"cannot bind to {} with addresses {} : {}",
addrs.1,
addrs_string,
e.to_string()
))
})?;
listeners.push(tcp_listener);
}
log_info!("Listening on {} {}", addrs.1, addrs_string);
}
// select on all listeners
let mut incoming = futures::stream::select_all(
listeners
.into_iter()
.map(TcpListener::into_incoming)
.map(Box::pin),
);
// Iterate over all incoming connections
// TODO : select on the shutdown stream too
while let Some(tcp) = incoming.next().await {
// TODO select peer_priv_ket according to config. if --domain-peer present and the connection is for that listener (PublicDomainPeer) then use the peer configured there
let key = peer_priv_key.clone();
async_std::task::spawn(async move {
accept(tcp.unwrap(), key).await;
});
}
Ok(())
}

@ -0,0 +1,35 @@
// Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
// All rights reserved.
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
use serde::{Deserialize, Serialize};
use ng_repo::types::PubKey;
use ng_net::types::{BrokerOverlayConfigV0, ListenerV0, RegistrationConfig};
/// DaemonConfig Version 0
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DaemonConfigV0 {
/// List of listeners for TCP (HTTP) incoming connections
pub listeners: Vec<ListenerV0>,
pub overlays_configs: Vec<BrokerOverlayConfigV0>,
pub registration: RegistrationConfig,
pub admin_user: Option<PubKey>,
pub registration_url: Option<String>,
}
/// Daemon config
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum DaemonConfig {
V0(DaemonConfigV0),
}

@ -0,0 +1,31 @@
// Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
// All rights reserved.
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
// use ng_repo::log::*;
// pub fn gen_broker_keys(key: Option<[u8; 32]>) -> [[u8; 32]; 4] {
// let key = match key {
// None => {
// let mut master_key = [0u8; 32];
// log_warn!("gen_broker_keys: No key provided, generating one");
// getrandom::fill(&mut master_key).expect("getrandom failed");
// master_key
// }
// Some(k) => k,
// };
// let peerid: [u8; 32];
// let wallet: [u8; 32];
// let sig: [u8; 32];
// peerid = blake3::derive_key("NextGraph Broker BLAKE3 key PeerId privkey", &key);
// wallet = blake3::derive_key("NextGraph Broker BLAKE3 key wallet encryption", &key);
// sig = blake3::derive_key("NextGraph Broker BLAKE3 key config signature", &key);
// [key, peerid, wallet, sig]
// }

@ -0,0 +1,38 @@
[package]
name = "ng-client-ws"
# version = "0.1.0"
description = "Websocket client library of NextGraph, a decentralized, secure and local-first web 3.0 ecosystem based on Semantic Web and CRDTs"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
homepage.workspace = true
keywords = ["crdt","e2ee","local-first","p2p","web3"]
documentation.workspace = true
rust-version.workspace = true
[dependencies]
serde_bare = "0.5.0"
either = "1.8.1"
futures = "0.3.24"
async-trait = "0.1.64"
async-std = { version = "1.12.0", features = ["attributes","unstable"] }
ng-repo = { path = "../ng-repo", version = "0.1.2" }
ng-net = { path = "../ng-net", version = "0.1.2" }
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen = "0.2.88"
ws_stream_wasm = "0.7"
pharos = "0.5"
[dev-dependencies]
wasm-bindgen-test = "^0.3"
[target.'cfg(target_arch = "wasm32")'.dependencies.getrandom]
version = "0.3.3"
features = ["wasm_js"]
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
getrandom = "0.3.3"
ng-async-tungstenite = { version = "0.22.2", git = "https://git.nextgraph.org/NextGraph/async-tungstenite.git", branch = "nextgraph", features = ["async-std-runtime"] }

@ -0,0 +1,56 @@
# ng-client-ws
![MSRV][rustc-image]
[![Apache 2.0 Licensed][license-image]][license-link]
[![MIT Licensed][license-image2]][license-link2]
Websocket client library of NextGraph
This repository is in active development at [https://git.nextgraph.org/NextGraph/nextgraph-rs](https://git.nextgraph.org/NextGraph/nextgraph-rs), a Gitea instance. For bug reports, issues, merge requests, and in order to join the dev team, please visit the link above and create an account (you can do so with a github account). The [github repo](https://github.com/nextgraph-org/nextgraph-rs) is just a read-only mirror that does not accept issues.
## NextGraph
> NextGraph brings about the convergence of P2P and Semantic Web technologies, towards a decentralized, secure and privacy-preserving cloud, based on CRDTs.
>
> This open source ecosystem provides solutions for end-users (a platform) and software developers (a framework), wishing to use or create **decentralized** apps featuring: **live collaboration** on rich-text documents, peer to peer communication with **end-to-end encryption**, offline-first, **local-first**, portable and interoperable data, total ownership of data and software, security and privacy. Centered on repositories containing **semantic data** (RDF), **rich text**, and structured data formats like **JSON**, synced between peers belonging to permissioned groups of users, it offers strong eventual consistency, thanks to the use of **CRDTs**. Documents can be linked together, signed, shared securely, queried using the **SPARQL** language and organized into sites and containers.
>
> More info here [https://nextgraph.org](https://nextgraph.org)
## Support
Documentation can be found here [https://docs.nextgraph.org](https://docs.nextgraph.org)
And our community forum where you can ask questions is here [https://forum.nextgraph.org](https://forum.nextgraph.org)
## How to use the library
NextGraph is not ready yet. You can subscribe to [our newsletter](https://list.nextgraph.org/subscription/form) to get updates, and support us with a [donation](https://nextgraph.org/donate/).
This library is used internally by [ngcli](../ngcli/README.md), [ng-app](../ng-app/README.md) and by [nextgraph, the Rust client library](../nextgraph/README.md) which you should be using instead. It is not meant to be used by other programs as-is.
## License
Licensed under either of
- Apache License, Version 2.0 ([LICENSE-APACHE2](LICENSE-APACHE2) or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
at your option.
`SPDX-License-Identifier: Apache-2.0 OR MIT`
### Contributions license
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in the work by you shall be dual licensed as below, without any
additional terms or conditions.
---
NextGraph received funding through the [NGI Assure Fund](https://nlnet.nl/assure) and the [NGI Zero Commons Fund](https://nlnet.nl/commonsfund/), both funds established by [NLnet](https://nlnet.nl/) Foundation with financial support from the European Commission's [Next Generation Internet](https://ngi.eu/) programme, under the aegis of DG Communications Networks, Content and Technology under grant agreements No 957073 and No 101092990, respectively.
[rustc-image]: https://img.shields.io/badge/rustc-1.81+-blue.svg
[license-image]: https://img.shields.io/badge/license-Apache2.0-blue.svg
[license-link]: https://git.nextgraph.org/NextGraph/nextgraph-rs/raw/branch/master/LICENSE-APACHE2
[license-image2]: https://img.shields.io/badge/license-MIT-blue.svg
[license-link2]: https://git.nextgraph.org/NextGraph/nextgraph-rs/src/branch/master/LICENSE-MIT

@ -0,0 +1,13 @@
// All rights reserved.
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
#[cfg(not(target_arch = "wasm32"))]
pub mod remote_ws;
#[cfg(target_arch = "wasm32")]
pub mod remote_ws_wasm;

@ -0,0 +1,394 @@
/*
* Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
//! WebSocket Remote Connection to a Broker
use async_std::task;
use either::Either;
use futures::{pin_mut, select, StreamExt};
use futures::{FutureExt, SinkExt};
use ng_async_tungstenite::{
async_std::{connect_async, ConnectStream},
tungstenite::{protocol::frame::coding::CloseCode, protocol::CloseFrame, Message},
WebSocketStream,
};
use ng_repo::errors::*;
use ng_repo::log::*;
use ng_repo::types::*;
use ng_net::connection::*;
use ng_net::types::*;
use ng_net::utils::{Receiver, Sender};
pub struct ConnectionWebSocket {}
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl IConnect for ConnectionWebSocket {
async fn open(
&self,
url: String,
peer_privk: PrivKey,
_peer_pubk: PubKey,
remote_peer: DirectPeerId,
config: StartConfig,
) -> Result<ConnectionBase, ProtocolError> {
let mut cnx = ConnectionBase::new(ConnectionDir::Client, TransportProtocol::WS);
let res = connect_async(url).await;
match res {
Err(_e) => {
log_debug!("Cannot connect: {:?}", _e);
Err(ProtocolError::ConnectionError)
}
Ok((websocket, _)) => {
cnx.start_read_loop(None, Some(peer_privk), Some(remote_peer));
let s = cnx.take_sender();
let r = cnx.take_receiver();
let mut shutdown = cnx.set_shutdown();
cnx.release_shutdown();
let _join = task::spawn(async move {
log_debug!("START of WS loop");
let res = ws_loop(websocket, s, r).await;
if res.is_err() {
let _ = shutdown.send(Either::Left(res.err().unwrap())).await;
} else {
let _ = shutdown.send(Either::Left(NetError::Closing)).await;
}
log_debug!("END of WS loop");
});
cnx.start(config).await?;
Ok(cnx)
}
}
}
async fn probe(&self, ip: IP, port: u16) -> Result<Option<PubKey>, ProtocolError> {
let mut cnx = ConnectionBase::new(ConnectionDir::Client, TransportProtocol::WS);
let url = format!("ws://{}:{}", ip, port);
let res = connect_async(url).await;
match res {
Err(_e) => {
log_debug!("Cannot connect: {:?}", _e);
Err(ProtocolError::ConnectionError)
}
Ok((websocket, _)) => {
cnx.start_read_loop(None, None, None);
let s = cnx.take_sender();
let r = cnx.take_receiver();
let mut shutdown = cnx.set_shutdown();
cnx.release_shutdown();
let _join = task::spawn(async move {
log_debug!("START of WS loop");
let res = ws_loop(websocket, s, r).await;
if res.is_err() {
let _ = shutdown.send(Either::Left(res.err().unwrap())).await;
} else {
let _ = shutdown.send(Either::Left(NetError::Closing)).await;
}
log_debug!("END of WS loop");
});
cnx.probe().await
}
}
}
}
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl IAccept for ConnectionWebSocket {
type Socket = WebSocketStream<ConnectStream>;
async fn accept(
&self,
remote_bind_address: BindAddress,
local_bind_address: BindAddress,
peer_privk: PrivKey,
socket: Self::Socket,
) -> Result<ConnectionBase, NetError> {
let mut cnx = ConnectionBase::new(ConnectionDir::Server, TransportProtocol::WS);
cnx.start_read_loop(
Some((local_bind_address, remote_bind_address)),
Some(peer_privk),
None,
);
let s = cnx.take_sender();
let r = cnx.take_receiver();
let mut shutdown = cnx.set_shutdown();
let _join = task::spawn(async move {
log_debug!("START of WS loop");
let res = ws_loop(socket, s, r).await;
if res.is_err() {
let _ = shutdown.send(Either::Left(res.err().unwrap())).await;
} else {
let _ = shutdown.send(Either::Left(NetError::Closing)).await;
}
log_debug!("END of WS loop");
});
Ok(cnx)
}
}
async fn close_ws(
stream: &mut WebSocketStream<ConnectStream>,
receiver: &mut Sender<ConnectionCommand>,
code: u16,
reason: &str,
) -> Result<(), NetError> {
log_debug!("close_ws {:?}", code);
let cmd = if code == 1000 {
ConnectionCommand::Close
} else if code < 4000 {
ConnectionCommand::Error(NetError::WsError)
} else if code < 4950 {
ConnectionCommand::ProtocolError(ProtocolError::try_from(code - 4000).unwrap())
} else {
ConnectionCommand::Error(NetError::try_from(code - 4949).unwrap())
};
log_debug!("sending to read loop {:?}", cmd);
let _ = futures::SinkExt::send(receiver, cmd).await;
stream
.close(Some(CloseFrame {
code: CloseCode::Library(code),
reason: std::borrow::Cow::Borrowed(reason),
}))
.await
.map_err(|_e| NetError::WsError)?;
Ok(())
}
async fn ws_loop(
mut ws: WebSocketStream<ConnectStream>,
sender: Receiver<ConnectionCommand>,
mut receiver: Sender<ConnectionCommand>,
) -> Result<(), NetError> {
async fn inner_loop(
stream: &mut WebSocketStream<ConnectStream>,
mut sender: Receiver<ConnectionCommand>,
receiver: &mut Sender<ConnectionCommand>,
) -> Result<ProtocolError, NetError> {
//let mut rx_sender = sender.fuse();
pin_mut!(stream);
loop {
select! {
r = stream.next().fuse() => match r {
Some(Ok(msg)) => {
//log_debug!("GOT MESSAGE {:?}", msg);
if msg.is_close() {
if let Message::Close(Some(cf)) = msg {
log_debug!("CLOSE from remote with closeframe: {} {}",cf.code, cf.reason);
let last_command = match cf.code {
CloseCode::Normal =>
ConnectionCommand::Close,
CloseCode::Library(c) => {
if c < 4950 {
ConnectionCommand::ProtocolError(
ProtocolError::try_from(c - 4000).unwrap(),
)
} else {
ConnectionCommand::Error(NetError::try_from(c - 4949).unwrap())
}
},
_ => ConnectionCommand::Error(NetError::WsError)
};
let _ = futures::SinkExt::send(receiver, last_command).await;
}
else {
let _ = futures::SinkExt::send(receiver, ConnectionCommand::Close).await;
log_debug!("CLOSE from remote");
}
return Ok(ProtocolError::Closing);
} else {
futures::SinkExt::send(receiver,ConnectionCommand::Msg(serde_bare::from_slice::<ProtocolMessage>(&msg.into_data())?)).await
.map_err(|_e| NetError::IoError)?;
}
},
Some(Err(_e)) => {log_debug!("GOT ERROR {:?}",_e);return Err(NetError::WsError);},
None => break
},
s = sender.next().fuse() => match s {
Some(msg) => {
//log_debug!("SENDING MESSAGE {:?}", msg);
match msg {
ConnectionCommand::Msg(m) => {
futures::SinkExt::send(&mut stream,Message::binary(serde_bare::to_vec(&m)?)).await.map_err(|_e| NetError::IoError)?;
},
ConnectionCommand::Error(e) => {
return Err(e);
},
ConnectionCommand::ProtocolError(e) => {
return Ok(e);
},
ConnectionCommand::Close => {
break;
},
ConnectionCommand::ReEnter => {
//do nothing. loop
}
}
},
None => break
},
}
}
Ok(ProtocolError::NoError)
}
match inner_loop(&mut ws, sender, &mut receiver).await {
Ok(proto_err) => {
if proto_err == ProtocolError::Closing {
log_debug!("ProtocolError::Closing");
let _ = ws.close(None).await;
} else if proto_err == ProtocolError::NoError {
close_ws(&mut ws, &mut receiver, 1000, "").await?;
} else {
let mut code = proto_err.clone() as u16;
if code > 949 {
code = ProtocolError::OtherError as u16;
}
close_ws(&mut ws, &mut receiver, code + 4000, &proto_err.to_string()).await?;
//return Err(NetError::ProtocolError);
}
}
Err(e) => {
close_ws(
&mut ws,
&mut receiver,
e.clone() as u16 + 4949,
&e.to_string(),
)
.await?;
return Err(e);
}
}
Ok(())
}
#[cfg(test)]
mod test {
use crate::remote_ws::*;
use ng_net::types::IP;
use ng_net::utils::{spawn_and_log_error, ResultSend};
use ng_net::{broker::*, WS_PORT};
use ng_repo::errors::NgError;
#[allow(unused_imports)]
use ng_repo::log::*;
use ng_repo::utils::generate_keypair;
use std::net::IpAddr;
use std::str::FromStr;
use std::sync::Arc;
#[async_std::test]
pub async fn test_ws() -> Result<(), NgError> {
let server_key: PubKey = "ALyGZgFaDDALXLppJZLS2TrMScG0TQIS68RzRcPv99aN".try_into()?;
log_debug!("server_key:{}", server_key);
let keys = generate_keypair();
let x_from_ed = keys.1.to_dh_from_ed();
log_debug!("Pub from X {}", x_from_ed);
let (client_priv, _client) = generate_keypair();
let (user_priv, user) = generate_keypair();
log_debug!("start connecting");
{
let res = BROKER
.write()
.await
.connect(
Arc::new(Box::new(ConnectionWebSocket {})),
keys.0,
keys.1,
server_key,
StartConfig::Client(ClientConfig {
url: format!("ws://localhost:{}", WS_PORT),
name: None,
user_priv,
client_priv,
info: ClientInfo::new(ClientType::Cli, "".into(), "".into()),
registration: None,
}),
)
.await;
log_debug!("broker.connect : {:?}", res);
assert!(res.is_err());
let err = res.unwrap_err();
assert!(
ProtocolError::NoLocalBrokerFound == err
|| ProtocolError::NoiseHandshakeFailed == err
);
}
BROKER.read().await.print_status();
async fn timer_close(remote_peer_id: DirectPeerId, user: Option<PubKey>) -> ResultSend<()> {
async move {
sleep!(std::time::Duration::from_secs(3));
log_debug!("timeout");
BROKER
.write()
.await
.close_peer_connection(&remote_peer_id, user)
.await;
}
.await;
Ok(())
}
spawn_and_log_error(timer_close(server_key, Some(user)));
//Broker::graceful_shutdown().await;
let _ = Broker::join_shutdown_with_timeout(std::time::Duration::from_secs(5)).await;
Ok(())
}
#[async_std::test]
pub async fn probe() -> Result<(), NgError> {
log_debug!("start probe");
{
let res = BROKER
.write()
.await
.probe(
Box::new(ConnectionWebSocket {}),
IP::try_from(&IpAddr::from_str("127.0.0.1").unwrap()).unwrap(),
WS_PORT,
)
.await;
log_debug!("broker.probe : {:?}", res);
res.expect("assume the probe succeeds");
}
//Broker::graceful_shutdown().await;
let _ = Broker::join_shutdown_with_timeout(std::time::Duration::from_secs(10)).await;
Ok(())
}
}

@ -0,0 +1,216 @@
/*
* Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
//! WebSocket for Wasm Remote Connection to a Broker
use either::Either;
use futures::FutureExt;
use futures::{select, SinkExt, StreamExt};
use {
pharos::{Observable, ObserveConfig},
wasm_bindgen::UnwrapThrowExt,
ws_stream_wasm::*,
};
use ng_repo::errors::*;
use ng_repo::log::*;
use ng_repo::types::*;
use ng_net::connection::*;
use ng_net::types::*;
use ng_net::utils::*;
pub struct ConnectionWebSocket {}
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl IConnect for ConnectionWebSocket {
async fn open(
&self,
url: String,
peer_privk: PrivKey,
_peer_pubk: PubKey,
remote_peer: DirectPeerId,
config: StartConfig,
) -> Result<ConnectionBase, ProtocolError> {
log_debug!("url {}", url);
let mut cnx = ConnectionBase::new(ConnectionDir::Client, TransportProtocol::WS);
let (ws, wsio) = WsMeta::connect(url, None).await.map_err(|_e| {
//log_debug!("{:?}", _e);
ProtocolError::ConnectionError
})?;
cnx.start_read_loop(None, Some(peer_privk), Some(remote_peer));
let shutdown = cnx.set_shutdown();
spawn_and_log_error(ws_loop(
ws,
wsio,
cnx.take_sender(),
cnx.take_receiver(),
shutdown,
));
cnx.start(config).await?;
Ok(cnx)
}
async fn probe(&self, ip: IP, port: u16) -> Result<Option<PubKey>, ProtocolError> {
let mut cnx = ConnectionBase::new(ConnectionDir::Client, TransportProtocol::WS);
let url = format!("ws://{}:{}", ip, port);
let (ws, wsio) = WsMeta::connect(url, None).await.map_err(|_e| {
//log_debug!("{:?}", _e);
ProtocolError::ConnectionError
})?;
cnx.start_read_loop(None, None, None);
let shutdown = cnx.set_shutdown();
spawn_and_log_error(ws_loop(
ws,
wsio,
cnx.take_sender(),
cnx.take_receiver(),
shutdown,
));
cnx.probe().await
}
}
async fn ws_loop(
mut ws: WsMeta,
mut stream: WsStream,
sender: Receiver<ConnectionCommand>,
mut receiver: Sender<ConnectionCommand>,
mut shutdown: Sender<Either<NetError, X25519PrivKey>>,
) -> ResultSend<()> {
async fn inner_loop(
stream: &mut WsStream,
mut sender: Receiver<ConnectionCommand>,
mut receiver: Sender<ConnectionCommand>,
) -> Result<ProtocolError, NetError> {
//let mut rx_sender = sender.fuse();
loop {
select! {
r = stream.next().fuse() => match r {
Some(msg) => {
//log_debug!("GOT MESSAGE {:?}", msg);
if let WsMessage::Binary(b) = msg {
receiver.send(ConnectionCommand::Msg(serde_bare::from_slice::<ProtocolMessage>(&b)?)).await
.map_err(|_e| NetError::IoError)?;
}
else {
break;
}
},
None => break
},
s = sender.next().fuse() => match s {
Some(msg) => {
//log_debug!("SENDING MESSAGE {:?}", msg);
match msg {
ConnectionCommand::Msg(m) => {
stream.send(WsMessage::Binary(serde_bare::to_vec(&m)?)).await.map_err(|_e| { log_debug!("{:?}",_e); return NetError::IoError;})?;
},
ConnectionCommand::Error(e) => {
return Err(e);
},
ConnectionCommand::ProtocolError(e) => {
return Ok(e);
},
ConnectionCommand::Close => {
break;
},
ConnectionCommand::ReEnter => {
//do nothing. loop
}
}
},
None => break
},
}
}
Ok(ProtocolError::NoError)
}
log_debug!("START of WS loop");
let mut events = ws
.observe(ObserveConfig::default())
//.observe(Filter::Pointer(WsEvent::is_closed).into())
.await
.expect_throw("observe");
match inner_loop(&mut stream, sender, receiver.clone()).await {
Ok(proto_err) => {
if proto_err == ProtocolError::NoError {
let _ = ws.close_code(1000).await; //.map_err(|_e| NetError::WsError)?;
log_debug!("CLOSED GRACEFULLY");
} else {
log_debug!("PROTOCOL ERR");
let mut code = proto_err.clone() as u16;
if code > 949 {
code = ProtocolError::OtherError as u16;
}
let _ = ws.close_reason(code + 4000, proto_err.to_string()).await;
//.map_err(|_e| NetError::WsError)?;
//return Err(Box::new(proto_err));
}
}
Err(e) => {
let _ = ws
.close_reason(e.clone() as u16 + 4949, e.to_string())
.await;
//.map_err(|_e| NetError::WsError)?;
//return Err(Box::new(e));
log_debug!("ERR {:?}", e);
}
}
let last_event = events.next().await;
log_debug!("WS closed {:?}", last_event.clone());
let last_command = match last_event {
None => ConnectionCommand::Close,
Some(WsEvent::Open) => ConnectionCommand::Error(NetError::WsError), // this should never happen
Some(WsEvent::Error) => ConnectionCommand::Error(NetError::ConnectionError),
Some(WsEvent::Closing) => ConnectionCommand::Close,
Some(WsEvent::Closed(ce)) => {
if ce.code == 1000 {
ConnectionCommand::Close
} else if ce.code < 4000 {
ConnectionCommand::Error(NetError::WsError)
} else if ce.code < 4950 {
ConnectionCommand::ProtocolError(ProtocolError::try_from(ce.code - 4000).unwrap())
} else {
ConnectionCommand::Error(NetError::try_from(ce.code - 4949).unwrap())
}
}
Some(WsEvent::WsErr(_e)) => ConnectionCommand::Error(NetError::WsError),
};
if let ConnectionCommand::Error(err) = last_command.clone() {
let _ = shutdown.send(Either::Left(err)).await;
} else {
let _ = shutdown.send(Either::Left(NetError::Closing)).await;
}
// if let ConnectionCommand::ProtocolError(err) = last_command.clone() {
//let _ = shutdown.send(Either::Left(NetError::ProtocolError)).await;
// otherwise, shutdown gracefully (with None). it is done automatically during destroy of shutdown
receiver
.send(last_command)
.await
.map_err(|_e| NetError::IoError)?;
log_debug!("END of WS loop");
Ok(())
}

@ -0,0 +1,54 @@
[package]
name = "ng-net"
# version = "0.1.0"
description = "Network library of NextGraph, a decentralized, secure and local-first web 3.0 ecosystem based on Semantic Web and CRDTs"
categories = ["network-programming"]
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
homepage.workspace = true
keywords = ["crdt","e2ee","local-first","p2p","self-hosted"]
documentation.workspace = true
rust-version.workspace = true
[badges]
maintenance = { status = "actively-developed" }
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_bare = "0.5.0"
serde_bytes = "0.11.7"
serde_json = "1.0"
lazy_static = "1.4.0"
once_cell = "1.17.1"
either = "1.8.1"
futures = "0.3.24"
async-trait = "0.1.64"
async-recursion = "1.1.1"
async-std = { version = "1.12.0", features = ["attributes","unstable"] }
unique_id = "0.1.5"
noise-protocol = "0.2.0"
noise-rust-crypto = "0.6.2"
ed25519-dalek = "1.0.1"
crypto_box = { version = "0.8.2", features = ["seal"] }
url = "2.4.0"
regex = "1.8.4"
base64-url = "2.0.0"
web-time = "0.2.0"
time = "0.3.41"
zeroize = { version = "1.7.0", features = ["zeroize_derive"] }
ng-repo = { path = "../ng-repo", version = "0.1.2" }
reqwest = { version = "0.11.18", features = ["json","native-tls-vendored"] }
[target.'cfg(target_arch = "wasm32")'.dependencies.getrandom]
version = "0.3.3"
features = ["wasm_js"]
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
getrandom = "0.3.3"
netdev = "0.26"
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
ng-async-tungstenite = { version = "0.22.2", git = "https://git.nextgraph.org/NextGraph/async-tungstenite.git", branch = "nextgraph", features = ["async-std-runtime", "async-native-tls"] }

@ -0,0 +1,56 @@
# ng-net
![MSRV][rustc-image]
[![Apache 2.0 Licensed][license-image]][license-link]
[![MIT Licensed][license-image2]][license-link2]
Network library of NextGraph
This repository is in active development at [https://git.nextgraph.org/NextGraph/nextgraph-rs](https://git.nextgraph.org/NextGraph/nextgraph-rs), a Gitea instance. For bug reports, issues, merge requests, and in order to join the dev team, please visit the link above and create an account (you can do so with a github account). The [github repo](https://github.com/nextgraph-org/nextgraph-rs) is just a read-only mirror that does not accept issues.
## NextGraph
> NextGraph brings about the convergence of P2P and Semantic Web technologies, towards a decentralized, secure and privacy-preserving cloud, based on CRDTs.
>
> This open source ecosystem provides solutions for end-users (a platform) and software developers (a framework), wishing to use or create **decentralized** apps featuring: **live collaboration** on rich-text documents, peer to peer communication with **end-to-end encryption**, offline-first, **local-first**, portable and interoperable data, total ownership of data and software, security and privacy. Centered on repositories containing **semantic data** (RDF), **rich text**, and structured data formats like **JSON**, synced between peers belonging to permissioned groups of users, it offers strong eventual consistency, thanks to the use of **CRDTs**. Documents can be linked together, signed, shared securely, queried using the **SPARQL** language and organized into sites and containers.
>
> More info here [https://nextgraph.org](https://nextgraph.org)
## Support
Documentation can be found here [https://docs.nextgraph.org](https://docs.nextgraph.org)
And our community forum where you can ask questions is here [https://forum.nextgraph.org](https://forum.nextgraph.org)
## How to use the library
NextGraph is not ready yet. You can subscribe to [our newsletter](https://list.nextgraph.org/subscription/form) to get updates, and support us with a [donation](https://nextgraph.org/donate/).
This library is used internally by [ngd](../ngd/README.md), [ngcli](../ngcli/README.md), [ng-app](../ng-app/README.md) and by [nextgraph, the Rust client library](../nextgraph/README.md) which you should be using instead. It is not meant to be used by other programs as-is.
## License
Licensed under either of
- Apache License, Version 2.0 ([LICENSE-APACHE2](LICENSE-APACHE2) or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
at your option.
`SPDX-License-Identifier: Apache-2.0 OR MIT`
### Contributions license
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in the work by you shall be dual licensed as below, without any
additional terms or conditions.
---
NextGraph received funding through the [NGI Assure Fund](https://nlnet.nl/assure) and the [NGI Zero Commons Fund](https://nlnet.nl/commonsfund/), both funds established by [NLnet](https://nlnet.nl/) Foundation with financial support from the European Commission's [Next Generation Internet](https://ngi.eu/) programme, under the aegis of DG Communications Networks, Content and Technology under grant agreements No 957073 and No 101092990, respectively.
[rustc-image]: https://img.shields.io/badge/rustc-1.81+-blue.svg
[license-image]: https://img.shields.io/badge/license-Apache2.0-blue.svg
[license-link]: https://git.nextgraph.org/NextGraph/nextgraph-rs/raw/branch/master/LICENSE-APACHE2
[license-image2]: https://img.shields.io/badge/license-MIT-blue.svg
[license-link2]: https://git.nextgraph.org/NextGraph/nextgraph-rs/src/branch/master/LICENSE-MIT

@ -0,0 +1,238 @@
/*
* Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
//! Actor handles messages in the Protocol. common types are here
use std::any::TypeId;
use std::marker::PhantomData;
use std::sync::Arc;
use async_std::stream::StreamExt;
use async_std::sync::Mutex;
use futures::{channel::mpsc, SinkExt};
use ng_repo::errors::{NgError, ProtocolError, ServerError};
use ng_repo::log::*;
use crate::utils::{spawn_and_log_error, Receiver, ResultSend, Sender};
use crate::{connection::*, types::ProtocolMessage};
impl TryFrom<ProtocolMessage> for () {
type Error = ProtocolError;
fn try_from(_msg: ProtocolMessage) -> Result<Self, Self::Error> {
Ok(())
}
}
#[doc(hidden)]
#[async_trait::async_trait]
pub trait EActor: Send + Sync + std::fmt::Debug {
async fn respond(
&mut self,
msg: ProtocolMessage,
fsm: Arc<Mutex<NoiseFSM>>,
) -> Result<(), ProtocolError>;
fn set_id(&mut self, _id: i64) {}
}
#[derive(Debug)]
pub(crate) struct Actor<
'a,
A: Into<ProtocolMessage> + std::fmt::Debug,
B: TryFrom<ProtocolMessage, Error = ProtocolError> + std::fmt::Debug + Sync,
> {
id: i64,
phantom_a: PhantomData<&'a A>,
phantom_b: PhantomData<&'a B>,
receiver: Option<Receiver<ConnectionCommand>>,
receiver_tx: Sender<ConnectionCommand>,
//initiator: bool,
}
#[derive(Debug)]
pub enum SoS<B> {
Single(B),
Stream(Receiver<B>),
}
impl<B> SoS<B> {
pub fn is_single(&self) -> bool {
if let Self::Single(_b) = self {
true
} else {
false
}
}
pub fn is_stream(&self) -> bool {
!self.is_single()
}
pub fn unwrap_single(self) -> B {
match self {
Self::Single(s) => s,
Self::Stream(_s) => {
panic!("called `unwrap_single()` on a `Stream` value")
}
}
}
pub fn unwrap_stream(self) -> Receiver<B> {
match self {
Self::Stream(s) => s,
Self::Single(_s) => {
panic!("called `unwrap_stream()` on a `Single` value")
}
}
}
}
impl<
A: Into<ProtocolMessage> + std::fmt::Debug + 'static,
B: TryFrom<ProtocolMessage, Error = ProtocolError> + Sync + Send + std::fmt::Debug + 'static,
> Actor<'_, A, B>
{
pub fn new(id: i64, _initiator: bool) -> Self {
let (receiver_tx, receiver) = mpsc::unbounded::<ConnectionCommand>();
Self {
id,
receiver: Some(receiver),
receiver_tx,
phantom_a: PhantomData,
phantom_b: PhantomData,
//initiator,
}
}
// pub fn verify(&self, msg: ProtocolMessage) -> bool {
// self.initiator && msg.type_id() == TypeId::of::<B>()
// || !self.initiator && msg.type_id() == TypeId::of::<A>()
// }
pub fn detach_receiver(&mut self) -> Receiver<ConnectionCommand> {
self.receiver.take().unwrap()
}
pub async fn request(
&mut self,
msg: ProtocolMessage,
fsm: Arc<Mutex<NoiseFSM>>,
) -> Result<SoS<B>, NgError> {
fsm.lock().await.send(msg).await?;
let mut receiver = self.receiver.take().unwrap();
match receiver.next().await {
Some(ConnectionCommand::Msg(msg)) => {
if let Some(bm) = msg.is_streamable() {
if bm.result() == Into::<u16>::into(ServerError::PartialContent)
&& TypeId::of::<B>() != TypeId::of::<()>()
{
let (mut b_sender, b_receiver) = mpsc::unbounded::<B>();
let response = msg.try_into().map_err(|e| {
log_err!("msg.try_into {}", e);
ProtocolError::ActorError
})?;
b_sender
.send(response)
.await
.map_err(|_err| ProtocolError::IoError)?;
async fn pump_stream<C: TryFrom<ProtocolMessage, Error = ProtocolError>>(
mut actor_receiver: Receiver<ConnectionCommand>,
mut sos_sender: Sender<C>,
fsm: Arc<Mutex<NoiseFSM>>,
id: i64,
) -> ResultSend<()> {
async move {
while let Some(ConnectionCommand::Msg(msg)) =
actor_receiver.next().await
{
if let Some(bm) = msg.is_streamable() {
if bm.result()
== Into::<u16>::into(ServerError::EndOfStream)
{
break;
}
let response = msg.try_into();
if response.is_err() {
// TODO deal with errors.
break;
}
if sos_sender.send(response.unwrap()).await.is_err() {
break;
}
} else {
// todo deal with error (not a ClientMessage)
break;
}
}
fsm.lock().await.remove_actor(id).await;
}
.await;
Ok(())
}
spawn_and_log_error(pump_stream::<B>(
receiver,
b_sender,
Arc::clone(&fsm),
self.id,
));
return Ok(SoS::<B>::Stream(b_receiver));
}
}
fsm.lock().await.remove_actor(self.id).await;
let server_error: Result<ServerError, NgError> = (&msg).try_into();
//log_debug!("server_error {:?}", server_error);
if server_error.is_ok() {
return Err(NgError::ServerError(server_error.unwrap()));
}
let response: B = match msg.try_into() {
Ok(b) => b,
Err(ProtocolError::ServerError) => {
return Err(NgError::ServerError(server_error?));
}
Err(e) => return Err(NgError::ProtocolError(e)),
};
Ok(SoS::<B>::Single(response))
}
Some(ConnectionCommand::ProtocolError(e)) => Err(e.into()),
Some(ConnectionCommand::Error(e)) => Err(ProtocolError::from(e).into()),
Some(ConnectionCommand::Close) => Err(ProtocolError::Closing.into()),
_ => Err(ProtocolError::ActorError.into()),
}
}
pub fn new_responder(id: i64) -> Box<Self> {
Box::new(Self::new(id, false))
}
pub fn get_receiver_tx(&self) -> Sender<ConnectionCommand> {
self.receiver_tx.clone()
}
pub fn id(&self) -> i64 {
self.id
}
}
#[cfg(test)]
mod test {
use crate::actor::*;
use crate::actors::*;
#[async_std::test]
pub async fn test_actor() {
let _a = Actor::<Noise, Noise>::new(1, true);
// a.handle(ProtocolMessage::Start(StartProtocol::Client(
// ClientHello::Noise3(Noise::V0(NoiseV0 { data: vec![] })),
// )))
// .await;
// a.handle(ProtocolMessage::Noise(Noise::V0(NoiseV0 { data: vec![] })))
// .await;
}
}

@ -0,0 +1,145 @@
/*
* Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
use std::sync::Arc;
use async_std::sync::Mutex;
use serde::{Deserialize, Serialize};
use ng_repo::errors::*;
use ng_repo::log::*;
use super::super::StartProtocol;
use crate::broker::BROKER;
use crate::connection::NoiseFSM;
use crate::types::*;
use crate::{actor::*, types::ProtocolMessage};
/// Add invitation
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AddInvitationV0 {
pub invite_code: InvitationCode,
pub expiry: u32,
pub memo: Option<String>,
pub tos_url: bool,
}
/// Add invitation
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum AddInvitation {
V0(AddInvitationV0),
}
impl AddInvitation {
pub fn code(&self) -> &InvitationCode {
match self {
AddInvitation::V0(o) => &o.invite_code,
}
}
pub fn expiry(&self) -> u32 {
match self {
AddInvitation::V0(o) => o.expiry,
}
}
pub fn memo(&self) -> &Option<String> {
match self {
AddInvitation::V0(o) => &o.memo,
}
}
pub fn tos_url(&self) -> bool {
match self {
AddInvitation::V0(o) => o.tos_url,
}
}
pub fn get_actor(&self) -> Box<dyn EActor> {
Actor::<AddInvitation, AdminResponse>::new_responder(0)
}
}
impl TryFrom<ProtocolMessage> for AddInvitation {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
if let ProtocolMessage::Start(StartProtocol::Admin(AdminRequest::V0(AdminRequestV0 {
content: AdminRequestContentV0::AddInvitation(a),
..
}))) = msg
{
Ok(a)
} else {
log_debug!("INVALID {:?}", msg);
Err(ProtocolError::InvalidValue)
}
}
}
impl From<AddInvitation> for ProtocolMessage {
fn from(_msg: AddInvitation) -> ProtocolMessage {
unimplemented!();
}
}
impl From<AddInvitation> for AdminRequestContentV0 {
fn from(msg: AddInvitation) -> AdminRequestContentV0 {
AdminRequestContentV0::AddInvitation(msg)
}
}
impl Actor<'_, AddInvitation, AdminResponse> {}
#[async_trait::async_trait]
impl EActor for Actor<'_, AddInvitation, AdminResponse> {
async fn respond(
&mut self,
msg: ProtocolMessage,
fsm: Arc<Mutex<NoiseFSM>>,
) -> Result<(), ProtocolError> {
let req = AddInvitation::try_from(msg)?;
let (url, bootstrap, sb) = {
let broker = BROKER.read().await;
let url = if req.tos_url() {
broker.get_registration_url().map(|s| s.clone())
} else {
None
};
(
url,
broker.get_bootstrap()?.clone(),
broker.get_server_broker()?,
)
};
{
sb.read()
.await
.add_invitation(req.code(), req.expiry(), req.memo())?;
}
let invitation = crate::types::Invitation::V0(InvitationV0::new(
bootstrap,
Some(req.code().get_symkey()),
None,
url,
));
let response: AdminResponseV0 = invitation.into();
fsm.lock().await.send(response.into()).await?;
Ok(())
}
}
impl From<Invitation> for AdminResponseV0 {
fn from(res: Invitation) -> AdminResponseV0 {
AdminResponseV0 {
id: 0,
result: 0,
content: AdminResponseContentV0::Invitation(res),
padding: vec![],
}
}
}

@ -0,0 +1,121 @@
/*
* Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
use std::sync::Arc;
use async_std::sync::Mutex;
use serde::{Deserialize, Serialize};
use ng_repo::errors::*;
use ng_repo::log::*;
use ng_repo::types::PubKey;
use super::super::StartProtocol;
use crate::broker::{ServerConfig, BROKER};
use crate::connection::NoiseFSM;
use crate::types::*;
use crate::{actor::*, types::ProtocolMessage};
/// Add user account
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct AddUserV0 {
/// User pub key
pub user: PubKey,
/// should the newly added user be an admin of the server
pub is_admin: bool,
}
/// Add user account
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum AddUser {
V0(AddUserV0),
}
impl AddUser {
pub fn user(&self) -> PubKey {
match self {
AddUser::V0(o) => o.user,
}
}
pub fn is_admin(&self) -> bool {
match self {
AddUser::V0(o) => o.is_admin,
}
}
pub fn get_actor(&self) -> Box<dyn EActor> {
Actor::<AddUser, AdminResponse>::new_responder(0)
}
}
impl TryFrom<ProtocolMessage> for AddUser {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
if let ProtocolMessage::Start(StartProtocol::Admin(AdminRequest::V0(AdminRequestV0 {
content: AdminRequestContentV0::AddUser(a),
..
}))) = msg
{
Ok(a)
} else {
log_debug!("INVALID {:?}", msg);
Err(ProtocolError::InvalidValue)
}
}
}
impl From<AddUser> for ProtocolMessage {
fn from(_msg: AddUser) -> ProtocolMessage {
unimplemented!();
}
}
impl From<AddUser> for AdminRequestContentV0 {
fn from(msg: AddUser) -> AdminRequestContentV0 {
AdminRequestContentV0::AddUser(msg)
}
}
impl Actor<'_, AddUser, AdminResponse> {}
#[async_trait::async_trait]
impl EActor for Actor<'_, AddUser, AdminResponse> {
async fn respond(
&mut self,
msg: ProtocolMessage,
fsm: Arc<Mutex<NoiseFSM>>,
) -> Result<(), ProtocolError> {
let req = AddUser::try_from(msg)?;
let res = {
let mut is_admin = req.is_admin();
let sb = {
let broker = BROKER.read().await;
if let Some(ServerConfig {
admin_user: Some(admin_user),
..
}) = broker.get_config()
{
if *admin_user == req.user() {
is_admin = true;
}
}
broker.get_server_broker()?
};
let lock = sb.read().await;
lock.add_user(req.user(), is_admin)
};
let response: AdminResponseV0 = res.into();
fsm.lock().await.send(response.into()).await?;
Ok(())
}
}

@ -0,0 +1,111 @@
/*
* Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
use std::sync::Arc;
use async_std::sync::Mutex;
use ng_repo::types::UserId;
use serde::{Deserialize, Serialize};
use ng_repo::errors::*;
use ng_repo::log::*;
use super::super::StartProtocol;
use crate::broker::BROKER;
use crate::connection::NoiseFSM;
use crate::types::*;
use crate::{actor::*, types::ProtocolMessage};
/// Create user and keeps credentials in the server (for use with headless API)
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct CreateUserV0 {}
/// Create user
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum CreateUser {
V0(CreateUserV0),
}
impl TryFrom<ProtocolMessage> for CreateUser {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
if let ProtocolMessage::Start(StartProtocol::Admin(AdminRequest::V0(AdminRequestV0 {
content: AdminRequestContentV0::CreateUser(a),
..
}))) = msg
{
Ok(a)
} else {
log_debug!("INVALID {:?}", msg);
Err(ProtocolError::InvalidValue)
}
}
}
impl From<CreateUser> for ProtocolMessage {
fn from(_msg: CreateUser) -> ProtocolMessage {
unimplemented!();
}
}
impl From<UserId> for ProtocolMessage {
fn from(_msg: UserId) -> ProtocolMessage {
unimplemented!();
}
}
impl TryFrom<ProtocolMessage> for UserId {
type Error = ProtocolError;
fn try_from(_msg: ProtocolMessage) -> Result<Self, Self::Error> {
unimplemented!();
}
}
impl From<CreateUser> for AdminRequestContentV0 {
fn from(msg: CreateUser) -> AdminRequestContentV0 {
AdminRequestContentV0::CreateUser(msg)
}
}
impl CreateUser {
pub fn get_actor(&self) -> Box<dyn EActor> {
Actor::<CreateUser, UserId>::new_responder(0)
}
}
impl Actor<'_, CreateUser, UserId> {}
#[async_trait::async_trait]
impl EActor for Actor<'_, CreateUser, UserId> {
async fn respond(
&mut self,
msg: ProtocolMessage,
fsm: Arc<Mutex<NoiseFSM>>,
) -> Result<(), ProtocolError> {
let _req = CreateUser::try_from(msg)?;
let res = {
let (broker_id, sb) = {
let b = BROKER.read().await;
(b.get_server_peer_id(), b.get_server_broker()?)
};
let lock = sb.read().await;
lock.create_user(&broker_id).await
};
let response: AdminResponseV0 = res.into();
fsm.lock().await.send(response.into()).await?;
Ok(())
}
}

@ -0,0 +1,94 @@
/*
* Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
use std::sync::Arc;
use async_std::sync::Mutex;
use serde::{Deserialize, Serialize};
use ng_repo::errors::*;
use ng_repo::types::PubKey;
use crate::broker::BROKER;
use crate::connection::NoiseFSM;
use crate::types::*;
use crate::{actor::*, types::ProtocolMessage};
use super::super::StartProtocol;
/// Delete user account V0
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct DelUserV0 {
/// User pub key
pub user: PubKey,
}
/// Delete user account
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum DelUser {
V0(DelUserV0),
}
impl DelUser {
pub fn user(&self) -> PubKey {
match self {
DelUser::V0(o) => o.user,
}
}
pub fn get_actor(&self) -> Box<dyn EActor> {
Actor::<DelUser, AdminResponse>::new_responder(0)
}
}
impl TryFrom<ProtocolMessage> for DelUser {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
if let ProtocolMessage::Start(StartProtocol::Admin(AdminRequest::V0(AdminRequestV0 {
content: AdminRequestContentV0::DelUser(a),
..
}))) = msg
{
Ok(a)
} else {
Err(ProtocolError::InvalidValue)
}
}
}
impl From<DelUser> for ProtocolMessage {
fn from(_msg: DelUser) -> ProtocolMessage {
unimplemented!();
}
}
impl From<DelUser> for AdminRequestContentV0 {
fn from(msg: DelUser) -> AdminRequestContentV0 {
AdminRequestContentV0::DelUser(msg)
}
}
impl Actor<'_, DelUser, AdminResponse> {}
#[async_trait::async_trait]
impl EActor for Actor<'_, DelUser, AdminResponse> {
async fn respond(
&mut self,
msg: ProtocolMessage,
fsm: Arc<Mutex<NoiseFSM>>,
) -> Result<(), ProtocolError> {
let req = DelUser::try_from(msg)?;
let sb = { BROKER.read().await.get_server_broker()? };
let res = { sb.read().await.del_user(req.user()) };
let response: AdminResponseV0 = res.into();
fsm.lock().await.send(response.into()).await?;
Ok(())
}
}

@ -0,0 +1,135 @@
/*
* Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
use std::sync::Arc;
use async_std::sync::Mutex;
use serde::{Deserialize, Serialize};
use ng_repo::errors::*;
#[allow(unused_imports)]
use ng_repo::log::*;
use super::super::StartProtocol;
use crate::broker::BROKER;
use crate::connection::NoiseFSM;
use crate::types::*;
use crate::{actor::*, types::ProtocolMessage};
/// List invitations registered on this broker
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct ListInvitationsV0 {
/// should list only the admin invitations.
pub admin: bool,
/// should list only the unique invitations.
pub unique: bool,
/// should list only the multi invitations.
pub multi: bool,
}
/// List invitations registered on this broker
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum ListInvitations {
V0(ListInvitationsV0),
}
impl ListInvitations {
pub fn admin(&self) -> bool {
match self {
Self::V0(o) => o.admin,
}
}
pub fn unique(&self) -> bool {
match self {
Self::V0(o) => o.unique,
}
}
pub fn multi(&self) -> bool {
match self {
Self::V0(o) => o.multi,
}
}
pub fn get_actor(&self) -> Box<dyn EActor> {
Actor::<ListInvitations, AdminResponse>::new_responder(0)
}
}
impl TryFrom<ProtocolMessage> for ListInvitations {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
if let ProtocolMessage::Start(StartProtocol::Admin(AdminRequest::V0(AdminRequestV0 {
content: AdminRequestContentV0::ListInvitations(a),
..
}))) = msg
{
Ok(a)
} else {
//log_debug!("INVALID {:?}", msg);
Err(ProtocolError::InvalidValue)
}
}
}
impl From<ListInvitations> for ProtocolMessage {
fn from(_msg: ListInvitations) -> ProtocolMessage {
unimplemented!();
}
}
impl From<ListInvitations> for AdminRequestContentV0 {
fn from(msg: ListInvitations) -> AdminRequestContentV0 {
AdminRequestContentV0::ListInvitations(msg)
}
}
impl Actor<'_, ListInvitations, AdminResponse> {}
#[async_trait::async_trait]
impl EActor for Actor<'_, ListInvitations, AdminResponse> {
async fn respond(
&mut self,
msg: ProtocolMessage,
fsm: Arc<Mutex<NoiseFSM>>,
) -> Result<(), ProtocolError> {
let req = ListInvitations::try_from(msg)?;
let sb = { BROKER.read().await.get_server_broker()? };
let res = {
sb.read()
.await
.list_invitations(req.admin(), req.unique(), req.multi())
};
let response: AdminResponseV0 = res.into();
fsm.lock().await.send(response.into()).await?;
Ok(())
}
}
impl From<Result<Vec<(InvitationCode, u32, Option<String>)>, ProtocolError>> for AdminResponseV0 {
fn from(
res: Result<Vec<(InvitationCode, u32, Option<String>)>, ProtocolError>,
) -> AdminResponseV0 {
match res {
Err(e) => AdminResponseV0 {
id: 0,
result: e.into(),
content: AdminResponseContentV0::EmptyResponse,
padding: vec![],
},
Ok(vec) => AdminResponseV0 {
id: 0,
result: 0,
content: AdminResponseContentV0::Invitations(vec),
padding: vec![],
},
}
}
}

@ -0,0 +1,95 @@
/*
* Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
use std::sync::Arc;
use async_std::sync::Mutex;
use serde::{Deserialize, Serialize};
use ng_repo::errors::*;
use super::super::StartProtocol;
use crate::broker::BROKER;
use crate::connection::NoiseFSM;
use crate::types::*;
use crate::{actor::*, types::ProtocolMessage};
/// List users registered on this broker
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct ListUsersV0 {
/// should list only the admins. if false, admin users will be excluded
pub admins: bool,
}
/// List users registered on this broker
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum ListUsers {
V0(ListUsersV0),
}
impl ListUsers {
pub fn admins(&self) -> bool {
match self {
Self::V0(o) => o.admins,
}
}
pub fn get_actor(&self) -> Box<dyn EActor> {
Actor::<ListUsers, AdminResponse>::new_responder(0)
}
}
impl TryFrom<ProtocolMessage> for ListUsers {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
if let ProtocolMessage::Start(StartProtocol::Admin(AdminRequest::V0(AdminRequestV0 {
content: AdminRequestContentV0::ListUsers(a),
..
}))) = msg
{
Ok(a)
} else {
//log_debug!("INVALID {:?}", msg);
Err(ProtocolError::InvalidValue)
}
}
}
impl From<ListUsers> for ProtocolMessage {
fn from(_msg: ListUsers) -> ProtocolMessage {
unimplemented!();
}
}
impl From<ListUsers> for AdminRequestContentV0 {
fn from(msg: ListUsers) -> AdminRequestContentV0 {
AdminRequestContentV0::ListUsers(msg)
}
}
impl Actor<'_, ListUsers, AdminResponse> {}
#[async_trait::async_trait]
impl EActor for Actor<'_, ListUsers, AdminResponse> {
async fn respond(
&mut self,
msg: ProtocolMessage,
fsm: Arc<Mutex<NoiseFSM>>,
) -> Result<(), ProtocolError> {
let req = ListUsers::try_from(msg)?;
let sb = { BROKER.read().await.get_server_broker()? };
let res = { sb.read().await.list_users(req.admins()) };
let response: AdminResponseV0 = res.into();
fsm.lock().await.send(response.into()).await?;
Ok(())
}
}

@ -0,0 +1,17 @@
pub mod add_user;
pub use add_user::*;
pub mod del_user;
pub use del_user::*;
pub mod list_users;
pub use list_users::*;
pub mod add_invitation;
pub use add_invitation::*;
pub mod list_invitations;
pub use list_invitations::*;
pub mod create_user;
pub use create_user::*;

@ -0,0 +1,3 @@
pub mod request;
pub mod session;

@ -0,0 +1,134 @@
/*
* Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
use std::sync::Arc;
use async_std::sync::Mutex;
use ng_repo::errors::*;
use ng_repo::log::*;
use crate::app_protocol::*;
use crate::broker::BROKER;
use crate::connection::NoiseFSM;
use crate::types::*;
use crate::{actor::*, types::ProtocolMessage};
impl AppRequest {
pub fn get_actor(&self, id: i64) -> Box<dyn EActor> {
Actor::<AppRequest, AppResponse>::new_responder(id)
}
}
impl TryFrom<ProtocolMessage> for AppRequest {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
if let AppMessageContentV0::Request(req) = msg.try_into()? {
Ok(req)
} else {
log_debug!("INVALID AppMessageContentV0::Request");
Err(ProtocolError::InvalidValue)
}
}
}
impl From<AppRequest> for ProtocolMessage {
fn from(request: AppRequest) -> ProtocolMessage {
AppMessageContentV0::Request(request).into()
}
}
impl From<AppMessageContentV0> for ProtocolMessage {
fn from(content: AppMessageContentV0) -> ProtocolMessage {
AppMessage::V0(AppMessageV0 {
content,
id: 0,
result: 0,
})
.into()
}
}
impl TryFrom<ProtocolMessage> for AppResponse {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
if let AppMessageContentV0::Response(res) = msg.try_into()? {
Ok(res)
} else {
log_err!("INVALID AppMessageContentV0::Response");
Err(ProtocolError::InvalidValue)
}
}
}
impl TryFrom<ProtocolMessage> for AppMessageContentV0 {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
if let ProtocolMessage::AppMessage(AppMessage::V0(AppMessageV0 {
content, result, ..
})) = msg
{
let err = ServerError::try_from(result).unwrap();
if !err.is_err() {
Ok(content)
} else {
Err(ProtocolError::ServerError)
}
} else {
log_err!("INVALID AppMessageContentV0 {:?}", msg);
Err(ProtocolError::InvalidValue)
}
}
}
impl From<AppResponse> for AppMessage {
fn from(response: AppResponse) -> AppMessage {
AppMessage::V0(AppMessageV0 {
content: AppMessageContentV0::Response(response),
id: 0,
result: 0,
})
}
}
impl From<AppResponse> for ProtocolMessage {
fn from(response: AppResponse) -> ProtocolMessage {
let app_msg: AppMessage = response.into();
app_msg.into()
}
}
impl Actor<'_, AppRequest, AppResponse> {}
#[async_trait::async_trait]
impl EActor for Actor<'_, AppRequest, AppResponse> {
async fn respond(
&mut self,
msg: ProtocolMessage,
fsm: Arc<Mutex<NoiseFSM>>,
) -> Result<(), ProtocolError> {
let req = AppRequest::try_from(msg)?;
let res = {
let sb = { BROKER.read().await.get_server_broker()? };
let lock = sb.read().await;
lock.app_process_request(req, self.id(), &fsm).await
};
if res.is_err() {
let server_err: ServerError = res.unwrap_err().into();
let app_message: AppMessage = server_err.into();
fsm.lock()
.await
.send_in_reply_to(app_message.into(), self.id())
.await?;
}
Ok(())
}
}

@ -0,0 +1,197 @@
/*
* Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
use std::sync::Arc;
use async_std::sync::Mutex;
use ng_repo::errors::*;
use ng_repo::log::*;
use crate::app_protocol::*;
use crate::broker::BROKER;
use crate::connection::NoiseFSM;
use crate::types::*;
use crate::{actor::*, types::ProtocolMessage};
impl AppSessionStart {
pub fn get_actor(&self, id: i64) -> Box<dyn EActor> {
Actor::<AppSessionStart, AppSessionStartResponse>::new_responder(id)
}
}
impl TryFrom<ProtocolMessage> for AppSessionStart {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
if let AppMessageContentV0::SessionStart(req) = msg.try_into()? {
Ok(req)
} else {
log_debug!("INVALID AppMessageContentV0::SessionStart");
Err(ProtocolError::InvalidValue)
}
}
}
impl From<AppSessionStart> for ProtocolMessage {
fn from(request: AppSessionStart) -> ProtocolMessage {
AppMessageContentV0::SessionStart(request).into()
}
}
impl TryFrom<ProtocolMessage> for AppSessionStartResponse {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
if let AppMessageContentV0::Response(AppResponse::V0(AppResponseV0::SessionStart(res))) =
msg.try_into()?
{
Ok(res)
} else {
log_debug!("INVALID AppSessionStartResponse");
Err(ProtocolError::InvalidValue)
}
}
}
impl From<AppSessionStartResponse> for AppMessage {
fn from(response: AppSessionStartResponse) -> AppMessage {
AppResponse::V0(AppResponseV0::SessionStart(response)).into()
}
}
impl From<AppSessionStartResponse> for ProtocolMessage {
fn from(response: AppSessionStartResponse) -> ProtocolMessage {
response.into()
}
}
impl Actor<'_, AppSessionStart, AppSessionStartResponse> {}
#[async_trait::async_trait]
impl EActor for Actor<'_, AppSessionStart, AppSessionStartResponse> {
async fn respond(
&mut self,
msg: ProtocolMessage,
fsm: Arc<Mutex<NoiseFSM>>,
) -> Result<(), ProtocolError> {
let req = AppSessionStart::try_from(msg)?;
let res = {
let lock = fsm.lock().await;
let remote = lock.remote_peer();
//TODO: if fsm.get_user_id is some, check that user_priv_key in credentials matches.
//TODO: if no user in fsm (headless), check user in request is allowed
if remote.is_none() {
Err(ServerError::BrokerError)
} else {
let (sb, broker_id) = {
let b = BROKER.read().await;
(b.get_server_broker()?, b.get_server_peer_id())
};
let lock = sb.read().await;
lock.app_session_start(req, remote.unwrap(), broker_id)
.await
}
};
let app_message: AppMessage = match res {
Err(e) => e.into(),
Ok(o) => o.into(),
};
fsm.lock()
.await
.send_in_reply_to(app_message.into(), self.id())
.await?;
Ok(())
}
}
///////////////////////
impl AppSessionStop {
pub fn get_actor(&self, id: i64) -> Box<dyn EActor> {
Actor::<AppSessionStop, EmptyAppResponse>::new_responder(id)
}
}
impl TryFrom<ProtocolMessage> for AppSessionStop {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
if let AppMessageContentV0::SessionStop(req) = msg.try_into()? {
Ok(req)
} else {
log_debug!("INVALID AppMessageContentV0::SessionStop");
Err(ProtocolError::InvalidValue)
}
}
}
impl TryFrom<ProtocolMessage> for EmptyAppResponse {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
let res: Result<AppMessageContentV0, ProtocolError> = msg.try_into();
if let AppMessageContentV0::EmptyResponse = res? {
Ok(EmptyAppResponse(()))
} else {
log_debug!("INVALID AppMessageContentV0::EmptyResponse");
Err(ProtocolError::InvalidValue)
}
}
}
impl From<AppSessionStop> for ProtocolMessage {
fn from(request: AppSessionStop) -> ProtocolMessage {
AppMessageContentV0::SessionStop(request).into()
}
}
impl From<Result<EmptyAppResponse, ServerError>> for ProtocolMessage {
fn from(res: Result<EmptyAppResponse, ServerError>) -> ProtocolMessage {
match res {
Ok(_a) => ServerError::Ok.into(),
Err(err) => AppMessage::V0(AppMessageV0 {
id: 0,
result: err.into(),
content: AppMessageContentV0::EmptyResponse,
}),
}
.into()
}
}
impl Actor<'_, AppSessionStop, EmptyAppResponse> {}
#[async_trait::async_trait]
impl EActor for Actor<'_, AppSessionStop, EmptyAppResponse> {
async fn respond(
&mut self,
msg: ProtocolMessage,
fsm: Arc<Mutex<NoiseFSM>>,
) -> Result<(), ProtocolError> {
let req = AppSessionStop::try_from(msg)?;
let res = {
let lock = fsm.lock().await;
let remote = lock.remote_peer();
if remote.is_none() {
Err(ServerError::BrokerError)
} else {
let sb = { BROKER.read().await.get_server_broker()? };
let lock = sb.read().await;
lock.app_session_stop(req, remote.as_ref().unwrap()).await
}
};
fsm.lock()
.await
.send_in_reply_to(res.into(), self.id())
.await?;
Ok(())
}
}

@ -0,0 +1,104 @@
/*
* Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
use std::sync::Arc;
use async_std::sync::Mutex;
use ng_repo::errors::*;
use ng_repo::log::*;
use crate::broker::BROKER;
use crate::connection::NoiseFSM;
use crate::types::*;
use crate::{actor::*, types::ProtocolMessage};
impl BlocksExist {
pub fn get_actor(&self, id: i64) -> Box<dyn EActor> {
Actor::<BlocksExist, BlocksFound>::new_responder(id)
}
}
impl TryFrom<ProtocolMessage> for BlocksExist {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
let req: ClientRequestContentV0 = msg.try_into()?;
if let ClientRequestContentV0::BlocksExist(a) = req {
Ok(a)
} else {
log_debug!("INVALID {:?}", req);
Err(ProtocolError::InvalidValue)
}
}
}
impl From<BlocksExist> for ProtocolMessage {
fn from(msg: BlocksExist) -> ProtocolMessage {
let overlay = *msg.overlay();
ProtocolMessage::from_client_request_v0(ClientRequestContentV0::BlocksExist(msg), overlay)
}
}
impl TryFrom<ProtocolMessage> for BlocksFound {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
let res: ClientResponseContentV0 = msg.try_into()?;
if let ClientResponseContentV0::BlocksFound(a) = res {
Ok(a)
} else {
log_debug!("INVALID {:?}", res);
Err(ProtocolError::InvalidValue)
}
}
}
impl From<BlocksFound> for ProtocolMessage {
fn from(b: BlocksFound) -> ProtocolMessage {
ClientResponseContentV0::BlocksFound(b).into()
}
}
impl Actor<'_, BlocksExist, BlocksFound> {}
#[async_trait::async_trait]
impl EActor for Actor<'_, BlocksExist, BlocksFound> {
async fn respond(
&mut self,
msg: ProtocolMessage,
fsm: Arc<Mutex<NoiseFSM>>,
) -> Result<(), ProtocolError> {
let req = BlocksExist::try_from(msg)?;
let sb = { BROKER.read().await.get_server_broker()? };
let overlay = req.overlay().clone();
let mut found = vec![];
let mut missing = vec![];
match req {
BlocksExist::V0(v0) => {
for block_id in v0.blocks {
let r = sb.read().await.has_block(&overlay, &block_id);
if r.is_err() {
missing.push(block_id);
} else {
found.push(block_id);
}
}
}
}
let res = Ok(BlocksFound::V0(BlocksFoundV0 { found, missing }));
fsm.lock()
.await
.send_in_reply_to(res.into(), self.id())
.await?;
Ok(())
}
}

@ -0,0 +1,132 @@
/*
* Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
use std::sync::Arc;
use async_recursion::async_recursion;
use async_std::sync::RwLock;
use async_std::sync::{Mutex, MutexGuard};
use ng_repo::errors::*;
use ng_repo::log::*;
use ng_repo::types::{Block, BlockId, OverlayId};
use crate::broker::BROKER;
use crate::connection::NoiseFSM;
use crate::server_broker::IServerBroker;
use crate::types::*;
use crate::{actor::*, types::ProtocolMessage};
impl BlocksGet {
pub fn get_actor(&self, id: i64) -> Box<dyn EActor> {
Actor::<BlocksGet, Block>::new_responder(id)
}
pub fn overlay(&self) -> &OverlayId {
match self {
Self::V0(v0) => v0.overlay.as_ref().unwrap(),
}
}
pub fn set_overlay(&mut self, overlay: OverlayId) {
match self {
Self::V0(v0) => v0.overlay = Some(overlay),
}
}
}
impl TryFrom<ProtocolMessage> for BlocksGet {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
let req: ClientRequestContentV0 = msg.try_into()?;
if let ClientRequestContentV0::BlocksGet(a) = req {
Ok(a)
} else {
log_debug!("INVALID {:?}", req);
Err(ProtocolError::InvalidValue)
}
}
}
impl From<BlocksGet> for ProtocolMessage {
fn from(msg: BlocksGet) -> ProtocolMessage {
let overlay = *msg.overlay();
ProtocolMessage::from_client_request_v0(ClientRequestContentV0::BlocksGet(msg), overlay)
}
}
impl Actor<'_, BlocksGet, Block> {}
#[async_trait::async_trait]
impl EActor for Actor<'_, BlocksGet, Block> {
async fn respond(
&mut self,
msg: ProtocolMessage,
fsm: Arc<Mutex<NoiseFSM>>,
) -> Result<(), ProtocolError> {
let req = BlocksGet::try_from(msg)?;
let server = { BROKER.read().await.get_server_broker()? };
let mut lock = fsm.lock().await;
let mut something_was_sent = false;
#[async_recursion]
async fn process_children(
children: &Vec<BlockId>,
server: &RwLock<dyn IServerBroker + Send + Sync>,
overlay: &OverlayId,
lock: &mut MutexGuard<'_, NoiseFSM>,
req_id: i64,
include_children: bool,
something_was_sent: &mut bool,
) {
for block_id in children {
if let Ok(block) = { server.read().await.get_block(overlay, block_id) } {
let grand_children = block.children().to_vec();
if let Err(_) = lock.send_in_reply_to(block.into(), req_id).await {
break;
}
*something_was_sent = true;
if include_children {
process_children(
&grand_children,
server,
overlay,
lock,
req_id,
include_children,
something_was_sent,
)
.await;
}
}
}
}
process_children(
req.ids(),
&server,
req.overlay(),
&mut lock,
self.id(),
req.include_children(),
&mut something_was_sent,
)
.await;
if !something_was_sent {
let re: Result<(), ServerError> = Err(ServerError::NotFound);
lock.send_in_reply_to(re.into(), self.id()).await?;
} else {
let re: Result<(), ServerError> = Err(ServerError::EndOfStream);
lock.send_in_reply_to(re.into(), self.id()).await?;
}
Ok(())
}
}

@ -0,0 +1,81 @@
/*
* Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
use std::sync::Arc;
use async_std::sync::Mutex;
use ng_repo::errors::*;
use ng_repo::log::*;
use crate::broker::BROKER;
use crate::connection::NoiseFSM;
use crate::types::*;
use crate::{actor::*, types::ProtocolMessage};
impl BlocksPut {
pub fn get_actor(&self, id: i64) -> Box<dyn EActor> {
Actor::<BlocksPut, ()>::new_responder(id)
}
}
impl TryFrom<ProtocolMessage> for BlocksPut {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
let req: ClientRequestContentV0 = msg.try_into()?;
if let ClientRequestContentV0::BlocksPut(a) = req {
Ok(a)
} else {
log_debug!("INVALID {:?}", req);
Err(ProtocolError::InvalidValue)
}
}
}
impl From<BlocksPut> for ProtocolMessage {
fn from(msg: BlocksPut) -> ProtocolMessage {
let overlay = *msg.overlay();
ProtocolMessage::from_client_request_v0(ClientRequestContentV0::BlocksPut(msg), overlay)
}
}
impl Actor<'_, BlocksPut, ()> {}
#[async_trait::async_trait]
impl EActor for Actor<'_, BlocksPut, ()> {
async fn respond(
&mut self,
msg: ProtocolMessage,
fsm: Arc<Mutex<NoiseFSM>>,
) -> Result<(), ProtocolError> {
let req = BlocksPut::try_from(msg)?;
let sb = { BROKER.read().await.get_server_broker()? };
let mut res: Result<(), ServerError> = Ok(());
let overlay = req.overlay().clone();
match req {
BlocksPut::V0(v0) => {
for block in v0.blocks {
let r = sb.read().await.put_block(&overlay, block);
if r.is_err() {
res = r;
break;
}
}
}
}
fsm.lock()
.await
.send_in_reply_to(res.into(), self.id())
.await?;
Ok(())
}
}

@ -0,0 +1,94 @@
/*
* Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
use std::sync::Arc;
use async_std::sync::Mutex;
use ng_repo::errors::*;
use ng_repo::log::*;
use ng_repo::types::OverlayId;
use crate::broker::BROKER;
use crate::connection::NoiseFSM;
use crate::types::*;
use crate::{actor::*, types::ProtocolMessage};
impl ClientEvent {
pub fn get_actor(&self, id: i64) -> Box<dyn EActor> {
Actor::<ClientEvent, ()>::new_responder(id)
}
}
impl TryFrom<ProtocolMessage> for ClientEvent {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
if let ProtocolMessage::ClientMessage(ClientMessage::V0(ClientMessageV0 {
content: ClientMessageContentV0::ClientEvent(e),
..
})) = msg
{
Ok(e)
} else {
log_debug!("INVALID {:?}", msg);
Err(ProtocolError::InvalidValue)
}
}
}
impl From<ClientEvent> for ProtocolMessage {
fn from(e: ClientEvent) -> ProtocolMessage {
ProtocolMessage::ClientMessage(ClientMessage::V0(ClientMessageV0 {
content: ClientMessageContentV0::ClientEvent(e),
overlay: OverlayId::nil(),
padding: vec![]
}))
}
}
impl Actor<'_, ClientEvent, ()> {}
#[async_trait::async_trait]
impl EActor for Actor<'_, ClientEvent, ()> {
async fn respond(
&mut self,
msg: ProtocolMessage,
fsm: Arc<Mutex<NoiseFSM>>,
) -> Result<(), ProtocolError> {
let req = ClientEvent::try_from(msg)?;
match req {
ClientEvent::InboxPopRequest => {
let sb = { BROKER.read().await.get_server_broker()? };
let user = {fsm.lock().await.user_id()?};
let res: Result<InboxMsg, ServerError> = {
sb.read().await.inbox_pop_for_user(user).await
};
if let Ok(msg) = res {
let _ = fsm
.lock()
.await
.send(ProtocolMessage::ClientMessage(ClientMessage::V0(
ClientMessageV0 {
overlay: msg.body.to_overlay.clone(),
padding: vec![],
content: ClientMessageContentV0::InboxReceive{msg, from_queue: true},
},
)))
.await;
}
}
}
Ok(())
}
}

@ -0,0 +1,125 @@
/*
* Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
use std::sync::Arc;
use async_std::sync::Mutex;
use ng_repo::errors::*;
use ng_repo::log::*;
use ng_repo::types::{Block, OverlayId};
use crate::broker::BROKER;
use crate::connection::NoiseFSM;
use crate::types::*;
use crate::{actor::*, types::ProtocolMessage};
impl CommitGet {
pub fn get_actor(&self, id: i64) -> Box<dyn EActor> {
Actor::<CommitGet, Block>::new_responder(id)
}
pub fn overlay(&self) -> &OverlayId {
match self {
Self::V0(v0) => v0.overlay.as_ref().unwrap(),
}
}
pub fn set_overlay(&mut self, overlay: OverlayId) {
match self {
Self::V0(v0) => v0.overlay = Some(overlay),
}
}
}
impl TryFrom<ProtocolMessage> for CommitGet {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
let req: ClientRequestContentV0 = msg.try_into()?;
if let ClientRequestContentV0::CommitGet(a) = req {
Ok(a)
} else {
log_debug!("INVALID {:?}", req);
Err(ProtocolError::InvalidValue)
}
}
}
impl From<CommitGet> for ProtocolMessage {
fn from(msg: CommitGet) -> ProtocolMessage {
let overlay = *msg.overlay();
ProtocolMessage::from_client_request_v0(ClientRequestContentV0::CommitGet(msg), overlay)
}
}
impl TryFrom<ProtocolMessage> for Block {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
let res: ClientResponseContentV0 = msg.try_into()?;
if let ClientResponseContentV0::Block(a) = res {
Ok(a)
} else {
log_debug!("INVALID {:?}", res);
Err(ProtocolError::InvalidValue)
}
}
}
impl From<Block> for ProtocolMessage {
fn from(b: Block) -> ProtocolMessage {
let mut cr: ClientResponse = ClientResponseContentV0::Block(b).into();
cr.set_result(ServerError::PartialContent.into());
cr.into()
}
}
impl Actor<'_, CommitGet, Block> {}
#[async_trait::async_trait]
impl EActor for Actor<'_, CommitGet, Block> {
async fn respond(
&mut self,
msg: ProtocolMessage,
fsm: Arc<Mutex<NoiseFSM>>,
) -> Result<(), ProtocolError> {
let req = CommitGet::try_from(msg)?;
let broker = { BROKER.read().await.get_server_broker()? };
let blocks_res = { broker.read().await.get_commit(req.overlay(), req.id()) };
// IF NEEDED, the get_commit could be changed to return a stream, and then the send_in_reply_to would be also totally async
match blocks_res {
Ok(blocks) => {
if blocks.is_empty() {
let re: Result<(), ServerError> = Err(ServerError::EmptyStream);
fsm.lock()
.await
.send_in_reply_to(re.into(), self.id())
.await?;
return Ok(());
}
let mut lock = fsm.lock().await;
for block in blocks {
lock.send_in_reply_to(block.into(), self.id()).await?;
}
let re: Result<(), ServerError> = Err(ServerError::EndOfStream);
lock.send_in_reply_to(re.into(), self.id()).await?;
}
Err(e) => {
let re: Result<(), ServerError> = Err(e);
fsm.lock()
.await
.send_in_reply_to(re.into(), self.id())
.await?;
}
}
Ok(())
}
}

@ -0,0 +1,121 @@
/*
* Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
use std::sync::Arc;
use async_std::sync::Mutex;
use ng_repo::errors::*;
use ng_repo::log::*;
use ng_repo::types::*;
#[cfg(not(target_arch = "wasm32"))]
use crate::broker::BROKER;
use crate::connection::NoiseFSM;
use crate::types::*;
use crate::{actor::*, types::ProtocolMessage};
impl PublishEvent {
pub fn get_actor(&self, id: i64) -> Box<dyn EActor> {
Actor::<PublishEvent, ()>::new_responder(id)
}
pub fn new(event: Event, overlay: OverlayId) -> PublishEvent {
PublishEvent(event, Some(overlay))
}
pub fn set_overlay(&mut self, overlay: OverlayId) {
self.1 = Some(overlay);
}
pub fn overlay(&self) -> &OverlayId {
self.1.as_ref().unwrap()
}
pub fn event(&self) -> &Event {
&self.0
}
pub fn take_event(self) -> Event {
self.0
}
}
impl TryFrom<ProtocolMessage> for PublishEvent {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
let req: ClientRequestContentV0 = msg.try_into()?;
if let ClientRequestContentV0::PublishEvent(a) = req {
Ok(a)
} else {
log_debug!("INVALID {:?}", req);
Err(ProtocolError::InvalidValue)
}
}
}
impl From<PublishEvent> for ProtocolMessage {
fn from(msg: PublishEvent) -> ProtocolMessage {
let overlay = msg.1.unwrap();
ProtocolMessage::from_client_request_v0(ClientRequestContentV0::PublishEvent(msg), overlay)
}
}
impl Actor<'_, PublishEvent, ()> {}
#[async_trait::async_trait]
impl EActor for Actor<'_, PublishEvent, ()> {
async fn respond(
&mut self,
_msg: ProtocolMessage,
_fsm: Arc<Mutex<NoiseFSM>>,
) -> Result<(), ProtocolError> {
#[cfg(not(target_arch = "wasm32"))]
{
let req = PublishEvent::try_from(_msg)?;
// send a ProtocolError if invalid signatures (will disconnect the client)
req.event().verify()?;
let overlay = req.overlay().clone();
let (user_id, remote_peer) = {
let fsm = _fsm.lock().await;
(
fsm.user_id()?,
fsm.remote_peer().ok_or(ProtocolError::ActorError)?,
)
};
let res = {
let broker = BROKER.read().await;
broker
.dispatch_event(&overlay, req.take_event(), &user_id, &remote_peer)
.await
};
if res.is_err() {
let res: Result<(), ServerError> = Err(res.unwrap_err());
_fsm.lock()
.await
.send_in_reply_to(res.into(), self.id())
.await?;
} else {
let broker = { BROKER.read().await.get_server_broker()? };
for client in res.unwrap() {
broker
.read()
.await
.remove_all_subscriptions_of_client(&client)
.await;
}
let finalres: Result<(), ServerError> = Ok(());
_fsm.lock()
.await
.send_in_reply_to(finalres.into(), self.id())
.await?;
}
}
Ok(())
}
}

@ -0,0 +1,74 @@
/*
* Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
use std::sync::Arc;
use async_std::sync::Mutex;
use ng_repo::errors::*;
use ng_repo::log::*;
use ng_repo::types::OverlayId;
use crate::broker::BROKER;
use crate::connection::NoiseFSM;
use crate::types::*;
use crate::{actor::*, types::ProtocolMessage};
impl InboxPost {
pub fn get_actor(&self, id: i64) -> Box<dyn EActor> {
Actor::<InboxPost, ()>::new_responder(id)
}
}
impl TryFrom<ProtocolMessage> for InboxPost {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
let req: ClientRequestContentV0 = msg.try_into()?;
if let ClientRequestContentV0::InboxPost(a) = req {
Ok(a)
} else {
log_debug!("INVALID {:?}", req);
Err(ProtocolError::InvalidValue)
}
}
}
impl From<InboxPost> for ProtocolMessage {
fn from(msg: InboxPost) -> ProtocolMessage {
ProtocolMessage::from_client_request_v0(
ClientRequestContentV0::InboxPost(msg),
OverlayId::nil(),
)
}
}
impl Actor<'_, InboxPost, ()> {}
#[async_trait::async_trait]
impl EActor for Actor<'_, InboxPost, ()> {
async fn respond(
&mut self,
msg: ProtocolMessage,
fsm: Arc<Mutex<NoiseFSM>>,
) -> Result<(), ProtocolError> {
let req = InboxPost::try_from(msg)?;
let sb = { BROKER.read().await.get_server_broker()? };
let res: Result<(), ServerError> = sb
.read()
.await.inbox_post(req).await;
fsm.lock()
.await
.send_in_reply_to(res.into(), self.id())
.await?;
Ok(())
}
}

@ -0,0 +1,91 @@
/*
* Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
use std::sync::Arc;
use async_std::sync::Mutex;
use ng_repo::errors::*;
use ng_repo::log::*;
use ng_repo::types::OverlayId;
use ng_repo::utils::verify;
use crate::broker::BROKER;
use crate::connection::NoiseFSM;
use crate::types::*;
use crate::{actor::*, types::ProtocolMessage};
impl InboxRegister {
pub fn get_actor(&self, id: i64) -> Box<dyn EActor> {
Actor::<InboxRegister, ()>::new_responder(id)
}
}
impl TryFrom<ProtocolMessage> for InboxRegister {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
let req: ClientRequestContentV0 = msg.try_into()?;
if let ClientRequestContentV0::InboxRegister(a) = req {
Ok(a)
} else {
log_debug!("INVALID {:?}", req);
Err(ProtocolError::InvalidValue)
}
}
}
impl From<InboxRegister> for ProtocolMessage {
fn from(msg: InboxRegister) -> ProtocolMessage {
ProtocolMessage::from_client_request_v0(
ClientRequestContentV0::InboxRegister(msg),
OverlayId::nil(),
)
}
}
impl Actor<'_, InboxRegister, ()> {}
#[async_trait::async_trait]
impl EActor for Actor<'_, InboxRegister, ()> {
async fn respond(
&mut self,
msg: ProtocolMessage,
fsm: Arc<Mutex<NoiseFSM>>,
) -> Result<(), ProtocolError> {
let req = InboxRegister::try_from(msg)?;
// verify registration
if verify(&req.challenge, req.sig, req.inbox_id).is_err() {
fsm.lock()
.await
.send_in_reply_to(Result::<(), _>::Err(ServerError::InvalidSignature).into(), self.id())
.await?;
return Ok(())
}
let sb = { BROKER.read().await.get_server_broker()? };
let user_id = {
let fsm = fsm.lock().await;
fsm.user_id()?
};
let res: Result<(), ServerError> = sb
.read()
.await.inbox_register(user_id, req);
fsm.lock()
.await
.send_in_reply_to(res.into(), self.id())
.await?;
Ok(())
}
}

@ -0,0 +1,25 @@
pub mod repo_pin_status;
pub mod pin_repo;
pub mod topic_sub;
pub mod event;
pub mod commit_get;
pub mod topic_sync_req;
pub mod blocks_put;
pub mod blocks_exist;
pub mod blocks_get;
pub mod wallet_put_export;
pub mod inbox_post;
pub mod inbox_register;
pub mod client_event;

@ -0,0 +1,230 @@
/*
* Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
use std::sync::Arc;
use async_std::sync::Mutex;
use ng_repo::errors::*;
use ng_repo::log::*;
use ng_repo::repo::Repo;
use ng_repo::types::*;
use crate::broker::BROKER;
use crate::connection::NoiseFSM;
use crate::types::*;
use crate::{actor::*, types::ProtocolMessage};
impl PinRepo {
pub fn get_actor(&self, id: i64) -> Box<dyn EActor> {
Actor::<PinRepo, RepoOpened>::new_responder(id)
}
pub fn for_branch(repo: &Repo, branch: &BranchId, broker_id: &DirectPeerId) -> PinRepo {
let overlay = OverlayAccess::new_write_access_from_store(&repo.store);
let mut rw_topics = Vec::with_capacity(1);
let mut ro_topics = vec![];
let branch = repo.branches.get(branch).unwrap();
if let Some(privkey) = &branch.topic_priv_key {
rw_topics.push(PublisherAdvert::new(
branch.topic.unwrap(),
privkey.clone(),
*broker_id,
));
} else {
ro_topics.push(branch.topic.unwrap());
}
PinRepo::V0(PinRepoV0 {
hash: repo.id.into(),
overlay,
// TODO: overlay_root_topic
overlay_root_topic: None,
expose_outer: false,
peers: vec![],
max_peer_count: 0,
//allowed_peers: vec![],
ro_topics,
rw_topics,
})
}
pub fn from_repo(repo: &Repo, broker_id: &DirectPeerId) -> PinRepo {
let overlay = OverlayAccess::new_write_access_from_store(&repo.store);
let mut rw_topics = Vec::with_capacity(repo.branches.len());
let mut ro_topics = vec![];
for (_, branch) in repo.branches.iter() {
if let Some(privkey) = &branch.topic_priv_key {
rw_topics.push(PublisherAdvert::new(
branch.topic.unwrap(),
privkey.clone(),
*broker_id,
));
} else {
ro_topics.push(branch.topic.unwrap());
}
}
PinRepo::V0(PinRepoV0 {
hash: repo.id.into(),
overlay,
// TODO: overlay_root_topic
overlay_root_topic: None,
expose_outer: false,
peers: vec![],
max_peer_count: 0,
//allowed_peers: vec![],
ro_topics,
rw_topics,
})
}
}
impl TryFrom<ProtocolMessage> for PinRepo {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
let req: ClientRequestContentV0 = msg.try_into()?;
if let ClientRequestContentV0::PinRepo(a) = req {
Ok(a)
} else {
log_debug!("INVALID {:?}", req);
Err(ProtocolError::InvalidValue)
}
}
}
impl From<PinRepo> for ProtocolMessage {
fn from(msg: PinRepo) -> ProtocolMessage {
let overlay = match msg {
PinRepo::V0(ref v0) => v0.overlay.overlay_id_for_client_protocol_purpose().clone(),
};
ProtocolMessage::from_client_request_v0(ClientRequestContentV0::PinRepo(msg), overlay)
}
}
impl TryFrom<ProtocolMessage> for RepoOpened {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
let res: ClientResponseContentV0 = msg.try_into()?;
if let ClientResponseContentV0::RepoOpened(a) = res {
Ok(a)
} else {
log_debug!("INVALID {:?}", res);
Err(ProtocolError::InvalidValue)
}
}
}
impl From<RepoOpened> for ProtocolMessage {
fn from(res: RepoOpened) -> ProtocolMessage {
ClientResponseContentV0::RepoOpened(res).into()
}
}
impl Actor<'_, RepoPinStatusReq, RepoPinStatus> {}
#[async_trait::async_trait]
impl EActor for Actor<'_, PinRepo, RepoOpened> {
async fn respond(
&mut self,
msg: ProtocolMessage,
fsm: Arc<Mutex<NoiseFSM>>,
) -> Result<(), ProtocolError> {
let req = PinRepo::try_from(msg)?;
let (sb, server_peer_id) = {
let b = BROKER.read().await;
(b.get_server_broker()?, b.get_server_peer_id())
};
// check the validity of the PublisherAdvert(s). this will return a ProtocolError (will close the connection)
for pub_ad in req.rw_topics() {
pub_ad.verify_for_broker(&server_peer_id)?;
}
let (user_id, remote_peer) = {
let fsm = fsm.lock().await;
(fsm.user_id()?, fsm.get_client_peer_id()?)
};
let result = {
match req.overlay_access() {
OverlayAccess::ReadOnly(r) => {
if r.is_inner()
|| req.overlay() != r
|| req.rw_topics().len() > 0
|| req.overlay_root_topic().is_some()
{
Err(ServerError::InvalidRequest)
} else {
sb.read()
.await
.pin_repo_read(
req.overlay(),
req.hash(),
&user_id,
req.ro_topics(),
&remote_peer,
)
.await
}
}
OverlayAccess::ReadWrite((w, r)) => {
if req.overlay() != w
|| !w.is_inner()
|| r.is_inner()
|| req.expose_outer() && req.rw_topics().is_empty()
{
// we do not allow to expose_outer if not a publisher for at least one topic
// TODO add a check on "|| overlay_root_topic.is_none()" because it should be mandatory to have one (not sent by client at the moment)
Err(ServerError::InvalidRequest)
} else {
sb.read()
.await
.pin_repo_write(
req.overlay_access(),
req.hash(),
&user_id,
req.ro_topics(),
req.rw_topics(),
req.overlay_root_topic(),
req.expose_outer(),
&remote_peer,
)
.await
}
}
OverlayAccess::WriteOnly(w) => {
if !w.is_inner() || req.overlay() != w || req.expose_outer() {
Err(ServerError::InvalidRequest)
} else {
sb.read()
.await
.pin_repo_write(
req.overlay_access(),
req.hash(),
&user_id,
req.ro_topics(),
req.rw_topics(),
req.overlay_root_topic(),
false,
&remote_peer,
)
.await
}
}
}
};
fsm.lock()
.await
.send_in_reply_to(result.into(), self.id())
.await?;
Ok(())
}
}

@ -0,0 +1,96 @@
/*
* Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
use std::sync::Arc;
use async_std::sync::Mutex;
use ng_repo::errors::*;
use ng_repo::log::*;
use crate::broker::BROKER;
use crate::connection::NoiseFSM;
use crate::types::*;
use crate::{actor::*, types::ProtocolMessage};
impl RepoPinStatusReq {
pub fn get_actor(&self, id: i64) -> Box<dyn EActor> {
Actor::<RepoPinStatusReq, RepoPinStatus>::new_responder(id)
}
}
impl TryFrom<ProtocolMessage> for RepoPinStatusReq {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
let req: ClientRequestContentV0 = msg.try_into()?;
if let ClientRequestContentV0::RepoPinStatusReq(a) = req {
Ok(a)
} else {
log_debug!("INVALID {:?}", req);
Err(ProtocolError::InvalidValue)
}
}
}
impl From<RepoPinStatusReq> for ProtocolMessage {
fn from(msg: RepoPinStatusReq) -> ProtocolMessage {
let overlay = *msg.overlay();
ProtocolMessage::from_client_request_v0(
ClientRequestContentV0::RepoPinStatusReq(msg),
overlay,
)
}
}
impl TryFrom<ProtocolMessage> for RepoPinStatus {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
let res: ClientResponseContentV0 = msg.try_into()?;
if let ClientResponseContentV0::RepoPinStatus(a) = res {
Ok(a)
} else {
log_debug!("INVALID {:?}", res);
Err(ProtocolError::InvalidValue)
}
}
}
impl From<RepoPinStatus> for ProtocolMessage {
fn from(res: RepoPinStatus) -> ProtocolMessage {
ClientResponseContentV0::RepoPinStatus(res).into()
}
}
impl Actor<'_, RepoPinStatusReq, RepoPinStatus> {}
#[async_trait::async_trait]
impl EActor for Actor<'_, RepoPinStatusReq, RepoPinStatus> {
async fn respond(
&mut self,
msg: ProtocolMessage,
fsm: Arc<Mutex<NoiseFSM>>,
) -> Result<(), ProtocolError> {
let req = RepoPinStatusReq::try_from(msg)?;
let sb = { BROKER.read().await.get_server_broker()? };
let res = {
sb.read().await.get_repo_pin_status(
req.overlay(),
req.hash(),
&fsm.lock().await.user_id()?,
)
};
fsm.lock()
.await
.send_in_reply_to(res.into(), self.id())
.await?;
Ok(())
}
}

@ -0,0 +1,139 @@
/*
* Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
use std::sync::Arc;
use async_std::sync::Mutex;
use ng_repo::errors::*;
use ng_repo::log::*;
use ng_repo::repo::{BranchInfo, Repo};
use ng_repo::types::*;
use crate::broker::BROKER;
use crate::connection::NoiseFSM;
use crate::types::*;
use crate::{actor::*, types::ProtocolMessage};
impl TopicSub {
pub fn get_actor(&self, id: i64) -> Box<dyn EActor> {
Actor::<TopicSub, TopicSubRes>::new_responder(id)
}
/// only set broker_id if you want to be a publisher
pub fn new(repo: &Repo, branch: &BranchInfo, broker_id: Option<&DirectPeerId>) -> TopicSub {
let (overlay, publisher) = if broker_id.is_some() && branch.topic_priv_key.is_some() {
(
repo.store.inner_overlay(),
Some(PublisherAdvert::new(
branch.topic.unwrap(),
branch.topic_priv_key.to_owned().unwrap(),
*broker_id.unwrap(),
)),
)
} else {
(repo.store.inner_overlay(), None)
};
TopicSub::V0(TopicSubV0 {
repo_hash: repo.id.into(),
overlay: Some(overlay),
topic: branch.topic.unwrap(),
publisher,
})
}
}
impl TryFrom<ProtocolMessage> for TopicSub {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
let req: ClientRequestContentV0 = msg.try_into()?;
if let ClientRequestContentV0::TopicSub(a) = req {
Ok(a)
} else {
log_debug!("INVALID {:?}", req);
Err(ProtocolError::InvalidValue)
}
}
}
impl From<TopicSub> for ProtocolMessage {
fn from(msg: TopicSub) -> ProtocolMessage {
let overlay = *msg.overlay();
ProtocolMessage::from_client_request_v0(ClientRequestContentV0::TopicSub(msg), overlay)
}
}
impl TryFrom<ProtocolMessage> for TopicSubRes {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
let res: ClientResponseContentV0 = msg.try_into()?;
if let ClientResponseContentV0::TopicSubRes(a) = res {
Ok(a)
} else {
log_debug!("INVALID {:?}", res);
Err(ProtocolError::InvalidValue)
}
}
}
impl From<TopicSubRes> for ProtocolMessage {
fn from(res: TopicSubRes) -> ProtocolMessage {
ClientResponseContentV0::TopicSubRes(res).into()
}
}
impl Actor<'_, TopicSub, TopicSubRes> {}
#[async_trait::async_trait]
impl EActor for Actor<'_, TopicSub, TopicSubRes> {
async fn respond(
&mut self,
msg: ProtocolMessage,
fsm: Arc<Mutex<NoiseFSM>>,
) -> Result<(), ProtocolError> {
let req = TopicSub::try_from(msg)?;
let (sb, server_peer_id) = {
let b = BROKER.read().await;
(b.get_server_broker()?, b.get_server_peer_id())
};
// check the validity of the PublisherAdvert. this will return a ProtocolError (will close the connection)
if let Some(advert) = req.publisher() {
advert.verify_for_broker(&server_peer_id)?;
}
let (user_id, remote_peer) = {
let fsm = fsm.lock().await;
(fsm.user_id()?, fsm.get_client_peer_id()?)
};
let res = {
sb.read()
.await
.topic_sub(
req.overlay(),
req.hash(),
req.topic(),
&user_id,
req.publisher(),
&remote_peer,
)
.await
};
fsm.lock()
.await
.send_in_reply_to(res.into(), self.id())
.await?;
Ok(())
}
}

@ -0,0 +1,151 @@
/*
* Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
use std::sync::Arc;
use async_std::sync::Mutex;
use ng_repo::errors::*;
use ng_repo::log::*;
use ng_repo::repo::Repo;
use ng_repo::types::*;
use crate::broker::BROKER;
use crate::connection::NoiseFSM;
use crate::types::*;
use crate::{actor::*, types::ProtocolMessage};
impl TopicSyncReq {
pub fn get_actor(&self, id: i64) -> Box<dyn EActor> {
Actor::<TopicSyncReq, TopicSyncRes>::new_responder(id)
}
pub fn new_empty(topic: TopicId, overlay: &OverlayId) -> Self {
TopicSyncReq::V0(TopicSyncReqV0 {
topic,
known_heads: vec![],
target_heads: vec![],
overlay: Some(*overlay),
known_commits: None,
})
}
pub fn new(
repo: &Repo,
topic_id: &TopicId,
known_heads: Vec<ObjectId>,
target_heads: Vec<ObjectId>,
known_commits: Option<BloomFilter>,
) -> TopicSyncReq {
TopicSyncReq::V0(TopicSyncReqV0 {
topic: *topic_id,
known_heads,
target_heads,
overlay: Some(repo.store.get_store_repo().overlay_id_for_read_purpose()),
known_commits,
})
}
}
impl TryFrom<ProtocolMessage> for TopicSyncReq {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
let req: ClientRequestContentV0 = msg.try_into()?;
if let ClientRequestContentV0::TopicSyncReq(a) = req {
Ok(a)
} else {
log_debug!("INVALID {:?}", req);
Err(ProtocolError::InvalidValue)
}
}
}
impl From<TopicSyncReq> for ProtocolMessage {
fn from(msg: TopicSyncReq) -> ProtocolMessage {
let overlay = *msg.overlay();
ProtocolMessage::from_client_request_v0(ClientRequestContentV0::TopicSyncReq(msg), overlay)
}
}
impl TryFrom<ProtocolMessage> for TopicSyncRes {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
let res: ClientResponseContentV0 = msg.try_into()?;
if let ClientResponseContentV0::TopicSyncRes(a) = res {
Ok(a)
} else {
log_debug!("INVALID {:?}", res);
Err(ProtocolError::InvalidValue)
}
}
}
impl From<TopicSyncRes> for ProtocolMessage {
fn from(b: TopicSyncRes) -> ProtocolMessage {
let mut cr: ClientResponse = ClientResponseContentV0::TopicSyncRes(b).into();
cr.set_result(ServerError::PartialContent.into());
cr.into()
}
}
impl Actor<'_, TopicSyncReq, TopicSyncRes> {}
#[async_trait::async_trait]
impl EActor for Actor<'_, TopicSyncReq, TopicSyncRes> {
async fn respond(
&mut self,
msg: ProtocolMessage,
fsm: Arc<Mutex<NoiseFSM>>,
) -> Result<(), ProtocolError> {
let req = TopicSyncReq::try_from(msg)?;
let sb = { BROKER.read().await.get_server_broker()? };
let res = {
sb.read().await.topic_sync_req(
req.overlay(),
req.topic(),
req.known_heads(),
req.target_heads(),
req.known_commits(),
)
};
// IF NEEDED, the topic_sync_req could be changed to return a stream, and then the send_in_reply_to would be also totally async
match res {
Ok(blocks) => {
if blocks.is_empty() {
let re: Result<(), ServerError> = Err(ServerError::EmptyStream);
fsm.lock()
.await
.send_in_reply_to(re.into(), self.id())
.await?;
return Ok(());
}
let mut lock = fsm.lock().await;
for block in blocks {
lock.send_in_reply_to(block.into(), self.id()).await?;
}
let re: Result<(), ServerError> = Err(ServerError::EndOfStream);
lock.send_in_reply_to(re.into(), self.id()).await?;
}
Err(e) => {
let re: Result<(), ServerError> = Err(e);
fsm.lock()
.await
.send_in_reply_to(re.into(), self.id())
.await?;
}
}
Ok(())
}
}

@ -0,0 +1,89 @@
/*
* Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
use std::sync::Arc;
use async_std::sync::Mutex;
use ng_repo::errors::*;
use ng_repo::log::*;
use ng_repo::types::OverlayId;
use crate::broker::BROKER;
use crate::connection::NoiseFSM;
use crate::types::*;
use crate::{actor::*, types::ProtocolMessage};
impl WalletPutExport {
pub fn get_actor(&self, id: i64) -> Box<dyn EActor> {
Actor::<WalletPutExport, ()>::new_responder(id)
}
}
impl TryFrom<ProtocolMessage> for WalletPutExport {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
let req: ClientRequestContentV0 = msg.try_into()?;
if let ClientRequestContentV0::WalletPutExport(a) = req {
Ok(a)
} else {
log_debug!("INVALID {:?}", req);
Err(ProtocolError::InvalidValue)
}
}
}
impl From<WalletPutExport> for ProtocolMessage {
fn from(msg: WalletPutExport) -> ProtocolMessage {
ProtocolMessage::from_client_request_v0(
ClientRequestContentV0::WalletPutExport(msg),
OverlayId::nil(),
)
}
}
impl Actor<'_, WalletPutExport, ()> {}
#[async_trait::async_trait]
impl EActor for Actor<'_, WalletPutExport, ()> {
async fn respond(
&mut self,
msg: ProtocolMessage,
fsm: Arc<Mutex<NoiseFSM>>,
) -> Result<(), ProtocolError> {
let req = WalletPutExport::try_from(msg)?;
let sb = { BROKER.read().await.get_server_broker()? };
let mut res: Result<(), ServerError> = Ok(());
match req {
WalletPutExport::V0(v0) => {
if v0.is_rendezvous {
res = sb
.read()
.await
.put_wallet_at_rendezvous(v0.rendezvous_id, v0.wallet)
.await;
} else {
sb.read()
.await
.put_wallet_export(v0.rendezvous_id, v0.wallet)
.await;
}
}
}
fsm.lock()
.await
.send_in_reply_to(res.into(), self.id())
.await?;
Ok(())
}
}

@ -0,0 +1,43 @@
/*
* Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
use std::sync::Arc;
use async_std::sync::Mutex;
use serde::{Deserialize, Serialize};
use ng_repo::errors::*;
use crate::connection::NoiseFSM;
use crate::{actor::*, types::ProtocolMessage};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Connecting();
impl From<Connecting> for ProtocolMessage {
fn from(_msg: Connecting) -> ProtocolMessage {
unimplemented!();
}
}
impl Actor<'_, Connecting, ()> {}
#[async_trait::async_trait]
impl EActor for Actor<'_, Connecting, ()> {
async fn respond(
&mut self,
_msg: ProtocolMessage,
fsm: Arc<Mutex<NoiseFSM>>,
) -> Result<(), ProtocolError> {
fsm.lock().await.remove_actor(0).await;
Ok(())
}
}

@ -0,0 +1,103 @@
/*
* Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
use std::sync::Arc;
use async_std::sync::Mutex;
use ng_repo::errors::*;
use ng_repo::log::*;
use ng_repo::object::Object;
use ng_repo::store::Store;
use ng_repo::types::Block;
use super::super::StartProtocol;
use crate::broker::BROKER;
use crate::connection::NoiseFSM;
use crate::types::*;
use crate::{actor::*, types::ProtocolMessage};
impl ExtObjectGetV0 {
pub fn get_actor(&self) -> Box<dyn EActor> {
Actor::<ExtObjectGetV0, Vec<Block>>::new_responder(0)
}
}
impl TryFrom<ProtocolMessage> for ExtObjectGetV0 {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
if let ProtocolMessage::Start(StartProtocol::Ext(ExtRequest::V0(ExtRequestV0 {
content: ExtRequestContentV0::ExtObjectGet(a),
..
}))) = msg
{
Ok(a)
} else {
log_debug!("INVALID {:?}", msg);
Err(ProtocolError::InvalidValue)
}
}
}
impl From<ExtObjectGetV0> for ProtocolMessage {
fn from(_msg: ExtObjectGetV0) -> ProtocolMessage {
unimplemented!();
}
}
impl From<ExtObjectGetV0> for ExtRequestContentV0 {
fn from(msg: ExtObjectGetV0) -> ExtRequestContentV0 {
ExtRequestContentV0::ExtObjectGet(msg)
}
}
impl TryFrom<ProtocolMessage> for Vec<Block> {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Vec<Block>, Self::Error> {
let content: ExtResponseContentV0 = msg.try_into()?;
if let ExtResponseContentV0::Blocks(res) = content {
Ok(res)
} else {
Err(ProtocolError::InvalidValue)
}
}
}
impl Actor<'_, ExtObjectGetV0, Vec<Block>> {}
#[async_trait::async_trait]
impl EActor for Actor<'_, ExtObjectGetV0, Vec<Block>> {
async fn respond(
&mut self,
msg: ProtocolMessage,
fsm: Arc<Mutex<NoiseFSM>>,
) -> Result<(), ProtocolError> {
let req = ExtObjectGetV0::try_from(msg)?;
let sb = {
let broker = BROKER.read().await;
broker.get_server_broker()?
};
let lock = sb.read().await;
let store = Store::new_from_overlay_id(&req.overlay, lock.get_block_storage());
let mut blocks = Vec::new();
for obj_id in req.ids {
// TODO: deal with RandomAccessFiles (or is it just working?)
if let Ok(obj) = Object::load_without_header(obj_id, None, &store) {
blocks.append(&mut obj.into_blocks());
//TODO: load the obj.files too (if req.include_files)
}
}
let response: ExtResponseV0 = Ok(ExtResponseContentV0::Blocks(blocks)).into();
fsm.lock().await.send(response.into()).await?;
Ok(())
}
}

@ -0,0 +1,3 @@
pub mod wallet_get_export;
pub mod get;

@ -0,0 +1,109 @@
/*
* Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
use std::sync::Arc;
use async_std::stream::StreamExt;
use async_std::sync::Mutex;
use ng_repo::errors::*;
use ng_repo::log::*;
use super::super::StartProtocol;
use crate::broker::BROKER;
use crate::connection::NoiseFSM;
use crate::types::*;
use crate::{actor::*, types::ProtocolMessage};
impl ExtWalletGetExportV0 {
pub fn get_actor(&self) -> Box<dyn EActor> {
Actor::<ExtWalletGetExportV0, ExportedWallet>::new_responder(0)
}
}
impl TryFrom<ProtocolMessage> for ExtWalletGetExportV0 {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
if let ProtocolMessage::Start(StartProtocol::Ext(ExtRequest::V0(ExtRequestV0 {
content: ExtRequestContentV0::WalletGetExport(a),
..
}))) = msg
{
Ok(a)
} else {
log_debug!("INVALID {:?}", msg);
Err(ProtocolError::InvalidValue)
}
}
}
impl From<ExtWalletGetExportV0> for ProtocolMessage {
fn from(_msg: ExtWalletGetExportV0) -> ProtocolMessage {
unimplemented!();
}
}
impl From<ExtWalletGetExportV0> for ExtRequestContentV0 {
fn from(msg: ExtWalletGetExportV0) -> ExtRequestContentV0 {
ExtRequestContentV0::WalletGetExport(msg)
}
}
impl TryFrom<ProtocolMessage> for ExportedWallet {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<ExportedWallet, Self::Error> {
let content: ExtResponseContentV0 = msg.try_into()?;
if let ExtResponseContentV0::Wallet(res) = content {
Ok(res)
} else {
Err(ProtocolError::InvalidValue)
}
}
}
impl Actor<'_, ExtWalletGetExportV0, ExportedWallet> {}
#[async_trait::async_trait]
impl EActor for Actor<'_, ExtWalletGetExportV0, ExportedWallet> {
async fn respond(
&mut self,
msg: ProtocolMessage,
fsm: Arc<Mutex<NoiseFSM>>,
) -> Result<(), ProtocolError> {
let req = ExtWalletGetExportV0::try_from(msg)?;
let result = if req.is_rendezvous {
let mut receiver = {
let broker = BROKER.read().await;
let sb = broker.get_server_broker()?;
let lock = sb.read().await;
lock.wait_for_wallet_at_rendezvous(req.id).await
};
match receiver.next().await {
None => Err(ServerError::BrokerError),
Some(Err(e)) => Err(e),
Some(Ok(w)) => Ok(ExtResponseContentV0::Wallet(w)),
}
} else {
{
let broker = BROKER.read().await;
let sb = broker.get_server_broker()?;
let lock = sb.read().await;
lock.get_wallet_export(req.id).await
}
.map(|wallet| ExtResponseContentV0::Wallet(wallet))
};
let response: ExtResponseV0 = result.into();
fsm.lock().await.send(response.into()).await?;
Ok(())
}
}

@ -0,0 +1,25 @@
//! List of actors, each one for a specific Protocol message
#[doc(hidden)]
pub mod noise;
pub use noise::*;
#[doc(hidden)]
pub mod start;
pub use start::*;
#[doc(hidden)]
pub mod probe;
pub use probe::*;
#[doc(hidden)]
pub mod connecting;
pub use connecting::*;
pub mod client;
pub mod admin;
pub mod app;
pub mod ext;

@ -0,0 +1,69 @@
/*
* Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
use std::sync::Arc;
use async_std::sync::Mutex;
use serde::{Deserialize, Serialize};
use ng_repo::errors::*;
use crate::{actor::*, connection::NoiseFSM, types::ProtocolMessage};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct NoiseV0 {
// contains the handshake messages or the encrypted content of a ProtocolMessage
#[serde(with = "serde_bytes")]
pub data: Vec<u8>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Noise {
V0(NoiseV0),
}
impl Noise {
pub fn data(&self) -> &[u8] {
match self {
Noise::V0(v0) => v0.data.as_slice(),
}
}
}
impl From<Noise> for ProtocolMessage {
fn from(msg: Noise) -> ProtocolMessage {
ProtocolMessage::Noise(msg)
}
}
impl TryFrom<ProtocolMessage> for Noise {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
if let ProtocolMessage::Noise(n) = msg {
Ok(n)
} else {
Err(ProtocolError::InvalidValue)
}
}
}
impl Actor<'_, Noise, Noise> {}
#[async_trait::async_trait]
impl EActor for Actor<'_, Noise, Noise> {
async fn respond(
&mut self,
_msg: ProtocolMessage,
_fsm: Arc<Mutex<NoiseFSM>>,
) -> Result<(), ProtocolError> {
Ok(())
}
}

@ -0,0 +1,73 @@
/*
* Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
use std::sync::Arc;
use async_std::sync::Mutex;
use serde::{Deserialize, Serialize};
use ng_repo::errors::*;
use crate::connection::NoiseFSM;
use crate::types::{ProbeResponse, MAGIC_NG_REQUEST};
use crate::{actor::*, types::ProtocolMessage};
/// Send to probe if the server is a NextGraph broker.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Probe {}
impl TryFrom<ProtocolMessage> for ProbeResponse {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
if let ProtocolMessage::ProbeResponse(res) = msg {
Ok(res)
} else {
Err(ProtocolError::InvalidValue)
}
}
}
impl TryFrom<ProtocolMessage> for Probe {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
if let ProtocolMessage::Probe(magic) = msg {
if magic == MAGIC_NG_REQUEST {
Ok(Probe {})
} else {
Err(ProtocolError::InvalidValue)
}
} else {
Err(ProtocolError::InvalidValue)
}
}
}
impl From<Probe> for ProtocolMessage {
fn from(_msg: Probe) -> ProtocolMessage {
ProtocolMessage::Probe(MAGIC_NG_REQUEST)
}
}
impl Actor<'_, Probe, ProbeResponse> {}
#[async_trait::async_trait]
impl EActor for Actor<'_, Probe, ProbeResponse> {
async fn respond(
&mut self,
msg: ProtocolMessage,
_fsm: Arc<Mutex<NoiseFSM>>,
) -> Result<(), ProtocolError> {
let _req = Probe::try_from(msg)?;
//let res = ProbeResponse();
//fsm.lock().await.send(res.into()).await?;
Ok(())
}
}

@ -0,0 +1,326 @@
/*
* Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
use std::any::{Any, TypeId};
use std::sync::Arc;
use async_std::sync::Mutex;
use serde::{Deserialize, Serialize};
use ng_repo::errors::*;
use ng_repo::log::*;
use ng_repo::types::UserId;
use crate::actors::noise::Noise;
use crate::connection::NoiseFSM;
use crate::types::{
AdminRequest, ClientInfo, CoreBrokerConnect, CoreBrokerConnectResponse, CoreMessage,
CoreMessageV0, CoreResponse, CoreResponseContentV0, CoreResponseV0, ExtRequest,
};
use crate::{actor::*, types::ProtocolMessage};
// pub struct Noise3(Noise);
/// Start chosen protocol
/// First message sent by the connecting peer
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum StartProtocol {
Client(ClientHello),
Ext(ExtRequest),
Core(CoreHello),
Admin(AdminRequest),
App(AppHello),
AppResponse(AppHelloResponse),
}
impl StartProtocol {
pub fn type_id(&self) -> TypeId {
match self {
StartProtocol::Client(a) => a.type_id(),
StartProtocol::Core(a) => a.type_id(),
StartProtocol::Ext(a) => a.type_id(),
StartProtocol::Admin(a) => a.type_id(),
StartProtocol::App(a) => a.type_id(),
StartProtocol::AppResponse(a) => a.type_id(),
}
}
pub fn get_actor(&self) -> Box<dyn EActor> {
match self {
StartProtocol::Client(a) => a.get_actor(),
StartProtocol::Core(a) => a.get_actor(),
StartProtocol::Ext(a) => a.get_actor(),
StartProtocol::Admin(a) => a.get_actor(),
StartProtocol::App(a) => a.get_actor(),
StartProtocol::AppResponse(_) => panic!("AppResponse is not a request"),
}
}
}
impl From<StartProtocol> for ProtocolMessage {
fn from(msg: StartProtocol) -> ProtocolMessage {
ProtocolMessage::Start(msg)
}
}
/// Core Hello (finalizes the Noise handshake and sends CoreConnect)
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CoreHello {
// contains the 3rd Noise handshake message "s,se"
pub noise: Noise,
/// Noise encrypted payload (a CoreMessage::CoreRequest::BrokerConnect)
#[serde(with = "serde_bytes")]
pub payload: Vec<u8>,
}
impl CoreHello {
pub fn get_actor(&self) -> Box<dyn EActor> {
Actor::<CoreBrokerConnect, CoreBrokerConnectResponse>::new_responder(0)
}
}
impl TryFrom<ProtocolMessage> for CoreBrokerConnectResponse {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
if let ProtocolMessage::CoreMessage(CoreMessage::V0(CoreMessageV0::Response(
CoreResponse::V0(CoreResponseV0 {
content: CoreResponseContentV0::BrokerConnectResponse(a),
..
}),
))) = msg
{
Ok(a)
} else {
log_debug!("INVALID {:?}", msg);
Err(ProtocolError::InvalidValue)
}
}
}
impl From<CoreHello> for ProtocolMessage {
fn from(msg: CoreHello) -> ProtocolMessage {
ProtocolMessage::Start(StartProtocol::Core(msg))
}
}
impl From<CoreBrokerConnect> for ProtocolMessage {
fn from(_msg: CoreBrokerConnect) -> ProtocolMessage {
unimplemented!();
}
}
impl Actor<'_, CoreBrokerConnect, CoreBrokerConnectResponse> {}
#[async_trait::async_trait]
impl EActor for Actor<'_, CoreBrokerConnect, CoreBrokerConnectResponse> {
async fn respond(
&mut self,
_msg: ProtocolMessage,
_fsm: Arc<Mutex<NoiseFSM>>,
) -> Result<(), ProtocolError> {
//let req = CoreBrokerConnect::try_from(msg)?;
// let res = CoreBrokerConnectResponse::V0(CoreBrokerConnectResponseV0 {
// successes: vec![],
// errors: vec![],
// });
// fsm.lock().await.send(res.into()).await?;
Ok(())
}
}
// /// External Hello (finalizes the Noise handshake and sends first ExtRequest)
// #[derive(Clone, Debug, Serialize, Deserialize)]
// pub struct ExtHello {
// // contains the 3rd Noise handshake message "s,se"
// pub noise: Noise,
// /// Noise encrypted payload (an ExtRequest)
// #[serde(with = "serde_bytes")]
// pub payload: Vec<u8>,
// }
// impl ExtHello {
// pub fn get_actor(&self) -> Box<dyn EActor> {
// Actor::<ExtHello, ExtResponse>::new_responder(0)
// }
// }
// impl From<ExtHello> for ProtocolMessage {
// fn from(msg: ExtHello) -> ProtocolMessage {
// ProtocolMessage::Start(StartProtocol::Ext(msg))
// }
// }
/// Client Hello
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum ClientHello {
// contains the 3rd Noise handshake message "s,se"
Noise3(Noise),
Local,
}
impl ClientHello {
pub fn type_id(&self) -> TypeId {
match self {
ClientHello::Noise3(a) => a.type_id(),
ClientHello::Local => TypeId::of::<ClientHello>(),
}
}
pub fn get_actor(&self) -> Box<dyn EActor> {
Actor::<ClientHello, ServerHello>::new_responder(0)
}
}
/// Server hello sent upon a client connection
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ServerHelloV0 {
/// Nonce for ClientAuth
#[serde(with = "serde_bytes")]
pub nonce: Vec<u8>,
}
/// Server hello sent upon a client connection
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum ServerHello {
V0(ServerHelloV0),
}
impl ServerHello {
pub fn nonce(&self) -> &Vec<u8> {
match self {
ServerHello::V0(o) => &o.nonce,
}
}
}
impl From<ClientHello> for ProtocolMessage {
fn from(msg: ClientHello) -> ProtocolMessage {
ProtocolMessage::Start(StartProtocol::Client(msg))
}
}
impl TryFrom<ProtocolMessage> for ClientHello {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
if let ProtocolMessage::Start(StartProtocol::Client(a)) = msg {
Ok(a)
} else {
Err(ProtocolError::InvalidValue)
}
}
}
impl TryFrom<ProtocolMessage> for ServerHello {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
if let ProtocolMessage::ServerHello(server_hello) = msg {
Ok(server_hello)
} else {
Err(ProtocolError::InvalidValue)
}
}
}
impl From<ServerHello> for ProtocolMessage {
fn from(msg: ServerHello) -> ProtocolMessage {
ProtocolMessage::ServerHello(msg)
}
}
impl Actor<'_, ClientHello, ServerHello> {}
#[async_trait::async_trait]
impl EActor for Actor<'_, ClientHello, ServerHello> {
async fn respond(
&mut self,
msg: ProtocolMessage,
fsm: Arc<Mutex<NoiseFSM>>,
) -> Result<(), ProtocolError> {
let _req = ClientHello::try_from(msg)?;
let res = ServerHello::V0(ServerHelloV0 { nonce: vec![] });
fsm.lock().await.send(res.into()).await?;
Ok(())
}
}
// impl Actor<'_, ExtHello, ExtResponse> {}
// #[async_trait::async_trait]
// impl EActor for Actor<'_, ExtHello, ExtResponse> {
// async fn respond(
// &mut self,
// _msg: ProtocolMessage,
// _fsm: Arc<Mutex<NoiseFSM>>,
// ) -> Result<(), ProtocolError> {
// Ok(())
// }
// }
// ///////////// APP HELLO ///////////////
/// App Hello (finalizes the Noise handshake and sends info about device, and the user_id.
/// not signing any nonce because anyway, in the next message "AppSessionRequest", the user_priv_key will be sent and checked again)
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AppHello {
// contains the 3rd Noise handshake message "s,se"
pub noise: Noise,
pub user: Option<UserId>, // None for Headless
pub info: ClientInfo,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AppHelloResponse {
pub result: u16,
}
impl AppHello {
pub fn get_actor(&self) -> Box<dyn EActor> {
Actor::<AppHello, AppHelloResponse>::new_responder(0)
}
}
impl From<AppHello> for ProtocolMessage {
fn from(msg: AppHello) -> ProtocolMessage {
ProtocolMessage::Start(StartProtocol::App(msg))
}
}
impl From<AppHelloResponse> for ProtocolMessage {
fn from(msg: AppHelloResponse) -> ProtocolMessage {
ProtocolMessage::Start(StartProtocol::AppResponse(msg))
}
}
impl TryFrom<ProtocolMessage> for AppHelloResponse {
type Error = ProtocolError;
fn try_from(msg: ProtocolMessage) -> Result<Self, Self::Error> {
if let ProtocolMessage::Start(StartProtocol::AppResponse(res)) = msg {
Ok(res)
} else {
Err(ProtocolError::InvalidValue)
}
}
}
impl Actor<'_, AppHello, AppHelloResponse> {}
#[async_trait::async_trait]
impl EActor for Actor<'_, AppHello, AppHelloResponse> {
async fn respond(
&mut self,
_msg: ProtocolMessage,
_fsm: Arc<Mutex<NoiseFSM>>,
) -> Result<(), ProtocolError> {
Ok(())
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -0,0 +1,53 @@
use time::{Month,Date};
use std::collections::HashMap;
use lazy_static::lazy_static;
pub struct BSPDetail<'a> {
pub domain: &'a str,
// ISO-2 code
pub country: &'a str,
// email address of sys admin
pub sysadmin: &'a str,
// owned or rented
pub owned: bool,
pub since: Date,
pub has_free: bool,
pub has_paid: bool,
pub official: bool,
pub description: &'a str,
}
lazy_static! {
pub static ref BSP_DETAILS: HashMap<&'static str, BSPDetail<'static>> = {
let mut d = HashMap::new();
d.insert("https://nextgraph.eu", BSPDetail {
domain: "nextgraph.eu",
country: "de",
sysadmin: "team@nextgraph.org",
owned: false,
since: Date::from_calendar_date(2024, Month::September,2).unwrap(),
has_free: true,
has_paid: false,
official: true,
description: "First official Broker Service Provider from NextGraph.org. Based in Europe."
});
assert!(d.insert("https://nextgraph.one", BSPDetail {
domain: "nextgraph.one",
country: "de",
sysadmin: "team@nextgraph.org",
owned: false,
since: Date::from_calendar_date(2025, Month::April,20).unwrap(),
has_free: true,
has_paid: false,
official: true,
description: "Second official Broker Service Provider from NextGraph.org. Based in Europe, but that could change."
}).is_none());
d
};
pub static ref BSP_ORIGINS: Vec<&'static str> = {
BSP_DETAILS.keys().cloned().collect()
};
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,54 @@
/*
* Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
pub mod types;
#[doc(hidden)]
pub mod app_protocol;
pub mod broker;
pub mod server_broker;
#[doc(hidden)]
pub mod connection;
pub mod actor;
pub mod actors;
pub mod utils;
#[doc(hidden)]
pub mod tests;
#[doc(hidden)]
pub mod bsps;
#[doc(hidden)]
pub static NG_BOOTSTRAP_LOCAL_PATH: &str = "/.ng_bootstrap";
#[cfg(debug_assertions)]
#[doc(hidden)]
pub static WS_PORT: u16 = 14400;
#[cfg(not(debug_assertions))]
#[doc(hidden)]
pub static WS_PORT: u16 = 80;
#[doc(hidden)]
pub static WS_PORT_ALTERNATE: [u16; 4] = [14400, 28800, 43200, 57600];
#[doc(hidden)]
pub static WS_PORT_ALTERNATE_SUPERUSER: u16 = 144;
#[doc(hidden)]
pub static WS_PORT_REVERSE_PROXY: u16 = 1440;

@ -0,0 +1,157 @@
/*
* Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
* All rights reserved.
* Licensed under the Apache License, Version 2.0
* <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
* or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
* at your option. All files in the project carrying such
* notice may not be copied, modified, or distributed except
* according to those terms.
*/
//! Trait for ServerBroker
use std::path::PathBuf;
use std::sync::Arc;
use async_std::sync::Mutex;
use ng_repo::block_storage::BlockStorage;
use ng_repo::errors::*;
use ng_repo::types::*;
use crate::app_protocol::{AppRequest, AppSessionStart, AppSessionStartResponse, AppSessionStop};
use crate::broker::ClientPeerId;
use crate::connection::NoiseFSM;
use crate::types::*;
use crate::utils::Receiver;
#[async_trait::async_trait]
pub trait IServerBroker: Send + Sync {
async fn remove_rendezvous(&self, rendezvous: &SymKey);
async fn put_wallet_export(&self, rendezvous: SymKey, export: ExportedWallet);
async fn get_wallet_export(&self, rendezvous: SymKey) -> Result<ExportedWallet, ServerError>;
async fn put_wallet_at_rendezvous(
&self,
rendezvous: SymKey,
export: ExportedWallet,
) -> Result<(), ServerError>;
async fn wait_for_wallet_at_rendezvous(
&self,
rendezvous: SymKey,
) -> Receiver<Result<ExportedWallet, ServerError>>;
async fn inbox_post(&self, post: InboxPost) -> Result<(), ServerError>;
fn inbox_register(&self, user_id: UserId, registration: InboxRegister) -> Result<(), ServerError>;
async fn inbox_pop_for_user(&self, user: UserId ) -> Result<InboxMsg, ServerError>;
fn get_path_users(&self) -> PathBuf;
fn get_block_storage(&self) -> Arc<std::sync::RwLock<dyn BlockStorage + Send + Sync>>;
fn put_block(&self, overlay_id: &OverlayId, block: Block) -> Result<(), ServerError>;
fn has_block(&self, overlay_id: &OverlayId, block_id: &BlockId) -> Result<(), ServerError>;
fn get_block(&self, overlay_id: &OverlayId, block_id: &BlockId) -> Result<Block, ServerError>;
async fn create_user(&self, broker_id: &DirectPeerId) -> Result<UserId, ProtocolError>;
fn get_user(&self, user_id: PubKey) -> Result<bool, ProtocolError>;
fn has_no_user(&self) -> Result<bool, ProtocolError>;
fn get_user_credentials(&self, user_id: &PubKey) -> Result<Credentials, ProtocolError>;
fn add_user_credentials(
&self,
user_id: &PubKey,
credentials: &Credentials,
) -> Result<(), ProtocolError>;
fn add_user(&self, user_id: PubKey, is_admin: bool) -> Result<(), ProtocolError>;
fn del_user(&self, user_id: PubKey) -> Result<(), ProtocolError>;
fn list_users(&self, admins: bool) -> Result<Vec<PubKey>, ProtocolError>;
fn list_invitations(
&self,
admin: bool,
unique: bool,
multi: bool,
) -> Result<Vec<(InvitationCode, u32, Option<String>)>, ProtocolError>;
fn add_invitation(
&self,
invite_code: &InvitationCode,
expiry: u32,
memo: &Option<String>,
) -> Result<(), ProtocolError>;
fn get_invitation_type(&self, invite: [u8; 32]) -> Result<u8, ProtocolError>;
fn remove_invitation(&self, invite: [u8; 32]) -> Result<(), ProtocolError>;
fn take_master_key(&mut self) -> Result<SymKey, ProtocolError>;
async fn app_process_request(
&self,
req: AppRequest,
request_id: i64,
fsm: &Mutex<NoiseFSM>,
) -> Result<(), ServerError>;
async fn app_session_start(
&self,
req: AppSessionStart,
remote_peer_id: DirectPeerId,
local_peer_id: DirectPeerId,
) -> Result<AppSessionStartResponse, ServerError>;
async fn app_session_stop(
&self,
req: AppSessionStop,
remote_peer_id: &DirectPeerId,
) -> Result<EmptyAppResponse, ServerError>;
fn next_seq_for_peer(&self, peer: &PeerId, seq: u64) -> Result<(), ServerError>;
fn get_repo_pin_status(
&self,
overlay: &OverlayId,
repo: &RepoHash,
user_id: &UserId,
) -> Result<RepoPinStatus, ServerError>;
async fn pin_repo_write(
&self,
overlay: &OverlayAccess,
repo: &RepoHash,
user_id: &UserId,
ro_topics: &Vec<TopicId>,
rw_topics: &Vec<PublisherAdvert>,
overlay_root_topic: &Option<TopicId>,
expose_outer: bool,
peer: &ClientPeerId,
) -> Result<RepoOpened, ServerError>;
async fn pin_repo_read(
&self,
overlay: &OverlayId,
repo: &RepoHash,
user_id: &UserId,
ro_topics: &Vec<TopicId>,
peer: &ClientPeerId,
) -> Result<RepoOpened, ServerError>;
async fn topic_sub(
&self,
overlay: &OverlayId,
repo: &RepoHash,
topic: &TopicId,
user_id: &UserId,
publisher: Option<&PublisherAdvert>,
peer: &ClientPeerId,
) -> Result<TopicSubRes, ServerError>;
fn get_commit(&self, overlay: &OverlayId, id: &ObjectId) -> Result<Vec<Block>, ServerError>;
async fn dispatch_event(
&self,
overlay: &OverlayId,
event: Event,
user_id: &UserId,
remote_peer: &PubKey,
) -> Result<Vec<ClientPeerId>, ServerError>;
async fn remove_all_subscriptions_of_client(&self, client: &ClientPeerId);
fn topic_sync_req(
&self,
overlay: &OverlayId,
topic: &TopicId,
known_heads: &Vec<ObjectId>,
target_heads: &Vec<ObjectId>,
known_commits: &Option<BloomFilter>,
) -> Result<Vec<TopicSyncRes>, ServerError>;
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,2 @@
#[doc(hidden)]
pub mod file;

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

Loading…
Cancel
Save