Compare commits
8
Commits
main
..
v1.14.1-pqc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad01fbcccb | ||
|
|
fa08792c78 | ||
|
|
1ead221c6a | ||
|
|
a6b0c58511 | ||
|
|
86c744897b | ||
|
|
d4568326af | ||
|
|
7cf826c6b6 | ||
|
|
c550fe0684 |
No files matched your search
@@ -31,14 +31,14 @@ jobs:
|
|||||||
- name: selftest
|
- name: selftest
|
||||||
run: |
|
run: |
|
||||||
make selftest
|
make selftest
|
||||||
./zerotier-selftest
|
./backone-selftest
|
||||||
- name: 'Tar files' # keeps permissions (execute)
|
- name: 'Tar files' # keeps permissions (execute)
|
||||||
run: tar -cvf zerotier-one.tar zerotier-one
|
run: tar -cvf backone.tar backone
|
||||||
- name: Archive production artifacts
|
- name: Archive production artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: zerotier-one-ubuntu-x64
|
name: backone-ubuntu-x64
|
||||||
path: zerotier-one.tar
|
path: backone.tar
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
build_macos:
|
build_macos:
|
||||||
@@ -75,14 +75,14 @@ jobs:
|
|||||||
- name: selftest
|
- name: selftest
|
||||||
run: |
|
run: |
|
||||||
make selftest
|
make selftest
|
||||||
./zerotier-selftest
|
./backone-selftest
|
||||||
- name: 'Tar files' # keeps permissions (execute)
|
- name: 'Tar files' # keeps permissions (execute)
|
||||||
run: tar -cvf zerotier-one.tar zerotier-one
|
run: tar -cvf backone.tar backone
|
||||||
- name: Archive production artifacts
|
- name: Archive production artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: zerotier-one-mac
|
name: backone-mac
|
||||||
path: zerotier-one.tar
|
path: backone.tar
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
|
|
||||||
@@ -118,6 +118,6 @@ jobs:
|
|||||||
- name: Archive production artifacts
|
- name: Archive production artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: zerotier-one-windows
|
name: backone-windows
|
||||||
path: windows/Build
|
path: windows/Build
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
name: Lint
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
clang-format:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
# Pin: format results differ across clang-format versions.
|
||||||
|
- run: pip install --user clang-format==23.1.1 && echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||||
|
- name: Check changed lines
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||||
|
BASE="${{ github.event.pull_request.base.sha }}"
|
||||||
|
elif [ "${{ github.event.before }}" != "" ] && [ "${{ github.event.before }}" != "0000000000000000000000000000000000000000" ]; then
|
||||||
|
BASE="${{ github.event.before }}"
|
||||||
|
else
|
||||||
|
BASE="$(git merge-base HEAD "origin/${{ github.event.repository.default_branch }}" || echo HEAD~1)"
|
||||||
|
fi
|
||||||
|
./tools/lint.sh "$BASE"
|
||||||
@@ -38,9 +38,9 @@ test() {
|
|||||||
export NS1="ip netns exec ns1"
|
export NS1="ip netns exec ns1"
|
||||||
export NS2="ip netns exec ns2"
|
export NS2="ip netns exec ns2"
|
||||||
|
|
||||||
export ZT1="$NS1 ./zerotier-cli -p9996 -D$(pwd)/node1"
|
export ZT1="$NS1 ./backone-cli -p9996 -D$(pwd)/node1"
|
||||||
# Specify custom port on one node to ensure that feature works
|
# Specify custom port on one node to ensure that feature works
|
||||||
export ZT2="$NS2 ./zerotier-cli -p9997 -D$(pwd)/node2"
|
export ZT2="$NS2 ./backone-cli -p9997 -D$(pwd)/node2"
|
||||||
|
|
||||||
echo -e "\nSetting up network namespaces..."
|
echo -e "\nSetting up network namespaces..."
|
||||||
echo "Setting up ns1"
|
echo "Setting up ns1"
|
||||||
@@ -102,13 +102,13 @@ test() {
|
|||||||
--xml=yes \
|
--xml=yes \
|
||||||
--xml-file=$FILENAME_MEMORY_LOG \
|
--xml-file=$FILENAME_MEMORY_LOG \
|
||||||
--leak-check=full \
|
--leak-check=full \
|
||||||
./zerotier-one node1 -p$ZT_PORT_NODE_1 -U >>node_1.log 2>&1 &
|
./backone node1 -p$ZT_PORT_NODE_1 -U >>node_1.log 2>&1 &
|
||||||
|
|
||||||
# Second instance, not run in memory profiler
|
# Second instance, not run in memory profiler
|
||||||
# Don't set up internet access until _after_ zerotier is running
|
# Don't set up internet access until _after_ zerotier is running
|
||||||
# This has been a source of stuckness in the past.
|
# This has been a source of stuckness in the past.
|
||||||
$NS2 ip addr del 192.168.1.2/24 dev veth3
|
$NS2 ip addr del 192.168.1.2/24 dev veth3
|
||||||
$NS2 sudo ./zerotier-one node2 -U -p$ZT_PORT_NODE_2 >>node_2.log 2>&1 &
|
$NS2 sudo ./backone node2 -U -p$ZT_PORT_NODE_2 >>node_2.log 2>&1 &
|
||||||
|
|
||||||
sleep 10; # New HTTP control plane is a bit sluggish, so we delay here
|
sleep 10; # New HTTP control plane is a bit sluggish, so we delay here
|
||||||
|
|
||||||
@@ -170,9 +170,9 @@ test() {
|
|||||||
|
|
||||||
echo -e "\n\nRunning ZeroTier processes:"
|
echo -e "\n\nRunning ZeroTier processes:"
|
||||||
echo -e "\nNode 1:\n"
|
echo -e "\nNode 1:\n"
|
||||||
$NS1 ps aux | grep zerotier-one
|
$NS1 ps aux | grep backone
|
||||||
echo -e "\nNode 2:\n"
|
echo -e "\nNode 2:\n"
|
||||||
$NS2 ps aux | grep zerotier-one
|
$NS2 ps aux | grep backone
|
||||||
|
|
||||||
echo -e "\n\nStatus of each instance:"
|
echo -e "\n\nStatus of each instance:"
|
||||||
|
|
||||||
@@ -319,10 +319,10 @@ exit_test_and_generate_report() {
|
|||||||
node2_id=$($ZT2 -j status | jq -r .address)
|
node2_id=$($ZT2 -j status | jq -r .address)
|
||||||
|
|
||||||
$ZT1 dump
|
$ZT1 dump
|
||||||
mv zerotier_dump.txt "$TEST_FILEPATH_PREFIX-node-dump-$node1_id.txt"
|
mv backone_dump.txt "$TEST_FILEPATH_PREFIX-node-dump-$node1_id.txt"
|
||||||
|
|
||||||
$ZT2 dump
|
$ZT2 dump
|
||||||
mv zerotier_dump.txt "$TEST_FILEPATH_PREFIX-node-dump-$node2_id.txt"
|
mv backone_dump.txt "$TEST_FILEPATH_PREFIX-node-dump-$node2_id.txt"
|
||||||
|
|
||||||
# Copy ZeroTier stdout/stderr logs
|
# Copy ZeroTier stdout/stderr logs
|
||||||
|
|
||||||
@@ -386,7 +386,7 @@ exit_test_and_generate_report() {
|
|||||||
"commit":"$ZTO_COMMIT",
|
"commit":"$ZTO_COMMIT",
|
||||||
"arch_m":"$(uname -m)",
|
"arch_m":"$(uname -m)",
|
||||||
"arch_a":"$(uname -a)",
|
"arch_a":"$(uname -a)",
|
||||||
"binary_size":"$(stat -c %s zerotier-one)",
|
"binary_size":"$(stat -c %s backone)",
|
||||||
"time_length_test":$time_length_test,
|
"time_length_test":$time_length_test,
|
||||||
"time_to_both_nodes_online":$time_to_both_nodes_online,
|
"time_to_both_nodes_online":$time_to_both_nodes_online,
|
||||||
"num_possible_bytes_lost": $POSSIBLY_LOST,
|
"num_possible_bytes_lost": $POSSIBLY_LOST,
|
||||||
@@ -448,7 +448,7 @@ spam_cli() {
|
|||||||
check_exit_on_invalid_identity() {
|
check_exit_on_invalid_identity() {
|
||||||
echo "Checking ZeroTier exits on invalid identity..."
|
echo "Checking ZeroTier exits on invalid identity..."
|
||||||
mkdir -p $(pwd)/exit_test
|
mkdir -p $(pwd)/exit_test
|
||||||
ZT1="sudo ./zerotier-one -p9999 $(pwd)/exit_test"
|
ZT1="sudo ./backone -p9999 $(pwd)/exit_test"
|
||||||
echo "asdfasdfasdfasdf" > $(pwd)/exit_test/identity.secret
|
echo "asdfasdfasdfasdf" > $(pwd)/exit_test/identity.secret
|
||||||
echo "asdfasdfasdfasdf" > $(pwd)/exit_test/authtoken.secret
|
echo "asdfasdfasdfasdf" > $(pwd)/exit_test/authtoken.secret
|
||||||
|
|
||||||
@@ -473,20 +473,20 @@ check_exit_on_invalid_identity() {
|
|||||||
check_bind_to_correct_ports() {
|
check_bind_to_correct_ports() {
|
||||||
PORT_NUMBER=$1
|
PORT_NUMBER=$1
|
||||||
echo "Checking bound ports:"
|
echo "Checking bound ports:"
|
||||||
sudo netstat -anp | grep "$PORT_NUMBER" | grep "zerotier"
|
sudo netstat -anp | grep "$PORT_NUMBER" | grep "backone"
|
||||||
if [[ $(sudo netstat -anp | grep "$PORT_NUMBER" | grep "zerotier" | grep "tcp") ]];
|
if [[ $(sudo netstat -anp | grep "$PORT_NUMBER" | grep "backone" | grep "tcp") ]];
|
||||||
then
|
then
|
||||||
:
|
:
|
||||||
else
|
else
|
||||||
exit_test_and_generate_report $TEST_FAIL "ZeroTier did not bind to tcp/$1"
|
exit_test_and_generate_report $TEST_FAIL "ZeroTier did not bind to tcp/$1"
|
||||||
fi
|
fi
|
||||||
if [[ $(sudo netstat -anp | grep "$PORT_NUMBER" | grep "zerotier" | grep "tcp6") ]];
|
if [[ $(sudo netstat -anp | grep "$PORT_NUMBER" | grep "backone" | grep "tcp6") ]];
|
||||||
then
|
then
|
||||||
:
|
:
|
||||||
else
|
else
|
||||||
exit_test_and_generate_report $TEST_FAIL "ZeroTier did not bind to tcp6/$1"
|
exit_test_and_generate_report $TEST_FAIL "ZeroTier did not bind to tcp6/$1"
|
||||||
fi
|
fi
|
||||||
if [[ $(sudo netstat -anp | grep "$PORT_NUMBER" | grep "zerotier" | grep "udp") ]];
|
if [[ $(sudo netstat -anp | grep "$PORT_NUMBER" | grep "backone" | grep "udp") ]];
|
||||||
then
|
then
|
||||||
:
|
:
|
||||||
else
|
else
|
||||||
|
|||||||
+9
-3
@@ -80,10 +80,12 @@ zt1-src.tar.gz
|
|||||||
node_modules
|
node_modules
|
||||||
zt1_update_*
|
zt1_update_*
|
||||||
debian/files
|
debian/files
|
||||||
debian/zerotier-one
|
debian/backone
|
||||||
debian/zerotier-one*.debhelper
|
debian/backone*.debhelper
|
||||||
debian/*.log
|
debian/*.log
|
||||||
debian/zerotier-one.substvars
|
debian/backone.substvars
|
||||||
|
debian/.debhelper/
|
||||||
|
debian/debhelper-build-stamp
|
||||||
root-watcher/config.json
|
root-watcher/config.json
|
||||||
|
|
||||||
# Java/Android/JNI build droppings
|
# Java/Android/JNI build droppings
|
||||||
@@ -154,3 +156,7 @@ rustybits/
|
|||||||
backone
|
backone
|
||||||
backone-cli
|
backone-cli
|
||||||
backone-idtool
|
backone-idtool
|
||||||
|
|
||||||
|
/backone-selftest
|
||||||
|
*.gcno
|
||||||
|
*.gcda
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
# Repository Guidelines
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
BackOne — fork of ZeroTier 1.14.1: peer-to-peer SDN daemon. Builds virtual Ethernet (TUN/TAP), tunnels L2 frames over encrypted UDP mesh, issues membership certs from an embedded controller. One fat binary `backone` doubles as CLI (`backone-cli`) and identity tool (`backone-idtool`) via `argv[0]` dispatch (`one.cpp:2140-2142`). Local JSON API on `127.0.0.1:9993`.
|
||||||
|
|
||||||
|
## Architecture & Data Flow
|
||||||
|
|
||||||
|
Four layers, top → bottom:
|
||||||
|
|
||||||
|
1. **Entry** — `one.cpp`: `main()` → `cli()` / `idtool()` / daemon → `OneService::newInstance()` + `run()`.
|
||||||
|
2. **Service** — `service/OneService.cpp` (`OneServiceImpl`, line 776): owns sockets (`Phy<>`), httplib control plane, TAP devices, `Node`, `EmbeddedNetworkController`, main loop.
|
||||||
|
3. **Core** — `node/`: OS-independent switch. Reached only through the C API (`ZT_Node_*` in `include/ZeroTierOne.h`, implemented at bottom of `node/Node.cpp`).
|
||||||
|
4. **OS glue** — `osdep/`: `Phy` (select()-based reactor, the only event loop), `EthernetTap` (per-OS TUN/TAP), `OSUtils`, `ManagedRoute`, `Thread`, `BlockingQueue`.
|
||||||
|
|
||||||
|
**Core pattern = inversion of control.** `OneServiceImpl` fills `struct ZT_Node_Callbacks` (`service/OneService.cpp:1045-1054`) with `Snode*` static functions; `node/` never touches OS code, it calls back. Shared context is `RuntimeEnvironment` passed as `const RuntimeEnvironment *RR` to every `node/` function (`node/RuntimeEnvironment.hpp`).
|
||||||
|
|
||||||
|
Flows:
|
||||||
|
|
||||||
|
- **Wire → app:** `Phy::poll` → `phyOnDatagram` (`service/OneService.cpp:2940`) → `Node::processWirePacket` (`node/Node.cpp:206`) → `Switch::onRemotePacket` → verb dispatch `switch` (`node/IncomingPacket.cpp:94-151`) → peer/network/crypto. Heavy work offloaded to `PacketMultiplexer` worker threads.
|
||||||
|
- **App → wire:** TAP handler → `tapFrameHandler` (`service/OneService.cpp:3776`) → `Node::processVirtualNetworkFrame` → `Switch::onLocalEthernet` → `Peer`/`Path` → `_phy.udpSend`.
|
||||||
|
- **Controller:** `IncomingPacket::_doNETWORK_CONFIG_REQUEST` → `EmbeddedNetworkController::request` posts `_RQEntry*` to `BlockingQueue` → `hardware_concurrency()` workers → `DBMirrorSet`/`DB` → signed cert back via `NetworkController::Sender`.
|
||||||
|
- **Main loop** (`OneServiceImpl::run`, `service/OneService.cpp:~1150-1310`): single-threaded, time-sliced; refresh binds → `processBackgroundTasks` when due → `_phy.poll(delay)`. `Phy::whack()` (self-pipe) is the only cross-thread-safe Phy call.
|
||||||
|
|
||||||
|
## Key Directories
|
||||||
|
|
||||||
|
| Path | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `one.cpp` | Daemon/CLI/idtool entry; privilege drop, daemonize, signals |
|
||||||
|
| `node/` | Overlay engine: `Switch`, `Topology`, `Peer`, `IncomingPacket`, `Identity`, `Bond`, `Metrics` — OS-independent by rule |
|
||||||
|
| `service/` | `OneService` (whole runtime), `SoftwareUpdater` |
|
||||||
|
| `controller/` | `EmbeddedNetworkController` + `DB` backends (FileDB default, LFDB, PostgreSQL, Redis) |
|
||||||
|
| `osdep/` | All OS-dependent code: `Phy`, `EthernetTap*`, `Binder`, `ManagedRoute`, netlink/DNS helpers |
|
||||||
|
| `include/` | Public C API (`ZeroTierOne.h`, 57KB) — changes ripple to Java/libzt/SDK |
|
||||||
|
| `ext/` | Vendored: nlohmann/json, cpp-httplib, prometheus-cpp-lite, libpqxx, redis++, miniupnpc, ASM crypto |
|
||||||
|
| `rustybits/` | Cargo workspace: `zeroidc` (SSO FFI), `smeeclient` (PostgreSQL workflow client) |
|
||||||
|
| `java/` | JNI wrapper (`java/jni/…Node.cpp`, ant build) |
|
||||||
|
| `tcp-proxy/` | Standalone TCP fallback relay; own Makefile (C++11) |
|
||||||
|
| `rule-compiler/` | JS network-rules compiler (`node cli.js <rules>`), npm |
|
||||||
|
| `pkg/`, `debian/`, `windows/` | Packaging (snap/QNAP/Synology/ASUSTOR/WD, RPM `backone.spec`, MSVC sln) |
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make # = make one → ./backone + backone-cli/backone-idtool symlinks
|
||||||
|
make -j$(nproc) one
|
||||||
|
make core # libbackonecore.a
|
||||||
|
make selftest && ./backone-selftest
|
||||||
|
make debug # ZT_DEBUG=1 (adds -g, forces ZT_TRACE=1)
|
||||||
|
make ZT_SANITIZE=1 one # ASan
|
||||||
|
make central-controller # ZT_CONTROLLER=1 (needs ext/ libpqxx + hiredis + redis++)
|
||||||
|
make install DESTDIR=/tmp/root
|
||||||
|
make debian | make redhat # debuild / rpmbuild
|
||||||
|
make manpages # cd doc && ./build.sh (needs ronn or node marked-man)
|
||||||
|
cd rustybits && cargo build # zeroidc/smeeclient (also auto via `make zeroidc`)
|
||||||
|
cd java && ant build_java | build_android | build_jar
|
||||||
|
cd tcp-proxy && make
|
||||||
|
```
|
||||||
|
|
||||||
|
`Makefile` is a uname dispatcher → `make-linux.mk` / `make-mac.mk` / `make-bsd.mk` / `make-netbsd.mk`. Windows: `MSBuild windows/ZeroTierOne.sln /property:Configuration=Release`. Top-level `CMakeLists.txt` is a 323B stub — never `cmake .` as product build.
|
||||||
|
|
||||||
|
Env knobs (make vars): `ZT_DEBUG`, `ZT_TRACE`, `ZT_SANITIZE`, `ZT_STATIC`, `ZT_OFFICIAL`, `ZT_CONTROLLER`, `ZT_SSO_SUPPORTED` (0 on Linux default → no cargo needed), `ZT_VAULT_SUPPORT`, `ZT_IA32`.
|
||||||
|
|
||||||
|
**Build hygiene — no header dependency tracking.** The makefiles emit no `-MMD` `.d` files, so `.o` outputs never rebuild when a header changes. A tree that ships prebuilt objects (root `one.o`, `node/*.o`, `libbackonecore.a` — present in this checkout) plus any `.hpp` edit produces ABI-mismatched objects; symptom is an instant `SIGSEGV` at `Topology::Topology` during `./backone` startup with a null call target. `make clean` after touching any `.hpp`, and verify startup after every build:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
rm -rf /tmp/bohome && mkdir -p /tmp/bohome
|
||||||
|
timeout 8 ./backone -p9999 -U /tmp/bohome # exit 124 (timeout) = healthy; 139 = crash
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Conventions & Common Patterns
|
||||||
|
|
||||||
|
- **Standard: C++17** (`-std=c++17` in all makefiles; `tcp-proxy` is C++11). Ignore stale comments claiming otherwise (`node/README.md` "No C++11", `osdep/BlockingQueue.hpp:30`).
|
||||||
|
- **Formatting**: `.clang-format` — LLVM base, **tabs always**, indent 4, Stroustrup braces, `BinPackArguments: false`, `PointerAlignment: Left`. Gate: `make lint` → `tools/lint.sh` (clang-format on lines changed vs base commit; `--all` checks whole files — whole repo has format debt, don't reformat untouched lines). No clang-tidy, no `-Werror`.
|
||||||
|
- **Namespace**: everything in `namespace ZeroTier { … }`, close with `} // namespace ZeroTier`. File-local helpers in anonymous namespace or `static`.
|
||||||
|
- **Naming**: classes `PascalCase`; methods `camelCase`; private members `_camelCase`; static C callbacks `S` + class (`SnodeWirePacketSendFunction`); macros/enums `ZT_UPPER_SNAKE`; include guards `ZT_<NAME>_HPP` (no `#pragma once`).
|
||||||
|
- **Includes**: quoted relative (`"../node/Constants.hpp"` from subdirs, `"node/Constants.hpp"` from root). Third-party via angle brackets resolved by `-isystem ext`. `node/Constants.hpp` FIRST in `node/` files — canonicalizes `__LINUX__`/`__APPLE__`/`__UNIX_LIKE__`/`__WINDOWS__`; never test raw `__linux__`/`_WIN32`.
|
||||||
|
- **Error handling**: three regimes — (1) C API boundary returns `ZT_ResultCode`, fatal = 100–999, checked via `ZT_ResultCode_isFatal`; (2) `node/` throws bare ints (`ZT_EXCEPTION_OUT_OF_BOUNDS`, `node/Constants.hpp:757+`), caught as `catch (int e)` in `OneServiceImpl::run`; (3) service/controller use `std::exception`/`catch (...)` at thread boundaries. **No exceptions across the C API.**
|
||||||
|
- **Threading**: no global lock; per-object `ZeroTier::Mutex` scoped guard (`Mutex::Lock _l(_m);`) or `std::mutex` + `lock_guard`/`shared_lock` in controller. Cross-thread wake only via `Phy::whack()`; producer/consumer via `BlockingQueue<T>`. Threads: `std::thread`.
|
||||||
|
- **Async**: single-threaded reactor (`Phy::poll` callbacks), not futures; no `std::async`/`future` in C++ paths (tokio only inside `rustybits/smeeclient`).
|
||||||
|
- **Memory**: raw `new`/`delete` in service/controller; intrusive `SharedPtr<T>` (`node/SharedPtr.hpp` — class needs `friend class SharedPtr<X>` + `AtomicCounter __refCount`) for `Peer`/`Network`/`Path`; `std::shared_ptr` for `DB` backends. Zero secrets with `Utils::burn`.
|
||||||
|
- **DI/state**: no framework. Two seams only: `ZT_Node_Callbacks` function-pointer struct (core ↔ OS) and `const RuntimeEnvironment *RR` as first param. Controller injected via `Node::setNetconfMaster`.
|
||||||
|
- **JSON**: `nlohmann::json` + typed accessors `OSUtils::jsonString/jsonInt/jsonBool` (defaults); API is type-sensitive.
|
||||||
|
- **Metrics**: `node/Metrics.hpp` namespace, increment inline (`Metrics::udp_recv += len;`).
|
||||||
|
- **License header**: every new `.cpp/.hpp/.h` gets the 11-line BSL block verbatim (copy `node/Mutex.hpp:1-12`).
|
||||||
|
- **Placement rule**: sockets/files/interfaces → `osdep/`; orchestration → `service/`; persistence → `controller/`; `node/` stays OS-independent. New compiled file must be added to `objects.mk` (`CORE_OBJS` or `ONE_OBJS`) or it won't link.
|
||||||
|
|
||||||
|
## Important Files
|
||||||
|
|
||||||
|
Read before editing:
|
||||||
|
|
||||||
|
1. `include/ZeroTierOne.h` — public contract (`ZT_Node_Callbacks:1733`, `ZT_ResultCode:375`); ripples to Java/SDK.
|
||||||
|
2. `node/Constants.hpp` — platform macros, exception ints, constants; include first.
|
||||||
|
3. `node/RuntimeEnvironment.hpp` — object graph every `node/` fn navigates.
|
||||||
|
4. `service/OneService.cpp` lines 690–1100 (callback table) and 1150–1400 (`run()` loop) — the integration seam.
|
||||||
|
5. `osdep/Phy.hpp` lines 55–160 — handler contract for any socket behavior.
|
||||||
|
6. `objects.mk` + `make-linux.mk:12,62-73,370-457` — what compiles with which flags.
|
||||||
|
7. Per-dir READMEs: `service/README.md` (local.conf schema + JSON API — primary ops doc), `controller/README.md`, `node/README.md`.
|
||||||
|
|
||||||
|
Module-specific: wire protocol → `node/IncomingPacket.cpp:94-151` + `node/Packet.hpp`; routing → `node/Switch.cpp`; DB backend → `controller/DB.hpp` + `EmbeddedNetworkController.cpp:465-580`; TAP → `osdep/EthernetTap.cpp`.
|
||||||
|
|
||||||
|
## Runtime/Tooling Preferences
|
||||||
|
|
||||||
|
- **Build**: GNU make (g++ or clang auto-detected, `make-linux.mk:3-10`); MSVC on Windows; ant + NDK for Java; cargo for `rustybits/`; npm for `rule-compiler/`.
|
||||||
|
- **No package manager for C++** — deps vendored in `ext/` (do not add new ones casually).
|
||||||
|
- **Daemon paths (Linux)**: home `/var/lib/backone` (macOS: `/Library/Application Support/BackOne`); port 9993; key files `identity.secret`, `authtoken.secret`, `networks.d/`, `local.conf`, `controller.db`. Unprivileged CLI: `cp authtoken.secret ~/.backOneOneAuthToken` (source: `one.cpp:283`).
|
||||||
|
- **CLI**: `backone-cli info|listpeers|listnetworks|join <nwid>|leave <nwid>` (`-j` JSON, `-p<port>`, `-D<dir>`); `backone-idtool generate|validate|getpublic|sign|verify|mkcom`.
|
||||||
|
- **Dangerous scripts**: `update_controllers.sh`, `cycle_controllers.sh` are live ZeroTier-Central kubectl ops — never run locally.
|
||||||
|
- **Rebrand is partial**: build outputs, CI (`build.yml`, `validate-linux.sh`), `Dockerfile.ci`, `make-bsd.mk`/`make-netbsd.mk`, and debian units now use `backone*`. Still upstream-named: `pkg/snap/snapcraft.yaml` (builds `github.com/zerotier/zerotierone.git` → broken) and cosmetic strings (`/etc/zerotier-version` in `Dockerfile.ci`, `windows/zerotier-cli.bat`, `pkg/` vendor paths). Expect occasional `zerotier-*` names in docs/scripts.
|
||||||
|
|
||||||
|
## Testing & QA
|
||||||
|
|
||||||
|
No C++ unit framework. Practical loop:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make selftest && ./backone-selftest # crypto KATs, identity, certs, packet codec, Phy loopback; non-zero exit on failure
|
||||||
|
make debug # ZT_DEBUG=1 build of one + selftest
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Sections** in `selftest.cpp`: `testCrypto`, `testIdentity`, `testCertificate`, `testPacket`, `testOther`, `testPhy` (binds 127.0.0.1 UDP/TCP loopback; no root needed).
|
||||||
|
- **CI** (`.github/workflows/`): `build.yml` = build smoke (`make`, `make selftest`); `validate.yml` = `make one ZT_COVERAGE=1 ZT_TRACE=1` then `.github/workflows/validate-linux.sh` (two-node netns + ping + iperf3 + valgrind) and `validate-report.sh` (fails on any definite leak or non-zero test exit — the only hard gate). `ZT_COVERAGE=1` adds `--coverage` flags (`make-linux.mk:322-326`); coverage recorded via gcovr, never thresholded.
|
||||||
|
- **Lint/format**: `make lint` → `tools/lint.sh` (clang-format on changed lines vs base; CI: `.github/workflows/lint.yml`, clang-format pinned 23.1.1). No clang-tidy, no `-Werror`. Do not reformat untouched lines.
|
||||||
|
- **Gaps**: no Rust tests, no Java tests (orphaned `java/test/` deleted), no `make test`/`check` target. Prefer extending `selftest.cpp` for regression coverage; `rule-compiler` has `npm test` (real, `rule-compiler/test.js`).
|
||||||
|
- **Windows smoke** (`windows/README.md`): run exe `-p9994 -C <tmpdir>` then `zerotier-cli.bat -p9994 -D<tmpdir> info|join`.
|
||||||
+6
-5
@@ -6,16 +6,17 @@ RUN apt-get update -qq && apt-get -qq install make clang
|
|||||||
COPY . .
|
COPY . .
|
||||||
RUN /usr/bin/make
|
RUN /usr/bin/make
|
||||||
RUN echo $PWD
|
RUN echo $PWD
|
||||||
RUN cp zerotier-one /usr/sbin
|
RUN cp backone /usr/sbin
|
||||||
|
|
||||||
FROM ubuntu:21.04
|
FROM ubuntu:21.04
|
||||||
|
|
||||||
COPY --from=stage /zerotier-one /usr/sbin
|
COPY --from=stage /backone /usr/sbin
|
||||||
RUN ln -sf /usr/sbin/zerotier-one /usr/sbin/zerotier-idtool
|
RUN ln -sf /usr/sbin/backone /usr/sbin/backone-idtool
|
||||||
RUN ln -sf /usr/sbin/zerotier-one /usr/sbin/zerotier-cli
|
RUN ln -sf /usr/sbin/backone /usr/sbin/backone-cli
|
||||||
|
|
||||||
RUN echo "${VERSION}" > /etc/zerotier-version
|
RUN echo "${VERSION}" > /etc/zerotier-version
|
||||||
RUN rm -rf /var/lib/zerotier-one
|
RUN rm -rf /var/lib/backone
|
||||||
|
ENV ZEROTIER_HOME=/var/lib/backone ZT_PID_FILE=backone.pid ZT_PORT_FILE=backone.port ZT_DAEMON=/usr/sbin/backone ZT_CLI=backone-cli
|
||||||
|
|
||||||
|
|
||||||
RUN apt-get -qq update
|
RUN apt-get -qq update
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "comms-core",
|
|
||||||
"name": "Comms Core",
|
|
||||||
"version": "1.17.1",
|
|
||||||
"requires": [],
|
|
||||||
"backend": {
|
|
||||||
"routes": ["/controller/comms"],
|
|
||||||
"websocketEvents": ["presence", "incoming_call", "call_state", "chat_message"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -27,9 +27,8 @@ ifeq ($(OSTYPE),NetBSD)
|
|||||||
include make-netbsd.mk
|
include make-netbsd.mk
|
||||||
endif
|
endif
|
||||||
|
|
||||||
ifeq ($(OS),Windows_NT)
|
lint:
|
||||||
include make-windows.mk
|
@./tools/lint.sh
|
||||||
endif
|
|
||||||
|
|
||||||
drone:
|
drone:
|
||||||
@echo "rendering .drone.yaml from .drone.jsonnet"
|
@echo "rendering .drone.yaml from .drone.jsonnet"
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -1,4 +1,4 @@
|
|||||||
# BackOne - (1.14.1)
|
# BackOne - (2.0.0)
|
||||||
---
|
---
|
||||||
[BackOne](https://backone.cloud)
|
[BackOne](https://backone.cloud)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
ZeroTier Release Notes
|
ZeroTier Release Notes
|
||||||
======
|
======
|
||||||
|
|
||||||
|
# 2026-10-01 -- Version 2.0.0
|
||||||
|
|
||||||
|
* BackOne version diverges from zerotier-one from this point: version
|
||||||
|
scheme starts at 2.0.0 to distinguish BackOne packages and binaries
|
||||||
|
from upstream ZeroTier One 1.14.x.
|
||||||
|
|
||||||
# 2024-09-12 -- Version 1.14.1
|
# 2024-09-12 -- Version 1.14.1
|
||||||
|
|
||||||
* Multithreaded packet I/O support! Currently this is just for Linux and must
|
* Multithreaded packet I/O support! Currently this is just for Linux and must
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ The following versions of ZeroTier One receive security updates
|
|||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| -------- | ------------------ |
|
| -------- | ------------------ |
|
||||||
|
| 2.0.x | :white_check_mark: |
|
||||||
| 1.14.x | :white_check_mark: |
|
| 1.14.x | :white_check_mark: |
|
||||||
| 1.12.x | :white_check_mark: |
|
| 1.12.x | :white_check_mark: |
|
||||||
| < 1.12.0 | :x: |
|
| < 1.12.0 | :x: |
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# SPEC — BackOne
|
||||||
|
|
||||||
|
## §G (goal)
|
||||||
|
|
||||||
|
Make BackOne quantum ready: phases 1-6 of `backone-quantum-fork-research.md` — liboqs + hybrid X25519+ML-KEM-768 / Ed25519+ML-DSA-65 handshake, identity/COM v2 double-sign, capability-bit negotiation. Phase 7 (AES-256-GCM) OUT.
|
||||||
|
|
||||||
|
## §C (constraints)
|
||||||
|
|
||||||
|
- C++17. GNU make = real build. Top `CMakeLists.txt` = stub, never product build.
|
||||||
|
- Deps vendored in `ext/`. No new dep for few-liners.
|
||||||
|
- Tabs always, `.clang-format` LLVM/Stroustrup, indent 4. No reformat of untouched lines.
|
||||||
|
- New `.cpp/.hpp/.h` gets 11-line BSL header verbatim (`node/Mutex.hpp:1-12`).
|
||||||
|
- `node/` stays OS-independent. Sockets/files → `osdep/`. Orchestration → `service/`. Persistence → `controller/`.
|
||||||
|
- No exceptions across C API. Fatal `ZT_ResultCode` = 100–999.
|
||||||
|
- One reactor thread (`Phy::poll`). Cross-thread wake only `Phy::whack()`.
|
||||||
|
- New compiled file → register in `objects.mk` or no link.
|
||||||
|
- Platforms: Linux (primary), macOS, BSD/NetBSD, Windows (msbuild).
|
||||||
|
- Header edits: no `-MMD` dep tracking → `make clean` after touching `.h`.
|
||||||
|
- PQC lock: liboqs static vendored `ext/`; hybrid key = KDF(classical ‖ pq); presets `hybrid` (ML-KEM-768/ML-DSA-65) + `pqconly` (intended ML-KEM-1024/ML-DSA-87 — not yet distinct, see T25)
|
||||||
|
- wire/identity/COM use versioned v2 + capability-bit fallback to classic
|
||||||
|
- `include/ZeroTierOne.h` changes additive-only (Java/libzt ripple)
|
||||||
|
- OUT: AES-256-GCM swap, trusted-path skipCrypto, forward secrecy, protocol standardization, crypto audit, production release/signing
|
||||||
|
- `?` address v2 derivation keeping 40-bit + PoW
|
||||||
|
- `?` VL1 fragmentation capacity for +4KB identity (check `node/Packet.*`)
|
||||||
|
- `?` liboqs version/tag + build flags for `ext/`
|
||||||
|
- `?` measured ML-KEM/ML-DSA latency (doc estimates unverified)
|
||||||
|
- `?` capability-bit placement in existing handshake
|
||||||
|
|
||||||
|
## §I (public surfaces)
|
||||||
|
|
||||||
|
| id | surface | detail |
|
||||||
|
|---|---|---|
|
||||||
|
| I.capi | C API | `include/ZeroTierOne.h`, `ZT_Node_*` (lines 1796-2258), `ZT_SDK_API`. Ripples to Java/libzt/SDK |
|
||||||
|
| I.api | HTTP JSON API | `127.0.0.1:9993`, `/status /peer /network /member /controller /metrics /info`, auth `X-ZT1-Auth` header or `?auth=` from `authtoken.secret` |
|
||||||
|
| I.cli | CLI | `backone-cli info\|listpeers\|listnetworks\|join\|leave` (`-j -p -D`), `backone-idtool generate\|validate\|getpublic\|sign\|verify\|mkcom` — symlink dispatch on `argv[0]` |
|
||||||
|
| I.cfg | config | home `/var/lib/backone` (macOS `/Library/Application Support/BackOne`), `local.conf`, `identity.secret`, `networks.d/`, `controller.db`, `backone.port`, `backone.pid` |
|
||||||
|
| I.build | build | `make`, `make one`, `make selftest`, `make core`, `make debug`, `make install`, `make debian/redhat`, `make central-controller`; env `ZT_*` knobs |
|
||||||
|
| I.ffi | Rust FFI | `rustybits/zeroidc` (`zeroidc_new`…), `smeeclient` — C header `zeroidc.h`, link on `ZT_SSO_SUPPORTED=1`/`ZT_CONTROLLER=1` |
|
||||||
|
| I.jni | Java SDK | `java/jni/…Node.cpp`, ant targets `build_java/build_android/build_jar` |
|
||||||
|
| I.pqc.cfg | config | `local.conf` `settings.pqcMode` (off/hybrid/pqconly); **absent = off/classic (opt-in)**, unknown value = warn + classic. Per-algorithm `kem`/`sig` selection is *not* wired (research doc only, see T25) |
|
||||||
|
| I.pqc.wire | wire | static-static hybrid, no ephemeral exchange: identity v2 (type byte 1) carries ML-KEM-768 pub + ML-DSA-65 pub; agree = KDF(X25519-identity-agree ‖ ML-KEM-encaps(peer static pub)) (`node/Identity.hpp:364-379` encaps / `:392+` decaps, driven from `node/Peer.cpp` `ensurePendingHybridCt`/`setHybridSessionKey`); capability via HELLO protocol-version bytes (`node/Packet.hpp:329-333`); `identity.secret`/`identity.public` v2 on disk |
|
||||||
|
| I.pqc.com | credentials | COM double-sign/verify — controller + `node/CertificateOfMembership.*` |
|
||||||
|
| I.rules | rules compiler | `node rule-compiler/cli.js <rules>` → rules/tags JSON |
|
||||||
|
|
||||||
|
## §V (invariants)
|
||||||
|
|
||||||
|
Derived from `selftest.cpp` + CI gates. `?` = code-derived, no test yet.
|
||||||
|
|
||||||
|
| id | invariant | evidence |
|
||||||
|
|---|---|---|
|
||||||
|
| V1 | Crypto KATs pass: Salsa20 vectors, C25519 agreement, Ed25519 sign/verify, SHA512, Poly1305 | `selftest.cpp:137` `testCrypto` |
|
||||||
|
| V2 | Identity known-good accepted, known-bad rejected | `selftest.cpp:475` `testIdentity` |
|
||||||
|
| V3 | CertificateOfMembership signs and `agreesWith` roundtrip | `selftest.cpp:577` `testCertificate` |
|
||||||
|
| V4 | Packet encode/decode roundtrip exact | `selftest.cpp:641` `testPacket` |
|
||||||
|
| V5 | Phy UDP+TCP loopback on 127.0.0.1 works, no root | `selftest.cpp:1002` `testPhy` |
|
||||||
|
| V6 | Zero definite leaks under valgrind; test exit code 0 | `.github/workflows/validate-report.sh` |
|
||||||
|
| V7 | `?` No exception crosses C API; `ZT_ResultCode_isFatal` only 100–999 | `include/ZeroTierOne.h:426` |
|
||||||
|
| V8 | `?` `node/` contains no OS calls (sockets/files/interfaces) | `node/README.md` rule |
|
||||||
|
| V9 | hybrid↔vanilla negotiates classic, link stays up | interop matrix |
|
||||||
|
| V10 | identity v2 survives 1280B MTU tunnel intact; v2 = type byte 1 (wire `Identity.hpp:219`, string slot `Identity.cpp:173`); address derivation hashes X25519 material only → v1/v2 addresses bit-identical; v1 parser rejects type ≠ 0 cleanly | interop matrix |
|
||||||
|
| V11 | ML-KEM/ML-DSA KATs + hybrid KDF vector + identity v2 roundtrip + COM double-sign verify pass | extended `selftest.cpp` |
|
||||||
|
| V12 | `pqcMode` negotiation honors `local.conf` (`off` -> no v2 fields) | live daemon: bogus `settings.pqcMode` -> `WARNING` (`service/OneService.cpp:1478`), `hybrid` -> clean (2026-10-03); `selftest` has no `pqcMode` coverage |
|
||||||
|
| V13 | hybrid agree = KDF(X25519-identity-agree ‖ ML-KEM static-static), no ephemeral exchange; hybrid↔hybrid agree + hybrid↔vanilla classic fallback pass | `node/Peer.cpp:105-117` (encaps) `node/IncomingPacket.cpp:534-542,697-703` (decaps) |
|
||||||
|
| V14 | COM double-sign = type byte 1→2 appends ML-DSA sig alongside Ed25519; v1 verify path unchanged; v1 rejects type 2 cleanly | `node/CertificateOfMembership.hpp:222-296`, `node/CertificateOfMembership.cpp:97-162`, `selftest.cpp:717` |
|
||||||
|
| V15 | handshake vs fragment loss measured: PQC identity in clear HELLO = 3273B → 3 frags (classic 137B → 1 frag, max 7 `Packet.hpp:235`); Monte Carlo 20k trials obs≈analytic at p∈{.01,.05,.10,.30}; fragment loss delays HELLO (retransmit), does not break handshake | `selftest.cpp:1713` benchmark |
|
||||||
|
| V16 | hybrid handshake puts a 1088 B ML-KEM-768 ct on the wire and both sides derive the same hybrid `_key`; classic/off peers carry none and fall back | write `node/Peer.cpp:493-502`, parse `node/IncomingPacket.cpp:534-542`, OK echo `node/IncomingPacket.cpp:608-619,697-703`; E2E lab M1-M5 all pass `tools/pqc-lab.sh` (2026-10-02; M3 68 / M4 90 large PQ datagrams on wire) |
|
||||||
|
|
||||||
|
## §T (tasks)
|
||||||
|
|
||||||
|
| id | status | task | cites |
|
||||||
|
|---|---|---|---|
|
||||||
|
| T1 | x | Fix CI build smoke: `build.yml` runs `./zerotier-selftest`, build emits `backone-selftest` (also `tar zerotier-one` → `backone`) | I.build,V6 |
|
||||||
|
| T2 | x | Fix `validate-linux.sh` binary names: invokes `./zerotier-one`, `./zerotier-cli` (lines 41-43,105,111,451), builds `backone*` | I.cli,V6 |
|
||||||
|
| T3 | x | Wire `ZT_COVERAGE` into makefiles — consumed by none, gcovr likely reports zeros `?` | I.build |
|
||||||
|
| T4 | x | `make install` manpage rule: expects `doc/backone.8`, `doc/backone-cli.1`, `doc/backone-idtool.1`; `doc/` only has `zerotier-*`, no rename rule | I.build |
|
||||||
|
| T5 | x | `make redhat`: `backone.spec` %install copies missing `debian/backone.service` + `ext/installfiles/linux/backone.init.rhel6` (actual files `zerotier-*`) | I.build |
|
||||||
|
| T6 | x | Rebrand `make-bsd.mk`/`make-netbsd.mk`: still emit/install `zerotier-one`, `/var/db/zerotier-one` vs code writing `backone.pid` | I.build |
|
||||||
|
| T7 | x | `Dockerfile.ci`: copies `zerotier-one` from build → fails; binary is `backone` | I.build |
|
||||||
|
| T8 | . | `pkg/snap/snapcraft.yaml`: builds upstream `github.com/zerotier/zerotierone.git`, expects `usr/sbin/zerotier-one` | I.build |
|
||||||
|
| T9 | . | Wire orphaned `java/test/StringUtilsTest.java` (JUnit4) into `java/build.xml` target or delete | I.jni |
|
||||||
|
| T10 | . | Replace `rule-compiler/package.json` `npm test` failing placeholder with real test of `rule-compiler.js` | I.rules |
|
||||||
|
| T11 | . | `?` Add `#[test]` coverage for `rustybits/zeroidc` FFI parse/exchange paths | I.ffi,V7 |
|
||||||
|
| T12 | x | Implement Redis config TODO (`service/OneService.cpp:1467` `// TODO: Redis config`) | I.cfg |
|
||||||
|
| T13 | . | Implement `LFDB::eraseNetwork`/`eraseMember` TODOs (`controller/LFDB.cpp:383,388`) | I.cfg |
|
||||||
|
| T14 | . | ~~Lint gate~~ done: `make lint` (`tools/lint.sh`, changed-lines clang-format) + `.github/workflows/lint.yml` | I.build |
|
||||||
|
| T15 | . | Fix `-Wsign-compare` suppression FIXME (`node/Bond.cpp:23`, `node/Node.cpp:40`) | I.build |
|
||||||
|
| T16 | x | Federation identity-collision payload FIXME (`node/IncomingPacket.cpp:211`) `?` scope | I.capi,V7 |
|
||||||
|
| T17 | x | Phase 1: vendor liboqs static into `ext/`, wire `make-linux.mk` + `objects.mk`, pin version/tag + flags | I.build |
|
||||||
|
| T18 | x | Phase 2: `node/PQHybrid.*` hybrid agree = KDF(X25519-identity-agree ‖ ML-KEM-768 encaps over peer static pub); phase-2 ephemeral-field mockup dropped | I.pqc.wire,V13 |
|
||||||
|
| T19 | x | Phase 3: identity v2 type byte 1, append ML-KEM+ML-DSA pubs, address hash X25519-only, v1 clean reject type ≠ 0 | I.pqc.wire,V10 |
|
||||||
|
| T20 | x | Phase 4: COM type byte 2 double-sign (Ed25519+ML-DSA-65), v1 rejects cleanly | I.pqc.com,V14 |
|
||||||
|
| T21 | . | Phase 5: `local.conf` `settings.pqcMode` parse (off/hybrid/pqconly) + capability negotiation via HELLO protocol-version bytes, fallback classic. Mode now selects *whether* PQ material is used; *which* preset is still hardcoded (T24) | I.pqc.cfg,V12,V9 |
|
||||||
|
| T22 | x | Phase 6: selftest extension — ML-KEM/ML-DSA KATs, hybrid KDF vector, identity v2 roundtrip + address stability, COM double-sign, interop hybrid↔vanilla matrix | I.build,V11,V10,V14 |
|
||||||
|
| T23 | x | Phase 6: handshake-vs-fragment-loss benchmark for PQC HELLO; cap/segment if loss breaks handshake | V15 |
|
||||||
|
| T24 | x | Plumb `settings.pqcMode` through the identity path: `Node(…,int pqcMode)` stores `_pqcMode`; `Identity::generate(bool pq,int pqcMode)` derives `pq` from the daemon mode; `OneService` reads `local.conf` before `new Node` — a fresh hybrid/pqconly node writes a type-1 identity, a classic one type-0. Fixes the G1 defect. | I.pqc.cfg,V12 |
|
||||||
|
| T25 | . | Distinct PQ parameter sets: `node/PQHybrid.*` hardcodes ML-KEM-768/ML-DSA-65 (`PQHybrid.cpp` static_asserts `OQS_KEM_ml_kem_768`/`OQS_SIG_ml_dsa_65`), so `hybrid` and `pqconly` currently select identical material, while `SPEC` §C claims ML-KEM-1024/ML-DSA-87 for `pqconly`. Parameterize the preset (key sizes, ct len, `_pendingHybridCt` buffer, HELLO guard) or drop the `pqconly` claim. Note a `pqconly` HELLO with a PQ identity is 3273B → 3 frags, well under the 10024B cap, so no clean-fail path is reachable today | I.pqc.wire,I.pqc.cfg,V15 |
|
||||||
|
|
||||||
|
T1/T2 = prerequisites — CI must invoke `backone-selftest` before PQC gates mean anything. T1–T16 backlog (rebrand/CI/lint leftovers); T17–T25 = PQC phases 1–6 + the pqconly preset gap.
|
||||||
|
|
||||||
|
Not tracked (low value `?`): `one.cpp:256` port/token cleanup, `MacKextEthernetTap.cpp:480` iface status, VLAN TODOs in `NetBSDEthernetTap.cpp:468`/`MacKextEthernetTap.cpp:685`, `Phy.hpp:325` unix sock type comment.
|
||||||
|
|
||||||
|
## §B (bug log)
|
||||||
|
|
||||||
|
| id | date | cause | fix |
|
||||||
|
|---|---|---|---|
|
||||||
|
| B1 | 2026-10-02 | Phase 5 (T21) advertised PQC capability in HELLO but never consumed it: `Peer::_key` was derived classically in the constructor and the hybrid ciphertext had no place in the HELLO/OK(HELLO) layout, so `hybridEligible()` was dead. | Wire hybrid static-static KEM ct into HELLO (lowest-address side writes `[moon count][moons][flag(1B)][ct 1088B]`, crypted with the classical key) and echo it in OK(HELLO); parser order matches `Peer::sendHELLO`/`_doHELLO`; `Peer::ensurePendingHybridCt()` lazily encapsulates on first capability sighting so the handshake completes in one round trip; direction guard `sender == lowest address` on both parses. Refuse to start when the on-disk identity cannot satisfy `settings.pqcMode` (classic↔PQ migration changes the address). |
|
||||||
|
| B2 | 2026-10-02 | `settings.pqcMode` absent (or unrecognized) resolved to HYBRID, and `Node`'s `pqcMode` default was HYBRID with no way to tell "unconfigured" from "configured hybrid". Every existing type-0 install would hit the B1 refusal guard at startup; the C API `ZT_Node_new` path would silently generate type-1 identities. | Absent/unknown `settings.pqcMode` now resolves to `ZT_PQC_MODE_CLASSIC` (PQC is opt-in, matching the research doc); `Node(…,int pqcMode = ZT_PQC_MODE_CLASSIC)`. Verified: classic identity + no config boots on 2.0.0 and stays 141 B; fresh node w/o config makes type-0; fresh node with `"hybrid"` makes type-1 (6415 B); classic + explicit `"off"` boots; classic + explicit `"hybrid"` still refuses; unknown value warns and runs classic. |
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
window.BackOneRingingPopup = {
|
|
||||||
_audio: null,
|
|
||||||
showIncomingCall({ callerName, callType, callId, onAccept, onReject }) {
|
|
||||||
const el = document.getElementById('ringing-popup');
|
|
||||||
if (!el) return;
|
|
||||||
el.classList.remove('ringing-hidden');
|
|
||||||
el.innerHTML = `
|
|
||||||
<div class="ringing-card">
|
|
||||||
<h2>Incoming ${callType} call</h2>
|
|
||||||
<p>${callerName || 'Unknown peer'}</p>
|
|
||||||
<button class="ringing-accept">Accept</button>
|
|
||||||
<button class="ringing-reject">Reject</button>
|
|
||||||
</div>`;
|
|
||||||
el.querySelector('.ringing-accept').onclick = () => { this.dismiss(); onAccept && onAccept(callId); };
|
|
||||||
el.querySelector('.ringing-reject').onclick = () => { this.dismiss(); onReject && onReject(callId); };
|
|
||||||
this.playRingtone();
|
|
||||||
if (Notification.permission === 'default') Notification.requestPermission();
|
|
||||||
if (Notification.permission === 'granted') {
|
|
||||||
new Notification(`Incoming ${callType} call`, { body: callerName || 'BackOne peer' });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dismiss() {
|
|
||||||
const el = document.getElementById('ringing-popup');
|
|
||||||
if (el) { el.classList.add('ringing-hidden'); el.innerHTML = ''; }
|
|
||||||
this.stopRingtone();
|
|
||||||
},
|
|
||||||
playRingtone() {
|
|
||||||
this.stopRingtone();
|
|
||||||
try {
|
|
||||||
this._audio = new Audio('data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmMcBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+DyvmwhBSuBzvLZiTYIG2m98OScTgwOUarm7blmHgU7k9n1unEiBC13y+/ek0EKElyx5+ytWBUIQ5zd8sFuJAUwgM/z2Ik6CBxpvfDknE4MDlGq5u25Zh4FO5PZ9br');
|
|
||||||
this._audio.loop = true;
|
|
||||||
this._audio.play().catch(() => {});
|
|
||||||
} catch (_) {}
|
|
||||||
},
|
|
||||||
stopRingtone() {
|
|
||||||
if (this._audio) { this._audio.pause(); this._audio = null; }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
async function loadAddonManifest() {
|
|
||||||
const res = await fetch('/app/addons/manifest.json', { cache: 'no-store' });
|
|
||||||
if (!res.ok) {
|
|
||||||
console.warn('manifest.json fetch failed:', res.status);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const data = await res.json();
|
|
||||||
return Array.isArray(data) ? data : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadAddonModule(entry) {
|
|
||||||
const entryPath = entry.frontend && entry.frontend.entry ? entry.frontend.entry : `/app/addons/${entry.id}/index.js`;
|
|
||||||
await import(entryPath);
|
|
||||||
const factory = window.BackOneAddons && window.BackOneAddons[entry.id];
|
|
||||||
return factory ? factory() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.BackOneAddonLoader = { loadAddonManifest, loadAddonModule };
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
/**
|
|
||||||
* BackOne API auth — same token as backone-cli (~/.backOneOneAuthToken / X-ZT1-Auth).
|
|
||||||
*/
|
|
||||||
window.BackOneAuth = (function () {
|
|
||||||
const LS_KEY = 'backone.authToken';
|
|
||||||
let cache = null;
|
|
||||||
|
|
||||||
function trimToken(s) {
|
|
||||||
return String(s || '').replace(/\s+/g, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
function tokenFromUrl() {
|
|
||||||
try {
|
|
||||||
const params = new URLSearchParams(location.search);
|
|
||||||
const fromQuery = trimToken(params.get('auth') || params.get('token'));
|
|
||||||
if (fromQuery) return fromQuery;
|
|
||||||
const hash = (location.hash || '').replace(/^#\/?/, '');
|
|
||||||
const hashParams = new URLSearchParams(hash.includes('?') ? hash.split('?')[1] : '');
|
|
||||||
return trimToken(hashParams.get('auth') || hashParams.get('token'));
|
|
||||||
} catch (_) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function probeToken(token) {
|
|
||||||
if (!token) return false;
|
|
||||||
try {
|
|
||||||
const res = await fetch('/network', {
|
|
||||||
cache: 'no-store',
|
|
||||||
headers: { 'X-ZT1-Auth': token }
|
|
||||||
});
|
|
||||||
return res.ok;
|
|
||||||
} catch (_) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchAuthtoken() {
|
|
||||||
for (const path of ['/api/backone/authtoken', '/app/api/authtoken']) {
|
|
||||||
try {
|
|
||||||
const res = await fetch(path, { cache: 'no-store' });
|
|
||||||
if (res.ok) {
|
|
||||||
const data = await res.json();
|
|
||||||
const token = trimToken(data && data.token);
|
|
||||||
if (token) return token;
|
|
||||||
}
|
|
||||||
} catch (_) {
|
|
||||||
/* try next */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resolveToken() {
|
|
||||||
const urlToken = tokenFromUrl();
|
|
||||||
if (urlToken) {
|
|
||||||
if (await probeToken(urlToken)) {
|
|
||||||
return urlToken;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const stored = trimToken(localStorage.getItem(LS_KEY));
|
|
||||||
if (stored && await probeToken(stored)) {
|
|
||||||
return stored;
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiToken = await fetchAuthtoken();
|
|
||||||
if (apiToken && await probeToken(apiToken)) {
|
|
||||||
return apiToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stored) return stored;
|
|
||||||
if (apiToken) return apiToken;
|
|
||||||
if (urlToken) return urlToken;
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getToken() {
|
|
||||||
if (cache) return cache;
|
|
||||||
cache = await resolveToken();
|
|
||||||
return cache || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function authHeaders(extra) {
|
|
||||||
const token = await getToken();
|
|
||||||
const headers = Object.assign({}, extra || {});
|
|
||||||
if (token) {
|
|
||||||
headers['X-ZT1-Auth'] = token;
|
|
||||||
}
|
|
||||||
return headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchWithAuth(path, options) {
|
|
||||||
options = options || {};
|
|
||||||
const headers = await authHeaders(options.headers || {});
|
|
||||||
return fetch(path, Object.assign({}, options, { headers }));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function apiGet(path) {
|
|
||||||
const res = await fetchWithAuth(path, { cache: 'no-store' });
|
|
||||||
if (!res.ok) throw new Error(`${path} returned ${res.status}`);
|
|
||||||
return res.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function apiGetFirst(paths) {
|
|
||||||
let lastErr = null;
|
|
||||||
for (const path of paths) {
|
|
||||||
try {
|
|
||||||
return await apiGet(path);
|
|
||||||
} catch (err) {
|
|
||||||
lastErr = err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw lastErr || new Error('API request failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function apiSend(path, method, body) {
|
|
||||||
const headers = await authHeaders(
|
|
||||||
body != null ? { 'Content-Type': 'application/json' } : {}
|
|
||||||
);
|
|
||||||
const res = await fetch(path, {
|
|
||||||
method,
|
|
||||||
headers,
|
|
||||||
body: body != null ? JSON.stringify(body) : undefined
|
|
||||||
});
|
|
||||||
if (!res.ok) {
|
|
||||||
const text = await res.text().catch(() => '');
|
|
||||||
throw new Error(text || `${method} ${path} returned ${res.status}`);
|
|
||||||
}
|
|
||||||
if (res.status === 204) {
|
|
||||||
return { result: true };
|
|
||||||
}
|
|
||||||
const text = await res.text();
|
|
||||||
if (!text) {
|
|
||||||
return { result: true };
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return JSON.parse(text);
|
|
||||||
} catch (_) {
|
|
||||||
return { result: true };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function apiSendFirst(paths, method, body) {
|
|
||||||
let lastErr = null;
|
|
||||||
for (const path of paths) {
|
|
||||||
try {
|
|
||||||
return await apiSend(path, method, body);
|
|
||||||
} catch (err) {
|
|
||||||
lastErr = err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw lastErr || new Error(`${method} failed`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Store token from ~/.backOneOneAuthToken (same as backone-cli). */
|
|
||||||
async function setUserToken(token) {
|
|
||||||
cache = trimToken(token);
|
|
||||||
if (cache) {
|
|
||||||
localStorage.setItem(LS_KEY, cache);
|
|
||||||
} else {
|
|
||||||
localStorage.removeItem(LS_KEY);
|
|
||||||
}
|
|
||||||
return cache;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function validateCurrentToken() {
|
|
||||||
cache = null;
|
|
||||||
const token = await getToken();
|
|
||||||
if (!token) return false;
|
|
||||||
return probeToken(token);
|
|
||||||
}
|
|
||||||
|
|
||||||
function invalidate() {
|
|
||||||
cache = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function init() {
|
|
||||||
return getToken();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
getToken,
|
|
||||||
authHeaders,
|
|
||||||
fetchWithAuth,
|
|
||||||
apiGet,
|
|
||||||
apiGetFirst,
|
|
||||||
apiSend,
|
|
||||||
apiSendFirst,
|
|
||||||
setUserToken,
|
|
||||||
validateCurrentToken,
|
|
||||||
invalidate,
|
|
||||||
init
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
window.BackOneContacts = (function () {
|
|
||||||
let cache = null;
|
|
||||||
|
|
||||||
async function load() {
|
|
||||||
if (cache) return cache;
|
|
||||||
try {
|
|
||||||
const res = await BackOneAuth.fetchWithAuth('/api/backone/contacts', { cache: 'no-store' })
|
|
||||||
.catch(() => BackOneAuth.fetchWithAuth('/app/api/contacts', { cache: 'no-store' }));
|
|
||||||
if (!res.ok) {
|
|
||||||
cache = {};
|
|
||||||
return cache;
|
|
||||||
}
|
|
||||||
cache = await res.json();
|
|
||||||
if (!cache || typeof cache !== 'object') cache = {};
|
|
||||||
} catch (_) {
|
|
||||||
cache = {};
|
|
||||||
}
|
|
||||||
return cache;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDisplayName(peerId, controllerName) {
|
|
||||||
const c = cache || {};
|
|
||||||
const entry = c[peerId];
|
|
||||||
if (entry && entry.nickname) return entry.nickname;
|
|
||||||
if (controllerName) return controllerName;
|
|
||||||
return peerId;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNickname(peerId) {
|
|
||||||
const entry = (cache || {})[peerId];
|
|
||||||
return entry && entry.nickname ? entry.nickname : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setNickname(peerId, nickname) {
|
|
||||||
await load();
|
|
||||||
const patch = {};
|
|
||||||
patch[peerId] = { nickname: nickname || '' };
|
|
||||||
const res = await BackOneAuth.fetchWithAuth('/api/backone/contacts', {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(patch)
|
|
||||||
}).catch(() => BackOneAuth.fetchWithAuth('/app/api/contacts', {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(patch)
|
|
||||||
}));
|
|
||||||
if (!res.ok) throw new Error('Failed to save nickname');
|
|
||||||
cache = await res.json();
|
|
||||||
return cache;
|
|
||||||
}
|
|
||||||
|
|
||||||
function invalidate() {
|
|
||||||
cache = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { load, getDisplayName, getNickname, setNickname, invalidate };
|
|
||||||
})();
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>BackOne</title>
|
|
||||||
<link rel="stylesheet" href="/app/styles.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1>BackOne</h1>
|
|
||||||
<div id="network-bar" class="network-bar">
|
|
||||||
<label class="network-bar-label">Network
|
|
||||||
<select id="global-network"></select>
|
|
||||||
</label>
|
|
||||||
<input id="global-join-id" type="text" maxlength="16" placeholder="Join network ID" autocomplete="off" />
|
|
||||||
<button type="button" id="global-join-btn">Join</button>
|
|
||||||
<button type="button" id="global-leave-btn">Leave</button>
|
|
||||||
<span id="global-network-status" class="network-bar-status"></span>
|
|
||||||
<div id="auth-token-bar" class="auth-token-bar auth-token-hidden">
|
|
||||||
<label class="auth-token-label">Auth token
|
|
||||||
<input id="auth-token-input" type="password" autocomplete="off"
|
|
||||||
placeholder="Paste from ~/.backOneOneAuthToken" />
|
|
||||||
</label>
|
|
||||||
<button type="button" id="auth-token-save">Save</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<nav id="addon-nav"></nav>
|
|
||||||
</header>
|
|
||||||
<main id="addon-panel">
|
|
||||||
<p>Select an addon from the navigation bar.</p>
|
|
||||||
</main>
|
|
||||||
<div id="ringing-popup" class="ringing-hidden"></div>
|
|
||||||
<script src="/app/auth-store.js"></script>
|
|
||||||
<script src="/app/contacts-store.js"></script>
|
|
||||||
<script src="/app/peer-directory.js"></script>
|
|
||||||
<script src="/app/network-session.js"></script>
|
|
||||||
<script src="/app/peer-picker.js"></script>
|
|
||||||
<script src="/app/addon-loader.js"></script>
|
|
||||||
<script src="/app/main.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
window.BackOneAddons = window.BackOneAddons || {};
|
|
||||||
|
|
||||||
const addonButtons = {};
|
|
||||||
let addonModules = {};
|
|
||||||
let coreRefreshTimer = null;
|
|
||||||
|
|
||||||
function hashAddonId() {
|
|
||||||
const h = (location.hash || '').replace(/^#\/?/, '').toLowerCase();
|
|
||||||
if (h === 'voice' || h === 'video' || h === 'chat') return h;
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function activateAddon(id) {
|
|
||||||
const btn = addonButtons[id];
|
|
||||||
if (!btn) return;
|
|
||||||
btn.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeHtml(s) {
|
|
||||||
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
||||||
}
|
|
||||||
|
|
||||||
function networkLabel(n) {
|
|
||||||
if (!n) return '';
|
|
||||||
return n.name ? `${n.name} (${n.id})` : n.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderCoreStatus(statusEl) {
|
|
||||||
const core = BackOneNetworkSession.getCoreStatus();
|
|
||||||
const net = BackOneNetworkSession.getActiveNetwork();
|
|
||||||
if (!core.online) {
|
|
||||||
statusEl.innerHTML = '<span class="core-dot offline"></span> BackOne offline — start <code>backone</code> service';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const netPart = net ? networkLabel(net) : 'no network joined';
|
|
||||||
const addr = core.address ? ` · ${escapeHtml(core.address)}` : '';
|
|
||||||
const authNote = core.authenticated
|
|
||||||
? ' · auth token OK'
|
|
||||||
: ' · <span class="peer-network-warn">paste <code>~/.backOneOneAuthToken</code> below</span>';
|
|
||||||
statusEl.innerHTML =
|
|
||||||
`<span class="core-dot online"></span> BackOne online${addr} · ` +
|
|
||||||
`<strong>${escapeHtml(netPart)}</strong> · ${core.leafPeerCount} peer(s)${authNote}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function initAuthTokenBar(statusEl, onSaved) {
|
|
||||||
const bar = document.getElementById('auth-token-bar');
|
|
||||||
const input = document.getElementById('auth-token-input');
|
|
||||||
const saveBtn = document.getElementById('auth-token-save');
|
|
||||||
if (!bar || !input || !saveBtn) return;
|
|
||||||
|
|
||||||
async function refreshAuthUi() {
|
|
||||||
const ok = await BackOneAuth.validateCurrentToken();
|
|
||||||
bar.classList.toggle('auth-token-hidden', ok);
|
|
||||||
if (!ok && !input.value) {
|
|
||||||
input.placeholder = 'Paste from ~/.backOneOneAuthToken';
|
|
||||||
}
|
|
||||||
return ok;
|
|
||||||
}
|
|
||||||
|
|
||||||
saveBtn.addEventListener('click', async () => {
|
|
||||||
const token = input.value.trim();
|
|
||||||
if (!token) {
|
|
||||||
if (statusEl) statusEl.textContent = 'Paste the token from ~/.backOneOneAuthToken';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
saveBtn.disabled = true;
|
|
||||||
await BackOneAuth.setUserToken(token);
|
|
||||||
BackOneAuth.invalidate();
|
|
||||||
const ok = await BackOneAuth.validateCurrentToken();
|
|
||||||
if (!ok) {
|
|
||||||
if (statusEl) statusEl.textContent = 'Token rejected — check ~/.backOneOneAuthToken matches the running backone service';
|
|
||||||
saveBtn.disabled = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
input.value = '';
|
|
||||||
bar.classList.add('auth-token-hidden');
|
|
||||||
if (typeof onSaved === 'function') await onSaved();
|
|
||||||
saveBtn.disabled = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
await refreshAuthUi();
|
|
||||||
return refreshAuthUi;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function initNetworkBar() {
|
|
||||||
const bar = document.getElementById('network-bar');
|
|
||||||
if (!bar) return;
|
|
||||||
|
|
||||||
const netSel = bar.querySelector('#global-network');
|
|
||||||
const joinInput = bar.querySelector('#global-join-id');
|
|
||||||
const joinBtn = bar.querySelector('#global-join-btn');
|
|
||||||
const leaveBtn = bar.querySelector('#global-leave-btn');
|
|
||||||
const statusEl = bar.querySelector('#global-network-status');
|
|
||||||
|
|
||||||
async function reloadNetworkList(selectId) {
|
|
||||||
await BackOneNetworkSession.syncFromCore();
|
|
||||||
const networks = await BackOneNetworkSession.loadNetworks();
|
|
||||||
netSel.innerHTML = networks.map(n =>
|
|
||||||
`<option value="${n.id}">${escapeHtml(networkLabel(n))} · ${escapeHtml(n.status || '')}</option>`
|
|
||||||
).join('') || '<option value="">— no networks joined —</option>';
|
|
||||||
|
|
||||||
const want = (selectId || BackOneNetworkSession.getNetworkId() || '').toLowerCase();
|
|
||||||
if (want && networks.some(n => (n.id || '').toLowerCase() === want)) {
|
|
||||||
netSel.value = networks.find(n => (n.id || '').toLowerCase() === want).id;
|
|
||||||
await BackOneNetworkSession.selectNetwork(netSel.value);
|
|
||||||
} else if (networks.length === 1) {
|
|
||||||
netSel.value = networks[0].id;
|
|
||||||
await BackOneNetworkSession.selectNetwork(networks[0].id);
|
|
||||||
} else if (networks.length > 0 && BackOneNetworkSession.getNetworkId()) {
|
|
||||||
netSel.value = BackOneNetworkSession.getNetworkId();
|
|
||||||
}
|
|
||||||
renderCoreStatus(statusEl);
|
|
||||||
}
|
|
||||||
|
|
||||||
netSel.addEventListener('change', async () => {
|
|
||||||
if (!netSel.value) return;
|
|
||||||
statusEl.textContent = 'Switching network…';
|
|
||||||
await BackOneNetworkSession.selectNetwork(netSel.value);
|
|
||||||
await BackOneNetworkSession.syncFromCore();
|
|
||||||
renderCoreStatus(statusEl);
|
|
||||||
});
|
|
||||||
|
|
||||||
joinBtn.addEventListener('click', async () => {
|
|
||||||
const id = BackOnePeerDirectory.normalizeNetworkId(joinInput.value);
|
|
||||||
if (!id) {
|
|
||||||
statusEl.textContent = 'Enter a 16-digit hexadecimal network ID';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
joinBtn.disabled = true;
|
|
||||||
leaveBtn.disabled = true;
|
|
||||||
statusEl.textContent = `Joining ${id}…`;
|
|
||||||
try {
|
|
||||||
await BackOneNetworkSession.joinNetwork(id);
|
|
||||||
joinInput.value = '';
|
|
||||||
await reloadNetworkList(id);
|
|
||||||
} catch (err) {
|
|
||||||
statusEl.textContent = err.message || 'Join failed';
|
|
||||||
} finally {
|
|
||||||
joinBtn.disabled = false;
|
|
||||||
leaveBtn.disabled = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
leaveBtn.addEventListener('click', async () => {
|
|
||||||
const id = netSel.value || BackOneNetworkSession.getNetworkId();
|
|
||||||
if (!id) {
|
|
||||||
statusEl.textContent = 'Select a network to leave';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!confirm(`Leave network ${networkLabel(BackOneNetworkSession.getActiveNetwork() || { id })}?`)) return;
|
|
||||||
joinBtn.disabled = true;
|
|
||||||
leaveBtn.disabled = true;
|
|
||||||
statusEl.textContent = `Leaving ${id}…`;
|
|
||||||
try {
|
|
||||||
await BackOneNetworkSession.leaveNetwork(id);
|
|
||||||
await reloadNetworkList();
|
|
||||||
} catch (err) {
|
|
||||||
statusEl.textContent = err.message || 'Leave failed';
|
|
||||||
} finally {
|
|
||||||
joinBtn.disabled = false;
|
|
||||||
leaveBtn.disabled = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
BackOneNetworkSession.subscribe(() => renderCoreStatus(statusEl));
|
|
||||||
|
|
||||||
const refreshAuthUi = await initAuthTokenBar(statusEl, () => reloadNetworkList());
|
|
||||||
|
|
||||||
await reloadNetworkList();
|
|
||||||
if (refreshAuthUi) await refreshAuthUi();
|
|
||||||
|
|
||||||
coreRefreshTimer = setInterval(() => {
|
|
||||||
BackOneNetworkSession.syncFromCore().then(() => renderCoreStatus(statusEl));
|
|
||||||
}, 45000);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function initAppShell() {
|
|
||||||
await BackOneAuth.init();
|
|
||||||
await initNetworkBar();
|
|
||||||
|
|
||||||
const nav = document.getElementById('addon-nav');
|
|
||||||
const panel = document.getElementById('addon-panel');
|
|
||||||
const addons = await BackOneAddonLoader.loadAddonManifest();
|
|
||||||
if (!addons.length) {
|
|
||||||
panel.innerHTML = '<p>No UI addons found. Install backone-addon-voice, backone-addon-video, or backone-addon-chat, then run: <code>sudo backone-updater rebuild-manifest</code> and restart backone.</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const entry of addons) {
|
|
||||||
const mod = await BackOneAddonLoader.loadAddonModule(entry);
|
|
||||||
if (mod) addonModules[entry.id] = mod;
|
|
||||||
const btn = document.createElement('button');
|
|
||||||
btn.dataset.addonId = entry.id;
|
|
||||||
btn.textContent = (entry.frontend && entry.frontend.navLabel) || entry.name || entry.id;
|
|
||||||
btn.onclick = () => {
|
|
||||||
nav.querySelectorAll('button').forEach(b => b.classList.remove('active'));
|
|
||||||
btn.classList.add('active');
|
|
||||||
panel.innerHTML = '';
|
|
||||||
if (addonModules[entry.id] && addonModules[entry.id].mount) {
|
|
||||||
addonModules[entry.id].mount(panel);
|
|
||||||
} else {
|
|
||||||
panel.innerHTML = `<p>Addon "${entry.id}" loaded.</p>`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
addonButtons[entry.id] = btn;
|
|
||||||
nav.appendChild(btn);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fromHash = hashAddonId();
|
|
||||||
if (fromHash && addonButtons[fromHash]) {
|
|
||||||
activateAddon(fromHash);
|
|
||||||
} else {
|
|
||||||
nav.querySelector('button')?.click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('hashchange', () => {
|
|
||||||
const id = hashAddonId();
|
|
||||||
if (id) activateAddon(id);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener('visibilitychange', () => {
|
|
||||||
if (document.visibilityState === 'visible') {
|
|
||||||
BackOneNetworkSession.syncFromCore();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', initAppShell);
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
/**
|
|
||||||
* Single BackOne core network for all addons (same as backone-cli listnetworks/join).
|
|
||||||
*/
|
|
||||||
window.BackOneNetworkSession = (function () {
|
|
||||||
const NETWORK_KEY = 'backone.lastNetworkId';
|
|
||||||
const PEER_KEY = 'backone.lastPeerId';
|
|
||||||
|
|
||||||
let activeNetwork = null;
|
|
||||||
let selectedPeer = null;
|
|
||||||
let coreStatus = { online: false, address: '', networkCount: 0, leafPeerCount: 0, authenticated: false };
|
|
||||||
const listeners = new Set();
|
|
||||||
|
|
||||||
function emit() {
|
|
||||||
const snap = {
|
|
||||||
network: activeNetwork,
|
|
||||||
networkId: getNetworkId(),
|
|
||||||
peer: selectedPeer,
|
|
||||||
core: { ...coreStatus }
|
|
||||||
};
|
|
||||||
listeners.forEach(fn => {
|
|
||||||
try { fn(snap); } catch (_) { /* ignore */ }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNetworkId() {
|
|
||||||
return (localStorage.getItem(NETWORK_KEY) || '').toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
function setNetworkId(id) {
|
|
||||||
const nwid = BackOnePeerDirectory.normalizeNetworkId(id);
|
|
||||||
if (nwid) {
|
|
||||||
localStorage.setItem(NETWORK_KEY, nwid);
|
|
||||||
} else {
|
|
||||||
localStorage.removeItem(NETWORK_KEY);
|
|
||||||
}
|
|
||||||
BackOnePeerDirectory.setLastNetworkId(nwid);
|
|
||||||
return nwid;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSelectedPeerId() {
|
|
||||||
return (localStorage.getItem(PEER_KEY) || '').toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
function setSelectedPeer(peer) {
|
|
||||||
selectedPeer = peer || null;
|
|
||||||
if (peer && peer.peerId) {
|
|
||||||
localStorage.setItem(PEER_KEY, peer.peerId);
|
|
||||||
} else {
|
|
||||||
localStorage.removeItem(PEER_KEY);
|
|
||||||
}
|
|
||||||
emit();
|
|
||||||
}
|
|
||||||
|
|
||||||
function pickDefaultNetworkId(networks) {
|
|
||||||
if (!networks.length) return '';
|
|
||||||
const stored = getNetworkId();
|
|
||||||
if (stored && networks.some(n => (n.id || '').toLowerCase() === stored)) {
|
|
||||||
return stored;
|
|
||||||
}
|
|
||||||
const preferred = networks.find(n => n.status === 'OK')
|
|
||||||
|| networks.find(n => n.status === 'REQUESTING_CONFIGURATION')
|
|
||||||
|| networks[0];
|
|
||||||
return preferred ? (preferred.id || '').toLowerCase() : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* On app start: read BackOne core state (same as backone-cli info / listnetworks / peers).
|
|
||||||
*/
|
|
||||||
async function syncFromCore() {
|
|
||||||
const token = await BackOneAuth.getToken();
|
|
||||||
const [status, networks, cliPeers] = await Promise.all([
|
|
||||||
BackOnePeerDirectory.loadCoreStatus(),
|
|
||||||
BackOnePeerDirectory.loadNetworks(),
|
|
||||||
BackOnePeerDirectory.loadCliPeers()
|
|
||||||
]);
|
|
||||||
|
|
||||||
coreStatus = {
|
|
||||||
online: !!(status && status.online),
|
|
||||||
address: (status && status.address) || '',
|
|
||||||
version: (status && status.version) || '',
|
|
||||||
networkCount: networks.length,
|
|
||||||
leafPeerCount: BackOnePeerDirectory.countLeafPeers(cliPeers),
|
|
||||||
authenticated: !!token
|
|
||||||
};
|
|
||||||
|
|
||||||
const pick = pickDefaultNetworkId(networks);
|
|
||||||
if (pick) {
|
|
||||||
setNetworkId(pick);
|
|
||||||
activeNetwork = networks.find(n => (n.id || '').toLowerCase() === pick)
|
|
||||||
|| await BackOnePeerDirectory.loadNetwork(pick);
|
|
||||||
} else {
|
|
||||||
setNetworkId('');
|
|
||||||
activeNetwork = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
emit();
|
|
||||||
return { coreStatus, activeNetwork, networks };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refresh() {
|
|
||||||
const id = getNetworkId();
|
|
||||||
if (!id) {
|
|
||||||
activeNetwork = null;
|
|
||||||
emit();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
activeNetwork = await BackOnePeerDirectory.loadNetwork(id);
|
|
||||||
if (!activeNetwork) {
|
|
||||||
activeNetwork = { id, name: id, status: 'UNKNOWN' };
|
|
||||||
}
|
|
||||||
emit();
|
|
||||||
return activeNetwork;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadNetworks() {
|
|
||||||
return BackOnePeerDirectory.loadNetworks();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function selectNetwork(nwid) {
|
|
||||||
const id = setNetworkId(nwid);
|
|
||||||
if (!id) {
|
|
||||||
await refresh();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function joinNetwork(nwid) {
|
|
||||||
const id = BackOnePeerDirectory.normalizeNetworkId(nwid);
|
|
||||||
if (!id) throw new Error('Network ID must be 16 hexadecimal characters');
|
|
||||||
await BackOnePeerDirectory.joinNetwork(id);
|
|
||||||
return syncFromCore();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function leaveNetwork(nwid) {
|
|
||||||
const id = BackOnePeerDirectory.normalizeNetworkId(nwid || getNetworkId());
|
|
||||||
if (!id) throw new Error('No network selected');
|
|
||||||
await BackOnePeerDirectory.leaveNetwork(id);
|
|
||||||
if (getNetworkId() === id) {
|
|
||||||
setNetworkId('');
|
|
||||||
selectedPeer = null;
|
|
||||||
localStorage.removeItem(PEER_KEY);
|
|
||||||
}
|
|
||||||
return syncFromCore();
|
|
||||||
}
|
|
||||||
|
|
||||||
function subscribe(fn) {
|
|
||||||
listeners.add(fn);
|
|
||||||
return () => listeners.delete(fn);
|
|
||||||
}
|
|
||||||
|
|
||||||
function requireNetworkId() {
|
|
||||||
const id = getNetworkId();
|
|
||||||
if (!id) throw new Error('Join a BackOne network first (same as backone-cli join)');
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCoreStatus() {
|
|
||||||
return { ...coreStatus };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
getNetworkId,
|
|
||||||
getActiveNetwork: () => activeNetwork,
|
|
||||||
getCoreStatus,
|
|
||||||
getSelectedPeer: () => selectedPeer,
|
|
||||||
getSelectedPeerId,
|
|
||||||
setSelectedPeer,
|
|
||||||
selectNetwork,
|
|
||||||
joinNetwork,
|
|
||||||
leaveNetwork,
|
|
||||||
loadNetworks,
|
|
||||||
syncFromCore,
|
|
||||||
refresh,
|
|
||||||
subscribe,
|
|
||||||
requireNetworkId
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
@@ -1,266 +0,0 @@
|
|||||||
window.BackOnePeerDirectory = (function () {
|
|
||||||
const NETWORK_KEY = 'backone.lastNetworkId';
|
|
||||||
const API = {
|
|
||||||
peers: ['/api/backone/peers', '/app/api/peers', '/peer'],
|
|
||||||
networks: ['/api/backone/networks', '/app/api/networks', '/network'],
|
|
||||||
status: ['/api/backone/status', '/app/api/status', '/status'],
|
|
||||||
network: (id) => [
|
|
||||||
`/api/backone/network/${id}`,
|
|
||||||
`/app/api/network/${id}`,
|
|
||||||
`/network/${id}`
|
|
||||||
],
|
|
||||||
members: (id) => [
|
|
||||||
`/api/backone/network/${id}/members`,
|
|
||||||
`/app/api/network/${id}/members`,
|
|
||||||
`/unstable/controller/network/${id}/member`
|
|
||||||
],
|
|
||||||
join: (id) => [
|
|
||||||
`/api/backone/network/${id}`,
|
|
||||||
`/app/api/network/${id}`,
|
|
||||||
`/network/${id}`
|
|
||||||
],
|
|
||||||
leave: (id) => [
|
|
||||||
`/api/backone/network/${id}`,
|
|
||||||
`/app/api/network/${id}`,
|
|
||||||
`/network/${id}`
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
function apiGet(path) {
|
|
||||||
return BackOneAuth.apiGet(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Same data as `backone-cli -j peers` (GET /peer). */
|
|
||||||
async function loadCliPeers() {
|
|
||||||
try {
|
|
||||||
const peers = await BackOneAuth.apiGetFirst(API.peers);
|
|
||||||
return Array.isArray(peers) ? peers : [];
|
|
||||||
} catch (_) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Same data as `backone-cli -j listnetworks` (GET /network). */
|
|
||||||
async function loadNetworks() {
|
|
||||||
try {
|
|
||||||
const nets = await BackOneAuth.apiGetFirst(API.networks);
|
|
||||||
return Array.isArray(nets) ? nets : [];
|
|
||||||
} catch (_) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Same data as `backone-cli -j info` (GET /status). */
|
|
||||||
async function loadCoreStatus() {
|
|
||||||
try {
|
|
||||||
return await BackOneAuth.apiGetFirst(API.status);
|
|
||||||
} catch (_) {
|
|
||||||
try {
|
|
||||||
const res = await fetch('/health', { cache: 'no-store' });
|
|
||||||
return { online: res.ok };
|
|
||||||
} catch (_2) {
|
|
||||||
return { online: false };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function countLeafPeers(cliPeers) {
|
|
||||||
if (!Array.isArray(cliPeers)) return 0;
|
|
||||||
return cliPeers.filter(p => !p.role || p.role === 'LEAF').length;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadNetwork(nwid) {
|
|
||||||
if (!nwid) return null;
|
|
||||||
const networks = await loadNetworks();
|
|
||||||
const found = networks.find(n => n.id === nwid);
|
|
||||||
if (found) return found;
|
|
||||||
try {
|
|
||||||
return await BackOneAuth.apiGetFirst(API.network(nwid));
|
|
||||||
} catch (_) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLastNetworkId() {
|
|
||||||
return localStorage.getItem(NETWORK_KEY) || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLastNetworkId(nwid) {
|
|
||||||
if (nwid) localStorage.setItem(NETWORK_KEY, nwid);
|
|
||||||
}
|
|
||||||
|
|
||||||
function peerIsOnline(p) {
|
|
||||||
if (!p) return false;
|
|
||||||
const paths = p.paths || [];
|
|
||||||
return paths.some(x => x.active && !x.expired);
|
|
||||||
}
|
|
||||||
|
|
||||||
function peerLinkLabel(p) {
|
|
||||||
if (!p) return '';
|
|
||||||
if (p.tunneled) return 'RELAY';
|
|
||||||
const paths = p.paths || [];
|
|
||||||
for (const path of paths) {
|
|
||||||
if (path.preferred && path.active && !path.expired) {
|
|
||||||
return 'DIRECT';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const path of paths) {
|
|
||||||
if (path.active && !path.expired) {
|
|
||||||
return 'DIRECT';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 'RELAY';
|
|
||||||
}
|
|
||||||
|
|
||||||
function peerPreferredPath(p) {
|
|
||||||
if (!p) return '';
|
|
||||||
const paths = p.paths || [];
|
|
||||||
for (const path of paths) {
|
|
||||||
if (path.preferred && path.address) return path.address;
|
|
||||||
}
|
|
||||||
for (const path of paths) {
|
|
||||||
if (path.active && path.address) return path.address;
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function cliPeerMap(cliPeers) {
|
|
||||||
const map = {};
|
|
||||||
for (const p of cliPeers) {
|
|
||||||
const id = (p.address || '').toLowerCase();
|
|
||||||
if (id) map[id] = p;
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadMemberMap(nwid) {
|
|
||||||
if (!nwid) return {};
|
|
||||||
try {
|
|
||||||
const data = await BackOneAuth.apiGetFirst(API.members(nwid));
|
|
||||||
const map = {};
|
|
||||||
if (data && Array.isArray(data.data)) {
|
|
||||||
for (const m of data.data) {
|
|
||||||
const id = (m.id || '').toLowerCase();
|
|
||||||
if (id) map[id] = m;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
} catch (_) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildPeerEntry(peerId, member, cliPeer, networkId, networkName) {
|
|
||||||
const controllerName = member ? (member.name || '') : '';
|
|
||||||
const ips = member && member.ipAssignments ? member.ipAssignments : [];
|
|
||||||
return {
|
|
||||||
peerId,
|
|
||||||
networkId: networkId || '',
|
|
||||||
networkName: networkName || '',
|
|
||||||
displayName: BackOneContacts.getDisplayName(peerId, controllerName),
|
|
||||||
controllerName,
|
|
||||||
nickname: BackOneContacts.getNickname(peerId),
|
|
||||||
online: peerIsOnline(cliPeer),
|
|
||||||
latency: cliPeer && typeof cliPeer.latency === 'number' ? cliPeer.latency : null,
|
|
||||||
role: (cliPeer && cliPeer.role) || (member ? 'LEAF' : ''),
|
|
||||||
version: (cliPeer && cliPeer.version) || '',
|
|
||||||
link: cliPeer ? peerLinkLabel(cliPeer) : '',
|
|
||||||
path: cliPeer ? peerPreferredPath(cliPeer) : '',
|
|
||||||
ip: ips.length ? ips[0] : '',
|
|
||||||
onNetwork: !!member
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadPeersForNetwork(nwid) {
|
|
||||||
if (!nwid) return [];
|
|
||||||
|
|
||||||
const [cliPeers, members, selfStatus, network] = await Promise.all([
|
|
||||||
loadCliPeers(),
|
|
||||||
loadMemberMap(nwid),
|
|
||||||
loadCoreStatus().catch(() => ({})),
|
|
||||||
loadNetwork(nwid)
|
|
||||||
]);
|
|
||||||
await BackOneContacts.load();
|
|
||||||
|
|
||||||
const selfId = ((selfStatus && selfStatus.address) || '').toLowerCase();
|
|
||||||
const networkId = nwid;
|
|
||||||
const networkName = network ? (network.name || network.id || nwid) : nwid;
|
|
||||||
const cliById = cliPeerMap(cliPeers);
|
|
||||||
const out = [];
|
|
||||||
|
|
||||||
if (Object.keys(members).length > 0) {
|
|
||||||
for (const peerId of Object.keys(members)) {
|
|
||||||
const member = members[peerId];
|
|
||||||
if (!member || member.authorized === false) continue;
|
|
||||||
if (peerId === selfId) continue;
|
|
||||||
out.push(buildPeerEntry(peerId, member, cliById[peerId], networkId, networkName));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (const peerId of Object.keys(cliById)) {
|
|
||||||
const p = cliById[peerId];
|
|
||||||
if (!peerId || peerId.length !== 10 || peerId === selfId) continue;
|
|
||||||
if (p.role && p.role !== 'LEAF') continue;
|
|
||||||
out.push(buildPeerEntry(peerId, null, p, networkId, networkName));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
out.sort((a, b) => {
|
|
||||||
if (a.online !== b.online) return a.online ? -1 : 1;
|
|
||||||
return a.displayName.localeCompare(b.displayName);
|
|
||||||
});
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
function searchPeers(query, list) {
|
|
||||||
const q = (query || '').trim().toLowerCase();
|
|
||||||
if (!q) return list;
|
|
||||||
return list.filter(p =>
|
|
||||||
p.displayName.toLowerCase().includes(q) ||
|
|
||||||
p.peerId.includes(q) ||
|
|
||||||
(p.networkName || '').toLowerCase().includes(q) ||
|
|
||||||
(p.networkId || '').includes(q) ||
|
|
||||||
(p.controllerName || '').toLowerCase().includes(q) ||
|
|
||||||
(p.role || '').toLowerCase().includes(q) ||
|
|
||||||
(p.path || '').toLowerCase().includes(q) ||
|
|
||||||
(p.ip || '').toLowerCase().includes(q)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeNetworkId(input) {
|
|
||||||
const id = String(input || '').replace(/[^0-9a-fA-F]/g, '').toLowerCase();
|
|
||||||
return id.length === 16 ? id : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function joinNetwork(nwid) {
|
|
||||||
const id = normalizeNetworkId(nwid);
|
|
||||||
if (!id) {
|
|
||||||
throw new Error('Network ID must be 16 hexadecimal characters');
|
|
||||||
}
|
|
||||||
return BackOneAuth.apiSendFirst(API.join(id), 'POST', {});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function leaveNetwork(nwid) {
|
|
||||||
const id = normalizeNetworkId(nwid);
|
|
||||||
if (!id) {
|
|
||||||
throw new Error('Network ID must be 16 hexadecimal characters');
|
|
||||||
}
|
|
||||||
return BackOneAuth.apiSendFirst(API.leave(id), 'DELETE');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
loadCliPeers,
|
|
||||||
loadNetworks,
|
|
||||||
loadNetwork,
|
|
||||||
loadCoreStatus,
|
|
||||||
countLeafPeers,
|
|
||||||
loadPeersForNetwork,
|
|
||||||
getLastNetworkId,
|
|
||||||
setLastNetworkId,
|
|
||||||
joinNetwork,
|
|
||||||
leaveNetwork,
|
|
||||||
normalizeNetworkId,
|
|
||||||
searchPeers,
|
|
||||||
peerIsOnline,
|
|
||||||
peerLinkLabel
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
window.BackOnePeerPicker = function PeerPicker(container, options) {
|
|
||||||
options = options || {};
|
|
||||||
const onSelect = options.onSelect || (() => {});
|
|
||||||
const onNetworkChange = options.onNetworkChange || (() => {});
|
|
||||||
const onAction = options.onAction || null;
|
|
||||||
const actionLabel = options.actionLabel || 'Select';
|
|
||||||
const showActionButton = options.showActionButton !== false;
|
|
||||||
const useSharedNetwork = options.useSharedNetwork !== false;
|
|
||||||
|
|
||||||
let selectedPeer = null;
|
|
||||||
let peers = [];
|
|
||||||
let currentNetwork = null;
|
|
||||||
let unsub = null;
|
|
||||||
|
|
||||||
container.innerHTML = `
|
|
||||||
<div class="peer-picker">
|
|
||||||
<div class="peer-picker-core">BackOne core · same network as <code>backone-cli</code></div>
|
|
||||||
<div id="pp-network-banner" class="peer-network-banner"></div>
|
|
||||||
<label class="peer-picker-row">Search
|
|
||||||
<input id="pp-search" type="search" placeholder="Name or peer ID" />
|
|
||||||
</label>
|
|
||||||
<ul id="pp-list" class="peer-list"></ul>
|
|
||||||
<div id="pp-selected" class="peer-selected"></div>
|
|
||||||
${showActionButton ? `<button id="pp-action" type="button" disabled>${actionLabel}</button>` : ''}
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
const netBanner = container.querySelector('#pp-network-banner');
|
|
||||||
const searchInput = container.querySelector('#pp-search');
|
|
||||||
const listEl = container.querySelector('#pp-list');
|
|
||||||
const selectedEl = container.querySelector('#pp-selected');
|
|
||||||
const actionBtn = container.querySelector('#pp-action');
|
|
||||||
|
|
||||||
function networkLabel(n) {
|
|
||||||
if (!n) return '';
|
|
||||||
return n.name ? `${n.name} (${n.id})` : n.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderNetworkBanner() {
|
|
||||||
if (!useSharedNetwork) return;
|
|
||||||
const id = BackOneNetworkSession.getNetworkId();
|
|
||||||
const core = BackOneNetworkSession.getCoreStatus();
|
|
||||||
if (!core.online) {
|
|
||||||
netBanner.innerHTML = '<span class="peer-network-warn">BackOne offline — peers from <code>backone-cli</code> unavailable</span>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!currentNetwork || !id) {
|
|
||||||
netBanner.innerHTML = '<span class="peer-network-warn">No network — core reports ' + core.networkCount + ' joined (use header bar or <code>backone-cli listnetworks</code>)</span>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
netBanner.textContent = `Network: ${networkLabel(currentNetwork)} · ${peers.length} contact(s) · ${core.leafPeerCount} VL1 peer(s) from core`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderList() {
|
|
||||||
const q = searchInput.value;
|
|
||||||
const filtered = BackOnePeerDirectory.searchPeers(q, peers);
|
|
||||||
const netTag = currentNetwork ? escapeHtml(currentNetwork.name || currentNetwork.id) : '';
|
|
||||||
listEl.innerHTML = filtered.map(p => {
|
|
||||||
const lat = p.latency != null ? `${p.latency}ms` : '—';
|
|
||||||
const meta = [p.role || 'LEAF', lat, p.link || ''].filter(Boolean).join(' · ');
|
|
||||||
const ip = p.ip ? ` · ${escapeHtml(p.ip)}` : '';
|
|
||||||
return `
|
|
||||||
<li class="peer-item ${p.online ? 'online' : 'offline'} ${selectedPeer && selectedPeer.peerId === p.peerId ? 'active' : ''}" data-id="${p.peerId}">
|
|
||||||
<span class="peer-dot" title="${p.online ? 'Online' : 'Offline'}"></span>
|
|
||||||
<span class="peer-main">
|
|
||||||
<strong class="peer-name">${escapeHtml(p.displayName)}</strong>
|
|
||||||
<small class="peer-sub">${escapeHtml(p.peerId)} · ${escapeHtml(meta)}${netTag ? ' · ' + netTag : ''}${ip}</small>
|
|
||||||
</span>
|
|
||||||
<button type="button" class="peer-edit" title="Edit nickname">✎</button>
|
|
||||||
</li>`;
|
|
||||||
}).join('') || '<li class="peer-empty">No peers on the active network — check <code>backone-cli peers</code> and controller members</li>';
|
|
||||||
|
|
||||||
listEl.querySelectorAll('.peer-item').forEach(li => {
|
|
||||||
li.addEventListener('click', (ev) => {
|
|
||||||
if (ev.target.classList.contains('peer-edit')) return;
|
|
||||||
const id = li.dataset.id;
|
|
||||||
selectedPeer = peers.find(x => x.peerId === id) || null;
|
|
||||||
if (selectedPeer) BackOneNetworkSession.setSelectedPeer(selectedPeer);
|
|
||||||
renderList();
|
|
||||||
renderSelected();
|
|
||||||
if (selectedPeer) onSelect(selectedPeer, currentNetwork);
|
|
||||||
});
|
|
||||||
const editBtn = li.querySelector('.peer-edit');
|
|
||||||
if (editBtn) {
|
|
||||||
editBtn.addEventListener('click', async (ev) => {
|
|
||||||
ev.stopPropagation();
|
|
||||||
const id = li.dataset.id;
|
|
||||||
const peer = peers.find(x => x.peerId === id);
|
|
||||||
if (!peer) return;
|
|
||||||
const nick = prompt('Nickname for ' + peer.peerId, peer.nickname || peer.controllerName || '');
|
|
||||||
if (nick === null) return;
|
|
||||||
await BackOneContacts.setNickname(peer.peerId, nick);
|
|
||||||
BackOneContacts.invalidate();
|
|
||||||
await refreshPeers();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderSelected() {
|
|
||||||
if (!selectedPeer) {
|
|
||||||
selectedEl.textContent = '';
|
|
||||||
if (actionBtn) actionBtn.disabled = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const net = currentNetwork ? networkLabel(currentNetwork) : BackOneNetworkSession.getNetworkId();
|
|
||||||
selectedEl.innerHTML = `Selected: <strong>${escapeHtml(selectedPeer.displayName)}</strong> (${escapeHtml(selectedPeer.peerId)})${net ? ` on <em>${escapeHtml(net)}</em>` : ''}`;
|
|
||||||
if (actionBtn) actionBtn.disabled = !BackOneNetworkSession.getNetworkId();
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeHtml(s) {
|
|
||||||
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshPeers() {
|
|
||||||
currentNetwork = BackOneNetworkSession.getActiveNetwork();
|
|
||||||
if (useSharedNetwork && BackOneNetworkSession.getNetworkId() && !currentNetwork) {
|
|
||||||
currentNetwork = await BackOneNetworkSession.refresh();
|
|
||||||
}
|
|
||||||
renderNetworkBanner();
|
|
||||||
const nwid = BackOneNetworkSession.getNetworkId();
|
|
||||||
await BackOneContacts.load();
|
|
||||||
peers = nwid ? await BackOnePeerDirectory.loadPeersForNetwork(nwid) : [];
|
|
||||||
const lastPeerId = BackOneNetworkSession.getSelectedPeerId();
|
|
||||||
if (lastPeerId) {
|
|
||||||
selectedPeer = peers.find(p => p.peerId === lastPeerId) || BackOneNetworkSession.getSelectedPeer();
|
|
||||||
}
|
|
||||||
renderList();
|
|
||||||
renderSelected();
|
|
||||||
onNetworkChange(currentNetwork, peers);
|
|
||||||
}
|
|
||||||
|
|
||||||
searchInput.addEventListener('input', renderList);
|
|
||||||
if (actionBtn) {
|
|
||||||
actionBtn.addEventListener('click', () => {
|
|
||||||
if (selectedPeer && onAction) onAction(selectedPeer, currentNetwork);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (useSharedNetwork) {
|
|
||||||
unsub = BackOneNetworkSession.subscribe(() => refreshPeers());
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshPeers();
|
|
||||||
|
|
||||||
return {
|
|
||||||
getSelectedPeer: () => selectedPeer,
|
|
||||||
getSelectedNetwork: () => currentNetwork,
|
|
||||||
refresh: refreshPeers,
|
|
||||||
destroy: () => { if (unsub) unsub(); }
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
body { font-family: system-ui, sans-serif; margin: 0; background: #0f1419; color: #e7ecf1; }
|
|
||||||
header { display: flex; align-items: center; gap: 1rem; padding: 1rem; background: #1a2332; border-bottom: 1px solid #2d3a4d; flex-wrap: wrap; }
|
|
||||||
header h1 { margin: 0; font-size: 1.25rem; }
|
|
||||||
.network-bar { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; flex: 1; min-width: 280px; }
|
|
||||||
.network-bar-label { font-size: 0.85rem; color: #8b9bb4; }
|
|
||||||
.network-bar select, .network-bar input { background: #0f1419; color: #e7ecf1; border: 1px solid #2d3a4d; border-radius: 4px; padding: 0.35rem 0.5rem; }
|
|
||||||
.network-bar input { width: 10rem; }
|
|
||||||
.network-bar button { background: #2d5a8a; color: #fff; border: none; padding: 0.35rem 0.65rem; border-radius: 4px; cursor: pointer; }
|
|
||||||
.network-bar-status { font-size: 0.8rem; color: #6b8cae; flex: 1 1 100%; }
|
|
||||||
.auth-token-bar { display: flex; align-items: center; gap: 0.5rem; flex: 1 1 100%; }
|
|
||||||
.auth-token-hidden { display: none; }
|
|
||||||
.auth-token-label { font-size: 0.85rem; color: #8b9bb4; }
|
|
||||||
.auth-token-bar input { background: #0f1419; color: #e7ecf1; border: 1px solid #2d3a4d; border-radius: 4px; padding: 0.35rem 0.5rem; width: 16rem; }
|
|
||||||
.auth-token-bar button { background: #2d5a8a; color: #fff; border: none; padding: 0.35rem 0.65rem; border-radius: 4px; cursor: pointer; }
|
|
||||||
.core-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 0.35rem; vertical-align: middle; }
|
|
||||||
.core-dot.online { background: #2ecc71; }
|
|
||||||
.core-dot.offline { background: #e74c3c; }
|
|
||||||
nav { display: flex; gap: 0.5rem; flex-wrap: wrap; }
|
|
||||||
nav button { background: #2d5a8a; color: #fff; border: none; padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer; }
|
|
||||||
nav button.active { background: #4a90d9; }
|
|
||||||
main { padding: 1.5rem; }
|
|
||||||
.ringing-hidden { display: none; }
|
|
||||||
#ringing-popup { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 1000; }
|
|
||||||
#ringing-popup.ringing-hidden { display: none; }
|
|
||||||
.ringing-card { background: #1a2332; padding: 2rem; border-radius: 12px; text-align: center; min-width: 280px; }
|
|
||||||
.ringing-card button { margin: 0.5rem; padding: 0.6rem 1.2rem; border: none; border-radius: 6px; cursor: pointer; }
|
|
||||||
.ringing-accept { background: #2ecc71; color: #000; }
|
|
||||||
.ringing-reject { background: #e74c3c; color: #fff; }
|
|
||||||
.peer-picker { max-width: 480px; }
|
|
||||||
.peer-picker-core { font-size: 0.75rem; color: #6b8cae; margin-bottom: 0.5rem; text-transform: uppercase; letter-spacing: 0.04em; }
|
|
||||||
.peer-network-banner { font-size: 0.85rem; color: #8b9bb4; margin-bottom: 0.75rem; padding: 0.4rem 0.5rem; background: #1a2332; border-radius: 4px; }
|
|
||||||
.peer-network-warn { color: #e6a23c; }
|
|
||||||
.peer-picker-row { display: block; margin-bottom: 0.75rem; }
|
|
||||||
.peer-picker-row select, .peer-picker-row input { width: 100%; margin-top: 0.25rem; padding: 0.4rem; background: #1a2332; color: #e7ecf1; border: 1px solid #2d3a4d; border-radius: 4px; }
|
|
||||||
.peer-list { list-style: none; padding: 0; margin: 0.5rem 0; max-height: 280px; overflow: auto; border: 1px solid #2d3a4d; border-radius: 6px; }
|
|
||||||
.peer-item { display: flex; align-items: center; gap: 0.5rem; padding: 0.6rem 0.75rem; cursor: pointer; border-bottom: 1px solid #2d3a4d; }
|
|
||||||
.peer-item:last-child { border-bottom: none; }
|
|
||||||
.peer-item:hover, .peer-item.active { background: #243044; }
|
|
||||||
.peer-dot { width: 8px; height: 8px; border-radius: 50%; background: #666; flex-shrink: 0; }
|
|
||||||
.peer-item.online .peer-dot { background: #2ecc71; }
|
|
||||||
.peer-main { flex: 1; min-width: 0; }
|
|
||||||
.peer-name { display: block; }
|
|
||||||
.peer-sub { color: #8b9bb4; font-size: 0.8rem; }
|
|
||||||
.peer-edit { background: transparent; border: none; color: #8b9bb4; cursor: pointer; padding: 0.25rem; }
|
|
||||||
.peer-empty { padding: 1rem; color: #8b9bb4; }
|
|
||||||
.peer-selected { margin: 0.5rem 0; min-height: 1.25rem; }
|
|
||||||
.chat-layout { display: flex; gap: 1rem; min-height: 400px; }
|
|
||||||
.chat-peers { flex: 0 0 280px; }
|
|
||||||
.chat-thread-panel { flex: 1; display: flex; flex-direction: column; }
|
|
||||||
.chat-thread-header { padding-bottom: 0.5rem; border-bottom: 1px solid #2d3a4d; margin-bottom: 0.5rem; }
|
|
||||||
#chat-thread { flex: 1; overflow: auto; border: 1px solid #2d3a4d; padding: 0.5rem; border-radius: 6px; margin-bottom: 0.5rem; }
|
|
||||||
.video-remote-label { position: absolute; bottom: 4px; left: 4px; background: rgba(0,0,0,0.6); padding: 2px 6px; font-size: 0.75rem; border-radius: 4px; }
|
|
||||||
.video-wrap { position: relative; display: inline-block; }
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "chat",
|
|
||||||
"name": "Chat P2P",
|
|
||||||
"version": "1.17.1",
|
|
||||||
"requires": ["comms-core", "webhub"],
|
|
||||||
"backoneCore": {
|
|
||||||
"apis": [
|
|
||||||
"/app/api/peers",
|
|
||||||
"/app/api/networks",
|
|
||||||
"POST /app/api/network/{nwid}",
|
|
||||||
"DELETE /app/api/network/{nwid}",
|
|
||||||
"/app/api/network/{nwid}",
|
|
||||||
"/app/api/network/{nwid}/members",
|
|
||||||
"/app/api/authtoken",
|
|
||||||
"/app/api/contacts"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"backend": {
|
|
||||||
"routes": ["/controller/comms/network"],
|
|
||||||
"websocketEvents": ["chat_message", "typing"]
|
|
||||||
},
|
|
||||||
"frontend": {
|
|
||||||
"entry": "/app/addons/chat/index.js",
|
|
||||||
"navLabel": "Chat",
|
|
||||||
"icon": "chat"
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"attachmentMaxBytes": 10485760
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
window.BackOneAddons = window.BackOneAddons || {};
|
|
||||||
|
|
||||||
window.BackOneAddons.chat = function chatAddon() {
|
|
||||||
const messagesByPeer = {};
|
|
||||||
let selectedPeer = null;
|
|
||||||
let root = null;
|
|
||||||
let unsubChat = null;
|
|
||||||
|
|
||||||
function escapeHtml(s) {
|
|
||||||
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearSessionMessages() {
|
|
||||||
Object.keys(messagesByPeer).forEach(k => delete messagesByPeer[k]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getThread(peerId) {
|
|
||||||
if (!messagesByPeer[peerId]) messagesByPeer[peerId] = [];
|
|
||||||
return messagesByPeer[peerId];
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatTime(ts) {
|
|
||||||
if (!ts) return '';
|
|
||||||
const d = new Date(ts);
|
|
||||||
return d.toLocaleString(undefined, { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' });
|
|
||||||
}
|
|
||||||
|
|
||||||
function networkLabel() {
|
|
||||||
const n = BackOneNetworkSession.getActiveNetwork();
|
|
||||||
return n ? (n.name || n.id) : BackOneNetworkSession.getNetworkId();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateConnectionBar() {
|
|
||||||
const bar = root?.querySelector('#chat-conn-bar');
|
|
||||||
if (!bar || !window.BackOneCommLayout) return;
|
|
||||||
bar.innerHTML = BackOneCommLayout.connectionBarHtml(
|
|
||||||
selectedPeer,
|
|
||||||
BackOneNetworkSession.getActiveNetwork()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderHeader() {
|
|
||||||
const header = root?.querySelector('#chat-header');
|
|
||||||
if (!header) return;
|
|
||||||
const net = networkLabel();
|
|
||||||
if (!selectedPeer) {
|
|
||||||
header.textContent = net ? `Select a contact on ${net}` : 'Join a network in the header bar';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
header.innerHTML = `<strong>${escapeHtml(selectedPeer.displayName || selectedPeer.peerId)}</strong> <small>${escapeHtml(selectedPeer.peerId)}${net ? ' · ' + escapeHtml(net) : ''}</small>`;
|
|
||||||
updateConnectionBar();
|
|
||||||
}
|
|
||||||
|
|
||||||
function selfName() {
|
|
||||||
const addr = (BackOneNetworkSession.getCoreStatus().address || '').toLowerCase();
|
|
||||||
return addr ? addr.slice(0, 10) + '…' : 'me';
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
mount(container) {
|
|
||||||
root = container;
|
|
||||||
clearSessionMessages();
|
|
||||||
container.innerHTML = `
|
|
||||||
<div class="comm-layout comm-split-layout">
|
|
||||||
<aside class="comm-sidebar">
|
|
||||||
<div class="comm-sidebar-head">
|
|
||||||
<h2>Chat P2P</h2>
|
|
||||||
<p class="comm-sidebar-sub">Contacts · session only</p>
|
|
||||||
</div>
|
|
||||||
<div class="comm-sidebar-body">
|
|
||||||
<div id="chat-peer-picker"></div>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
<div class="comm-main">
|
|
||||||
<div class="chat-thread-panel">
|
|
||||||
<div class="chat-thread-header" id="chat-header">Select a contact</div>
|
|
||||||
<div id="chat-thread" class="chat-messages"></div>
|
|
||||||
<div class="chat-compose">
|
|
||||||
<textarea id="chat-input" rows="2" placeholder="Type a message" disabled></textarea>
|
|
||||||
<div class="chat-compose-row">
|
|
||||||
<button type="button" id="chat-send" class="btn-primary" disabled>Send</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="chat-conn-bar" class="comm-connection-bar"></div>`;
|
|
||||||
|
|
||||||
const thread = container.querySelector('#chat-thread');
|
|
||||||
const input = container.querySelector('#chat-input');
|
|
||||||
const sendBtn = container.querySelector('#chat-send');
|
|
||||||
|
|
||||||
const render = () => {
|
|
||||||
if (!selectedPeer) {
|
|
||||||
thread.innerHTML = '<p class="chat-empty">Choose a contact from the list</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const messages = getThread(selectedPeer.peerId);
|
|
||||||
if (!messages.length) {
|
|
||||||
thread.innerHTML = '<p class="chat-empty">No messages yet — start the conversation</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
thread.innerHTML = messages.map(m => `
|
|
||||||
<div class="chat-message">
|
|
||||||
<div class="chat-message-meta">
|
|
||||||
<span class="chat-message-sender">${escapeHtml(m.senderName || m.sender || 'peer')}</span>
|
|
||||||
<span class="chat-message-time">${escapeHtml(formatTime(m.at))}</span>
|
|
||||||
</div>
|
|
||||||
${m.body ? `<div class="chat-message-body">${escapeHtml(m.body)}</div>` : ''}
|
|
||||||
</div>`).join('');
|
|
||||||
thread.scrollTop = thread.scrollHeight;
|
|
||||||
};
|
|
||||||
|
|
||||||
function updateInputs() {
|
|
||||||
const networkId = BackOneNetworkSession.getNetworkId();
|
|
||||||
const ok = !!networkId && !!selectedPeer;
|
|
||||||
input.disabled = !ok;
|
|
||||||
sendBtn.disabled = !ok;
|
|
||||||
renderHeader();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (unsubChat) unsubChat();
|
|
||||||
unsubChat = BackOneWebHubClient.subscribe('chat_message', (msg) => {
|
|
||||||
const networkId = BackOneNetworkSession.getNetworkId();
|
|
||||||
if (!networkId || (msg.networkId && msg.networkId.toLowerCase() !== networkId)) return;
|
|
||||||
const senderId = (msg.senderId || '').toLowerCase();
|
|
||||||
const peerId = selectedPeer?.peerId?.toLowerCase();
|
|
||||||
if (!peerId || senderId !== peerId) {
|
|
||||||
if (senderId) {
|
|
||||||
getThread(senderId).push({
|
|
||||||
sender: senderId,
|
|
||||||
senderName: msg.senderName || senderId,
|
|
||||||
body: msg.body || '',
|
|
||||||
at: msg.at || Date.now()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
getThread(peerId).push({
|
|
||||||
sender: senderId,
|
|
||||||
senderName: msg.senderName || senderId,
|
|
||||||
body: msg.body || '',
|
|
||||||
at: msg.at || Date.now()
|
|
||||||
});
|
|
||||||
render();
|
|
||||||
});
|
|
||||||
|
|
||||||
BackOnePeerPicker(container.querySelector('#chat-peer-picker'), {
|
|
||||||
layout: 'sidebar',
|
|
||||||
showActionButton: false,
|
|
||||||
onSelect(peer) {
|
|
||||||
selectedPeer = peer;
|
|
||||||
updateInputs();
|
|
||||||
render();
|
|
||||||
},
|
|
||||||
onNetworkChange() {
|
|
||||||
selectedPeer = null;
|
|
||||||
clearSessionMessages();
|
|
||||||
updateInputs();
|
|
||||||
render();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
BackOneNetworkSession.subscribe(() => {
|
|
||||||
updateInputs();
|
|
||||||
render();
|
|
||||||
});
|
|
||||||
|
|
||||||
sendBtn.onclick = () => {
|
|
||||||
const networkId = BackOneNetworkSession.getNetworkId();
|
|
||||||
if (!selectedPeer || !networkId) return;
|
|
||||||
const body = input.value.trim();
|
|
||||||
if (!body) return;
|
|
||||||
const now = Date.now();
|
|
||||||
const name = selfName();
|
|
||||||
BackOneWebHubClient.send({
|
|
||||||
type: 'chat_message',
|
|
||||||
networkId,
|
|
||||||
targetId: selectedPeer.peerId,
|
|
||||||
senderName: name,
|
|
||||||
body
|
|
||||||
});
|
|
||||||
getThread(selectedPeer.peerId).push({
|
|
||||||
sender: 'me',
|
|
||||||
senderName: name,
|
|
||||||
body,
|
|
||||||
at: now
|
|
||||||
});
|
|
||||||
input.value = '';
|
|
||||||
render();
|
|
||||||
};
|
|
||||||
|
|
||||||
updateInputs();
|
|
||||||
render();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "comms-core",
|
|
||||||
"name": "Comms Core",
|
|
||||||
"version": "1.17.1",
|
|
||||||
"requires": [],
|
|
||||||
"backend": {
|
|
||||||
"routes": ["/controller/comms"],
|
|
||||||
"websocketEvents": ["presence", "incoming_call", "call_state", "chat_message"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
-- BackOne comms-core addon schema (PostgreSQL)
|
|
||||||
CREATE TABLE IF NOT EXISTS backone_calls (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
network_id TEXT NOT NULL,
|
|
||||||
caller_id TEXT NOT NULL,
|
|
||||||
callee_id TEXT NOT NULL,
|
|
||||||
call_type TEXT NOT NULL CHECK (call_type IN ('voice', 'video')),
|
|
||||||
state TEXT NOT NULL,
|
|
||||||
sdp_offer TEXT,
|
|
||||||
sdp_answer TEXT,
|
|
||||||
created_at BIGINT NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS backone_chat_messages (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
network_id TEXT NOT NULL,
|
|
||||||
sender_id TEXT NOT NULL,
|
|
||||||
body TEXT,
|
|
||||||
attachment_id TEXT,
|
|
||||||
created_at BIGINT NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS backone_attachments (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
network_id TEXT NOT NULL,
|
|
||||||
sender_id TEXT NOT NULL,
|
|
||||||
filename TEXT NOT NULL,
|
|
||||||
mime_type TEXT NOT NULL,
|
|
||||||
size_bytes BIGINT NOT NULL,
|
|
||||||
storage_path TEXT NOT NULL,
|
|
||||||
created_at BIGINT NOT NULL
|
|
||||||
);
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "secure-chat",
|
|
||||||
"name": "Secure Chat",
|
|
||||||
"version": "1.17.1",
|
|
||||||
"requires": ["comms-core", "webhub"],
|
|
||||||
"backoneCore": {
|
|
||||||
"apis": [
|
|
||||||
"/app/api/peers",
|
|
||||||
"/app/api/networks",
|
|
||||||
"POST /app/api/network/{nwid}",
|
|
||||||
"DELETE /app/api/network/{nwid}",
|
|
||||||
"/app/api/network/{nwid}",
|
|
||||||
"/app/api/network/{nwid}/members",
|
|
||||||
"/app/api/authtoken",
|
|
||||||
"/app/api/contacts"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"backend": {
|
|
||||||
"routes": ["/controller/comms/network"],
|
|
||||||
"websocketEvents": ["secure_chat_message"]
|
|
||||||
},
|
|
||||||
"frontend": {
|
|
||||||
"entry": "/app/addons/secure-chat/index.js",
|
|
||||||
"navLabel": "Secure Chat",
|
|
||||||
"icon": "chat"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
window.BackOneAddons = window.BackOneAddons || {};
|
|
||||||
|
|
||||||
window.BackOneAddons['secure-chat'] = function secureChatAddon() {
|
|
||||||
const messagesByPeer = {};
|
|
||||||
let selectedPeer = null;
|
|
||||||
let root = null;
|
|
||||||
let unsubChat = null;
|
|
||||||
|
|
||||||
function escapeHtml(s) {
|
|
||||||
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearSessionMessages() {
|
|
||||||
Object.keys(messagesByPeer).forEach(k => delete messagesByPeer[k]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getThread(peerId) {
|
|
||||||
if (!messagesByPeer[peerId]) messagesByPeer[peerId] = [];
|
|
||||||
return messagesByPeer[peerId];
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatTime(ts) {
|
|
||||||
if (!ts) return '';
|
|
||||||
const d = new Date(ts);
|
|
||||||
return d.toLocaleString(undefined, { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' });
|
|
||||||
}
|
|
||||||
|
|
||||||
function networkLabel() {
|
|
||||||
const n = BackOneNetworkSession.getActiveNetwork();
|
|
||||||
return n ? (n.name || n.id) : BackOneNetworkSession.getNetworkId();
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderHeader() {
|
|
||||||
const header = root?.querySelector('#secure-chat-header');
|
|
||||||
if (!header) return;
|
|
||||||
const net = networkLabel();
|
|
||||||
if (!selectedPeer) {
|
|
||||||
header.textContent = net ? `Select a contact on ${net}` : 'Join a network in the header bar';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
header.innerHTML = `<strong>${escapeHtml(selectedPeer.displayName || selectedPeer.peerId)}</strong> <small>${escapeHtml(selectedPeer.peerId)}${net ? ' · ' + escapeHtml(net) : ''}</small>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function selfName() {
|
|
||||||
const addr = (BackOneNetworkSession.getCoreStatus().address || '').toLowerCase();
|
|
||||||
return addr ? addr.slice(0, 10) + '…' : 'me';
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
mount(container) {
|
|
||||||
root = container;
|
|
||||||
clearSessionMessages();
|
|
||||||
container.innerHTML = `
|
|
||||||
<div class="comm-layout comm-split-layout">
|
|
||||||
<aside class="comm-sidebar">
|
|
||||||
<div class="comm-sidebar-head">
|
|
||||||
<h2>Secure Chat</h2>
|
|
||||||
<p class="comm-sidebar-sub">Contacts · session only</p>
|
|
||||||
</div>
|
|
||||||
<div class="comm-sidebar-body">
|
|
||||||
<div id="secure-chat-peer-picker"></div>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
<div class="comm-main">
|
|
||||||
<div class="chat-thread-panel">
|
|
||||||
<div class="chat-thread-header" id="secure-chat-header">Select a contact</div>
|
|
||||||
<div id="secure-chat-thread" class="chat-messages"></div>
|
|
||||||
<div class="chat-compose">
|
|
||||||
<textarea id="secure-chat-input" rows="2" placeholder="Type a secure message" disabled></textarea>
|
|
||||||
<div class="chat-compose-row">
|
|
||||||
<button type="button" id="secure-chat-send" class="btn-primary" disabled>Send</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
const thread = container.querySelector('#secure-chat-thread');
|
|
||||||
const input = container.querySelector('#secure-chat-input');
|
|
||||||
const sendBtn = container.querySelector('#secure-chat-send');
|
|
||||||
|
|
||||||
const render = () => {
|
|
||||||
if (!selectedPeer) {
|
|
||||||
thread.innerHTML = '<p class="chat-empty">Choose a contact from the list</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const messages = getThread(selectedPeer.peerId);
|
|
||||||
if (!messages.length) {
|
|
||||||
thread.innerHTML = '<p class="chat-empty">No messages yet — start the conversation</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
thread.innerHTML = messages.map(m => `
|
|
||||||
<div class="chat-message">
|
|
||||||
<div class="chat-message-meta">
|
|
||||||
<span class="chat-message-sender">${escapeHtml(m.senderName || m.sender || 'me')}</span>
|
|
||||||
<span class="chat-message-time">${escapeHtml(formatTime(m.at))}</span>
|
|
||||||
</div>
|
|
||||||
${m.body ? `<div class="chat-message-body">${escapeHtml(m.body)}</div>` : ''}
|
|
||||||
</div>`).join('');
|
|
||||||
thread.scrollTop = thread.scrollHeight;
|
|
||||||
};
|
|
||||||
|
|
||||||
function updateInputs() {
|
|
||||||
const networkId = BackOneNetworkSession.getNetworkId();
|
|
||||||
const ok = !!networkId && !!selectedPeer;
|
|
||||||
input.disabled = !ok;
|
|
||||||
sendBtn.disabled = !ok;
|
|
||||||
renderHeader();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (unsubChat) unsubChat();
|
|
||||||
unsubChat = BackOneWebHubClient.subscribe('chat_message', (msg) => {
|
|
||||||
const networkId = BackOneNetworkSession.getNetworkId();
|
|
||||||
if (!networkId || (msg.networkId && msg.networkId.toLowerCase() !== networkId)) return;
|
|
||||||
const senderId = (msg.senderId || '').toLowerCase();
|
|
||||||
const peerId = selectedPeer?.peerId?.toLowerCase();
|
|
||||||
if (!peerId || senderId !== peerId) {
|
|
||||||
if (senderId) {
|
|
||||||
getThread(senderId).push({
|
|
||||||
sender: senderId,
|
|
||||||
senderName: msg.senderName || senderId,
|
|
||||||
body: msg.body || '',
|
|
||||||
at: msg.at || Date.now()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
getThread(peerId).push({
|
|
||||||
sender: senderId,
|
|
||||||
senderName: msg.senderName || senderId,
|
|
||||||
body: msg.body || '',
|
|
||||||
at: msg.at || Date.now()
|
|
||||||
});
|
|
||||||
render();
|
|
||||||
});
|
|
||||||
|
|
||||||
BackOnePeerPicker(container.querySelector('#secure-chat-peer-picker'), {
|
|
||||||
layout: 'sidebar',
|
|
||||||
showActionButton: false,
|
|
||||||
onSelect(peer) {
|
|
||||||
selectedPeer = peer;
|
|
||||||
updateInputs();
|
|
||||||
render();
|
|
||||||
},
|
|
||||||
onNetworkChange() {
|
|
||||||
selectedPeer = null;
|
|
||||||
clearSessionMessages();
|
|
||||||
updateInputs();
|
|
||||||
render();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
BackOneNetworkSession.subscribe(() => {
|
|
||||||
updateInputs();
|
|
||||||
render();
|
|
||||||
});
|
|
||||||
|
|
||||||
sendBtn.onclick = () => {
|
|
||||||
const networkId = BackOneNetworkSession.getNetworkId();
|
|
||||||
if (!selectedPeer || !networkId) return;
|
|
||||||
const body = input.value.trim();
|
|
||||||
if (!body) return;
|
|
||||||
const now = Date.now();
|
|
||||||
const name = selfName();
|
|
||||||
BackOneWebHubClient.send({
|
|
||||||
type: 'chat_message',
|
|
||||||
networkId,
|
|
||||||
targetId: selectedPeer.peerId,
|
|
||||||
senderName: name,
|
|
||||||
body
|
|
||||||
});
|
|
||||||
getThread(selectedPeer.peerId).push({
|
|
||||||
sender: 'me',
|
|
||||||
senderName: name,
|
|
||||||
body,
|
|
||||||
at: now
|
|
||||||
});
|
|
||||||
input.value = '';
|
|
||||||
render();
|
|
||||||
};
|
|
||||||
|
|
||||||
updateInputs();
|
|
||||||
render();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "updater",
|
|
||||||
"name": "BackOne Updater",
|
|
||||||
"version": "1.17.1",
|
|
||||||
"requires": [],
|
|
||||||
"backend": {
|
|
||||||
"routes": [],
|
|
||||||
"websocketEvents": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
if [ -f "${SCRIPT_DIR}/platform-paths.sh" ]; then
|
|
||||||
# shellcheck source=platform-paths.sh
|
|
||||||
. "${SCRIPT_DIR}/platform-paths.sh"
|
|
||||||
else
|
|
||||||
# shellcheck source=../../pkg/common/platform-paths.sh
|
|
||||||
. "${SCRIPT_DIR}/../../pkg/common/platform-paths.sh"
|
|
||||||
fi
|
|
||||||
backone_resolve_paths
|
|
||||||
|
|
||||||
HOME_DIR="${BACKONE_HOME}"
|
|
||||||
STAMP="${HOME_DIR}/.installed-version"
|
|
||||||
LOG_DIR="${BACKONE_LOG_DIR:-/var/log/backone}"
|
|
||||||
LOG_FILE="${LOG_DIR}/updater.log"
|
|
||||||
MIGRATIONS="${BACKONE_ADDONS_DIR}/updater/migrations"
|
|
||||||
LEGACY_HOME="/var/lib/zerotier-one"
|
|
||||||
|
|
||||||
log() {
|
|
||||||
mkdir -p "$LOG_DIR"
|
|
||||||
echo "$(date -Iseconds) $*" | tee -a "$LOG_FILE"
|
|
||||||
}
|
|
||||||
|
|
||||||
read_version() {
|
|
||||||
if [ -f "$STAMP" ]; then
|
|
||||||
cat "$STAMP"
|
|
||||||
else
|
|
||||||
echo '{"package":"backone","version":"0.0.0","schemaVersion":0}'
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
write_version() {
|
|
||||||
local version="${1:-$(cat debian/version 2>/dev/null || echo 1.15.0)}"
|
|
||||||
mkdir -p "$HOME_DIR"
|
|
||||||
cat > "$STAMP" <<EOF
|
|
||||||
{"package":"backone","version":"${version}","schemaVersion":2,"updatedAt":"$(date -Iseconds)"}
|
|
||||||
EOF
|
|
||||||
chown backone:backone "$STAMP" 2>/dev/null || true
|
|
||||||
}
|
|
||||||
|
|
||||||
run_migrations() {
|
|
||||||
local scope="${1:-all}"
|
|
||||||
[ -d "$MIGRATIONS" ] || return 0
|
|
||||||
for script in "$MIGRATIONS"/*; do
|
|
||||||
[ -f "$script" ] || continue
|
|
||||||
case "$(basename "$script")" in
|
|
||||||
001_*)
|
|
||||||
[ "$scope" = "all" ] || [ "$scope" = "core" ] || continue
|
|
||||||
;;
|
|
||||||
002_*)
|
|
||||||
[ "$scope" = "all" ] || [ "$scope" = "core" ] || continue
|
|
||||||
;;
|
|
||||||
003_*)
|
|
||||||
[ "$scope" = "all" ] || [ "$scope" = "core" ] || continue
|
|
||||||
;;
|
|
||||||
004_*|005_*)
|
|
||||||
[ "$scope" = "all" ] || [ "$scope" = "core" ] || continue
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
if [ "$scope" != "all" ] && [ "$scope" != "$ADDON_SCOPE" ]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
log "Running migration: $(basename "$script")"
|
|
||||||
BACKONE_HOME="$HOME_DIR" bash "$script" || log "Migration warning: $(basename "$script")"
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
migrate_zerotier() {
|
|
||||||
if [ -d "$LEGACY_HOME" ] && [ ! -f "$HOME_DIR/identity.public" ]; then
|
|
||||||
log "Migrating $LEGACY_HOME -> $HOME_DIR"
|
|
||||||
mkdir -p "$HOME_DIR"
|
|
||||||
cp -a "$LEGACY_HOME/." "$HOME_DIR/" 2>/dev/null || true
|
|
||||||
ln -sfn "$HOME_DIR" "$LEGACY_HOME" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
rebuild_manifest() {
|
|
||||||
local registry="${HOME_DIR}/addons.d/enabled.json"
|
|
||||||
mkdir -p "${HOME_DIR}/addons.d"
|
|
||||||
if [ ! -f "$registry" ]; then
|
|
||||||
echo '[]' > "$registry"
|
|
||||||
fi
|
|
||||||
local script_dir manifest_script
|
|
||||||
script_dir="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
|
|
||||||
manifest_script="${script_dir}/rebuild-addon-manifest.sh"
|
|
||||||
if [ ! -x "$manifest_script" ]; then
|
|
||||||
echo "backone-updater: ${manifest_script} not found" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
local count
|
|
||||||
count="$(BACKONE_HOME="$HOME_DIR" BACKONE_ADDONS_DIR="${BACKONE_ADDONS_DIR}" bash "$manifest_script")"
|
|
||||||
log "Wrote ${HOME_DIR}/app/addons/manifest.json (${count} UI addons)"
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd_migrate() {
|
|
||||||
local scope="all"
|
|
||||||
ADDON_SCOPE=""
|
|
||||||
while [ $# -gt 0 ]; do
|
|
||||||
case "$1" in
|
|
||||||
--core) scope="core"; shift ;;
|
|
||||||
--addon) ADDON_SCOPE="$2"; scope="addon"; shift 2 ;;
|
|
||||||
--dry-run)
|
|
||||||
log "Dry run: would migrate scope=$scope addon=$ADDON_SCOPE"
|
|
||||||
ls -1 "$MIGRATIONS" 2>/dev/null || true
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*) shift ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
migrate_zerotier
|
|
||||||
run_migrations "$scope"
|
|
||||||
write_version
|
|
||||||
rebuild_manifest
|
|
||||||
log "Migration complete"
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd_status() {
|
|
||||||
echo "Installed: $(read_version)"
|
|
||||||
echo "Package version: $(cat "${BACKONE_ADDONS_DIR}/comms-core/addon.json" 2>/dev/null | grep '"version"' | head -1 || cat debian/version 2>/dev/null || echo unknown)"
|
|
||||||
echo "Addons registry: ${HOME_DIR}/addons.d/enabled.json"
|
|
||||||
}
|
|
||||||
|
|
||||||
case "${1:-}" in
|
|
||||||
migrate) shift; cmd_migrate "$@" ;;
|
|
||||||
status) cmd_status ;;
|
|
||||||
rebuild-manifest) rebuild_manifest ;;
|
|
||||||
*) echo "Usage: backone-updater migrate [--core|--addon ID] [--dry-run] | status | rebuild-manifest" >&2; exit 1 ;;
|
|
||||||
esac
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Deep-merge JSON into /var/lib/backone/local.conf (idempotent).
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
HOME_DIR="${BACKONE_HOME:-/var/lib/backone}"
|
|
||||||
CONF="${HOME_DIR}/local.conf"
|
|
||||||
PATCH="${1:-}"
|
|
||||||
|
|
||||||
if [ -z "$PATCH" ] && [ ! -t 0 ]; then
|
|
||||||
PATCH="$(cat)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$PATCH" ]; then
|
|
||||||
echo "Usage: merge-local-conf.sh '<json>' or pipe JSON on stdin" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
python3 - "$CONF" "$PATCH" <<'PY'
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
conf_path, patch_raw = sys.argv[1], sys.argv[2]
|
|
||||||
|
|
||||||
def deep_merge(base, patch):
|
|
||||||
if not isinstance(base, dict) or not isinstance(patch, dict):
|
|
||||||
return patch
|
|
||||||
out = dict(base)
|
|
||||||
for k, v in patch.items():
|
|
||||||
if k in out and isinstance(out[k], dict) and isinstance(v, dict):
|
|
||||||
out[k] = deep_merge(out[k], v)
|
|
||||||
else:
|
|
||||||
out[k] = v
|
|
||||||
return out
|
|
||||||
|
|
||||||
patch = json.loads(patch_raw)
|
|
||||||
current = {}
|
|
||||||
if os.path.isfile(conf_path):
|
|
||||||
with open(conf_path, encoding="utf-8") as f:
|
|
||||||
try:
|
|
||||||
current = json.load(f)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
current = {}
|
|
||||||
if not isinstance(current, dict):
|
|
||||||
current = {}
|
|
||||||
|
|
||||||
merged = deep_merge(current, patch)
|
|
||||||
os.makedirs(os.path.dirname(conf_path), exist_ok=True)
|
|
||||||
with open(conf_path, "w", encoding="utf-8") as f:
|
|
||||||
json.dump(merged, f, indent=2)
|
|
||||||
f.write("\n")
|
|
||||||
PY
|
|
||||||
|
|
||||||
chown backone:backone "$CONF" 2>/dev/null || true
|
|
||||||
chmod 644 "$CONF" 2>/dev/null || true
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -euo pipefail
|
|
||||||
BACKONE_HOME="${BACKONE_HOME:-/var/lib/backone}"
|
|
||||||
LEGACY="/var/lib/zerotier-one"
|
|
||||||
if [ -d "$LEGACY" ] && [ ! -f "$BACKONE_HOME/identity.public" ]; then
|
|
||||||
mkdir -p "$BACKONE_HOME"
|
|
||||||
cp -an "$LEGACY/." "$BACKONE_HOME/" 2>/dev/null || cp -a "$LEGACY/." "$BACKONE_HOME/"
|
|
||||||
fi
|
|
||||||
if ! getent passwd backone >/dev/null 2>&1 && getent passwd zerotier-one >/dev/null 2>&1; then
|
|
||||||
usermod -l backone -d "$BACKONE_HOME" zerotier-one 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
chown -R backone:backone "$BACKONE_HOME" 2>/dev/null || true
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -euo pipefail
|
|
||||||
BACKONE_HOME="${BACKONE_HOME:-/var/lib/backone}"
|
|
||||||
mkdir -p "$BACKONE_HOME/addons.d" "$BACKONE_HOME/app/addons"
|
|
||||||
if [ ! -f "$BACKONE_HOME/addons.d/enabled.json" ]; then
|
|
||||||
echo '[]' > "$BACKONE_HOME/addons.d/enabled.json"
|
|
||||||
fi
|
|
||||||
chown -R backone:backone "$BACKONE_HOME/addons.d" "$BACKONE_HOME/app" 2>/dev/null || true
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -euo pipefail
|
|
||||||
BACKONE_HOME="${BACKONE_HOME:-/var/lib/backone}"
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
||||||
if [ -x "${SCRIPT_DIR}/merge-local-conf.sh" ]; then
|
|
||||||
BACKONE_HOME="$BACKONE_HOME" bash "${SCRIPT_DIR}/merge-local-conf.sh" '{"settings":{"enableWebServer":true}}'
|
|
||||||
fi
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -euo pipefail
|
|
||||||
BACKONE_HOME="${BACKONE_HOME:-/var/lib/backone}"
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
||||||
if [ -x "${SCRIPT_DIR}/merge-local-conf.sh" ]; then
|
|
||||||
BACKONE_HOME="$BACKONE_HOME" bash "${SCRIPT_DIR}/merge-local-conf.sh" '{
|
|
||||||
"settings": {
|
|
||||||
"icePreferOverlay": true,
|
|
||||||
"iceServers": [{"urls": "stun:stun.l.google.com:19302"}],
|
|
||||||
"iceFallbackTimeoutMs": 8000,
|
|
||||||
"webhubPort": 9994
|
|
||||||
}
|
|
||||||
}'
|
|
||||||
fi
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -euo pipefail
|
|
||||||
BACKONE_HOME="${BACKONE_HOME:-/var/lib/backone}"
|
|
||||||
CONF="${BACKONE_HOME}/local.conf"
|
|
||||||
|
|
||||||
python3 - "$CONF" <<'PY'
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
path = sys.argv[1]
|
|
||||||
defaults = ["comms-core", "webhub", "voice", "video", "chat"]
|
|
||||||
current = {}
|
|
||||||
if os.path.isfile(path):
|
|
||||||
with open(path, encoding="utf-8") as f:
|
|
||||||
try:
|
|
||||||
current = json.load(f)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
current = {}
|
|
||||||
if not isinstance(current, dict):
|
|
||||||
current = {}
|
|
||||||
settings = current.setdefault("settings", {})
|
|
||||||
if not isinstance(settings, dict):
|
|
||||||
settings = {}
|
|
||||||
current["settings"] = settings
|
|
||||||
enabled = settings.get("enabledAddons")
|
|
||||||
if enabled is None:
|
|
||||||
# No explicit list — all installed addons stay enabled.
|
|
||||||
sys.exit(0)
|
|
||||||
if not isinstance(enabled, list):
|
|
||||||
enabled = []
|
|
||||||
merged = list(enabled)
|
|
||||||
for addon in defaults:
|
|
||||||
if addon not in merged:
|
|
||||||
merged.append(addon)
|
|
||||||
settings["enabledAddons"] = merged
|
|
||||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
||||||
with open(path, "w", encoding="utf-8") as f:
|
|
||||||
json.dump(current, f, indent=2)
|
|
||||||
f.write("\n")
|
|
||||||
PY
|
|
||||||
|
|
||||||
chown backone:backone "$CONF" 2>/dev/null || true
|
|
||||||
chmod 644 "$CONF" 2>/dev/null || true
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Writes $BACKONE_HOME/app/addons/manifest.json from installed addon.json files.
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
if [ -f "${SCRIPT_DIR}/platform-paths.sh" ]; then
|
|
||||||
# shellcheck source=platform-paths.sh
|
|
||||||
. "${SCRIPT_DIR}/platform-paths.sh"
|
|
||||||
else
|
|
||||||
# shellcheck source=../../pkg/common/platform-paths.sh
|
|
||||||
. "${SCRIPT_DIR}/../../pkg/common/platform-paths.sh"
|
|
||||||
fi
|
|
||||||
backone_resolve_paths
|
|
||||||
|
|
||||||
HOME_DIR="${BACKONE_HOME}"
|
|
||||||
ADDONS_DIR="${BACKONE_ADDONS_DIR}"
|
|
||||||
MANIFEST="${HOME_DIR}/app/addons/manifest.json"
|
|
||||||
|
|
||||||
mkdir -p "${HOME_DIR}/app/addons" "${HOME_DIR}/addons.d"
|
|
||||||
|
|
||||||
if ! command -v python3 >/dev/null 2>&1; then
|
|
||||||
echo "rebuild-addon-manifest: python3 required" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
python3 - "$ADDONS_DIR" "$MANIFEST" <<'PY'
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
addons_dir, manifest_path = sys.argv[1], sys.argv[2]
|
|
||||||
out = []
|
|
||||||
if os.path.isdir(addons_dir):
|
|
||||||
for name in sorted(os.listdir(addons_dir)):
|
|
||||||
path = os.path.join(addons_dir, name, "addon.json")
|
|
||||||
if not os.path.isfile(path):
|
|
||||||
continue
|
|
||||||
with open(path, encoding="utf-8") as f:
|
|
||||||
m = json.load(f)
|
|
||||||
if "frontend" not in m:
|
|
||||||
continue
|
|
||||||
item = {
|
|
||||||
"id": m["id"],
|
|
||||||
"name": m.get("name", m["id"]),
|
|
||||||
"version": m.get("version", "0.0.0"),
|
|
||||||
"frontend": m["frontend"],
|
|
||||||
}
|
|
||||||
if "requires" in m:
|
|
||||||
item["requires"] = m["requires"]
|
|
||||||
out.append(item)
|
|
||||||
|
|
||||||
os.makedirs(os.path.dirname(manifest_path), exist_ok=True)
|
|
||||||
with open(manifest_path, "w", encoding="utf-8") as f:
|
|
||||||
json.dump(out, f, indent=2)
|
|
||||||
f.write("\n")
|
|
||||||
print(len(out))
|
|
||||||
PY
|
|
||||||
|
|
||||||
chown backone:backone "$MANIFEST" 2>/dev/null || true
|
|
||||||
chmod 644 "$MANIFEST" 2>/dev/null || true
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# Keep ~/.backOneOneAuthToken and /var/lib/backone/authtoken.secret in sync (same as backone-cli).
|
|
||||||
set -e
|
|
||||||
|
|
||||||
BACKONE_HOME="${BACKONE_HOME:-/var/lib/backone}"
|
|
||||||
SECRET="${BACKONE_HOME}/authtoken.secret"
|
|
||||||
|
|
||||||
trim_token() {
|
|
||||||
tr -d '\n\r \t' < "$1"
|
|
||||||
}
|
|
||||||
|
|
||||||
sync_for_user() {
|
|
||||||
user="$1"
|
|
||||||
home="$2"
|
|
||||||
[ -n "$user" ] && [ -n "$home" ] && [ -d "$home" ] || return 0
|
|
||||||
user_token="${home}/.backOneOneAuthToken"
|
|
||||||
|
|
||||||
if [ -f "$user_token" ] && [ -s "$user_token" ]; then
|
|
||||||
ut="$(trim_token "$user_token")"
|
|
||||||
[ -n "$ut" ] || return 0
|
|
||||||
st=""
|
|
||||||
if [ -f "$SECRET" ]; then
|
|
||||||
st="$(trim_token "$SECRET" 2>/dev/null || true)"
|
|
||||||
fi
|
|
||||||
if [ "$ut" != "$st" ]; then
|
|
||||||
printf '%s' "$ut" > "$SECRET"
|
|
||||||
chown backone:backone "$SECRET" 2>/dev/null || true
|
|
||||||
chmod 0600 "$SECRET" 2>/dev/null || true
|
|
||||||
echo "Synced ${SECRET} from ${user_token}"
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f "$SECRET" ]; then
|
|
||||||
install -o "$user" -g "$(id -gn "$user" 2>/dev/null || echo "$user")" -m 0600 "$SECRET" "$user_token" 2>/dev/null \
|
|
||||||
|| cp "$SECRET" "$user_token"
|
|
||||||
chown "$user":"$(id -gn "$user" 2>/dev/null || echo "$user")" "$user_token" 2>/dev/null || true
|
|
||||||
chmod 0600 "$user_token" 2>/dev/null || true
|
|
||||||
echo "Installed ${user_token} from ${SECRET}"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
if [ -n "${SUDO_USER:-}" ] && [ "$SUDO_USER" != "root" ]; then
|
|
||||||
home="$(getent passwd "$SUDO_USER" | cut -d: -f6)"
|
|
||||||
sync_for_user "$SUDO_USER" "$home"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "${USER:-}" ] && [ "$USER" != "root" ] && [ "$USER" != "${SUDO_USER:-}" ]; then
|
|
||||||
home="$(getent passwd "$USER" | cut -d: -f6)"
|
|
||||||
sync_for_user "$USER" "$home"
|
|
||||||
fi
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "video",
|
|
||||||
"name": "Video Call P2P",
|
|
||||||
"version": "1.17.1",
|
|
||||||
"requires": ["comms-core", "webhub", "voice"],
|
|
||||||
"backoneCore": {
|
|
||||||
"apis": [
|
|
||||||
"/app/api/peers",
|
|
||||||
"/app/api/networks",
|
|
||||||
"POST /app/api/network/{nwid}",
|
|
||||||
"DELETE /app/api/network/{nwid}",
|
|
||||||
"/app/api/network/{nwid}",
|
|
||||||
"/app/api/network/{nwid}/members",
|
|
||||||
"/app/api/authtoken",
|
|
||||||
"/app/api/contacts"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"backend": {
|
|
||||||
"routes": ["/controller/comms/call"],
|
|
||||||
"websocketEvents": ["incoming_call", "call_state"]
|
|
||||||
},
|
|
||||||
"frontend": {
|
|
||||||
"entry": "/app/addons/video/index.js",
|
|
||||||
"navLabel": "Video",
|
|
||||||
"icon": "video"
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"enableVideoCall": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
window.BackOneAddons = window.BackOneAddons || {};
|
|
||||||
|
|
||||||
window.BackOneAddons.video = function videoAddon() {
|
|
||||||
let picker = null;
|
|
||||||
let selectedPeer = null;
|
|
||||||
let inCall = false;
|
|
||||||
let micEnabled = true;
|
|
||||||
let camEnabled = true;
|
|
||||||
let root = null;
|
|
||||||
|
|
||||||
function updateConnectionBar() {
|
|
||||||
const bar = root?.querySelector('#video-conn-bar');
|
|
||||||
if (!bar || !window.BackOneCommLayout) return;
|
|
||||||
bar.innerHTML = BackOneCommLayout.connectionBarHtml(
|
|
||||||
selectedPeer,
|
|
||||||
BackOneNetworkSession.getActiveNetwork()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateHeadStatus() {
|
|
||||||
const statusEl = root?.querySelector('#video-head-status');
|
|
||||||
const breadcrumb = root?.querySelector('#video-breadcrumb-peer');
|
|
||||||
if (!statusEl) return;
|
|
||||||
if (inCall && selectedPeer) {
|
|
||||||
statusEl.innerHTML = `<span class="connected">Connected</span> · ${selectedPeer.latency != null ? selectedPeer.latency + 'ms' : '—'}`;
|
|
||||||
if (breadcrumb) breadcrumb.textContent = selectedPeer.peerId;
|
|
||||||
} else if (selectedPeer) {
|
|
||||||
statusEl.textContent = 'Ready';
|
|
||||||
if (breadcrumb) breadcrumb.textContent = selectedPeer.peerId;
|
|
||||||
} else {
|
|
||||||
statusEl.textContent = '';
|
|
||||||
if (breadcrumb) breadcrumb.textContent = '—';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateCallButtons() {
|
|
||||||
const callBtn = root?.querySelector('#video-call');
|
|
||||||
if (callBtn) callBtn.disabled = !selectedPeer || !BackOneNetworkSession.getNetworkId() || inCall;
|
|
||||||
const selectedLabel = root?.querySelector('#video-selected-label');
|
|
||||||
if (selectedLabel) {
|
|
||||||
selectedLabel.innerHTML = selectedPeer
|
|
||||||
? `Selected: <strong>${selectedPeer.peerId.slice(0, 10)}…</strong>`
|
|
||||||
: 'Selected: <strong>—</strong>';
|
|
||||||
}
|
|
||||||
updateHeadStatus();
|
|
||||||
updateConnectionBar();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateCallLayout() {
|
|
||||||
const layout = root?.querySelector('.comm-layout');
|
|
||||||
if (layout) layout.classList.toggle('video-in-call', inCall);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showPlaceholder(show) {
|
|
||||||
const stage = root?.querySelector('.video-stage');
|
|
||||||
const remote = root?.querySelector('#video-remote');
|
|
||||||
const phLabel = root?.querySelector('#video-ph-label');
|
|
||||||
if (stage) stage.classList.toggle('has-remote', !show);
|
|
||||||
if (remote) remote.classList.toggle('active', !show);
|
|
||||||
if (phLabel && show) {
|
|
||||||
phLabel.textContent = inCall
|
|
||||||
? 'Waiting for remote peer…'
|
|
||||||
: (selectedPeer ? 'Ready to call' : 'Select a peer to start a video call');
|
|
||||||
}
|
|
||||||
updateCallLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
function bindStreams() {
|
|
||||||
const local = root?.querySelector('#video-local');
|
|
||||||
const remote = root?.querySelector('#video-remote');
|
|
||||||
const localStream = BackOneWebRtcSession.getLocalStream();
|
|
||||||
const remoteStream = BackOneWebRtcSession.getRemoteStream();
|
|
||||||
if (local && localStream) local.srcObject = localStream;
|
|
||||||
if (remote && remoteStream && remoteStream.getTracks().length) {
|
|
||||||
remote.srcObject = remoteStream;
|
|
||||||
showPlaceholder(false);
|
|
||||||
root?.querySelector('#video-breadcrumb-state')?.textContent = 'Connected to';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function hangUp() {
|
|
||||||
await BackOneWebRtcSession.hangUp();
|
|
||||||
const local = root?.querySelector('#video-local');
|
|
||||||
const remote = root?.querySelector('#video-remote');
|
|
||||||
if (local) local.srcObject = null;
|
|
||||||
if (remote) remote.srcObject = null;
|
|
||||||
inCall = false;
|
|
||||||
micEnabled = true;
|
|
||||||
camEnabled = true;
|
|
||||||
const breadcrumbState = root?.querySelector('#video-breadcrumb-state');
|
|
||||||
if (breadcrumbState) breadcrumbState.textContent = 'Select peer';
|
|
||||||
showPlaceholder(true);
|
|
||||||
updateCallButtons();
|
|
||||||
window.BackOneRingingPopup?.dismiss();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
mount(container) {
|
|
||||||
root = container;
|
|
||||||
container.innerHTML = `
|
|
||||||
<div class="comm-layout">
|
|
||||||
<aside class="comm-sidebar">
|
|
||||||
<div class="comm-sidebar-head">
|
|
||||||
<h2>Video Call P2P</h2>
|
|
||||||
<p class="comm-sidebar-sub">BackOne core · same network as backone-cli</p>
|
|
||||||
</div>
|
|
||||||
<div class="comm-sidebar-body">
|
|
||||||
<div id="video-picker"></div>
|
|
||||||
</div>
|
|
||||||
<div class="comm-sidebar-actions">
|
|
||||||
<div id="video-selected-label" class="comm-selected-label">Selected: <strong>—</strong></div>
|
|
||||||
<button type="button" id="video-call" class="btn-primary" disabled>Video Call</button>
|
|
||||||
<button type="button" id="video-hangup" class="btn-secondary">Hang Up</button>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
<div class="comm-main">
|
|
||||||
<div class="comm-main-head">
|
|
||||||
<div class="comm-breadcrumb">Video Call P2P > <span id="video-breadcrumb-state">Select peer</span> <strong id="video-breadcrumb-peer">—</strong></div>
|
|
||||||
<div id="video-head-status" class="comm-head-status"></div>
|
|
||||||
</div>
|
|
||||||
<div class="comm-stage video-stage">
|
|
||||||
<div class="video-remote-main">
|
|
||||||
<div id="video-placeholder" class="comm-remote-placeholder">
|
|
||||||
<div class="avatar">👤</div>
|
|
||||||
<div class="peer-id" id="video-ph-peer">—</div>
|
|
||||||
<div id="video-ph-label">Select a peer to start a video call</div>
|
|
||||||
</div>
|
|
||||||
<video id="video-remote" class="video-remote-stream" autoplay playsinline></video>
|
|
||||||
</div>
|
|
||||||
<div class="video-local-pip">
|
|
||||||
<span class="video-pip-label">You</span>
|
|
||||||
<video id="video-local" autoplay muted playsinline></video>
|
|
||||||
</div>
|
|
||||||
<div class="comm-remote-tag"><span class="dot"></span> Remote</div>
|
|
||||||
<div class="comm-call-controls">
|
|
||||||
<button type="button" id="video-mic">Mic</button>
|
|
||||||
<button type="button" id="video-cam">Camera</button>
|
|
||||||
<button type="button" id="video-fs">Fullscreen</button>
|
|
||||||
<button type="button" id="video-hangup-stage" class="danger">Hang Up</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="video-conn-bar" class="comm-connection-bar"></div>`;
|
|
||||||
|
|
||||||
const callBtn = container.querySelector('#video-call');
|
|
||||||
|
|
||||||
picker = BackOnePeerPicker(container.querySelector('#video-picker'), {
|
|
||||||
layout: 'sidebar',
|
|
||||||
showActionButton: false,
|
|
||||||
onSelect(peer) {
|
|
||||||
selectedPeer = peer;
|
|
||||||
const phPeer = root.querySelector('#video-ph-peer');
|
|
||||||
if (phPeer) phPeer.textContent = peer.peerId;
|
|
||||||
if (!inCall) showPlaceholder(true);
|
|
||||||
updateCallButtons();
|
|
||||||
},
|
|
||||||
onNetworkChange() {
|
|
||||||
if (!inCall) selectedPeer = null;
|
|
||||||
updateCallButtons();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
BackOneNetworkSession.subscribe(() => updateCallButtons());
|
|
||||||
BackOneWebRtcSession.subscribe((event) => {
|
|
||||||
if (event === 'call-started') {
|
|
||||||
inCall = true;
|
|
||||||
updateCallButtons();
|
|
||||||
bindStreams();
|
|
||||||
}
|
|
||||||
if (event === 'remote-track' || event === 'call-accepted') bindStreams();
|
|
||||||
if (event === 'call-ended') {
|
|
||||||
inCall = false;
|
|
||||||
showPlaceholder(true);
|
|
||||||
updateCallButtons();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
callBtn.onclick = async () => {
|
|
||||||
const peer = selectedPeer || picker.getSelectedPeer();
|
|
||||||
if (!peer || !BackOneNetworkSession.getNetworkId()) return;
|
|
||||||
try {
|
|
||||||
inCall = true;
|
|
||||||
root.querySelector('#video-breadcrumb-state').textContent = 'Connecting to';
|
|
||||||
updateCallLayout();
|
|
||||||
updateCallButtons();
|
|
||||||
await BackOneWebRtcSession.startOutgoingCall(peer, 'video');
|
|
||||||
bindStreams();
|
|
||||||
} catch (err) {
|
|
||||||
await hangUp();
|
|
||||||
alert(err.message || 'Call failed');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
container.querySelector('#video-hangup').onclick = () => hangUp();
|
|
||||||
container.querySelector('#video-hangup-stage').onclick = () => hangUp();
|
|
||||||
|
|
||||||
container.querySelector('#video-mic').onclick = () => {
|
|
||||||
const localStream = BackOneWebRtcSession.getLocalStream();
|
|
||||||
if (!localStream) return;
|
|
||||||
micEnabled = !micEnabled;
|
|
||||||
localStream.getAudioTracks().forEach(t => { t.enabled = micEnabled; });
|
|
||||||
container.querySelector('#video-mic').textContent = micEnabled ? 'Mic' : 'Mic off';
|
|
||||||
};
|
|
||||||
|
|
||||||
container.querySelector('#video-cam').onclick = () => {
|
|
||||||
const localStream = BackOneWebRtcSession.getLocalStream();
|
|
||||||
if (!localStream) return;
|
|
||||||
camEnabled = !camEnabled;
|
|
||||||
localStream.getVideoTracks().forEach(t => { t.enabled = camEnabled; });
|
|
||||||
container.querySelector('#video-cam').textContent = camEnabled ? 'Camera' : 'Camera off';
|
|
||||||
};
|
|
||||||
|
|
||||||
container.querySelector('#video-fs').onclick = () => {
|
|
||||||
const stage = container.querySelector('.comm-stage');
|
|
||||||
if (stage.requestFullscreen) stage.requestFullscreen();
|
|
||||||
};
|
|
||||||
|
|
||||||
const active = BackOneWebRtcSession.getActiveCall();
|
|
||||||
inCall = !!active && active.callType === 'video';
|
|
||||||
showPlaceholder(true);
|
|
||||||
if (inCall) bindStreams();
|
|
||||||
updateCallButtons();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "voice",
|
|
||||||
"name": "Voice Call P2P",
|
|
||||||
"version": "1.17.1",
|
|
||||||
"requires": ["comms-core", "webhub"],
|
|
||||||
"backoneCore": {
|
|
||||||
"apis": [
|
|
||||||
"/app/api/peers",
|
|
||||||
"/app/api/networks",
|
|
||||||
"POST /app/api/network/{nwid}",
|
|
||||||
"DELETE /app/api/network/{nwid}",
|
|
||||||
"/app/api/network/{nwid}",
|
|
||||||
"/app/api/network/{nwid}/members",
|
|
||||||
"/app/api/authtoken",
|
|
||||||
"/app/api/contacts"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"backend": {
|
|
||||||
"routes": ["/controller/comms/call"],
|
|
||||||
"websocketEvents": ["incoming_call", "call_state"]
|
|
||||||
},
|
|
||||||
"frontend": {
|
|
||||||
"entry": "/app/addons/voice/index.js",
|
|
||||||
"navLabel": "Voice",
|
|
||||||
"icon": "phone"
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"enableVoiceCall": true,
|
|
||||||
"ringTimeoutSec": 30
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
window.BackOneAddons = window.BackOneAddons || {};
|
|
||||||
|
|
||||||
window.BackOneAddons.voice = function voiceAddon() {
|
|
||||||
let picker = null;
|
|
||||||
let selectedPeer = null;
|
|
||||||
let inCall = false;
|
|
||||||
let micEnabled = true;
|
|
||||||
let root = null;
|
|
||||||
let remoteAudio = null;
|
|
||||||
|
|
||||||
function escapeHtml(s) {
|
|
||||||
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateConnectionBar() {
|
|
||||||
const bar = root?.querySelector('#voice-conn-bar');
|
|
||||||
if (!bar || !window.BackOneCommLayout) return;
|
|
||||||
bar.innerHTML = BackOneCommLayout.connectionBarHtml(
|
|
||||||
selectedPeer,
|
|
||||||
BackOneNetworkSession.getActiveNetwork()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateHeadStatus() {
|
|
||||||
const statusEl = root?.querySelector('#voice-head-status');
|
|
||||||
const breadcrumb = root?.querySelector('#voice-breadcrumb-peer');
|
|
||||||
if (!statusEl) return;
|
|
||||||
if (inCall && selectedPeer) {
|
|
||||||
statusEl.innerHTML = `<span class="connected">Connected</span> · ${selectedPeer.latency != null ? selectedPeer.latency + 'ms' : '—'}`;
|
|
||||||
if (breadcrumb) breadcrumb.textContent = selectedPeer.peerId;
|
|
||||||
} else if (selectedPeer) {
|
|
||||||
statusEl.textContent = 'Ready';
|
|
||||||
if (breadcrumb) breadcrumb.textContent = selectedPeer.peerId;
|
|
||||||
} else {
|
|
||||||
statusEl.textContent = '';
|
|
||||||
if (breadcrumb) breadcrumb.textContent = '—';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateActiveBanner() {
|
|
||||||
const banner = root?.querySelector('#voice-active-banner');
|
|
||||||
const label = root?.querySelector('#voice-active-label');
|
|
||||||
if (!banner) return;
|
|
||||||
banner.style.display = inCall && selectedPeer ? 'flex' : 'none';
|
|
||||||
if (label && selectedPeer) {
|
|
||||||
label.textContent = `In call with ${selectedPeer.displayName || selectedPeer.peerId}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateCallButtons() {
|
|
||||||
const callBtn = root?.querySelector('#voice-call');
|
|
||||||
if (callBtn) callBtn.disabled = !selectedPeer || !BackOneNetworkSession.getNetworkId() || inCall;
|
|
||||||
const selectedLabel = root?.querySelector('#voice-selected-label');
|
|
||||||
if (selectedLabel) {
|
|
||||||
selectedLabel.innerHTML = selectedPeer
|
|
||||||
? `Selected: <strong>${selectedPeer.peerId.slice(0, 10)}…</strong>`
|
|
||||||
: 'Selected: <strong>—</strong>';
|
|
||||||
}
|
|
||||||
updateHeadStatus();
|
|
||||||
updateConnectionBar();
|
|
||||||
updateActiveBanner();
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncInCallState() {
|
|
||||||
const active = BackOneWebRtcSession.getActiveCall();
|
|
||||||
inCall = !!active && (active.callType === 'voice' || !active.callType);
|
|
||||||
if (inCall && active.peer) {
|
|
||||||
selectedPeer = active.peer.peerId ? active.peer : selectedPeer;
|
|
||||||
root?.querySelector('#voice-breadcrumb-state')?.textContent = 'Connected to';
|
|
||||||
}
|
|
||||||
updateCallButtons();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function hangUp() {
|
|
||||||
await BackOneWebRtcSession.hangUp();
|
|
||||||
inCall = false;
|
|
||||||
micEnabled = true;
|
|
||||||
if (remoteAudio) remoteAudio.srcObject = null;
|
|
||||||
const breadcrumbState = root?.querySelector('#voice-breadcrumb-state');
|
|
||||||
if (breadcrumbState) breadcrumbState.textContent = 'Select peer';
|
|
||||||
updateCallButtons();
|
|
||||||
window.BackOneRingingPopup?.dismiss();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
mount(container) {
|
|
||||||
root = container;
|
|
||||||
container.innerHTML = `
|
|
||||||
<div class="comm-layout comm-split-layout">
|
|
||||||
<aside class="comm-sidebar">
|
|
||||||
<div class="comm-sidebar-head">
|
|
||||||
<h2>Voice Call P2P</h2>
|
|
||||||
<p class="comm-sidebar-sub">Contacts</p>
|
|
||||||
</div>
|
|
||||||
<div class="comm-sidebar-body">
|
|
||||||
<div id="voice-picker"></div>
|
|
||||||
</div>
|
|
||||||
<div class="comm-sidebar-actions">
|
|
||||||
<div id="voice-selected-label" class="comm-selected-label">Selected: <strong>—</strong></div>
|
|
||||||
<button type="button" id="voice-call" class="btn-primary" disabled>Voice Call</button>
|
|
||||||
<button type="button" id="voice-hangup" class="btn-secondary">Hang Up</button>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
<div class="comm-main">
|
|
||||||
<div class="comm-main-head">
|
|
||||||
<div class="comm-breadcrumb">Voice Call P2P > <span id="voice-breadcrumb-state">Select peer</span> <strong id="voice-breadcrumb-peer">—</strong></div>
|
|
||||||
<div id="voice-head-status" class="comm-head-status"></div>
|
|
||||||
</div>
|
|
||||||
<div class="voice-main-panel">
|
|
||||||
<div id="voice-active-banner" class="voice-active-banner" style="display:none">
|
|
||||||
<div>
|
|
||||||
<span class="voice-active-dot"></span>
|
|
||||||
<span id="voice-active-label">In call</span>
|
|
||||||
</div>
|
|
||||||
<div class="voice-active-controls">
|
|
||||||
<button type="button" id="voice-mic">Mic</button>
|
|
||||||
<button type="button" id="voice-hangup-stage" class="danger">Hang Up</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="voice-session-hint">Live calls only — no call history is stored.</p>
|
|
||||||
<audio id="voice-remote-audio" autoplay playsinline class="voice-remote-audio"></audio>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="voice-conn-bar" class="comm-connection-bar"></div>`;
|
|
||||||
|
|
||||||
remoteAudio = container.querySelector('#voice-remote-audio');
|
|
||||||
const callBtn = container.querySelector('#voice-call');
|
|
||||||
|
|
||||||
picker = BackOnePeerPicker(container.querySelector('#voice-picker'), {
|
|
||||||
layout: 'sidebar',
|
|
||||||
showActionButton: false,
|
|
||||||
onSelect(peer) {
|
|
||||||
selectedPeer = peer;
|
|
||||||
updateCallButtons();
|
|
||||||
},
|
|
||||||
onNetworkChange() {
|
|
||||||
if (!inCall) selectedPeer = null;
|
|
||||||
updateCallButtons();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
BackOneNetworkSession.subscribe(() => updateCallButtons());
|
|
||||||
BackOneWebRtcSession.subscribe((event) => {
|
|
||||||
if (event === 'call-started' || event === 'call-ended' || event === 'remote-track') {
|
|
||||||
syncInCallState();
|
|
||||||
const stream = BackOneWebRtcSession.getRemoteStream();
|
|
||||||
if (remoteAudio && stream) remoteAudio.srcObject = stream;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
callBtn.onclick = async () => {
|
|
||||||
const peer = selectedPeer || picker.getSelectedPeer();
|
|
||||||
if (!peer || !BackOneNetworkSession.getNetworkId()) return;
|
|
||||||
try {
|
|
||||||
await BackOneWebRtcSession.startOutgoingCall(peer, 'voice');
|
|
||||||
inCall = true;
|
|
||||||
root.querySelector('#voice-breadcrumb-state').textContent = 'Connecting to';
|
|
||||||
updateCallButtons();
|
|
||||||
} catch (err) {
|
|
||||||
await hangUp();
|
|
||||||
alert(err.message || 'Call failed');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
container.querySelector('#voice-hangup').onclick = () => hangUp();
|
|
||||||
container.querySelector('#voice-hangup-stage').onclick = () => hangUp();
|
|
||||||
|
|
||||||
container.querySelector('#voice-mic').onclick = () => {
|
|
||||||
const localStream = BackOneWebRtcSession.getLocalStream();
|
|
||||||
if (!localStream) return;
|
|
||||||
micEnabled = !micEnabled;
|
|
||||||
localStream.getAudioTracks().forEach(t => { t.enabled = micEnabled; });
|
|
||||||
container.querySelector('#voice-mic').textContent = micEnabled ? 'Mic' : 'Mic off';
|
|
||||||
};
|
|
||||||
|
|
||||||
syncInCallState();
|
|
||||||
updateCallButtons();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "webhub",
|
|
||||||
"name": "WebSocket Signaling Hub",
|
|
||||||
"version": "1.17.1",
|
|
||||||
"requires": ["comms-core"],
|
|
||||||
"backend": {
|
|
||||||
"routes": [],
|
|
||||||
"websocketEvents": ["presence", "incoming_call", "call_state", "ice_candidate", "chat_message", "typing"],
|
|
||||||
"sidecar": {
|
|
||||||
"command": "/usr/lib/backone/addons/webhub/backone-webhub",
|
|
||||||
"port": 9994,
|
|
||||||
"bind": "127.0.0.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,343 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""BackOne WebHub — ephemeral in-memory WebSocket signaling (no DB, no history)."""
|
|
||||||
import asyncio
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import secrets
|
|
||||||
import time
|
|
||||||
import urllib.parse
|
|
||||||
|
|
||||||
try:
|
|
||||||
import websockets
|
|
||||||
from websockets.server import serve
|
|
||||||
except ImportError:
|
|
||||||
raise SystemExit("python3-websockets is required")
|
|
||||||
|
|
||||||
BACKONE_HOME = os.environ.get("BACKONE_HOME", "/var/lib/backone")
|
|
||||||
AUTH_PATH = os.path.join(BACKONE_HOME, "authtoken.secret")
|
|
||||||
CONF_PATH = os.path.join(BACKONE_HOME, "local.conf")
|
|
||||||
DEFAULT_PORT = 9994
|
|
||||||
RING_TIMEOUT_SEC = 30
|
|
||||||
|
|
||||||
peers = {} # peer_id -> {ws, network_id, registered_at}
|
|
||||||
calls = {} # call_id -> call dict
|
|
||||||
ice_queues = {} # call_id -> list of {from, candidate, ...}
|
|
||||||
peer_to_calls = {} # peer_id -> set(call_id)
|
|
||||||
|
|
||||||
|
|
||||||
def load_auth_token():
|
|
||||||
try:
|
|
||||||
with open(AUTH_PATH, encoding="utf-8") as f:
|
|
||||||
t = f.read().strip()
|
|
||||||
return t if t else None
|
|
||||||
except OSError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def load_port():
|
|
||||||
try:
|
|
||||||
with open(CONF_PATH, encoding="utf-8") as f:
|
|
||||||
cfg = json.load(f)
|
|
||||||
settings = cfg.get("settings") or {}
|
|
||||||
return int(settings.get("webhubPort", DEFAULT_PORT))
|
|
||||||
except (OSError, ValueError, TypeError, json.JSONDecodeError):
|
|
||||||
return DEFAULT_PORT
|
|
||||||
|
|
||||||
|
|
||||||
def valid_token(token):
|
|
||||||
expected = load_auth_token()
|
|
||||||
if not expected or not token:
|
|
||||||
return False
|
|
||||||
return secrets.compare_digest(token.strip(), expected.strip())
|
|
||||||
|
|
||||||
|
|
||||||
def new_call_id():
|
|
||||||
return f"call_{int(time.time() * 1000)}_{secrets.token_hex(4)}"
|
|
||||||
|
|
||||||
|
|
||||||
def peer_key(peer_id):
|
|
||||||
return (peer_id or "").lower()
|
|
||||||
|
|
||||||
|
|
||||||
def send_json(ws, payload):
|
|
||||||
return ws.send(json.dumps(payload))
|
|
||||||
|
|
||||||
|
|
||||||
async def push_to_peer(target_id, payload):
|
|
||||||
key = peer_key(target_id)
|
|
||||||
entry = peers.get(key)
|
|
||||||
if not entry:
|
|
||||||
return False
|
|
||||||
try:
|
|
||||||
await send_json(entry["ws"], payload)
|
|
||||||
return True
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def track_call_for_peer(pid, call_id):
|
|
||||||
key = peer_key(pid)
|
|
||||||
if key not in peer_to_calls:
|
|
||||||
peer_to_calls[key] = set()
|
|
||||||
peer_to_calls[key].add(call_id)
|
|
||||||
|
|
||||||
|
|
||||||
def untrack_call(call_id, call):
|
|
||||||
for pid in (call.get("callerId"), call.get("calleeId")):
|
|
||||||
key = peer_key(pid)
|
|
||||||
if key in peer_to_calls:
|
|
||||||
peer_to_calls[key].discard(call_id)
|
|
||||||
if not peer_to_calls[key]:
|
|
||||||
del peer_to_calls[key]
|
|
||||||
|
|
||||||
|
|
||||||
def purge_call(call_id):
|
|
||||||
call = calls.pop(call_id, None)
|
|
||||||
ice_queues.pop(call_id, None)
|
|
||||||
if call:
|
|
||||||
untrack_call(call_id, call)
|
|
||||||
|
|
||||||
|
|
||||||
def end_calls_for_peer(peer_id):
|
|
||||||
key = peer_key(peer_id)
|
|
||||||
for call_id in list(peer_to_calls.get(key, set())):
|
|
||||||
call = calls.get(call_id)
|
|
||||||
if not call:
|
|
||||||
continue
|
|
||||||
other = call["calleeId"] if peer_key(call["callerId"]) == key else call["callerId"]
|
|
||||||
asyncio.create_task(push_to_peer(other, {
|
|
||||||
"type": "call_state",
|
|
||||||
"callId": call_id,
|
|
||||||
"state": "ended",
|
|
||||||
"reason": "peer_disconnect"
|
|
||||||
}))
|
|
||||||
purge_call(call_id)
|
|
||||||
|
|
||||||
|
|
||||||
async def ring_timeout_watch():
|
|
||||||
while True:
|
|
||||||
await asyncio.sleep(5)
|
|
||||||
now = time.time()
|
|
||||||
for call_id, call in list(calls.items()):
|
|
||||||
if call.get("state") != "ringing":
|
|
||||||
continue
|
|
||||||
if now - call.get("createdAt", now) > RING_TIMEOUT_SEC:
|
|
||||||
await push_to_peer(call["callerId"], {
|
|
||||||
"type": "call_state",
|
|
||||||
"callId": call_id,
|
|
||||||
"state": "rejected",
|
|
||||||
"reason": "timeout"
|
|
||||||
})
|
|
||||||
purge_call(call_id)
|
|
||||||
|
|
||||||
|
|
||||||
async def handle_message(ws, state, msg):
|
|
||||||
mtype = msg.get("type")
|
|
||||||
if mtype == "auth":
|
|
||||||
token = msg.get("token", "")
|
|
||||||
if not valid_token(token):
|
|
||||||
await send_json(ws, {"type": "error", "error": "unauthorized"})
|
|
||||||
await ws.close()
|
|
||||||
return False
|
|
||||||
state["authed"] = True
|
|
||||||
await send_json(ws, {"type": "auth_ok"})
|
|
||||||
return True
|
|
||||||
|
|
||||||
if not state.get("authed"):
|
|
||||||
await send_json(ws, {"type": "error", "error": "not authenticated"})
|
|
||||||
return True
|
|
||||||
|
|
||||||
if mtype == "register":
|
|
||||||
peer_id = peer_key(msg.get("peerId", ""))
|
|
||||||
network_id = (msg.get("networkId") or "").lower()
|
|
||||||
if not peer_id:
|
|
||||||
await send_json(ws, {"type": "error", "error": "peerId required"})
|
|
||||||
return True
|
|
||||||
peers[peer_id] = {"ws": ws, "networkId": network_id, "registeredAt": time.time()}
|
|
||||||
state["peerId"] = peer_id
|
|
||||||
await send_json(ws, {"type": "registered", "peerId": peer_id})
|
|
||||||
return True
|
|
||||||
|
|
||||||
peer_id = state.get("peerId")
|
|
||||||
if not peer_id:
|
|
||||||
await send_json(ws, {"type": "error", "error": "register first"})
|
|
||||||
return True
|
|
||||||
|
|
||||||
if mtype == "call_offer":
|
|
||||||
target_id = peer_key(msg.get("targetId", ""))
|
|
||||||
call_type = msg.get("callType") or "voice"
|
|
||||||
sdp = msg.get("sdp", "")
|
|
||||||
network_id = (msg.get("networkId") or peers[peer_id].get("networkId") or "").lower()
|
|
||||||
if not target_id or not sdp:
|
|
||||||
await send_json(ws, {"type": "error", "error": "targetId and sdp required"})
|
|
||||||
return True
|
|
||||||
call_id = new_call_id()
|
|
||||||
call = {
|
|
||||||
"callId": call_id,
|
|
||||||
"callType": call_type,
|
|
||||||
"networkId": network_id,
|
|
||||||
"callerId": peer_id,
|
|
||||||
"calleeId": target_id,
|
|
||||||
"state": "ringing",
|
|
||||||
"offerSdp": sdp,
|
|
||||||
"answerSdp": "",
|
|
||||||
"createdAt": time.time()
|
|
||||||
}
|
|
||||||
calls[call_id] = call
|
|
||||||
ice_queues[call_id] = []
|
|
||||||
track_call_for_peer(peer_id, call_id)
|
|
||||||
track_call_for_peer(target_id, call_id)
|
|
||||||
delivered = await push_to_peer(target_id, {
|
|
||||||
"type": "incoming_call",
|
|
||||||
"callId": call_id,
|
|
||||||
"callType": call_type,
|
|
||||||
"networkId": network_id,
|
|
||||||
"callerId": peer_id,
|
|
||||||
"callerName": msg.get("callerName", peer_id),
|
|
||||||
"sdp": sdp,
|
|
||||||
"overlayIps": msg.get("overlayIps", [])
|
|
||||||
})
|
|
||||||
await send_json(ws, {
|
|
||||||
"type": "call_state",
|
|
||||||
"callId": call_id,
|
|
||||||
"state": "ringing",
|
|
||||||
"delivered": delivered
|
|
||||||
})
|
|
||||||
return True
|
|
||||||
|
|
||||||
if mtype == "call_answer":
|
|
||||||
call_id = msg.get("callId", "")
|
|
||||||
sdp = msg.get("sdp", "")
|
|
||||||
call = calls.get(call_id)
|
|
||||||
if not call or peer_key(call["calleeId"]) != peer_id:
|
|
||||||
await send_json(ws, {"type": "error", "error": "call not found"})
|
|
||||||
return True
|
|
||||||
call["answerSdp"] = sdp
|
|
||||||
call["state"] = "accepted"
|
|
||||||
await push_to_peer(call["callerId"], {
|
|
||||||
"type": "call_state",
|
|
||||||
"callId": call_id,
|
|
||||||
"state": "accepted",
|
|
||||||
"sdp": sdp
|
|
||||||
})
|
|
||||||
await send_json(ws, {"type": "call_state", "callId": call_id, "state": "accepted"})
|
|
||||||
return True
|
|
||||||
|
|
||||||
if mtype == "call_reject":
|
|
||||||
call_id = msg.get("callId", "")
|
|
||||||
call = calls.get(call_id)
|
|
||||||
if call and peer_key(call["calleeId"]) == peer_id:
|
|
||||||
await push_to_peer(call["callerId"], {
|
|
||||||
"type": "call_state",
|
|
||||||
"callId": call_id,
|
|
||||||
"state": "rejected"
|
|
||||||
})
|
|
||||||
purge_call(call_id)
|
|
||||||
return True
|
|
||||||
|
|
||||||
if mtype == "call_end":
|
|
||||||
call_id = msg.get("callId", "")
|
|
||||||
call = calls.get(call_id)
|
|
||||||
if call and peer_id in (peer_key(call["callerId"]), peer_key(call["calleeId"])):
|
|
||||||
other = call["calleeId"] if peer_key(call["callerId"]) == peer_id else call["callerId"]
|
|
||||||
await push_to_peer(other, {
|
|
||||||
"type": "call_state",
|
|
||||||
"callId": call_id,
|
|
||||||
"state": "ended"
|
|
||||||
})
|
|
||||||
purge_call(call_id)
|
|
||||||
return True
|
|
||||||
|
|
||||||
if mtype == "ice_candidate":
|
|
||||||
call_id = msg.get("callId", "")
|
|
||||||
call = calls.get(call_id)
|
|
||||||
if not call:
|
|
||||||
return True
|
|
||||||
entry = {
|
|
||||||
"from": peer_id,
|
|
||||||
"candidate": msg.get("candidate"),
|
|
||||||
"sdpMid": msg.get("sdpMid"),
|
|
||||||
"sdpMLineIndex": msg.get("sdpMLineIndex"),
|
|
||||||
"at": time.time()
|
|
||||||
}
|
|
||||||
ice_queues.setdefault(call_id, []).append(entry)
|
|
||||||
other = call["calleeId"] if peer_key(call["callerId"]) == peer_id else call["callerId"]
|
|
||||||
await push_to_peer(other, {
|
|
||||||
"type": "ice_candidate",
|
|
||||||
"callId": call_id,
|
|
||||||
**{k: entry[k] for k in ("candidate", "sdpMid", "sdpMLineIndex")}
|
|
||||||
})
|
|
||||||
return True
|
|
||||||
|
|
||||||
if mtype == "ice_fetch":
|
|
||||||
call_id = msg.get("callId", "")
|
|
||||||
since = float(msg.get("since", 0))
|
|
||||||
queued = [e for e in ice_queues.get(call_id, []) if e.get("at", 0) > since and e.get("from") != peer_id]
|
|
||||||
await send_json(ws, {"type": "ice_candidates", "callId": call_id, "candidates": queued})
|
|
||||||
return True
|
|
||||||
|
|
||||||
if mtype == "chat_message":
|
|
||||||
target_id = peer_key(msg.get("targetId", ""))
|
|
||||||
if target_id:
|
|
||||||
await push_to_peer(target_id, {
|
|
||||||
"type": "chat_message",
|
|
||||||
"networkId": msg.get("networkId", ""),
|
|
||||||
"senderId": peer_id,
|
|
||||||
"senderName": msg.get("senderName", peer_id),
|
|
||||||
"body": msg.get("body", ""),
|
|
||||||
"at": int(time.time() * 1000)
|
|
||||||
})
|
|
||||||
return True
|
|
||||||
|
|
||||||
if mtype == "typing":
|
|
||||||
target_id = peer_key(msg.get("targetId", ""))
|
|
||||||
if target_id:
|
|
||||||
await push_to_peer(target_id, {
|
|
||||||
"type": "typing",
|
|
||||||
"networkId": msg.get("networkId", ""),
|
|
||||||
"senderId": peer_id,
|
|
||||||
"typing": bool(msg.get("typing", True))
|
|
||||||
})
|
|
||||||
return True
|
|
||||||
|
|
||||||
await send_json(ws, {"type": "error", "error": f"unknown type: {mtype}"})
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
async def connection_handler(ws, path):
|
|
||||||
state = {"authed": False, "peerId": None}
|
|
||||||
query = urllib.parse.parse_qs(urllib.parse.urlparse(path).query)
|
|
||||||
if "auth" in query and valid_token(query["auth"][0]):
|
|
||||||
state["authed"] = True
|
|
||||||
await send_json(ws, {"type": "auth_ok"})
|
|
||||||
|
|
||||||
try:
|
|
||||||
async for raw in ws:
|
|
||||||
try:
|
|
||||||
msg = json.loads(raw)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
await send_json(ws, {"type": "error", "error": "invalid json"})
|
|
||||||
continue
|
|
||||||
if not await handle_message(ws, state, msg):
|
|
||||||
break
|
|
||||||
finally:
|
|
||||||
pid = state.get("peerId")
|
|
||||||
if pid and peers.get(pid, {}).get("ws") is ws:
|
|
||||||
del peers[pid]
|
|
||||||
end_calls_for_peer(pid)
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
port = load_port()
|
|
||||||
host = "127.0.0.1"
|
|
||||||
asyncio.create_task(ring_timeout_watch())
|
|
||||||
async with serve(connection_handler, host, port, ping_interval=20, ping_timeout=20):
|
|
||||||
print(f"BackOne WebHub listening on ws://{host}:{port}", flush=True)
|
|
||||||
await asyncio.Future()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
try:
|
|
||||||
asyncio.run(main())
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
pass
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=BackOne WebSocket signaling hub (webhub)
|
|
||||||
After=network.target backone.service
|
|
||||||
Wants=backone.service
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
ExecStart=/usr/lib/backone/addons/webhub/backone-webhub
|
|
||||||
Restart=always
|
|
||||||
RestartSec=3
|
|
||||||
User=backone
|
|
||||||
Group=backone
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
Vendored
-57
@@ -1,57 +0,0 @@
|
|||||||
window.BackOneRingingPopup = {
|
|
||||||
_audio: null,
|
|
||||||
showIncomingCall({ callerName, callType, callId, onAccept, onReject }) {
|
|
||||||
const el = document.getElementById('ringing-popup');
|
|
||||||
if (!el) return;
|
|
||||||
el.classList.remove('ringing-hidden');
|
|
||||||
el.innerHTML = `
|
|
||||||
<div class="ringing-card">
|
|
||||||
<h2>Incoming ${callType} call</h2>
|
|
||||||
<p>${callerName || 'Unknown peer'}</p>
|
|
||||||
<button class="ringing-accept">Accept</button>
|
|
||||||
<button class="ringing-reject">Reject</button>
|
|
||||||
</div>`;
|
|
||||||
el.querySelector('.ringing-accept').onclick = () => { this.dismiss(); onAccept && onAccept(callId); };
|
|
||||||
el.querySelector('.ringing-reject').onclick = () => { this.dismiss(); onReject && onReject(callId); };
|
|
||||||
this.playRingtone();
|
|
||||||
if (Notification.permission === 'default') Notification.requestPermission();
|
|
||||||
if (Notification.permission === 'granted') {
|
|
||||||
new Notification(`Incoming ${callType} call`, { body: callerName || 'BackOne peer' });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dismiss() {
|
|
||||||
const el = document.getElementById('ringing-popup');
|
|
||||||
if (el) { el.classList.add('ringing-hidden'); el.innerHTML = ''; }
|
|
||||||
this.stopRingtone();
|
|
||||||
},
|
|
||||||
playRingtone() {
|
|
||||||
this.stopRingtone();
|
|
||||||
try {
|
|
||||||
this._audio = new Audio('data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmMcBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+DyvmwhBSuBzvLZiTYIG2m98OScTgwOUarm7blmHgU7k9n1unEiBC13y+/ek0EKElyx5+ytWBUIQ5zd8sFuJAUwgM/z2Ik6CBxpvfDknE4MDlGq5u25Zh4FO5PZ9br');
|
|
||||||
this._audio.loop = true;
|
|
||||||
this._audio.play().catch(() => {});
|
|
||||||
} catch (_) {}
|
|
||||||
},
|
|
||||||
stopRingtone() {
|
|
||||||
if (this._audio) { this._audio.pause(); this._audio = null; }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
async function loadAddonManifest() {
|
|
||||||
const res = await fetch('/app/addons/manifest.json', { cache: 'no-store' });
|
|
||||||
if (!res.ok) {
|
|
||||||
console.warn('manifest.json fetch failed:', res.status);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const data = await res.json();
|
|
||||||
return Array.isArray(data) ? data : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadAddonModule(entry) {
|
|
||||||
const entryPath = entry.frontend && entry.frontend.entry ? entry.frontend.entry : `/app/addons/${entry.id}/index.js`;
|
|
||||||
await import(entryPath);
|
|
||||||
const factory = window.BackOneAddons && window.BackOneAddons[entry.id];
|
|
||||||
return factory ? factory() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.BackOneAddonLoader = { loadAddonManifest, loadAddonModule };
|
|
||||||
Vendored
-195
@@ -1,195 +0,0 @@
|
|||||||
/**
|
|
||||||
* BackOne API auth — same token as backone-cli (~/.backOneOneAuthToken / X-ZT1-Auth).
|
|
||||||
*/
|
|
||||||
window.BackOneAuth = (function () {
|
|
||||||
const LS_KEY = 'backone.authToken';
|
|
||||||
let cache = null;
|
|
||||||
|
|
||||||
function trimToken(s) {
|
|
||||||
return String(s || '').replace(/\s+/g, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
function tokenFromUrl() {
|
|
||||||
try {
|
|
||||||
const params = new URLSearchParams(location.search);
|
|
||||||
const fromQuery = trimToken(params.get('auth') || params.get('token'));
|
|
||||||
if (fromQuery) return fromQuery;
|
|
||||||
const hash = (location.hash || '').replace(/^#\/?/, '');
|
|
||||||
const hashParams = new URLSearchParams(hash.includes('?') ? hash.split('?')[1] : '');
|
|
||||||
return trimToken(hashParams.get('auth') || hashParams.get('token'));
|
|
||||||
} catch (_) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function probeToken(token) {
|
|
||||||
if (!token) return false;
|
|
||||||
try {
|
|
||||||
const res = await fetch('/network', {
|
|
||||||
cache: 'no-store',
|
|
||||||
headers: { 'X-ZT1-Auth': token }
|
|
||||||
});
|
|
||||||
return res.ok;
|
|
||||||
} catch (_) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchAuthtoken() {
|
|
||||||
for (const path of ['/api/backone/authtoken', '/app/api/authtoken']) {
|
|
||||||
try {
|
|
||||||
const res = await fetch(path, { cache: 'no-store' });
|
|
||||||
if (res.ok) {
|
|
||||||
const data = await res.json();
|
|
||||||
const token = trimToken(data && data.token);
|
|
||||||
if (token) return token;
|
|
||||||
}
|
|
||||||
} catch (_) {
|
|
||||||
/* try next */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resolveToken() {
|
|
||||||
const urlToken = tokenFromUrl();
|
|
||||||
if (urlToken) {
|
|
||||||
if (await probeToken(urlToken)) {
|
|
||||||
return urlToken;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const stored = trimToken(localStorage.getItem(LS_KEY));
|
|
||||||
if (stored && await probeToken(stored)) {
|
|
||||||
return stored;
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiToken = await fetchAuthtoken();
|
|
||||||
if (apiToken && await probeToken(apiToken)) {
|
|
||||||
return apiToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stored) return stored;
|
|
||||||
if (apiToken) return apiToken;
|
|
||||||
if (urlToken) return urlToken;
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getToken() {
|
|
||||||
if (cache) return cache;
|
|
||||||
cache = await resolveToken();
|
|
||||||
return cache || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function authHeaders(extra) {
|
|
||||||
const token = await getToken();
|
|
||||||
const headers = Object.assign({}, extra || {});
|
|
||||||
if (token) {
|
|
||||||
headers['X-ZT1-Auth'] = token;
|
|
||||||
}
|
|
||||||
return headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchWithAuth(path, options) {
|
|
||||||
options = options || {};
|
|
||||||
const headers = await authHeaders(options.headers || {});
|
|
||||||
return fetch(path, Object.assign({}, options, { headers }));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function apiGet(path) {
|
|
||||||
const res = await fetchWithAuth(path, { cache: 'no-store' });
|
|
||||||
if (!res.ok) throw new Error(`${path} returned ${res.status}`);
|
|
||||||
return res.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function apiGetFirst(paths) {
|
|
||||||
let lastErr = null;
|
|
||||||
for (const path of paths) {
|
|
||||||
try {
|
|
||||||
return await apiGet(path);
|
|
||||||
} catch (err) {
|
|
||||||
lastErr = err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw lastErr || new Error('API request failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function apiSend(path, method, body) {
|
|
||||||
const headers = await authHeaders(
|
|
||||||
body != null ? { 'Content-Type': 'application/json' } : {}
|
|
||||||
);
|
|
||||||
const res = await fetch(path, {
|
|
||||||
method,
|
|
||||||
headers,
|
|
||||||
body: body != null ? JSON.stringify(body) : undefined
|
|
||||||
});
|
|
||||||
if (!res.ok) {
|
|
||||||
const text = await res.text().catch(() => '');
|
|
||||||
throw new Error(text || `${method} ${path} returned ${res.status}`);
|
|
||||||
}
|
|
||||||
if (res.status === 204) {
|
|
||||||
return { result: true };
|
|
||||||
}
|
|
||||||
const text = await res.text();
|
|
||||||
if (!text) {
|
|
||||||
return { result: true };
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return JSON.parse(text);
|
|
||||||
} catch (_) {
|
|
||||||
return { result: true };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function apiSendFirst(paths, method, body) {
|
|
||||||
let lastErr = null;
|
|
||||||
for (const path of paths) {
|
|
||||||
try {
|
|
||||||
return await apiSend(path, method, body);
|
|
||||||
} catch (err) {
|
|
||||||
lastErr = err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw lastErr || new Error(`${method} failed`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Store token from ~/.backOneOneAuthToken (same as backone-cli). */
|
|
||||||
async function setUserToken(token) {
|
|
||||||
cache = trimToken(token);
|
|
||||||
if (cache) {
|
|
||||||
localStorage.setItem(LS_KEY, cache);
|
|
||||||
} else {
|
|
||||||
localStorage.removeItem(LS_KEY);
|
|
||||||
}
|
|
||||||
return cache;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function validateCurrentToken() {
|
|
||||||
cache = null;
|
|
||||||
const token = await getToken();
|
|
||||||
if (!token) return false;
|
|
||||||
return probeToken(token);
|
|
||||||
}
|
|
||||||
|
|
||||||
function invalidate() {
|
|
||||||
cache = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function init() {
|
|
||||||
return getToken();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
getToken,
|
|
||||||
authHeaders,
|
|
||||||
fetchWithAuth,
|
|
||||||
apiGet,
|
|
||||||
apiGetFirst,
|
|
||||||
apiSend,
|
|
||||||
apiSendFirst,
|
|
||||||
setUserToken,
|
|
||||||
validateCurrentToken,
|
|
||||||
invalidate,
|
|
||||||
init
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
Vendored
-57
@@ -1,57 +0,0 @@
|
|||||||
window.BackOneContacts = (function () {
|
|
||||||
let cache = null;
|
|
||||||
|
|
||||||
async function load() {
|
|
||||||
if (cache) return cache;
|
|
||||||
try {
|
|
||||||
const res = await BackOneAuth.fetchWithAuth('/api/backone/contacts', { cache: 'no-store' })
|
|
||||||
.catch(() => BackOneAuth.fetchWithAuth('/app/api/contacts', { cache: 'no-store' }));
|
|
||||||
if (!res.ok) {
|
|
||||||
cache = {};
|
|
||||||
return cache;
|
|
||||||
}
|
|
||||||
cache = await res.json();
|
|
||||||
if (!cache || typeof cache !== 'object') cache = {};
|
|
||||||
} catch (_) {
|
|
||||||
cache = {};
|
|
||||||
}
|
|
||||||
return cache;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDisplayName(peerId, controllerName) {
|
|
||||||
const c = cache || {};
|
|
||||||
const entry = c[peerId];
|
|
||||||
if (entry && entry.nickname) return entry.nickname;
|
|
||||||
if (controllerName) return controllerName;
|
|
||||||
return peerId;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNickname(peerId) {
|
|
||||||
const entry = (cache || {})[peerId];
|
|
||||||
return entry && entry.nickname ? entry.nickname : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setNickname(peerId, nickname) {
|
|
||||||
await load();
|
|
||||||
const patch = {};
|
|
||||||
patch[peerId] = { nickname: nickname || '' };
|
|
||||||
const res = await BackOneAuth.fetchWithAuth('/api/backone/contacts', {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(patch)
|
|
||||||
}).catch(() => BackOneAuth.fetchWithAuth('/app/api/contacts', {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(patch)
|
|
||||||
}));
|
|
||||||
if (!res.ok) throw new Error('Failed to save nickname');
|
|
||||||
cache = await res.json();
|
|
||||||
return cache;
|
|
||||||
}
|
|
||||||
|
|
||||||
function invalidate() {
|
|
||||||
cache = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { load, getDisplayName, getNickname, setNickname, invalidate };
|
|
||||||
})();
|
|
||||||
Vendored
-45
@@ -1,45 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>BackOne</title>
|
|
||||||
<link rel="stylesheet" href="/app/styles.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header class="app-header">
|
|
||||||
<div class="app-brand">
|
|
||||||
<h1>BackOne</h1>
|
|
||||||
<span id="core-status-badge" class="core-badge offline">Offline</span>
|
|
||||||
</div>
|
|
||||||
<div id="network-bar" class="network-bar">
|
|
||||||
<label class="network-bar-label">Network
|
|
||||||
<select id="global-network"></select>
|
|
||||||
</label>
|
|
||||||
<input id="global-join-id" type="text" maxlength="16" placeholder="Join network ID" autocomplete="off" />
|
|
||||||
<button type="button" id="global-join-btn">Join</button>
|
|
||||||
<button type="button" id="global-leave-btn">Leave</button>
|
|
||||||
<span id="global-network-status" class="network-bar-status" aria-live="polite"></span>
|
|
||||||
<div id="auth-token-bar" class="auth-token-bar auth-token-hidden">
|
|
||||||
<label class="auth-token-label">Auth token
|
|
||||||
<input id="auth-token-input" type="password" autocomplete="off"
|
|
||||||
placeholder="Paste from ~/.backOneOneAuthToken" />
|
|
||||||
</label>
|
|
||||||
<button type="button" id="auth-token-save">Save</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<nav id="addon-nav" class="addon-nav"></nav>
|
|
||||||
</header>
|
|
||||||
<main id="addon-panel" class="addon-panel"></main>
|
|
||||||
<div id="ringing-popup" class="ringing-hidden"></div>
|
|
||||||
<script src="/app/auth-store.js"></script>
|
|
||||||
<script src="/app/contacts-store.js"></script>
|
|
||||||
<script src="/app/peer-directory.js"></script>
|
|
||||||
<script src="/app/network-session.js"></script>
|
|
||||||
<script src="/app/webhub-client.js"></script>
|
|
||||||
<script src="/app/webrtc-session.js"></script>
|
|
||||||
<script src="/app/peer-picker.js"></script>
|
|
||||||
<script src="/app/addon-loader.js"></script>
|
|
||||||
<script src="/app/main.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Vendored
-265
@@ -1,265 +0,0 @@
|
|||||||
window.BackOneAddons = window.BackOneAddons || {};
|
|
||||||
|
|
||||||
const addonButtons = {};
|
|
||||||
let addonModules = {};
|
|
||||||
let coreRefreshTimer = null;
|
|
||||||
|
|
||||||
function hashAddonId() {
|
|
||||||
const h = (location.hash || '').replace(/^#\/?/, '').toLowerCase();
|
|
||||||
if (h === 'voice' || h === 'video' || h === 'chat') return h;
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function activateAddon(id) {
|
|
||||||
const btn = addonButtons[id];
|
|
||||||
if (!btn) return;
|
|
||||||
btn.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeHtml(s) {
|
|
||||||
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
||||||
}
|
|
||||||
|
|
||||||
function networkLabel(n) {
|
|
||||||
if (!n) return '';
|
|
||||||
return n.name ? `${n.name} (${n.id})` : n.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateCoreBadge(statusEl) {
|
|
||||||
const badge = document.getElementById('core-status-badge');
|
|
||||||
const core = BackOneNetworkSession.getCoreStatus();
|
|
||||||
if (badge) {
|
|
||||||
badge.textContent = core.online ? 'Online' : 'Offline';
|
|
||||||
badge.classList.toggle('online', core.online);
|
|
||||||
badge.classList.toggle('offline', !core.online);
|
|
||||||
}
|
|
||||||
if (!statusEl) return;
|
|
||||||
if (!core.online) {
|
|
||||||
statusEl.textContent = 'Start the backone service to use networks and peers';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const net = BackOneNetworkSession.getActiveNetwork();
|
|
||||||
const netPart = net ? networkLabel(net) : 'no network joined';
|
|
||||||
statusEl.textContent = `${netPart} · ${core.leafPeerCount} peer(s)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function initAuthTokenBar(statusEl, onSaved) {
|
|
||||||
const bar = document.getElementById('auth-token-bar');
|
|
||||||
const input = document.getElementById('auth-token-input');
|
|
||||||
const saveBtn = document.getElementById('auth-token-save');
|
|
||||||
if (!bar || !input || !saveBtn) return;
|
|
||||||
|
|
||||||
async function refreshAuthUi() {
|
|
||||||
const ok = await BackOneAuth.validateCurrentToken();
|
|
||||||
bar.classList.toggle('auth-token-hidden', ok);
|
|
||||||
if (!ok && !input.value) {
|
|
||||||
input.placeholder = 'Paste from ~/.backOneOneAuthToken';
|
|
||||||
}
|
|
||||||
return ok;
|
|
||||||
}
|
|
||||||
|
|
||||||
saveBtn.addEventListener('click', async () => {
|
|
||||||
const token = input.value.trim();
|
|
||||||
if (!token) {
|
|
||||||
if (statusEl) statusEl.textContent = 'Paste the token from ~/.backOneOneAuthToken';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
saveBtn.disabled = true;
|
|
||||||
await BackOneAuth.setUserToken(token);
|
|
||||||
BackOneAuth.invalidate();
|
|
||||||
const ok = await BackOneAuth.validateCurrentToken();
|
|
||||||
if (!ok) {
|
|
||||||
if (statusEl) statusEl.textContent = 'Token rejected — check ~/.backOneOneAuthToken';
|
|
||||||
saveBtn.disabled = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
input.value = '';
|
|
||||||
bar.classList.add('auth-token-hidden');
|
|
||||||
if (typeof onSaved === 'function') await onSaved();
|
|
||||||
saveBtn.disabled = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
await refreshAuthUi();
|
|
||||||
return refreshAuthUi;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function initNetworkBar() {
|
|
||||||
const bar = document.getElementById('network-bar');
|
|
||||||
if (!bar) return;
|
|
||||||
|
|
||||||
const netSel = bar.querySelector('#global-network');
|
|
||||||
const joinInput = bar.querySelector('#global-join-id');
|
|
||||||
const joinBtn = bar.querySelector('#global-join-btn');
|
|
||||||
const leaveBtn = bar.querySelector('#global-leave-btn');
|
|
||||||
const statusEl = bar.querySelector('#global-network-status');
|
|
||||||
|
|
||||||
async function reloadNetworkList(selectId) {
|
|
||||||
await BackOneNetworkSession.syncFromCore();
|
|
||||||
const networks = await BackOneNetworkSession.loadNetworks();
|
|
||||||
netSel.innerHTML = networks.map(n =>
|
|
||||||
`<option value="${n.id}">${escapeHtml(networkLabel(n))} · ${escapeHtml(n.status || 'OK')}</option>`
|
|
||||||
).join('') || '<option value="">— no networks —</option>';
|
|
||||||
|
|
||||||
const want = (selectId || BackOneNetworkSession.getNetworkId() || '').toLowerCase();
|
|
||||||
if (want && networks.some(n => (n.id || '').toLowerCase() === want)) {
|
|
||||||
netSel.value = networks.find(n => (n.id || '').toLowerCase() === want).id;
|
|
||||||
await BackOneNetworkSession.selectNetwork(netSel.value);
|
|
||||||
} else if (networks.length === 1) {
|
|
||||||
netSel.value = networks[0].id;
|
|
||||||
await BackOneNetworkSession.selectNetwork(networks[0].id);
|
|
||||||
} else if (networks.length > 0 && BackOneNetworkSession.getNetworkId()) {
|
|
||||||
netSel.value = BackOneNetworkSession.getNetworkId();
|
|
||||||
}
|
|
||||||
updateCoreBadge(statusEl);
|
|
||||||
}
|
|
||||||
|
|
||||||
netSel.addEventListener('change', async () => {
|
|
||||||
if (!netSel.value) return;
|
|
||||||
statusEl.textContent = 'Switching network…';
|
|
||||||
await BackOneNetworkSession.selectNetwork(netSel.value);
|
|
||||||
await BackOneNetworkSession.syncFromCore();
|
|
||||||
updateCoreBadge(statusEl);
|
|
||||||
});
|
|
||||||
|
|
||||||
joinBtn.addEventListener('click', async () => {
|
|
||||||
const id = BackOnePeerDirectory.normalizeNetworkId(joinInput.value);
|
|
||||||
if (!id) {
|
|
||||||
statusEl.textContent = 'Enter a 16-digit hexadecimal network ID';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
joinBtn.disabled = true;
|
|
||||||
leaveBtn.disabled = true;
|
|
||||||
statusEl.textContent = `Joining ${id}…`;
|
|
||||||
try {
|
|
||||||
await BackOneNetworkSession.joinNetwork(id);
|
|
||||||
joinInput.value = '';
|
|
||||||
await reloadNetworkList(id);
|
|
||||||
} catch (err) {
|
|
||||||
statusEl.textContent = err.message || 'Join failed';
|
|
||||||
} finally {
|
|
||||||
joinBtn.disabled = false;
|
|
||||||
leaveBtn.disabled = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
leaveBtn.addEventListener('click', async () => {
|
|
||||||
const id = netSel.value || BackOneNetworkSession.getNetworkId();
|
|
||||||
if (!id) {
|
|
||||||
statusEl.textContent = 'Select a network to leave';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!confirm(`Leave network ${networkLabel(BackOneNetworkSession.getActiveNetwork() || { id })}?`)) return;
|
|
||||||
joinBtn.disabled = true;
|
|
||||||
leaveBtn.disabled = true;
|
|
||||||
statusEl.textContent = `Leaving ${id}…`;
|
|
||||||
try {
|
|
||||||
await BackOneNetworkSession.leaveNetwork(id);
|
|
||||||
await reloadNetworkList();
|
|
||||||
} catch (err) {
|
|
||||||
statusEl.textContent = err.message || 'Leave failed';
|
|
||||||
} finally {
|
|
||||||
joinBtn.disabled = false;
|
|
||||||
leaveBtn.disabled = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
BackOneNetworkSession.subscribe(() => updateCoreBadge(statusEl));
|
|
||||||
|
|
||||||
const refreshAuthUi = await initAuthTokenBar(statusEl, () => reloadNetworkList());
|
|
||||||
|
|
||||||
await reloadNetworkList();
|
|
||||||
if (refreshAuthUi) await refreshAuthUi();
|
|
||||||
|
|
||||||
coreRefreshTimer = setInterval(() => {
|
|
||||||
BackOneNetworkSession.syncFromCore().then(() => updateCoreBadge(statusEl));
|
|
||||||
}, 45000);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function initCommsStack() {
|
|
||||||
await BackOneWebHubClient.connect();
|
|
||||||
await BackOneWebRtcSession.init();
|
|
||||||
|
|
||||||
BackOneWebHubClient.subscribe('incoming_call', (msg) => {
|
|
||||||
if (BackOneWebRtcSession.isInCall()) return;
|
|
||||||
BackOneRingingPopup.showIncomingCall({
|
|
||||||
callerName: msg.callerName || msg.callerId,
|
|
||||||
callType: msg.callType || 'voice',
|
|
||||||
callId: msg.callId,
|
|
||||||
onAccept: async () => {
|
|
||||||
const type = msg.callType || 'voice';
|
|
||||||
const addonId = type === 'video' ? 'video' : 'voice';
|
|
||||||
if (addonButtons[addonId]) activateAddon(addonId);
|
|
||||||
try {
|
|
||||||
await BackOneWebRtcSession.acceptIncomingCall(msg);
|
|
||||||
} catch (err) {
|
|
||||||
alert(err.message || 'Could not accept call');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onReject: (callId) => {
|
|
||||||
BackOneWebRtcSession.rejectIncomingCall(callId || msg.callId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function initAppShell() {
|
|
||||||
await BackOneAuth.init();
|
|
||||||
await initNetworkBar();
|
|
||||||
await initCommsStack();
|
|
||||||
|
|
||||||
const nav = document.getElementById('addon-nav');
|
|
||||||
const panel = document.getElementById('addon-panel');
|
|
||||||
const addons = await BackOneAddonLoader.loadAddonManifest();
|
|
||||||
if (!addons.length) {
|
|
||||||
panel.innerHTML = '<p>No UI addons found. Install backone-addon-secure-chat, backone-addon-voice, backone-addon-video, or backone-addon-chat, then run: <code>sudo backone-updater rebuild-manifest</code> and restart backone.</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const navOrder = ['secure-chat', 'chat', 'video', 'voice'];
|
|
||||||
const sorted = [...addons].sort((a, b) => {
|
|
||||||
const ia = navOrder.indexOf(a.id);
|
|
||||||
const ib = navOrder.indexOf(b.id);
|
|
||||||
return (ia < 0 ? 99 : ia) - (ib < 0 ? 99 : ib);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const entry of sorted) {
|
|
||||||
const mod = await BackOneAddonLoader.loadAddonModule(entry);
|
|
||||||
if (mod) addonModules[entry.id] = mod;
|
|
||||||
const btn = document.createElement('button');
|
|
||||||
btn.dataset.addonId = entry.id;
|
|
||||||
btn.textContent = (entry.frontend && entry.frontend.navLabel) || entry.name || entry.id;
|
|
||||||
btn.onclick = () => {
|
|
||||||
nav.querySelectorAll('button').forEach(b => b.classList.remove('active'));
|
|
||||||
btn.classList.add('active');
|
|
||||||
panel.innerHTML = '';
|
|
||||||
location.hash = entry.id;
|
|
||||||
if (addonModules[entry.id] && addonModules[entry.id].mount) {
|
|
||||||
addonModules[entry.id].mount(panel);
|
|
||||||
} else {
|
|
||||||
panel.innerHTML = `<p>Addon "${entry.id}" loaded.</p>`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
addonButtons[entry.id] = btn;
|
|
||||||
nav.appendChild(btn);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fromHash = hashAddonId();
|
|
||||||
if (fromHash && addonButtons[fromHash]) {
|
|
||||||
activateAddon(fromHash);
|
|
||||||
} else {
|
|
||||||
nav.querySelector('button')?.click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('hashchange', () => {
|
|
||||||
const id = hashAddonId();
|
|
||||||
if (id) activateAddon(id);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener('visibilitychange', () => {
|
|
||||||
if (document.visibilityState === 'visible') {
|
|
||||||
BackOneNetworkSession.syncFromCore();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', initAppShell);
|
|
||||||
Vendored
-178
@@ -1,178 +0,0 @@
|
|||||||
/**
|
|
||||||
* Single BackOne core network for all addons (same as backone-cli listnetworks/join).
|
|
||||||
*/
|
|
||||||
window.BackOneNetworkSession = (function () {
|
|
||||||
const NETWORK_KEY = 'backone.lastNetworkId';
|
|
||||||
const PEER_KEY = 'backone.lastPeerId';
|
|
||||||
|
|
||||||
let activeNetwork = null;
|
|
||||||
let selectedPeer = null;
|
|
||||||
let coreStatus = { online: false, address: '', networkCount: 0, leafPeerCount: 0, authenticated: false };
|
|
||||||
const listeners = new Set();
|
|
||||||
|
|
||||||
function emit() {
|
|
||||||
const snap = {
|
|
||||||
network: activeNetwork,
|
|
||||||
networkId: getNetworkId(),
|
|
||||||
peer: selectedPeer,
|
|
||||||
core: { ...coreStatus }
|
|
||||||
};
|
|
||||||
listeners.forEach(fn => {
|
|
||||||
try { fn(snap); } catch (_) { /* ignore */ }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNetworkId() {
|
|
||||||
return (localStorage.getItem(NETWORK_KEY) || '').toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
function setNetworkId(id) {
|
|
||||||
const nwid = BackOnePeerDirectory.normalizeNetworkId(id);
|
|
||||||
if (nwid) {
|
|
||||||
localStorage.setItem(NETWORK_KEY, nwid);
|
|
||||||
} else {
|
|
||||||
localStorage.removeItem(NETWORK_KEY);
|
|
||||||
}
|
|
||||||
BackOnePeerDirectory.setLastNetworkId(nwid);
|
|
||||||
return nwid;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSelectedPeerId() {
|
|
||||||
return (localStorage.getItem(PEER_KEY) || '').toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
function setSelectedPeer(peer) {
|
|
||||||
selectedPeer = peer || null;
|
|
||||||
if (peer && peer.peerId) {
|
|
||||||
localStorage.setItem(PEER_KEY, peer.peerId);
|
|
||||||
} else {
|
|
||||||
localStorage.removeItem(PEER_KEY);
|
|
||||||
}
|
|
||||||
emit();
|
|
||||||
}
|
|
||||||
|
|
||||||
function pickDefaultNetworkId(networks) {
|
|
||||||
if (!networks.length) return '';
|
|
||||||
const stored = getNetworkId();
|
|
||||||
if (stored && networks.some(n => (n.id || '').toLowerCase() === stored)) {
|
|
||||||
return stored;
|
|
||||||
}
|
|
||||||
const preferred = networks.find(n => n.status === 'OK')
|
|
||||||
|| networks.find(n => n.status === 'REQUESTING_CONFIGURATION')
|
|
||||||
|| networks[0];
|
|
||||||
return preferred ? (preferred.id || '').toLowerCase() : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* On app start: read BackOne core state (same as backone-cli info / listnetworks / peers).
|
|
||||||
*/
|
|
||||||
async function syncFromCore() {
|
|
||||||
const token = await BackOneAuth.getToken();
|
|
||||||
const [status, networks, cliPeers] = await Promise.all([
|
|
||||||
BackOnePeerDirectory.loadCoreStatus(),
|
|
||||||
BackOnePeerDirectory.loadNetworks(),
|
|
||||||
BackOnePeerDirectory.loadCliPeers()
|
|
||||||
]);
|
|
||||||
|
|
||||||
coreStatus = {
|
|
||||||
online: !!(status && status.online),
|
|
||||||
address: (status && status.address) || '',
|
|
||||||
version: (status && status.version) || '',
|
|
||||||
networkCount: networks.length,
|
|
||||||
leafPeerCount: BackOnePeerDirectory.countLeafPeers(cliPeers),
|
|
||||||
authenticated: !!token
|
|
||||||
};
|
|
||||||
|
|
||||||
const pick = pickDefaultNetworkId(networks);
|
|
||||||
if (pick) {
|
|
||||||
setNetworkId(pick);
|
|
||||||
activeNetwork = networks.find(n => (n.id || '').toLowerCase() === pick)
|
|
||||||
|| await BackOnePeerDirectory.loadNetwork(pick);
|
|
||||||
} else {
|
|
||||||
setNetworkId('');
|
|
||||||
activeNetwork = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
emit();
|
|
||||||
return { coreStatus, activeNetwork, networks };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refresh() {
|
|
||||||
const id = getNetworkId();
|
|
||||||
if (!id) {
|
|
||||||
activeNetwork = null;
|
|
||||||
emit();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
activeNetwork = await BackOnePeerDirectory.loadNetwork(id);
|
|
||||||
if (!activeNetwork) {
|
|
||||||
activeNetwork = { id, name: id, status: 'UNKNOWN' };
|
|
||||||
}
|
|
||||||
emit();
|
|
||||||
return activeNetwork;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadNetworks() {
|
|
||||||
return BackOnePeerDirectory.loadNetworks();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function selectNetwork(nwid) {
|
|
||||||
const id = setNetworkId(nwid);
|
|
||||||
if (!id) {
|
|
||||||
await refresh();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function joinNetwork(nwid) {
|
|
||||||
const id = BackOnePeerDirectory.normalizeNetworkId(nwid);
|
|
||||||
if (!id) throw new Error('Network ID must be 16 hexadecimal characters');
|
|
||||||
await BackOnePeerDirectory.joinNetwork(id);
|
|
||||||
return syncFromCore();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function leaveNetwork(nwid) {
|
|
||||||
const id = BackOnePeerDirectory.normalizeNetworkId(nwid || getNetworkId());
|
|
||||||
if (!id) throw new Error('No network selected');
|
|
||||||
await BackOnePeerDirectory.leaveNetwork(id);
|
|
||||||
if (getNetworkId() === id) {
|
|
||||||
setNetworkId('');
|
|
||||||
selectedPeer = null;
|
|
||||||
localStorage.removeItem(PEER_KEY);
|
|
||||||
}
|
|
||||||
return syncFromCore();
|
|
||||||
}
|
|
||||||
|
|
||||||
function subscribe(fn) {
|
|
||||||
listeners.add(fn);
|
|
||||||
return () => listeners.delete(fn);
|
|
||||||
}
|
|
||||||
|
|
||||||
function requireNetworkId() {
|
|
||||||
const id = getNetworkId();
|
|
||||||
if (!id) throw new Error('Join a BackOne network first (same as backone-cli join)');
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCoreStatus() {
|
|
||||||
return { ...coreStatus };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
getNetworkId,
|
|
||||||
getActiveNetwork: () => activeNetwork,
|
|
||||||
getCoreStatus,
|
|
||||||
getSelectedPeer: () => selectedPeer,
|
|
||||||
getSelectedPeerId,
|
|
||||||
setSelectedPeer,
|
|
||||||
selectNetwork,
|
|
||||||
joinNetwork,
|
|
||||||
leaveNetwork,
|
|
||||||
loadNetworks,
|
|
||||||
syncFromCore,
|
|
||||||
refresh,
|
|
||||||
subscribe,
|
|
||||||
requireNetworkId
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
Vendored
-266
@@ -1,266 +0,0 @@
|
|||||||
window.BackOnePeerDirectory = (function () {
|
|
||||||
const NETWORK_KEY = 'backone.lastNetworkId';
|
|
||||||
const API = {
|
|
||||||
peers: ['/api/backone/peers', '/app/api/peers', '/peer'],
|
|
||||||
networks: ['/api/backone/networks', '/app/api/networks', '/network'],
|
|
||||||
status: ['/api/backone/status', '/app/api/status', '/status'],
|
|
||||||
network: (id) => [
|
|
||||||
`/api/backone/network/${id}`,
|
|
||||||
`/app/api/network/${id}`,
|
|
||||||
`/network/${id}`
|
|
||||||
],
|
|
||||||
members: (id) => [
|
|
||||||
`/api/backone/network/${id}/members`,
|
|
||||||
`/app/api/network/${id}/members`,
|
|
||||||
`/unstable/controller/network/${id}/member`
|
|
||||||
],
|
|
||||||
join: (id) => [
|
|
||||||
`/api/backone/network/${id}`,
|
|
||||||
`/app/api/network/${id}`,
|
|
||||||
`/network/${id}`
|
|
||||||
],
|
|
||||||
leave: (id) => [
|
|
||||||
`/api/backone/network/${id}`,
|
|
||||||
`/app/api/network/${id}`,
|
|
||||||
`/network/${id}`
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
function apiGet(path) {
|
|
||||||
return BackOneAuth.apiGet(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Same data as `backone-cli -j peers` (GET /peer). */
|
|
||||||
async function loadCliPeers() {
|
|
||||||
try {
|
|
||||||
const peers = await BackOneAuth.apiGetFirst(API.peers);
|
|
||||||
return Array.isArray(peers) ? peers : [];
|
|
||||||
} catch (_) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Same data as `backone-cli -j listnetworks` (GET /network). */
|
|
||||||
async function loadNetworks() {
|
|
||||||
try {
|
|
||||||
const nets = await BackOneAuth.apiGetFirst(API.networks);
|
|
||||||
return Array.isArray(nets) ? nets : [];
|
|
||||||
} catch (_) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Same data as `backone-cli -j info` (GET /status). */
|
|
||||||
async function loadCoreStatus() {
|
|
||||||
try {
|
|
||||||
return await BackOneAuth.apiGetFirst(API.status);
|
|
||||||
} catch (_) {
|
|
||||||
try {
|
|
||||||
const res = await fetch('/health', { cache: 'no-store' });
|
|
||||||
return { online: res.ok };
|
|
||||||
} catch (_2) {
|
|
||||||
return { online: false };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function countLeafPeers(cliPeers) {
|
|
||||||
if (!Array.isArray(cliPeers)) return 0;
|
|
||||||
return cliPeers.filter(p => !p.role || p.role === 'LEAF').length;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadNetwork(nwid) {
|
|
||||||
if (!nwid) return null;
|
|
||||||
const networks = await loadNetworks();
|
|
||||||
const found = networks.find(n => n.id === nwid);
|
|
||||||
if (found) return found;
|
|
||||||
try {
|
|
||||||
return await BackOneAuth.apiGetFirst(API.network(nwid));
|
|
||||||
} catch (_) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLastNetworkId() {
|
|
||||||
return localStorage.getItem(NETWORK_KEY) || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLastNetworkId(nwid) {
|
|
||||||
if (nwid) localStorage.setItem(NETWORK_KEY, nwid);
|
|
||||||
}
|
|
||||||
|
|
||||||
function peerIsOnline(p) {
|
|
||||||
if (!p) return false;
|
|
||||||
const paths = p.paths || [];
|
|
||||||
return paths.some(x => x.active && !x.expired);
|
|
||||||
}
|
|
||||||
|
|
||||||
function peerLinkLabel(p) {
|
|
||||||
if (!p) return '';
|
|
||||||
if (p.tunneled) return 'RELAY';
|
|
||||||
const paths = p.paths || [];
|
|
||||||
for (const path of paths) {
|
|
||||||
if (path.preferred && path.active && !path.expired) {
|
|
||||||
return 'DIRECT';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const path of paths) {
|
|
||||||
if (path.active && !path.expired) {
|
|
||||||
return 'DIRECT';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 'RELAY';
|
|
||||||
}
|
|
||||||
|
|
||||||
function peerPreferredPath(p) {
|
|
||||||
if (!p) return '';
|
|
||||||
const paths = p.paths || [];
|
|
||||||
for (const path of paths) {
|
|
||||||
if (path.preferred && path.address) return path.address;
|
|
||||||
}
|
|
||||||
for (const path of paths) {
|
|
||||||
if (path.active && path.address) return path.address;
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function cliPeerMap(cliPeers) {
|
|
||||||
const map = {};
|
|
||||||
for (const p of cliPeers) {
|
|
||||||
const id = (p.address || '').toLowerCase();
|
|
||||||
if (id) map[id] = p;
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadMemberMap(nwid) {
|
|
||||||
if (!nwid) return {};
|
|
||||||
try {
|
|
||||||
const data = await BackOneAuth.apiGetFirst(API.members(nwid));
|
|
||||||
const map = {};
|
|
||||||
if (data && Array.isArray(data.data)) {
|
|
||||||
for (const m of data.data) {
|
|
||||||
const id = (m.id || '').toLowerCase();
|
|
||||||
if (id) map[id] = m;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
} catch (_) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildPeerEntry(peerId, member, cliPeer, networkId, networkName) {
|
|
||||||
const controllerName = member ? (member.name || '') : '';
|
|
||||||
const ips = member && member.ipAssignments ? member.ipAssignments : [];
|
|
||||||
return {
|
|
||||||
peerId,
|
|
||||||
networkId: networkId || '',
|
|
||||||
networkName: networkName || '',
|
|
||||||
displayName: BackOneContacts.getDisplayName(peerId, controllerName),
|
|
||||||
controllerName,
|
|
||||||
nickname: BackOneContacts.getNickname(peerId),
|
|
||||||
online: peerIsOnline(cliPeer),
|
|
||||||
latency: cliPeer && typeof cliPeer.latency === 'number' ? cliPeer.latency : null,
|
|
||||||
role: (cliPeer && cliPeer.role) || (member ? 'LEAF' : ''),
|
|
||||||
version: (cliPeer && cliPeer.version) || '',
|
|
||||||
link: cliPeer ? peerLinkLabel(cliPeer) : '',
|
|
||||||
path: cliPeer ? peerPreferredPath(cliPeer) : '',
|
|
||||||
ip: ips.length ? ips[0] : '',
|
|
||||||
onNetwork: !!member
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadPeersForNetwork(nwid) {
|
|
||||||
if (!nwid) return [];
|
|
||||||
|
|
||||||
const [cliPeers, members, selfStatus, network] = await Promise.all([
|
|
||||||
loadCliPeers(),
|
|
||||||
loadMemberMap(nwid),
|
|
||||||
loadCoreStatus().catch(() => ({})),
|
|
||||||
loadNetwork(nwid)
|
|
||||||
]);
|
|
||||||
await BackOneContacts.load();
|
|
||||||
|
|
||||||
const selfId = ((selfStatus && selfStatus.address) || '').toLowerCase();
|
|
||||||
const networkId = nwid;
|
|
||||||
const networkName = network ? (network.name || network.id || nwid) : nwid;
|
|
||||||
const cliById = cliPeerMap(cliPeers);
|
|
||||||
const out = [];
|
|
||||||
|
|
||||||
if (Object.keys(members).length > 0) {
|
|
||||||
for (const peerId of Object.keys(members)) {
|
|
||||||
const member = members[peerId];
|
|
||||||
if (!member || member.authorized === false) continue;
|
|
||||||
if (peerId === selfId) continue;
|
|
||||||
out.push(buildPeerEntry(peerId, member, cliById[peerId], networkId, networkName));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (const peerId of Object.keys(cliById)) {
|
|
||||||
const p = cliById[peerId];
|
|
||||||
if (!peerId || peerId.length !== 10 || peerId === selfId) continue;
|
|
||||||
if (p.role && p.role !== 'LEAF') continue;
|
|
||||||
out.push(buildPeerEntry(peerId, null, p, networkId, networkName));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
out.sort((a, b) => {
|
|
||||||
if (a.online !== b.online) return a.online ? -1 : 1;
|
|
||||||
return a.displayName.localeCompare(b.displayName);
|
|
||||||
});
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
function searchPeers(query, list) {
|
|
||||||
const q = (query || '').trim().toLowerCase();
|
|
||||||
if (!q) return list;
|
|
||||||
return list.filter(p =>
|
|
||||||
p.displayName.toLowerCase().includes(q) ||
|
|
||||||
p.peerId.includes(q) ||
|
|
||||||
(p.networkName || '').toLowerCase().includes(q) ||
|
|
||||||
(p.networkId || '').includes(q) ||
|
|
||||||
(p.controllerName || '').toLowerCase().includes(q) ||
|
|
||||||
(p.role || '').toLowerCase().includes(q) ||
|
|
||||||
(p.path || '').toLowerCase().includes(q) ||
|
|
||||||
(p.ip || '').toLowerCase().includes(q)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeNetworkId(input) {
|
|
||||||
const id = String(input || '').replace(/[^0-9a-fA-F]/g, '').toLowerCase();
|
|
||||||
return id.length === 16 ? id : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function joinNetwork(nwid) {
|
|
||||||
const id = normalizeNetworkId(nwid);
|
|
||||||
if (!id) {
|
|
||||||
throw new Error('Network ID must be 16 hexadecimal characters');
|
|
||||||
}
|
|
||||||
return BackOneAuth.apiSendFirst(API.join(id), 'POST', {});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function leaveNetwork(nwid) {
|
|
||||||
const id = normalizeNetworkId(nwid);
|
|
||||||
if (!id) {
|
|
||||||
throw new Error('Network ID must be 16 hexadecimal characters');
|
|
||||||
}
|
|
||||||
return BackOneAuth.apiSendFirst(API.leave(id), 'DELETE');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
loadCliPeers,
|
|
||||||
loadNetworks,
|
|
||||||
loadNetwork,
|
|
||||||
loadCoreStatus,
|
|
||||||
countLeafPeers,
|
|
||||||
loadPeersForNetwork,
|
|
||||||
getLastNetworkId,
|
|
||||||
setLastNetworkId,
|
|
||||||
joinNetwork,
|
|
||||||
leaveNetwork,
|
|
||||||
normalizeNetworkId,
|
|
||||||
searchPeers,
|
|
||||||
peerIsOnline,
|
|
||||||
peerLinkLabel
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
Vendored
-193
@@ -1,193 +0,0 @@
|
|||||||
window.BackOnePeerPicker = function PeerPicker(container, options) {
|
|
||||||
options = options || {};
|
|
||||||
const onSelect = options.onSelect || (() => {});
|
|
||||||
const onNetworkChange = options.onNetworkChange || (() => {});
|
|
||||||
const onAction = options.onAction || null;
|
|
||||||
const actionLabel = options.actionLabel || 'Select';
|
|
||||||
const showActionButton = options.showActionButton !== false;
|
|
||||||
const useSharedNetwork = options.useSharedNetwork !== false;
|
|
||||||
const layout = options.layout || 'default';
|
|
||||||
|
|
||||||
let selectedPeer = null;
|
|
||||||
let peers = [];
|
|
||||||
let currentNetwork = null;
|
|
||||||
let unsub = null;
|
|
||||||
|
|
||||||
if (layout === 'sidebar') {
|
|
||||||
container.innerHTML = `
|
|
||||||
<div class="peer-picker">
|
|
||||||
<div id="pp-network-banner" class="peer-info-box"></div>
|
|
||||||
<label class="peer-picker-row">
|
|
||||||
<input id="pp-search" type="search" placeholder="Name or peer ID" />
|
|
||||||
</label>
|
|
||||||
<ul id="pp-list" class="peer-list"></ul>
|
|
||||||
</div>`;
|
|
||||||
} else {
|
|
||||||
container.innerHTML = `
|
|
||||||
<div class="peer-picker">
|
|
||||||
<div class="peer-picker-core">BackOne core · same network as <code>backone-cli</code></div>
|
|
||||||
<div id="pp-network-banner" class="peer-network-banner"></div>
|
|
||||||
<label class="peer-picker-row">Search
|
|
||||||
<input id="pp-search" type="search" placeholder="Name or peer ID" />
|
|
||||||
</label>
|
|
||||||
<ul id="pp-list" class="peer-list"></ul>
|
|
||||||
<div id="pp-selected" class="peer-selected"></div>
|
|
||||||
${showActionButton ? `<button id="pp-action" type="button" disabled>${actionLabel}</button>` : ''}
|
|
||||||
</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const netBanner = container.querySelector('#pp-network-banner');
|
|
||||||
const searchInput = container.querySelector('#pp-search');
|
|
||||||
const listEl = container.querySelector('#pp-list');
|
|
||||||
const selectedEl = container.querySelector('#pp-selected');
|
|
||||||
const actionBtn = container.querySelector('#pp-action');
|
|
||||||
|
|
||||||
function networkLabel(n) {
|
|
||||||
if (!n) return '';
|
|
||||||
return n.name ? `${n.name} (${n.id})` : n.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderNetworkBanner() {
|
|
||||||
if (!useSharedNetwork) return;
|
|
||||||
const id = BackOneNetworkSession.getNetworkId();
|
|
||||||
const core = BackOneNetworkSession.getCoreStatus();
|
|
||||||
if (!core.online) {
|
|
||||||
netBanner.innerHTML = '<span class="peer-network-warn">BackOne offline — peers unavailable</span>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!currentNetwork || !id) {
|
|
||||||
netBanner.innerHTML = '<span class="peer-network-warn">No network — join one in the header bar</span>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const labelSuffix = `${peers.length} contact(s) · ${core.leafPeerCount} VL1 peer(s) from core`;
|
|
||||||
if (layout === 'sidebar') {
|
|
||||||
netBanner.textContent = `${networkLabel(currentNetwork)} · ${labelSuffix}`;
|
|
||||||
} else {
|
|
||||||
netBanner.textContent = `Network: ${networkLabel(currentNetwork)} · ${textSuffix}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderList() {
|
|
||||||
const q = searchInput.value;
|
|
||||||
const filtered = BackOnePeerDirectory.searchPeers(q, peers);
|
|
||||||
const netTag = currentNetwork ? (currentNetwork.name || currentNetwork.id) : '';
|
|
||||||
listEl.innerHTML = filtered.map(p => {
|
|
||||||
const lat = p.latency != null ? `${p.latency}ms` : '—';
|
|
||||||
const link = p.link || 'DIRECT';
|
|
||||||
const meta = [p.role || 'LEAF', lat, link, netTag].filter(Boolean).join(' · ');
|
|
||||||
return `
|
|
||||||
<li class="peer-item ${p.online ? 'online' : 'offline'} ${selectedPeer && selectedPeer.peerId === p.peerId ? 'active' : ''}" data-id="${p.peerId}">
|
|
||||||
<span class="peer-dot" title="${p.online ? 'Online' : 'Offline'}"></span>
|
|
||||||
<span class="peer-main">
|
|
||||||
<strong class="peer-name">${escapeHtml(p.displayName || p.peerId)}</strong>
|
|
||||||
<small class="peer-sub">${escapeHtml(meta)}</small>
|
|
||||||
</span>
|
|
||||||
<button type="button" class="peer-edit" title="Edit nickname">✎</button>
|
|
||||||
</li>`;
|
|
||||||
}).join('') || '<li class="peer-empty">No peers on this network</li>';
|
|
||||||
|
|
||||||
listEl.querySelectorAll('.peer-item').forEach(li => {
|
|
||||||
li.addEventListener('click', (ev) => {
|
|
||||||
if (ev.target.classList.contains('peer-edit')) return;
|
|
||||||
const id = li.dataset.id;
|
|
||||||
selectedPeer = peers.find(x => x.peerId === id) || null;
|
|
||||||
if (selectedPeer) BackOneNetworkSession.setSelectedPeer(selectedPeer);
|
|
||||||
renderList();
|
|
||||||
renderSelected();
|
|
||||||
if (selectedPeer) onSelect(selectedPeer, currentNetwork);
|
|
||||||
});
|
|
||||||
const editBtn = li.querySelector('.peer-edit');
|
|
||||||
if (editBtn) {
|
|
||||||
editBtn.addEventListener('click', async (ev) => {
|
|
||||||
ev.stopPropagation();
|
|
||||||
const id = li.dataset.id;
|
|
||||||
const peer = peers.find(x => x.peerId === id);
|
|
||||||
if (!peer) return;
|
|
||||||
const nick = prompt('Nickname for ' + peer.peerId, peer.nickname || peer.controllerName || '');
|
|
||||||
if (nick === null) return;
|
|
||||||
await BackOneContacts.setNickname(peer.peerId, nick);
|
|
||||||
BackOneContacts.invalidate();
|
|
||||||
await refreshPeers();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderSelected() {
|
|
||||||
if (layout === 'sidebar') return;
|
|
||||||
if (!selectedPeer) {
|
|
||||||
if (selectedEl) selectedEl.textContent = '';
|
|
||||||
if (actionBtn) actionBtn.disabled = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const net = currentNetwork ? networkLabel(currentNetwork) : BackOneNetworkSession.getNetworkId();
|
|
||||||
if (selectedEl) {
|
|
||||||
selectedEl.innerHTML = `Selected: <strong>${escapeHtml(selectedPeer.displayName)}</strong> (${escapeHtml(selectedPeer.peerId)})${net ? ` on <em>${escapeHtml(net)}</em>` : ''}`;
|
|
||||||
}
|
|
||||||
if (actionBtn) actionBtn.disabled = !BackOneNetworkSession.getNetworkId();
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeHtml(s) {
|
|
||||||
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshPeers() {
|
|
||||||
currentNetwork = BackOneNetworkSession.getActiveNetwork();
|
|
||||||
if (useSharedNetwork && BackOneNetworkSession.getNetworkId() && !currentNetwork) {
|
|
||||||
currentNetwork = await BackOneNetworkSession.refresh();
|
|
||||||
}
|
|
||||||
renderNetworkBanner();
|
|
||||||
const nwid = BackOneNetworkSession.getNetworkId();
|
|
||||||
await BackOneContacts.load();
|
|
||||||
peers = nwid ? await BackOnePeerDirectory.loadPeersForNetwork(nwid) : [];
|
|
||||||
const lastPeerId = BackOneNetworkSession.getSelectedPeerId();
|
|
||||||
if (lastPeerId) {
|
|
||||||
selectedPeer = peers.find(p => p.peerId === lastPeerId) || BackOneNetworkSession.getSelectedPeer();
|
|
||||||
}
|
|
||||||
renderList();
|
|
||||||
renderSelected();
|
|
||||||
onNetworkChange(currentNetwork, peers);
|
|
||||||
}
|
|
||||||
|
|
||||||
searchInput.addEventListener('input', renderList);
|
|
||||||
if (actionBtn) {
|
|
||||||
actionBtn.addEventListener('click', () => {
|
|
||||||
if (selectedPeer && onAction) onAction(selectedPeer, currentNetwork);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (useSharedNetwork) {
|
|
||||||
unsub = BackOneNetworkSession.subscribe(() => refreshPeers());
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshPeers();
|
|
||||||
|
|
||||||
return {
|
|
||||||
getSelectedPeer: () => selectedPeer,
|
|
||||||
getSelectedNetwork: () => currentNetwork,
|
|
||||||
getPeers: () => peers,
|
|
||||||
refresh: refreshPeers,
|
|
||||||
destroy: () => { if (unsub) unsub(); }
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
window.BackOneCommLayout = {
|
|
||||||
connectionBarHtml(peer, network) {
|
|
||||||
const netLabel = network
|
|
||||||
? (network.name ? `${network.name} (${network.id})` : network.id)
|
|
||||||
: BackOneNetworkSession.getNetworkId() || '—';
|
|
||||||
const lat = peer && peer.latency != null ? `${peer.latency}ms` : '—';
|
|
||||||
const latClass = peer && peer.latency != null && peer.latency < 100 ? 'latency-ok' : '';
|
|
||||||
return `
|
|
||||||
<div class="comm-stat"><label>Network ID</label><span>${escapeHtml(netLabel)}</span></div>
|
|
||||||
<div class="comm-stat"><label>Peer ID</label><span>${peer ? escapeHtml(peer.peerId) : '—'}</span></div>
|
|
||||||
<div class="comm-stat"><label>Role</label><span>${peer ? escapeHtml(peer.role || 'LEAF') : '—'}</span></div>
|
|
||||||
<div class="comm-stat"><label>Transport</label><span>${peer ? escapeHtml(peer.link || 'DIRECT') : '—'}</span></div>
|
|
||||||
<div class="comm-stat"><label>Latency</label><span class="${latClass}">${lat}</span></div>
|
|
||||||
<div class="comm-stat"><label>Quality</label><span class="comm-quality"><span></span><span></span><span></span><span></span></span></div>`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function escapeHtml(s) {
|
|
||||||
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
||||||
}
|
|
||||||
Vendored
-944
@@ -1,944 +0,0 @@
|
|||||||
:root {
|
|
||||||
--bg: #0f1419;
|
|
||||||
--surface: #1a2332;
|
|
||||||
--surface-2: #243044;
|
|
||||||
--border: #2d3a4d;
|
|
||||||
--text: #e7ecf1;
|
|
||||||
--muted: #8b9bb4;
|
|
||||||
--accent: #4a90d9;
|
|
||||||
--accent-dim: #2d5a8a;
|
|
||||||
--ok: #2ecc71;
|
|
||||||
--warn: #e6a23c;
|
|
||||||
--danger: #e74c3c;
|
|
||||||
}
|
|
||||||
|
|
||||||
* { box-sizing: border-box; }
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: system-ui, -apple-system, sans-serif;
|
|
||||||
margin: 0;
|
|
||||||
background: var(--bg);
|
|
||||||
color: var(--text);
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
padding: 0.65rem 1rem;
|
|
||||||
background: var(--surface);
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-brand {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.6rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-brand h1 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.15rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.core-badge {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
padding: 0.15rem 0.55rem;
|
|
||||||
border-radius: 999px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.core-badge.online {
|
|
||||||
background: rgba(46, 204, 113, 0.15);
|
|
||||||
color: var(--ok);
|
|
||||||
border: 1px solid rgba(46, 204, 113, 0.35);
|
|
||||||
}
|
|
||||||
|
|
||||||
.core-badge.offline {
|
|
||||||
background: rgba(231, 76, 60, 0.12);
|
|
||||||
color: var(--danger);
|
|
||||||
border: 1px solid rgba(231, 76, 60, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.network-bar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.45rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
flex: 1;
|
|
||||||
min-width: 240px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.network-bar-label { font-size: 0.82rem; color: var(--muted); }
|
|
||||||
|
|
||||||
.network-bar select,
|
|
||||||
.network-bar input,
|
|
||||||
.auth-token-bar input {
|
|
||||||
background: var(--bg);
|
|
||||||
color: var(--text);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 0.35rem 0.5rem;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.network-bar input { width: 9.5rem; }
|
|
||||||
|
|
||||||
.network-bar button,
|
|
||||||
.auth-token-bar button {
|
|
||||||
background: var(--accent-dim);
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
padding: 0.35rem 0.65rem;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.network-bar-status {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: #6b8cae;
|
|
||||||
flex: 1 1 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-token-bar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
flex: 1 1 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-token-hidden { display: none; }
|
|
||||||
|
|
||||||
.auth-token-label { font-size: 0.82rem; color: var(--muted); }
|
|
||||||
|
|
||||||
.auth-token-bar input { width: 14rem; }
|
|
||||||
|
|
||||||
.addon-nav {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.35rem;
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.addon-nav button {
|
|
||||||
background: transparent;
|
|
||||||
color: var(--muted);
|
|
||||||
border: 1px solid transparent;
|
|
||||||
padding: 0.4rem 0.85rem;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.88rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.addon-nav button:hover { color: var(--text); background: var(--surface-2); }
|
|
||||||
|
|
||||||
.addon-nav button.active {
|
|
||||||
background: var(--accent);
|
|
||||||
color: #fff;
|
|
||||||
border-color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.addon-panel {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.addon-panel > p {
|
|
||||||
padding: 1.5rem;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* —— comm layout (voice / video / chat) —— */
|
|
||||||
|
|
||||||
.comm-layout {
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comm-sidebar {
|
|
||||||
width: 300px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
border-right: 1px solid var(--border);
|
|
||||||
background: var(--bg);
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Chat & voice: 25% contact list / 75% main panel */
|
|
||||||
.comm-layout.comm-split-layout .comm-sidebar {
|
|
||||||
width: 25%;
|
|
||||||
max-width: 25%;
|
|
||||||
flex: 0 0 25%;
|
|
||||||
min-width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comm-layout.comm-split-layout .comm-main {
|
|
||||||
flex: 1 1 75%;
|
|
||||||
width: 75%;
|
|
||||||
max-width: 75%;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comm-layout.comm-split-layout .comm-sidebar-head {
|
|
||||||
padding: 0.75rem 0.75rem 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comm-layout.comm-split-layout .comm-sidebar-head h2 {
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comm-layout.comm-split-layout .comm-sidebar-body {
|
|
||||||
padding: 0.5rem 0.65rem;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comm-layout.comm-split-layout .comm-sidebar-actions {
|
|
||||||
padding: 0.5rem 0.65rem 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comm-sidebar-head {
|
|
||||||
padding: 1rem 1rem 0.5rem;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.comm-sidebar-head h2 {
|
|
||||||
margin: 0 0 0.35rem;
|
|
||||||
font-size: 1.05rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comm-sidebar-sub {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 0.72rem;
|
|
||||||
color: var(--muted);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comm-sidebar-body {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
overflow: auto;
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comm-sidebar-actions {
|
|
||||||
padding: 0.75rem 1rem 1rem;
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comm-selected-label {
|
|
||||||
font-size: 0.78rem;
|
|
||||||
color: var(--muted);
|
|
||||||
margin-bottom: 0.15rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comm-selected-label strong {
|
|
||||||
color: var(--text);
|
|
||||||
font-family: ui-monospace, monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comm-main {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-width: 0;
|
|
||||||
min-height: 0;
|
|
||||||
background: #0a0e14;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comm-main-head {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0.65rem 1rem;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
background: var(--surface);
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comm-breadcrumb {
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.comm-breadcrumb strong { color: var(--text); }
|
|
||||||
|
|
||||||
.comm-head-status {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 0.82rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comm-head-status .connected {
|
|
||||||
color: var(--ok);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comm-stage {
|
|
||||||
flex: 1;
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-height: 0;
|
|
||||||
background: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comm-stage video {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
background: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comm-remote-placeholder {
|
|
||||||
text-align: center;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.comm-remote-placeholder .avatar {
|
|
||||||
width: 72px;
|
|
||||||
height: 72px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--surface-2);
|
|
||||||
margin: 0 auto 0.75rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 2rem;
|
|
||||||
color: #5a6a7d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comm-remote-placeholder .peer-id {
|
|
||||||
font-family: ui-monospace, monospace;
|
|
||||||
color: var(--text);
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comm-remote-tag {
|
|
||||||
position: absolute;
|
|
||||||
top: 12px;
|
|
||||||
left: 12px;
|
|
||||||
background: rgba(0, 0, 0, 0.55);
|
|
||||||
padding: 0.2rem 0.55rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.35rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comm-remote-tag .dot {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.comm-call-controls {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 1.25rem;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
padding: 0.45rem;
|
|
||||||
background: rgba(26, 35, 50, 0.92);
|
|
||||||
border-radius: 10px;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.comm-call-controls button {
|
|
||||||
background: var(--surface-2);
|
|
||||||
color: var(--text);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
padding: 0.45rem 0.85rem;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.82rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comm-call-controls button.danger {
|
|
||||||
background: var(--danger);
|
|
||||||
border-color: var(--danger);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comm-connection-bar {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 1.25rem 2rem;
|
|
||||||
padding: 0.65rem 1rem;
|
|
||||||
background: var(--surface);
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
font-size: 0.78rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comm-stat label {
|
|
||||||
display: block;
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 0.68rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
margin-bottom: 0.15rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comm-stat span {
|
|
||||||
font-family: ui-monospace, monospace;
|
|
||||||
font-size: 0.82rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comm-stat .latency-ok { color: var(--ok); }
|
|
||||||
|
|
||||||
.comm-quality {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
gap: 2px;
|
|
||||||
height: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comm-quality span {
|
|
||||||
width: 3px;
|
|
||||||
background: var(--ok);
|
|
||||||
border-radius: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comm-quality span:nth-child(1) { height: 4px; }
|
|
||||||
.comm-quality span:nth-child(2) { height: 7px; }
|
|
||||||
.comm-quality span:nth-child(3) { height: 10px; }
|
|
||||||
.comm-quality span:nth-child(4) { height: 14px; }
|
|
||||||
|
|
||||||
/* —— peer picker —— */
|
|
||||||
|
|
||||||
.peer-picker { display: flex; flex-direction: column; flex: 1; min-height: 0; }
|
|
||||||
|
|
||||||
.peer-info-box {
|
|
||||||
font-size: 0.82rem;
|
|
||||||
color: #9eb4d0;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
padding: 0.55rem 0.65rem;
|
|
||||||
background: rgba(45, 90, 138, 0.25);
|
|
||||||
border: 1px solid rgba(74, 144, 217, 0.25);
|
|
||||||
border-radius: 6px;
|
|
||||||
line-height: 1.35;
|
|
||||||
}
|
|
||||||
|
|
||||||
.peer-picker-row { display: block; margin-bottom: 0.65rem; }
|
|
||||||
|
|
||||||
.peer-picker-row input {
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 0.25rem;
|
|
||||||
padding: 0.45rem 0.55rem;
|
|
||||||
background: var(--surface);
|
|
||||||
color: var(--text);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.peer-list {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
--peer-row-height: 3.25rem;
|
|
||||||
max-height: calc(var(--peer-row-height) * 10);
|
|
||||||
flex: 0 1 auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: auto;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 6px;
|
|
||||||
scrollbar-gutter: stable;
|
|
||||||
}
|
|
||||||
|
|
||||||
.peer-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 0.5rem;
|
|
||||||
min-height: var(--peer-row-height);
|
|
||||||
padding: 0.55rem 0.65rem;
|
|
||||||
cursor: pointer;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.peer-item:last-child { border-bottom: none; }
|
|
||||||
|
|
||||||
.peer-item:hover,
|
|
||||||
.peer-item.active {
|
|
||||||
background: var(--surface-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.peer-item.active { border-left: 3px solid var(--accent); padding-left: calc(0.65rem - 3px); }
|
|
||||||
|
|
||||||
.peer-dot {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #666;
|
|
||||||
flex-shrink: 0;
|
|
||||||
margin-top: 0.35rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.peer-item.online .peer-dot { background: var(--ok); }
|
|
||||||
|
|
||||||
.peer-main { flex: 1; min-width: 0; }
|
|
||||||
|
|
||||||
.peer-name {
|
|
||||||
display: block;
|
|
||||||
font-family: ui-monospace, monospace;
|
|
||||||
font-size: 0.88rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.peer-sub {
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 0.72rem;
|
|
||||||
display: block;
|
|
||||||
margin-top: 0.15rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.peer-edit {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
color: var(--muted);
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0.2rem;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.peer-empty { padding: 1rem; color: var(--muted); font-size: 0.85rem; }
|
|
||||||
|
|
||||||
.peer-network-warn { color: var(--warn); }
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background: var(--accent);
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
padding: 0.55rem 1rem;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.88rem;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:disabled {
|
|
||||||
opacity: 0.45;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
background: var(--surface-2);
|
|
||||||
color: var(--text);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
padding: 0.55rem 1rem;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.88rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* —— chat —— */
|
|
||||||
|
|
||||||
.chat-thread-panel {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comm-split-layout .chat-thread-panel {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-thread-header {
|
|
||||||
flex-shrink: 0;
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
margin: 0;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
background: var(--surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-thread-header small {
|
|
||||||
color: var(--muted);
|
|
||||||
font-weight: normal;
|
|
||||||
margin-left: 0.35rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-messages,
|
|
||||||
#chat-thread {
|
|
||||||
flex: 1;
|
|
||||||
overflow: auto;
|
|
||||||
padding: 1rem;
|
|
||||||
margin: 0;
|
|
||||||
border: none;
|
|
||||||
border-radius: 0;
|
|
||||||
background: #0a0e14;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-empty {
|
|
||||||
color: var(--muted);
|
|
||||||
text-align: center;
|
|
||||||
margin: 2rem 0;
|
|
||||||
font-size: 0.88rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-message {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
max-width: 85%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-message-meta {
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-message-sender {
|
|
||||||
color: #e8a54b;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.88rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-message-time {
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 0.72rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-message-body {
|
|
||||||
color: var(--text);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
line-height: 1.45;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-message-file {
|
|
||||||
margin-top: 0.35rem;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-compose {
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
background: var(--surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-compose textarea {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.5rem;
|
|
||||||
background: var(--surface);
|
|
||||||
color: var(--text);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 6px;
|
|
||||||
resize: vertical;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-compose-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* —— voice —— */
|
|
||||||
|
|
||||||
.voice-main-panel {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
background: #0a0e14;
|
|
||||||
}
|
|
||||||
|
|
||||||
.voice-active-banner {
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 1rem;
|
|
||||||
padding: 0.65rem 1rem;
|
|
||||||
background: rgba(45, 90, 138, 0.35);
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
font-size: 0.88rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.voice-active-dot {
|
|
||||||
display: inline-block;
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--ok);
|
|
||||||
margin-right: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.voice-active-controls {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.voice-active-controls button {
|
|
||||||
background: var(--surface-2);
|
|
||||||
color: var(--text);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
padding: 0.35rem 0.75rem;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.82rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.voice-active-controls button.danger {
|
|
||||||
background: var(--danger);
|
|
||||||
border-color: var(--danger);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.voice-session-hint {
|
|
||||||
margin: 1rem;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.voice-remote-audio {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.voice-history-panel {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.voice-history-title {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 600;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
flex-shrink: 0;
|
|
||||||
background: var(--surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
.voice-call-history {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.voice-call-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.voice-call-item:hover {
|
|
||||||
background: var(--surface-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.voice-call-icon {
|
|
||||||
width: 1.75rem;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 1rem;
|
|
||||||
color: var(--accent);
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.voice-call-main {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.voice-call-name {
|
|
||||||
display: block;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-family: ui-monospace, monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.voice-call-sub {
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.voice-call-when {
|
|
||||||
flex-shrink: 0;
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.voice-call-empty {
|
|
||||||
padding: 2rem 1rem;
|
|
||||||
text-align: center;
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 0.88rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* legacy voice stage (unused) */
|
|
||||||
.voice-stage-inner {
|
|
||||||
text-align: center;
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.voice-stage-inner .avatar {
|
|
||||||
width: 96px;
|
|
||||||
height: 96px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--surface-2);
|
|
||||||
margin: 0 auto 1rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* —— ringing popup —— */
|
|
||||||
|
|
||||||
.ringing-hidden { display: none; }
|
|
||||||
|
|
||||||
#ringing-popup {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.6);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ringing-popup.ringing-hidden { display: none; }
|
|
||||||
|
|
||||||
.ringing-card {
|
|
||||||
background: var(--surface);
|
|
||||||
padding: 2rem;
|
|
||||||
border-radius: 12px;
|
|
||||||
text-align: center;
|
|
||||||
min-width: 280px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ringing-card button {
|
|
||||||
margin: 0.5rem;
|
|
||||||
padding: 0.6rem 1.2rem;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ringing-accept { background: var(--ok); color: #000; }
|
|
||||||
.ringing-reject { background: var(--danger); color: #fff; }
|
|
||||||
|
|
||||||
/* —— video call: remote fills main stage, local is PiP —— */
|
|
||||||
|
|
||||||
.comm-layout.video-in-call .comm-sidebar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comm-layout.video-in-call .comm-main-head {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 4;
|
|
||||||
background: linear-gradient(to bottom, rgba(10, 14, 20, 0.85), transparent);
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-stage .video-remote-main {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-stage .video-remote-stream {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.25s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-stage .video-remote-stream.active {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-stage.has-remote .comm-remote-placeholder {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-stage .comm-remote-tag {
|
|
||||||
z-index: 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-local-pip {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 4.5rem;
|
|
||||||
right: 1rem;
|
|
||||||
width: 180px;
|
|
||||||
z-index: 3;
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 2px solid rgba(255, 255, 255, 0.2);
|
|
||||||
background: #000;
|
|
||||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.45);
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-pip-label {
|
|
||||||
position: absolute;
|
|
||||||
top: 6px;
|
|
||||||
left: 8px;
|
|
||||||
z-index: 1;
|
|
||||||
font-size: 0.68rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
color: rgba(255, 255, 255, 0.85);
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
padding: 0.1rem 0.35rem;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-local-pip video {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
display: block;
|
|
||||||
aspect-ratio: 4 / 3;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-stage .comm-call-controls {
|
|
||||||
z-index: 4;
|
|
||||||
}
|
|
||||||
Vendored
-170
@@ -1,170 +0,0 @@
|
|||||||
/**
|
|
||||||
* WebSocket client for backone-webhub (127.0.0.1, authtoken auth).
|
|
||||||
*/
|
|
||||||
window.BackOneWebHubClient = (function () {
|
|
||||||
let ws = null;
|
|
||||||
let connected = false;
|
|
||||||
let reconnectDelay = 1000;
|
|
||||||
let reconnectTimer = null;
|
|
||||||
let pollTimer = null;
|
|
||||||
let port = 9994;
|
|
||||||
let selfPeerId = '';
|
|
||||||
const seenIncoming = new Set();
|
|
||||||
const handlers = new Map();
|
|
||||||
|
|
||||||
function emit(type, msg) {
|
|
||||||
const set = handlers.get(type);
|
|
||||||
if (!set) return;
|
|
||||||
set.forEach(fn => {
|
|
||||||
try { fn(msg); } catch (_) { /* ignore */ }
|
|
||||||
});
|
|
||||||
const all = handlers.get('*');
|
|
||||||
if (all) {
|
|
||||||
all.forEach(fn => {
|
|
||||||
try { fn(type, msg); } catch (_) { /* ignore */ }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function subscribe(type, fn) {
|
|
||||||
if (!handlers.has(type)) handlers.set(type, new Set());
|
|
||||||
handlers.get(type).add(fn);
|
|
||||||
return () => handlers.get(type)?.delete(fn);
|
|
||||||
}
|
|
||||||
|
|
||||||
function send(msg) {
|
|
||||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
||||||
ws.send(JSON.stringify(msg));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function registerPeer() {
|
|
||||||
const core = BackOneNetworkSession.getCoreStatus();
|
|
||||||
selfPeerId = ((core && core.address) || '').toLowerCase();
|
|
||||||
const networkId = BackOneNetworkSession.getNetworkId();
|
|
||||||
if (!selfPeerId) return;
|
|
||||||
send({ type: 'register', peerId: selfPeerId, networkId });
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopHttpPoll() {
|
|
||||||
if (pollTimer) {
|
|
||||||
clearInterval(pollTimer);
|
|
||||||
pollTimer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function startHttpPoll() {
|
|
||||||
stopHttpPoll();
|
|
||||||
pollTimer = setInterval(async () => {
|
|
||||||
if (connected) return;
|
|
||||||
const peerId = selfPeerId || ((BackOneNetworkSession.getCoreStatus().address || '').toLowerCase());
|
|
||||||
if (!peerId) return;
|
|
||||||
try {
|
|
||||||
const res = await BackOneAuth.fetchWithAuth(
|
|
||||||
`/controller/comms/call/incoming?peerId=${encodeURIComponent(peerId)}`
|
|
||||||
);
|
|
||||||
if (!res.ok) return;
|
|
||||||
const data = await res.json();
|
|
||||||
const calls = Array.isArray(data.calls) ? data.calls : [];
|
|
||||||
for (const call of calls) {
|
|
||||||
if (seenIncoming.has(call.callId)) continue;
|
|
||||||
seenIncoming.add(call.callId);
|
|
||||||
emit('incoming_call', {
|
|
||||||
type: 'incoming_call',
|
|
||||||
callId: call.callId,
|
|
||||||
callType: call.callType || 'voice',
|
|
||||||
callerId: call.callerId,
|
|
||||||
callerName: call.callerName || call.callerId,
|
|
||||||
networkId: call.networkId,
|
|
||||||
sdp: call.sdp
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (_) { /* ignore */ }
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function scheduleReconnect() {
|
|
||||||
if (reconnectTimer) return;
|
|
||||||
reconnectTimer = setTimeout(() => {
|
|
||||||
reconnectTimer = null;
|
|
||||||
connect();
|
|
||||||
}, reconnectDelay);
|
|
||||||
reconnectDelay = Math.min(reconnectDelay * 2, 30000);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadPort() {
|
|
||||||
try {
|
|
||||||
const res = await BackOneAuth.fetchWithAuth('/app/api/webrtc-config');
|
|
||||||
if (res.ok) {
|
|
||||||
const cfg = await res.json();
|
|
||||||
if (cfg.webhubPort) port = cfg.webhubPort;
|
|
||||||
}
|
|
||||||
} catch (_) { /* ignore */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function connect() {
|
|
||||||
await loadPort();
|
|
||||||
const token = await BackOneAuth.getToken();
|
|
||||||
if (!token) return false;
|
|
||||||
|
|
||||||
if (ws) {
|
|
||||||
try { ws.close(); } catch (_) { /* ignore */ }
|
|
||||||
ws = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const url = `ws://127.0.0.1:${port}/?auth=${encodeURIComponent(token)}`;
|
|
||||||
try {
|
|
||||||
ws = new WebSocket(url);
|
|
||||||
} catch (_) {
|
|
||||||
connected = false;
|
|
||||||
startHttpPoll();
|
|
||||||
scheduleReconnect();
|
|
||||||
resolve(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ws.onopen = () => {
|
|
||||||
connected = true;
|
|
||||||
reconnectDelay = 1000;
|
|
||||||
stopHttpPoll();
|
|
||||||
send({ type: 'auth', token });
|
|
||||||
registerPeer();
|
|
||||||
resolve(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onmessage = (ev) => {
|
|
||||||
try {
|
|
||||||
const msg = JSON.parse(ev.data);
|
|
||||||
if (msg && msg.type) emit(msg.type, msg);
|
|
||||||
} catch (_) { /* ignore */ }
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onclose = () => {
|
|
||||||
connected = false;
|
|
||||||
ws = null;
|
|
||||||
startHttpPoll();
|
|
||||||
scheduleReconnect();
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onerror = () => {
|
|
||||||
connected = false;
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
BackOneNetworkSession.subscribe(() => {
|
|
||||||
if (connected) registerPeer();
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
connect,
|
|
||||||
send,
|
|
||||||
subscribe,
|
|
||||||
isConnected: () => connected,
|
|
||||||
getSelfPeerId: () => selfPeerId,
|
|
||||||
registerPeer
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
Vendored
-352
@@ -1,352 +0,0 @@
|
|||||||
/**
|
|
||||||
* Shared WebRTC session — signaling via WebHub WS with HTTP fallback.
|
|
||||||
*/
|
|
||||||
window.BackOneWebRtcSession = (function () {
|
|
||||||
let config = {
|
|
||||||
icePreferOverlay: true,
|
|
||||||
iceFallbackTimeoutMs: 8000,
|
|
||||||
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
|
|
||||||
};
|
|
||||||
let pc = null;
|
|
||||||
let localStream = null;
|
|
||||||
let remoteStream = null;
|
|
||||||
let activeCall = null;
|
|
||||||
let iceFallbackTimer = null;
|
|
||||||
let httpIcePollTimer = null;
|
|
||||||
let httpAnswerPollTimer = null;
|
|
||||||
const listeners = new Set();
|
|
||||||
|
|
||||||
function emit(event, detail) {
|
|
||||||
listeners.forEach(fn => {
|
|
||||||
try { fn(event, detail); } catch (_) { /* ignore */ }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function subscribe(fn) {
|
|
||||||
listeners.add(fn);
|
|
||||||
return () => listeners.delete(fn);
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearTimers() {
|
|
||||||
if (iceFallbackTimer) { clearTimeout(iceFallbackTimer); iceFallbackTimer = null; }
|
|
||||||
if (httpIcePollTimer) { clearInterval(httpIcePollTimer); httpIcePollTimer = null; }
|
|
||||||
if (httpAnswerPollTimer) { clearInterval(httpAnswerPollTimer); httpAnswerPollTimer = null; }
|
|
||||||
}
|
|
||||||
|
|
||||||
function overlayIceServers() {
|
|
||||||
return config.icePreferOverlay ? [] : (config.iceServers || []);
|
|
||||||
}
|
|
||||||
|
|
||||||
function fullIceServers() {
|
|
||||||
return config.iceServers || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function createPeerConnection(peer) {
|
|
||||||
clearTimers();
|
|
||||||
if (pc) {
|
|
||||||
try { pc.close(); } catch (_) { /* ignore */ }
|
|
||||||
}
|
|
||||||
pc = new RTCPeerConnection({ iceServers: overlayIceServers() });
|
|
||||||
remoteStream = new MediaStream();
|
|
||||||
pc.ontrack = (ev) => {
|
|
||||||
ev.streams[0].getTracks().forEach(t => remoteStream.addTrack(t));
|
|
||||||
emit('remote-track', { stream: remoteStream });
|
|
||||||
};
|
|
||||||
pc.onicecandidate = (ev) => {
|
|
||||||
if (!ev.candidate || !activeCall) return;
|
|
||||||
relayIce(activeCall.callId, {
|
|
||||||
candidate: ev.candidate.candidate,
|
|
||||||
sdpMid: ev.candidate.sdpMid,
|
|
||||||
sdpMLineIndex: ev.candidate.sdpMLineIndex
|
|
||||||
});
|
|
||||||
};
|
|
||||||
pc.onconnectionstatechange = () => {
|
|
||||||
emit('connection-state', { state: pc.connectionState });
|
|
||||||
if (pc.connectionState === 'connected') clearTimers();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (config.icePreferOverlay) {
|
|
||||||
iceFallbackTimer = setTimeout(() => {
|
|
||||||
if (!pc || pc.connectionState === 'connected') return;
|
|
||||||
try {
|
|
||||||
pc.setConfiguration({ iceServers: fullIceServers() });
|
|
||||||
pc.restartIce();
|
|
||||||
} catch (_) { /* ignore */ }
|
|
||||||
}, config.iceFallbackTimeoutMs || 8000);
|
|
||||||
}
|
|
||||||
return pc;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function relayIce(callId, candidate) {
|
|
||||||
if (BackOneWebHubClient.isConnected()) {
|
|
||||||
BackOneWebHubClient.send({
|
|
||||||
type: 'ice_candidate',
|
|
||||||
callId,
|
|
||||||
...candidate
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const peerId = BackOneWebHubClient.getSelfPeerId()
|
|
||||||
|| ((BackOneNetworkSession.getCoreStatus().address || '').toLowerCase());
|
|
||||||
await BackOneAuth.fetchWithAuth(`/controller/comms/call/${encodeURIComponent(callId)}/ice`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ ...candidate, fromPeerId: peerId })
|
|
||||||
}).catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
function startHttpIcePoll(callId) {
|
|
||||||
let since = 0;
|
|
||||||
httpIcePollTimer = setInterval(async () => {
|
|
||||||
if (!pc || !activeCall || activeCall.callId !== callId) return;
|
|
||||||
const peerId = BackOneWebHubClient.getSelfPeerId()
|
|
||||||
|| ((BackOneNetworkSession.getCoreStatus().address || '').toLowerCase());
|
|
||||||
try {
|
|
||||||
const res = await BackOneAuth.fetchWithAuth(
|
|
||||||
`/controller/comms/call/${encodeURIComponent(callId)}/ice?since=${since}&peerId=${encodeURIComponent(peerId)}`
|
|
||||||
);
|
|
||||||
if (!res.ok) return;
|
|
||||||
const data = await res.json();
|
|
||||||
const list = Array.isArray(data.candidates) ? data.candidates : [];
|
|
||||||
for (const c of list) {
|
|
||||||
try {
|
|
||||||
await pc.addIceCandidate({
|
|
||||||
candidate: c.candidate,
|
|
||||||
sdpMid: c.sdpMid,
|
|
||||||
sdpMLineIndex: c.sdpMLineIndex
|
|
||||||
});
|
|
||||||
since = Math.max(since, c.at || 0);
|
|
||||||
} catch (_) { /* ignore */ }
|
|
||||||
}
|
|
||||||
} catch (_) { /* ignore */ }
|
|
||||||
}, 1500);
|
|
||||||
}
|
|
||||||
|
|
||||||
function watchHubCallState() {
|
|
||||||
return BackOneWebHubClient.subscribe('call_state', async (msg) => {
|
|
||||||
if (!activeCall || msg.callId !== activeCall.callId) return;
|
|
||||||
if (msg.state === 'accepted' && msg.sdp && activeCall.role === 'caller') {
|
|
||||||
try {
|
|
||||||
await pc.setRemoteDescription({ type: 'answer', sdp: msg.sdp });
|
|
||||||
emit('call-accepted', activeCall);
|
|
||||||
} catch (_) { /* ignore */ }
|
|
||||||
}
|
|
||||||
if (msg.state === 'ended' || msg.state === 'rejected') {
|
|
||||||
hangUp(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let unsubHub = [];
|
|
||||||
|
|
||||||
async function waitForOutgoingCallId() {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const unsub = BackOneWebHubClient.subscribe('call_state', (msg) => {
|
|
||||||
if (msg.callId && msg.state === 'ringing') {
|
|
||||||
unsub();
|
|
||||||
resolve(msg.callId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setTimeout(() => { unsub(); resolve(null); }, 5000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function init() {
|
|
||||||
try {
|
|
||||||
const res = await BackOneAuth.fetchWithAuth('/app/api/webrtc-config');
|
|
||||||
if (res.ok) {
|
|
||||||
const cfg = await res.json();
|
|
||||||
config = { ...config, ...cfg };
|
|
||||||
}
|
|
||||||
} catch (_) { /* ignore */ }
|
|
||||||
|
|
||||||
unsubHub.forEach(fn => fn && fn());
|
|
||||||
unsubHub = [
|
|
||||||
watchHubCallState(),
|
|
||||||
BackOneWebHubClient.subscribe('ice_candidate', async (msg) => {
|
|
||||||
if (!pc || !activeCall || msg.callId !== activeCall.callId) return;
|
|
||||||
try {
|
|
||||||
await pc.addIceCandidate({
|
|
||||||
candidate: msg.candidate,
|
|
||||||
sdpMid: msg.sdpMid,
|
|
||||||
sdpMLineIndex: msg.sdpMLineIndex
|
|
||||||
});
|
|
||||||
} catch (_) { /* ignore */ }
|
|
||||||
})
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getMedia(callType) {
|
|
||||||
const audio = true;
|
|
||||||
const video = callType === 'video';
|
|
||||||
localStream = await navigator.mediaDevices.getUserMedia({ audio, video });
|
|
||||||
return localStream;
|
|
||||||
}
|
|
||||||
|
|
||||||
function attachLocalTracks() {
|
|
||||||
if (!pc || !localStream) return;
|
|
||||||
localStream.getTracks().forEach(t => pc.addTrack(t, localStream));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function startOutgoingCall(peer, callType) {
|
|
||||||
if (!peer || activeCall) throw new Error('Already in a call');
|
|
||||||
const networkId = BackOneNetworkSession.getNetworkId();
|
|
||||||
const selfId = ((BackOneNetworkSession.getCoreStatus().address || '').toLowerCase());
|
|
||||||
createPeerConnection(peer);
|
|
||||||
localStream = await getMedia(callType);
|
|
||||||
attachLocalTracks();
|
|
||||||
|
|
||||||
const offer = await pc.createOffer();
|
|
||||||
await pc.setLocalDescription(offer);
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
type: 'call_offer',
|
|
||||||
callType,
|
|
||||||
networkId,
|
|
||||||
targetId: peer.peerId,
|
|
||||||
callerName: peer.displayName || peer.peerId,
|
|
||||||
sdp: offer.sdp,
|
|
||||||
overlayIps: peer.ip ? [peer.ip] : []
|
|
||||||
};
|
|
||||||
|
|
||||||
let callId = null;
|
|
||||||
if (BackOneWebHubClient.isConnected()) {
|
|
||||||
BackOneWebHubClient.send(payload);
|
|
||||||
callId = await waitForOutgoingCallId();
|
|
||||||
if (!callId) throw new Error('Call signaling timeout');
|
|
||||||
} else {
|
|
||||||
const res = await BackOneAuth.fetchWithAuth('/controller/comms/call', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
callType,
|
|
||||||
type: callType,
|
|
||||||
networkId,
|
|
||||||
targetId: peer.peerId,
|
|
||||||
callerId: selfId,
|
|
||||||
callerName: peer.displayName || peer.peerId,
|
|
||||||
sdp: offer.sdp
|
|
||||||
})
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error('Call signaling failed');
|
|
||||||
const data = await res.json();
|
|
||||||
callId = data.callId;
|
|
||||||
startHttpAnswerPoll(callId);
|
|
||||||
}
|
|
||||||
|
|
||||||
activeCall = {
|
|
||||||
callId,
|
|
||||||
role: 'caller',
|
|
||||||
callType,
|
|
||||||
peer,
|
|
||||||
networkId
|
|
||||||
};
|
|
||||||
if (!BackOneWebHubClient.isConnected()) startHttpIcePoll(activeCall.callId);
|
|
||||||
emit('call-started', activeCall);
|
|
||||||
return activeCall;
|
|
||||||
}
|
|
||||||
|
|
||||||
function startHttpAnswerPoll(callId) {
|
|
||||||
httpAnswerPollTimer = setInterval(async () => {
|
|
||||||
if (!pc || !activeCall || activeCall.callId !== callId) return;
|
|
||||||
try {
|
|
||||||
const res = await BackOneAuth.fetchWithAuth(`/controller/comms/call/${encodeURIComponent(callId)}`);
|
|
||||||
if (!res.ok) return;
|
|
||||||
const data = await res.json();
|
|
||||||
if (data.state === 'accepted' && data.answerSdp) {
|
|
||||||
await pc.setRemoteDescription({ type: 'answer', sdp: data.answerSdp });
|
|
||||||
clearInterval(httpAnswerPollTimer);
|
|
||||||
httpAnswerPollTimer = null;
|
|
||||||
emit('call-accepted', activeCall);
|
|
||||||
}
|
|
||||||
} catch (_) { /* ignore */ }
|
|
||||||
}, 1500);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function acceptIncomingCall(incoming) {
|
|
||||||
if (!incoming || activeCall) throw new Error('Already in a call');
|
|
||||||
const callType = incoming.callType || 'voice';
|
|
||||||
const callId = incoming.callId;
|
|
||||||
createPeerConnection({ peerId: incoming.callerId });
|
|
||||||
localStream = await getMedia(callType);
|
|
||||||
attachLocalTracks();
|
|
||||||
|
|
||||||
await pc.setRemoteDescription({ type: 'offer', sdp: incoming.sdp });
|
|
||||||
const answer = await pc.createAnswer();
|
|
||||||
await pc.setLocalDescription(answer);
|
|
||||||
|
|
||||||
if (BackOneWebHubClient.isConnected()) {
|
|
||||||
BackOneWebHubClient.send({
|
|
||||||
type: 'call_answer',
|
|
||||||
callId,
|
|
||||||
sdp: answer.sdp
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await BackOneAuth.fetchWithAuth(`/controller/comms/call/${encodeURIComponent(callId)}/answer`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ sdp: answer.sdp })
|
|
||||||
});
|
|
||||||
startHttpIcePoll(callId);
|
|
||||||
}
|
|
||||||
|
|
||||||
activeCall = {
|
|
||||||
callId,
|
|
||||||
role: 'callee',
|
|
||||||
callType,
|
|
||||||
peer: { peerId: incoming.callerId, displayName: incoming.callerName },
|
|
||||||
networkId: incoming.networkId
|
|
||||||
};
|
|
||||||
emit('call-started', activeCall);
|
|
||||||
return activeCall;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function rejectIncomingCall(callId) {
|
|
||||||
if (BackOneWebHubClient.isConnected()) {
|
|
||||||
BackOneWebHubClient.send({ type: 'call_reject', callId });
|
|
||||||
} else {
|
|
||||||
await BackOneAuth.fetchWithAuth(`/controller/comms/call/${encodeURIComponent(callId)}/reject`, {
|
|
||||||
method: 'POST'
|
|
||||||
}).catch(() => {});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function hangUp(signalRemote = true) {
|
|
||||||
const callId = activeCall?.callId;
|
|
||||||
clearTimers();
|
|
||||||
if (signalRemote && callId) {
|
|
||||||
if (BackOneWebHubClient.isConnected()) {
|
|
||||||
BackOneWebHubClient.send({ type: 'call_end', callId });
|
|
||||||
} else {
|
|
||||||
await BackOneAuth.fetchWithAuth(`/controller/comms/call/${encodeURIComponent(callId)}/end`, {
|
|
||||||
method: 'POST'
|
|
||||||
}).catch(() => {});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (pc) {
|
|
||||||
try { pc.close(); } catch (_) { /* ignore */ }
|
|
||||||
pc = null;
|
|
||||||
}
|
|
||||||
if (localStream) {
|
|
||||||
localStream.getTracks().forEach(t => t.stop());
|
|
||||||
localStream = null;
|
|
||||||
}
|
|
||||||
remoteStream = null;
|
|
||||||
activeCall = null;
|
|
||||||
emit('call-ended', {});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
init,
|
|
||||||
subscribe,
|
|
||||||
startOutgoingCall,
|
|
||||||
acceptIncomingCall,
|
|
||||||
rejectIncomingCall,
|
|
||||||
hangUp,
|
|
||||||
getPeerConnection: () => pc,
|
|
||||||
getLocalStream: () => localStream,
|
|
||||||
getRemoteStream: () => remoteStream,
|
|
||||||
getActiveCall: () => activeCall,
|
|
||||||
isInCall: () => !!activeCall
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "app-tray",
|
|
||||||
"name": "BackOne Tray",
|
|
||||||
"version": "1.17.0",
|
|
||||||
"requires": []
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
[Desktop Entry]
|
|
||||||
Type=Application
|
|
||||||
Name=BackOne Tray
|
|
||||||
Comment=Start BackOne tray icon at login
|
|
||||||
Exec=/usr/bin/backone-tray
|
|
||||||
Icon=network-workgroup
|
|
||||||
Terminal=false
|
|
||||||
X-GNOME-Autostart-enabled=true
|
|
||||||
Hidden=false
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
[Desktop Entry]
|
|
||||||
Type=Application
|
|
||||||
Name=BackOne Tray
|
|
||||||
Comment=BackOne system tray for voice, video, and chat
|
|
||||||
Exec=/usr/bin/backone-tray
|
|
||||||
Icon=network-workgroup
|
|
||||||
Terminal=false
|
|
||||||
Categories=Network;
|
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""BackOne system tray — opens /app UI and addon deep links."""
|
|
||||||
import os
|
|
||||||
import signal
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
import urllib.request
|
|
||||||
|
|
||||||
import gi
|
|
||||||
gi.require_version('Gtk', '3.0')
|
|
||||||
try:
|
|
||||||
gi.require_version('AyatanaAppIndicator3', '0.1')
|
|
||||||
from gi.repository import AyatanaAppIndicator3 as AppIndicator3
|
|
||||||
except (ImportError, ValueError):
|
|
||||||
gi.require_version('AppIndicator3', '0.1')
|
|
||||||
from gi.repository import AppIndicator3
|
|
||||||
|
|
||||||
from gi.repository import GLib, Gtk
|
|
||||||
|
|
||||||
APP_URL = os.environ.get('BACKONE_APP_URL', 'http://127.0.0.1:9993/app/')
|
|
||||||
HEALTH_URL = os.environ.get('BACKONE_HEALTH_URL', 'http://127.0.0.1:9993/health')
|
|
||||||
ICON_NAME = 'network-workgroup'
|
|
||||||
SERVICE_WAIT_SECONDS = int(os.environ.get('BACKONE_SERVICE_WAIT', '45'))
|
|
||||||
|
|
||||||
|
|
||||||
class BackOneTray:
|
|
||||||
def __init__(self):
|
|
||||||
self._windows = []
|
|
||||||
self._service_ready = False
|
|
||||||
self.indicator = AppIndicator3.Indicator.new(
|
|
||||||
'backone-tray',
|
|
||||||
ICON_NAME,
|
|
||||||
AppIndicator3.IndicatorCategory.APPLICATION_STATUS,
|
|
||||||
)
|
|
||||||
self.indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
|
|
||||||
self.indicator.set_menu(self._build_menu())
|
|
||||||
try:
|
|
||||||
self.indicator.connect('activate', lambda *_: self._open_url(APP_URL))
|
|
||||||
except TypeError:
|
|
||||||
pass
|
|
||||||
self._update_status()
|
|
||||||
GLib.timeout_add_seconds(30, self._on_timer)
|
|
||||||
GLib.idle_add(self._bootstrap_service)
|
|
||||||
|
|
||||||
def _build_menu(self):
|
|
||||||
menu = Gtk.Menu()
|
|
||||||
|
|
||||||
def add_item(label, callback):
|
|
||||||
item = Gtk.MenuItem(label=label)
|
|
||||||
item.connect('activate', lambda *_: callback())
|
|
||||||
item.show()
|
|
||||||
menu.append(item)
|
|
||||||
return item
|
|
||||||
|
|
||||||
add_item('Open BackOne App', lambda: self._open_url(APP_URL))
|
|
||||||
add_item('Voice', lambda: self._open_url(APP_URL + '#/voice'))
|
|
||||||
add_item('Video', lambda: self._open_url(APP_URL + '#/video'))
|
|
||||||
add_item('Chat', lambda: self._open_url(APP_URL + '#/chat'))
|
|
||||||
menu.append(Gtk.SeparatorMenuItem())
|
|
||||||
add_item('Restart BackOne Service', self._restart_service)
|
|
||||||
add_item('Quit Tray', Gtk.main_quit)
|
|
||||||
menu.show()
|
|
||||||
return menu
|
|
||||||
|
|
||||||
def _service_ok(self):
|
|
||||||
try:
|
|
||||||
with urllib.request.urlopen(HEALTH_URL, timeout=2) as resp:
|
|
||||||
return resp.status == 200
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _systemd_active(self):
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
['systemctl', 'is-active', 'backone'],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=5,
|
|
||||||
)
|
|
||||||
return result.returncode == 0 and result.stdout.strip() == 'active'
|
|
||||||
except (OSError, subprocess.TimeoutExpired):
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _start_service(self):
|
|
||||||
for cmd in (
|
|
||||||
['systemctl', 'start', 'backone'],
|
|
||||||
['pkexec', 'systemctl', 'start', 'backone'],
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
cmd,
|
|
||||||
capture_output=True,
|
|
||||||
timeout=30,
|
|
||||||
)
|
|
||||||
if result.returncode == 0:
|
|
||||||
return True
|
|
||||||
except (OSError, subprocess.TimeoutExpired):
|
|
||||||
continue
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _wait_for_service(self, timeout=SERVICE_WAIT_SECONDS):
|
|
||||||
deadline = time.time() + timeout
|
|
||||||
while time.time() < deadline:
|
|
||||||
if self._service_ok():
|
|
||||||
self._service_ready = True
|
|
||||||
return True
|
|
||||||
time.sleep(0.5)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _ensure_service(self):
|
|
||||||
if self._service_ok():
|
|
||||||
self._service_ready = True
|
|
||||||
return True
|
|
||||||
if not self._systemd_active():
|
|
||||||
self._start_service()
|
|
||||||
return self._wait_for_service()
|
|
||||||
|
|
||||||
def _bootstrap_service(self):
|
|
||||||
def worker():
|
|
||||||
ready = self._ensure_service()
|
|
||||||
GLib.idle_add(self._on_bootstrap_done, ready)
|
|
||||||
threading.Thread(target=worker, daemon=True).start()
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _on_bootstrap_done(self, ready):
|
|
||||||
self._service_ready = ready
|
|
||||||
self._update_status()
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _update_status(self):
|
|
||||||
online = self._service_ok()
|
|
||||||
self._service_ready = online
|
|
||||||
title = 'BackOne (online)' if online else 'BackOne (offline)'
|
|
||||||
self.indicator.set_title(title)
|
|
||||||
|
|
||||||
def _on_timer(self):
|
|
||||||
self._update_status()
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _show_offline_dialog(self):
|
|
||||||
dialog = Gtk.MessageDialog(
|
|
||||||
transient_for=None,
|
|
||||||
flags=0,
|
|
||||||
message_type=Gtk.MessageType.ERROR,
|
|
||||||
buttons=Gtk.ButtonsType.OK,
|
|
||||||
text='BackOne service is not running',
|
|
||||||
)
|
|
||||||
dialog.format_secondary_text(
|
|
||||||
'Could not reach the BackOne web server on 127.0.0.1:9993.\n\n'
|
|
||||||
'Use "Restart BackOne Service" from the tray menu, or run:\n'
|
|
||||||
' sudo systemctl start backone'
|
|
||||||
)
|
|
||||||
dialog.run()
|
|
||||||
dialog.destroy()
|
|
||||||
|
|
||||||
def _open_url(self, url):
|
|
||||||
if not self._ensure_service():
|
|
||||||
self._show_offline_dialog()
|
|
||||||
return
|
|
||||||
if self._try_webkit(url):
|
|
||||||
return
|
|
||||||
for cmd in (
|
|
||||||
['xdg-open', url],
|
|
||||||
['gio', 'open', url],
|
|
||||||
['sensible-browser', url],
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
||||||
return
|
|
||||||
except OSError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
def _try_webkit(self, url):
|
|
||||||
try:
|
|
||||||
gi.require_version('WebKit2', '4.1')
|
|
||||||
except ValueError:
|
|
||||||
try:
|
|
||||||
gi.require_version('WebKit2', '4.0')
|
|
||||||
except ValueError:
|
|
||||||
return False
|
|
||||||
from gi.repository import WebKit2
|
|
||||||
|
|
||||||
win = Gtk.Window(title='BackOne')
|
|
||||||
win.set_default_size(900, 640)
|
|
||||||
view = WebKit2.WebView()
|
|
||||||
view.load_uri(url)
|
|
||||||
win.add(view)
|
|
||||||
win.connect('destroy', lambda w: self._windows.remove(w) if w in self._windows else None)
|
|
||||||
self._windows.append(win)
|
|
||||||
win.show_all()
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _restart_service(self):
|
|
||||||
subprocess.Popen(
|
|
||||||
['pkexec', 'systemctl', 'restart', 'backone'],
|
|
||||||
stdout=subprocess.DEVNULL,
|
|
||||||
stderr=subprocess.DEVNULL,
|
|
||||||
)
|
|
||||||
def worker():
|
|
||||||
ready = self._wait_for_service(timeout=30)
|
|
||||||
GLib.idle_add(self._on_bootstrap_done, ready)
|
|
||||||
threading.Thread(target=worker, daemon=True).start()
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
|
||||||
BackOneTray()
|
|
||||||
Gtk.main()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
sys.exit(main())
|
|
||||||
+30
-24
@@ -81,36 +81,42 @@ int main(int argc,char **argv)
|
|||||||
std::vector<World::Root> roots;
|
std::vector<World::Root> roots;
|
||||||
|
|
||||||
const uint64_t id = ZT_WORLD_ID_EARTH;
|
const uint64_t id = ZT_WORLD_ID_EARTH;
|
||||||
const uint64_t ts = 1567191349589ULL; // August 30th, 2019
|
const uint64_t ts = 1567191349589ULL; // August 30th, 2019
|
||||||
|
//const uint64_t ts = 1778645159589ULL; // May 13th, 2026
|
||||||
|
|
||||||
// Los Angeles
|
// MVNET server1.saltis.id
|
||||||
roots.push_back(World::Root());
|
roots.push_back(World::Root());
|
||||||
roots.back().identity = Identity("3a46f1bf30:0:76e66fab33e28549a62ee2064d1843273c2c300ba45c3f20bef02dbad225723bb59a9bb4b13535730961aeecf5a163ace477cceb0727025b99ac14a5166a09a3");
|
roots.back().identity = Identity("41cff2b17b:0:1f62d405a755ed8522e903edee7e2166fa357658a6cac8e6739b9e463898107538f09419e5cf5c2d139468c04ad1e0a08b59680ee2f07b2e50e670f81fd1dc3d");
|
||||||
roots.back().stableEndpoints.push_back(InetAddress("185.180.13.82/9993"));
|
roots.back().stableEndpoints.push_back(InetAddress("103.80.237.27/9993"));
|
||||||
roots.back().stableEndpoints.push_back(InetAddress("2a02:6ea0:c815::/9993"));
|
|
||||||
|
|
||||||
// Miami
|
// MVNET server2.saltis.id
|
||||||
roots.push_back(World::Root());
|
roots.push_back(World::Root());
|
||||||
roots.back().identity = Identity("de8950a8b2:0:1b3ada8251b91b6b6fa6535b8c7e2460918f4f729abdec97d3c7f3796868fb02f0de0b0ee554b2d59fc3524743eebfcf5315e790ed6d92db5bd10c28c09b40ef");
|
roots.back().identity = Identity("e4bfc89a12:0:93f764c53030b11c4b9f824cdade6fb731d26b5150f326ca484dbb3acb33a065d5431524fe7ce517512b13c21c4ab5edb04a2e145cefa33ffdf39a855b966c21");
|
||||||
roots.back().stableEndpoints.push_back(InetAddress("207.246.73.245/443"));
|
roots.back().stableEndpoints.push_back(InetAddress("103.80.237.163/9993"));
|
||||||
roots.back().stableEndpoints.push_back(InetAddress("2001:19f0:9002:5cb:ec4:7aff:fe8f:69d9/443"));
|
|
||||||
|
|
||||||
// Tokyo
|
// IDC server1a.saltis.id
|
||||||
roots.push_back(World::Root());
|
|
||||||
roots.back().identity = Identity("34e0a5e174:0:93efb50934788f856d5cfb9ca5be88e85b40965586b75befac900df77352c145a1ba7007569d37c77bfe52c0999f3bdc67a47a4a6000b720a883ce47aa2fb7f8");
|
|
||||||
roots.back().stableEndpoints.push_back(InetAddress("147.75.92.2/443"));
|
|
||||||
roots.back().stableEndpoints.push_back(InetAddress("2604:1380:3000:7100::1/443"));
|
|
||||||
|
|
||||||
// Amsterdam
|
|
||||||
roots.push_back(World::Root());
|
|
||||||
roots.back().identity = Identity("992fcf1db7:0:206ed59350b31916f749a1f85dffb3a8787dcbf83b8c6e9448d4e3ea0e3369301be716c3609344a9d1533850fb4460c50af43322bcfc8e13d3301a1f1003ceb6");
|
|
||||||
roots.back().stableEndpoints.push_back(InetAddress("195.181.173.159/443"));
|
|
||||||
roots.back().stableEndpoints.push_back(InetAddress("2a02:6ea0:c024::/443"));
|
|
||||||
|
|
||||||
// Alice
|
|
||||||
//roots.push_back(World::Root());
|
//roots.push_back(World::Root());
|
||||||
//roots.back().identity = Identity("9d219039f3:0:01f0922a98e3b34ebcbff333269dc265d7a020aab69d72be4d4acc9c8c9294785771256cd1d942a90d1bd1d2dca3ea84ef7d85afe6611fb43ff0b74126d90a6e");
|
//roots.back().identity = Identity("e620faa6a2:0:f450e2de69d0a70fa3183af50e881a96daac6a736fbb6004492cf97da774de0e003b060ec19e4c9b4288a60a99b4a60464b475ec2d1efb1b0c4a9b924b8f4bf1");
|
||||||
//roots.back().stableEndpoints.push_back(InetAddress("188.166.94.177/9993")); // Amsterdam
|
//roots.back().stableEndpoints.push_back(InetAddress("103.185.47.53/9993"));
|
||||||
|
|
||||||
|
// IDC server2a.saltis.id
|
||||||
|
roots.push_back(World::Root());
|
||||||
|
roots.back().identity = Identity("35b7a18f36:0:e7d0dc4f655c0a2adb72aa9529e97d0fdf1e72bf12a6e753c6b366d88b388b645702edbd8dfeda21c3654f2d73c28fecc7c4713b335ffa61b09e322a7b93c18a");
|
||||||
|
roots.back().stableEndpoints.push_back(InetAddress("103.185.47.52/9993"));
|
||||||
|
|
||||||
|
// GW-Bridge Metro Link
|
||||||
|
//roots.push_back(World::Root());
|
||||||
|
//roots.back().identity = Identity("117cd947d5:0:8b15bfe3798aa1529abf6def4d4d8eb3ff5cbd50c04ddaf7747adc00a55aef5b6666953ee30a2b6c3c852db7426c42760b7d26c63acdc6489fd11bb36690d966");
|
||||||
|
//roots.back().stableEndpoints.push_back(InetAddress("103.87.149.147/9993")); // Amsterdam
|
||||||
|
//roots.back().stableEndpoints.push_back(InetAddress("203.30.255.130/9993")); // Amsterdam
|
||||||
|
//roots.back().stableEndpoints.push_back(InetAddress("192.168.80.5/9993")); // Amsterdam
|
||||||
|
|
||||||
|
// GW-Bridge Metro Link
|
||||||
|
roots.push_back(World::Root());
|
||||||
|
roots.back().identity = Identity("82ff7e0148:0:01a99aac0d7084a59c03831517cdcd9ced2af58bc61de92ab45e0ea2ea86e2692935c84e9923dafce9675f3cc5dd46846d52b5f31e0e6726cb94884b9b6c5518");
|
||||||
|
roots.back().stableEndpoints.push_back(InetAddress("103.87.149.150/9993")); // Amsterdam
|
||||||
|
roots.back().stableEndpoints.push_back(InetAddress("192.168.80.5/9993")); // Amsterdam
|
||||||
|
|
||||||
//roots.back().stableEndpoints.push_back(InetAddress("2a03:b0c0:2:d0::7d:1/9993")); // Amsterdam
|
//roots.back().stableEndpoints.push_back(InetAddress("2a03:b0c0:2:d0::7d:1/9993")); // Amsterdam
|
||||||
//roots.back().stableEndpoints.push_back(InetAddress("154.66.197.33/9993")); // Johannesburg
|
//roots.back().stableEndpoints.push_back(InetAddress("154.66.197.33/9993")); // Johannesburg
|
||||||
//roots.back().stableEndpoints.push_back(InetAddress("2c0f:f850:154:197::33/9993")); // Johannesburg
|
//roots.back().stableEndpoints.push_back(InetAddress("2c0f:f850:154:197::33/9993")); // Johannesburg
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,199 @@
|
|||||||
|
# Deep Research: Fork ZeroTier 1.14.1 dengan Enkripsi Quantum-Resistant
|
||||||
|
|
||||||
|
**Tanggal:** 29 September 2026
|
||||||
|
**Target:** Fork ZeroTier One 1.14.1 (C++), tambahkan Post-Quantum Cryptography (PQC)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Ringkasan Eksekutif
|
||||||
|
|
||||||
|
ZeroTier 1.14.1 memakai kriptografi klasik: **Curve25519/Ed25519** (key agreement + signature), **Salsa20** (stream cipher), **Poly1305** (MAC). Semua ini **rusak oleh komputer kuantum** (Shor's algorithm untuk ECC, Grover untuk symmetric — Salsa20 256-bit masih aman dari Grover, tapi kunci yang bocor dari ECDH membuatnya ikut rusak).
|
||||||
|
|
||||||
|
Catatan penting: **ZeroTier, Inc. sudah merilis "ZeroTier Quantum"** (Maret 2026) — produk komersial terpisah berbasis **Rust**, dengan skema **ML-KEM-1024 (FIPS 203) + ML-DSA-87 (FIPS 204) + P-384 hybrid + AES-256-GCM**, sesuai **NSA CNSA 2.0**. Produk itu **bukan fork dari kode 1.14.1** dan tidak open-source di repo yang sama. Jadi fork 1.14.1 dengan PQC tetap pekerjaan yang valid — kita pakai prinsip desain yang sama (hybrid PQC) tapi di atas kode C++ yang ada.
|
||||||
|
|
||||||
|
**Rekomendasi inti: Hybrid PQC** — jalankan ML-KEM (Kyber) **paralel** dengan X25519, dan ML-DSA (Dilithium) **paralel** dengan Ed25519, lalu gabungkan output-nya. Keamanan tetap terjaga jika salah satu keluarga algoritma ternyata lemah.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Arsitektur Kriptografi ZeroTier 1.14.1 (Baseline)
|
||||||
|
|
||||||
|
Sumber: docs.zerotier.com/protocol, repo github.com/zerotier/ZeroTierOne
|
||||||
|
|
||||||
|
### 2.1 Titik-Titik Kriptografi di Kode
|
||||||
|
|
||||||
|
| Komponen | File | Fungsi |
|
||||||
|
|---|---|---|
|
||||||
|
| Identity (X25519/Ed25519 keypair) | `node/Identity.hpp/.cpp` | 40-bit ZT address diturunkan dari public key; PoW anti-collision |
|
||||||
|
| ECDH key agreement | `node/C25519.*` | Ephemeral key exchange antar peer |
|
||||||
|
| Signature (Ed25519) | `node/Salsa20.cpp` area / Ed25519 impl | Signing credential, certificate of membership |
|
||||||
|
| Packet encryption | `node/Salsa20.*`, NaCl-style box | Salsa20 encrypt-then-MAC Poly1305 |
|
||||||
|
| MAC | `node/Poly1305.*` | Autentikasi pesan |
|
||||||
|
| Credential signing | `node/CertificateOfMembership.*`, controller | Controller sign membership pakai secret key |
|
||||||
|
|
||||||
|
### 2.2 Alur Koneksi Peer (yang perlu dimodifikasi)
|
||||||
|
|
||||||
|
1. Node A kirim **HELLO** berisi identity public key (X25519 + Ed25519) — 40-bit address diturunkan dari hash public key.
|
||||||
|
2. Handshake **ECDH** menghasilkan shared secret → derive key Salsa20+Poly1305.
|
||||||
|
3. Semua payload VL1 di-encrypt end-to-end dengan kunci tsb.
|
||||||
|
4. Credential (certificate of membership, dll) ditandatangani Ed25519.
|
||||||
|
|
||||||
|
### 2.3 Batasan Penting
|
||||||
|
|
||||||
|
- **Tidak ada forward secrecy** (by design — lihat issue #204 GitHub).
|
||||||
|
- Salsa20/Poly1305 komposisi = NaCl reference (`crypto_box`).
|
||||||
|
- Trusted paths bisa skip enkripsi (jangan disentuh).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Standar Post-Quantum (NIST + NSA CNSA 2.0)
|
||||||
|
|
||||||
|
### 3.1 FIPS Final (Agustus 2024)
|
||||||
|
|
||||||
|
| Standar | Nama lama | Fungsi | Parameter relevan |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **FIPS 203 — ML-KEM** | CRYSTALS-Kyber | Key Encapsulation (ganti ECDH) | ML-KEM-512/768/1024 |
|
||||||
|
| **FIPS 204 — ML-DSA** | CRYSTALS-Dilithium | Digital signature (ganti Ed25519) | ML-DSA-44/65/87 |
|
||||||
|
| **FIPS 205 — SLH-DSA** | SPHINCS+ | Signature hash-based (backup) | SLH-DSA-128s/192s/256s |
|
||||||
|
|
||||||
|
### 3.2 CNSA 2.0 (NSA) — target yang dipakai ZeroTier Quantum
|
||||||
|
|
||||||
|
- **Key exchange:** ML-KEM-1024
|
||||||
|
- **Signature:** ML-DSA-87
|
||||||
|
- **Symmetric:** AES-256-GCM, HMAC-SHA-512
|
||||||
|
- **Classical hybrid:** P-384 (opsional tapi disarankan)
|
||||||
|
- Timeline: transisi wajib sampai **2033**; untuk data "harvest now, decrypt later" **mulai sekarang**.
|
||||||
|
|
||||||
|
### 3.3 Ukuran Parameter (penting untuk protocol overhead)
|
||||||
|
|
||||||
|
| Algoritma | Public key | Ciphertext/Signature |
|
||||||
|
|---|---|---|
|
||||||
|
| ML-KEM-768 | 1184 B | 1088 B |
|
||||||
|
| ML-KEM-1024 | 1568 B | 1568 B |
|
||||||
|
| ML-DSA-65 | 1952 B | 3293 B |
|
||||||
|
| ML-DSA-87 | 2592 B | 4627 B |
|
||||||
|
| X25519 (baseline) | 32 B | 32 B |
|
||||||
|
| Ed25519 (baseline) | 32 B | 64 B |
|
||||||
|
|
||||||
|
**Implikasi:** HELLO/identity packet harus diperbesar ~2-4 KB. Perlu cek MTU/fragmentasi packet VL1. Hybrid mode = gabung klasik+PQC (mis. XOR/KDF kedua shared secret).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Strategi Integrasi (untuk fork 1.14.1 C++)
|
||||||
|
|
||||||
|
### 4.1 Library PQC
|
||||||
|
|
||||||
|
**Pilihan utama: [liboqs](https://github.com/open-quantum-safe/liboqs)** (Open Quantum Safe project)
|
||||||
|
- Implementasi C, portable, sudah di-review luas
|
||||||
|
- Sediakan `oqs_kem_encaps/decaps` (ML-KEM) dan `oqs_sig_sign/verify` (ML-DSA)
|
||||||
|
- License: Apache-2.0 (kompatibel fork)
|
||||||
|
- Alternatif: `pqcrypto` (Rust, perlu FFI), implementasi reference NIST (C, FIPS-exact tapi lebih mentah)
|
||||||
|
|
||||||
|
Build: tambah liboqs sebagai git submodule / static lib di `make-linux.sh` / CMake.
|
||||||
|
|
||||||
|
### 4.2 Patch Per Titik Integrasi
|
||||||
|
|
||||||
|
#### A. Handshake Key Agreement (Wajib)
|
||||||
|
|
||||||
|
```
|
||||||
|
Handshake Lama: shared = X25519(my_eph_priv, their_eph_pub)
|
||||||
|
Handshake Baru: shared = KDF( X25519(...) ‖ ML-KEM-768/1024_encaps(...) )
|
||||||
|
```
|
||||||
|
|
||||||
|
- Tambah field baru di packet HELLO/KEY: `kem_public_key` (1184–1568 B) + `kem_ciphertext`.
|
||||||
|
- **Hybrid:** derive final key dari **kedua** shared secret via BLAKE2b/KDF — aman jika salah satu pecah.
|
||||||
|
- Mockup kode:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// node/PQHybrid.hpp
|
||||||
|
#include <oqs/oqs.h>
|
||||||
|
|
||||||
|
struct HybridShared {
|
||||||
|
uint8_t classical[32]; // X25519
|
||||||
|
uint8_t pq[KEM_LEN]; // ML-KEM shared secret
|
||||||
|
};
|
||||||
|
|
||||||
|
void hybrid_kem(const uint8_t* peer_kem_pk,
|
||||||
|
const uint8_t* my_eph_sk, const uint8_t* peer_eph_pk,
|
||||||
|
HybridShared& out, uint8_t* kem_ct) {
|
||||||
|
// classical
|
||||||
|
crypto_scalarmult(out.classical, my_eph_sk, peer_eph_pk);
|
||||||
|
// pq
|
||||||
|
OQS_KEM* kem = OQS_KEM_new("ML-KEM-768");
|
||||||
|
kem->encaps(out.pq, kem_ct, peer_kem_pk);
|
||||||
|
OQS_KEM_free(kem);
|
||||||
|
}
|
||||||
|
// final key = KDF(classical ‖ pq) -> masuk ke Salsa20/ChaCha20 key schedule
|
||||||
|
```
|
||||||
|
|
||||||
|
#### B. Identity & Signature (Wajib)
|
||||||
|
|
||||||
|
- Identity saat ini: X25519 + Ed25519, address 40-bit diturunkan dari hash public key.
|
||||||
|
- **Tambah ML-DSA-87 keypair** ke identity (field baru di `Identity` serialize).
|
||||||
|
- **Tantangan:** ukuran identity membengkak (2592 B pk + 4627 B sig per sign). Serialisasi `identity.public` di file dan di wire harus diperbesar — pakai **versioned format** (byte versi baru = identity v2, fallback ke v1 untuk backward compat).
|
||||||
|
- Signing credential controller: verify **kedua** signature (Ed25519 DAN ML-DSA) selama transisi (double-sign).
|
||||||
|
|
||||||
|
#### C. Packet Encryption (Opsional, rendah risiko)
|
||||||
|
|
||||||
|
- Salsa20 256-bit: Grover butuh ~2^128 — **masih aman** selama key-nya tidak bocor.
|
||||||
|
- Setelah A & B selesai, enkripsi data tidak wajib diganti. Tapi jika mau ikut CNSA 2.0: ganti ke **AES-256-GCM** (liboqs/OpenSSL sudah ada) — buang Poly1305. Ini pekerjaan terpisah, lakukan terakhir.
|
||||||
|
|
||||||
|
### 4.3 Versioning & Kompatibilitas
|
||||||
|
|
||||||
|
- Tambah **capability bit** di handshake: bit "PQC_SUPPORTED".
|
||||||
|
- Peer lawas (v1.14.1 vanilla) → fallback mode klasik saja.
|
||||||
|
- Peer baru → mode hybrid / pure-PQC, bisa dikonfigurasi via local.conf:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"settings": {
|
||||||
|
"pqcMode": "hybrid", // "off" | "hybrid" | "pqconly"
|
||||||
|
"kem": "ML-KEM-768",
|
||||||
|
"sig": "ML-DSA-65"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
*(Preset: `hybrid` = disarankan; `pqconly` = CNSA 2.0 strict dengan ML-KEM-1024/ML-DSA-87)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Roadmap Implementasi
|
||||||
|
|
||||||
|
| Fase | Item | Estimasi |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | Fork repo, tag `v1.14.1-pqc`, integrate liboqs, build pipeline | 1-2 hari |
|
||||||
|
| 2 | Patch handshake: hybrid X25519+ML-KEM, wire format v2 | 3-5 hari |
|
||||||
|
| 3 | Patch identity/signature: ML-DSA-87 double-sign, versioned serialize | 3-5 hari |
|
||||||
|
| 4 | Credential (COM) controller double-sign + verify | 2-3 hari |
|
||||||
|
| 5 | Config flag + negotiation + backward compat test | 2 hari |
|
||||||
|
| 6 | Test matrix: 2 node hybrid↔hybrid, hybrid↔vanilla, soak test throughput | 3-5 hari |
|
||||||
|
| 7 | (Opsional) AES-256-GCM ganti Salsa20 untuk CNSA penuh | 3-5 hari |
|
||||||
|
|
||||||
|
**Total realistis: 3-4 minggu** untuk 1 developer familiar dengan codebase.
|
||||||
|
|
||||||
|
### Risiko
|
||||||
|
|
||||||
|
1. **Ukuran packet** — identity/HELLO +~4 KB; cek fragmentasi MTU 1280/1500 dan L2 frame VL2.
|
||||||
|
2. **CPU overhead** — ML-KEM keygen ~2-3 ms (sekali per handshake, bukan per-packet); ML-DSA sign ~5-10 ms. Dampak kecil karena handshake jarang. Verifikasi dengan benchmark.
|
||||||
|
3. **Identity collision/derivation** — address 40-bit diturunkan dari public key; format baru harus tetap menjaga PoW.
|
||||||
|
4. **Audit kripto** — jangan deploy produksi tanpa review. liboqs = implementasi standard, tapi *pemakaian* (KDF hybrid, wire format) harus di-audit.
|
||||||
|
5. **Lisensi** — ZeroTier One: BSL 1.1 (Business Source License, Open Source Edition). Fork untuk pemakaian internal/non-komersial OK; cek klausa sebelum distribusi komersial.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Referensi
|
||||||
|
|
||||||
|
1. ZeroTier Protocol Docs — https://docs.zerotier.com/protocol/
|
||||||
|
2. ZeroTier Quantum (produk resmi) — https://www.zerotier.com/quantum/
|
||||||
|
3. Research Notes on 2.x Cryptography — https://www.zerotier.com/news/research-notes-on-2-x-cryptography/
|
||||||
|
4. ZeroTierOne source — https://github.com/zerotier/ZeroTierOne
|
||||||
|
5. NIST FIPS 203/204/205 (ML-KEM, ML-DSA, SLH-DSA) — Aug 2024
|
||||||
|
6. NSA CNSA 2.0 FAQ — https://media.defense.gov/2022/Sep/07/2003071834/-1/-1/0/CSA_CNSA_2.0_ALGORITHMS_.PDF
|
||||||
|
7. liboqs (Open Quantum Safe) — https://github.com/open-quantum-safe/liboqs
|
||||||
|
8. ZeroTier blog: "Defusing the Q-Bomb" — ML-KEM untuk key establishment, ML-DSA untuk signature
|
||||||
|
9. ZeroTier blog: "NIST FIPS 203 Explained for Defense and Government Network Architects"
|
||||||
|
10. IETF hybrid KEM design — draft-ietf-tls-hybrid-design; NIST IR 8547 (migration guidance)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Disusun oleh Luci (OpenClaw) — 29 Sep 2026*
|
||||||
Binary file not shown.
+3
-9
@@ -1,5 +1,5 @@
|
|||||||
Name: backone
|
Name: backone
|
||||||
Version: 1.17.1
|
Version: 2.0.0
|
||||||
Release: 1%{?dist}
|
Release: 1%{?dist}
|
||||||
Summary: BackOne global ethernet switch
|
Summary: BackOne global ethernet switch
|
||||||
|
|
||||||
@@ -155,14 +155,8 @@ chmod 0755 $RPM_BUILD_ROOT/etc/init.d/backone
|
|||||||
%endif
|
%endif
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
* Tue Jun 30 2026 Dedy Sutanto <dsutanto@backone.cloud> - 1.17.1
|
* Thu Oct 1 2026 Dedy Sutanto <dsutanto@backone.cloud> - 2.0.0
|
||||||
- Fix /api/backone routes and reinstallable Web UI packages
|
- see https://github.com/proitlab/BackOne for release notes
|
||||||
|
|
||||||
* Tue Jun 30 2026 Dedy Sutanto <dsutanto@backone.cloud> - 1.17.0
|
|
||||||
- Tray app, auto web server, peer contact UI
|
|
||||||
|
|
||||||
* Tue Jun 30 2026 Dedy Sutanto <dsutanto@backone.cloud> - 1.16.0
|
|
||||||
- Release notes
|
|
||||||
|
|
||||||
* Tue Oct 8 2024 Dedy Sutanto <dsutanto@backone.cloud> - 1.14.1
|
* Tue Oct 8 2024 Dedy Sutanto <dsutanto@backone.cloud> - 1.14.1
|
||||||
- see https://github.com/proitlab/BackOne for release notes
|
- see https://github.com/proitlab/BackOne for release notes
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
if [ $# -lt 1 ]; then
|
|
||||||
echo "Usage: $0 <version> [changelog message]" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
VERSION="$1"
|
|
||||||
MESSAGE="${2:-see https://github.com/proitlab/BackOne for release notes}"
|
|
||||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
|
||||||
IFS='.' read -r MAJ MIN REV <<< "${VERSION%%-*}"
|
|
||||||
REV="${REV:-0}"
|
|
||||||
|
|
||||||
sed -i "s/^#define ZEROTIER_ONE_VERSION_MAJOR .*/#define ZEROTIER_ONE_VERSION_MAJOR ${MAJ}/" "$ROOT/version.h"
|
|
||||||
sed -i "s/^#define ZEROTIER_ONE_VERSION_MINOR .*/#define ZEROTIER_ONE_VERSION_MINOR ${MIN}/" "$ROOT/version.h"
|
|
||||||
sed -i "s/^#define ZEROTIER_ONE_VERSION_REVISION .*/#define ZEROTIER_ONE_VERSION_REVISION ${REV}/" "$ROOT/version.h"
|
|
||||||
sed -i "s/^#define ZEROTIER_ONE_VERSION_BUILD .*/#define ZEROTIER_ONE_VERSION_BUILD 0/" "$ROOT/version.h"
|
|
||||||
|
|
||||||
echo "$VERSION" > "$ROOT/debian/version"
|
|
||||||
|
|
||||||
"$ROOT/ci/scripts/munge_debian_changelog.sh" "$ROOT/debian/changelog" "$VERSION" \
|
|
||||||
"Dedy Sutanto <dsutanto@backone.cloud>" "$MESSAGE"
|
|
||||||
|
|
||||||
"$ROOT/ci/scripts/munge_rpm_spec.sh" "$ROOT/backone.spec" "$VERSION" \
|
|
||||||
"Dedy Sutanto <dsutanto@backone.cloud>" "$MESSAGE"
|
|
||||||
|
|
||||||
for addon in comms-core webhub voice video chat secure-chat updater app-tray; do
|
|
||||||
if [ -f "$ROOT/addons/$addon/addon.json" ]; then
|
|
||||||
sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"${VERSION}\"/" "$ROOT/addons/$addon/addon.json"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Bumped to ${VERSION}"
|
|
||||||
@@ -31,7 +31,7 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
awk -v version=${VERSION} -v date=${DATE} -v name=${NAME} -v message=${MESSAGE} \
|
awk -v version=${VERSION} -v date=${DATE} -v name=${NAME} -v message=${MESSAGE} \
|
||||||
'BEGIN{print "backone (" version ") stable; urgency=medium\n\n * " message "\n\n -- " name " " date "\n" }{ print }' \
|
'BEGIN{print "zerotier-one (" version ") stable; urgency=medium\n\n * " message "\n\n -- " name " " date "\n" }{ print }' \
|
||||||
${FILE} > ${FILE}.new
|
${FILE} > ${FILE}.new
|
||||||
|
|
||||||
mv ${FILE}.new ${FILE}
|
mv ${FILE}.new ${FILE}
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# Keep ~/.backOneOneAuthToken and /var/lib/backone/authtoken.secret in sync (same as backone-cli).
|
|
||||||
set -e
|
|
||||||
|
|
||||||
BACKONE_HOME="${BACKONE_HOME:-/var/lib/backone}"
|
|
||||||
SECRET="${BACKONE_HOME}/authtoken.secret"
|
|
||||||
|
|
||||||
trim_token() {
|
|
||||||
tr -d '\n\r \t' < "$1"
|
|
||||||
}
|
|
||||||
|
|
||||||
sync_for_user() {
|
|
||||||
user="$1"
|
|
||||||
home="$2"
|
|
||||||
[ -n "$user" ] && [ -n "$home" ] && [ -d "$home" ] || return 0
|
|
||||||
user_token="${home}/.backOneOneAuthToken"
|
|
||||||
|
|
||||||
if [ -f "$user_token" ] && [ -s "$user_token" ]; then
|
|
||||||
ut="$(trim_token "$user_token")"
|
|
||||||
[ -n "$ut" ] || return 0
|
|
||||||
st=""
|
|
||||||
if [ -f "$SECRET" ]; then
|
|
||||||
st="$(trim_token "$SECRET" 2>/dev/null || true)"
|
|
||||||
fi
|
|
||||||
if [ "$ut" != "$st" ]; then
|
|
||||||
printf '%s' "$ut" > "$SECRET"
|
|
||||||
chown backone:backone "$SECRET" 2>/dev/null || true
|
|
||||||
chmod 0600 "$SECRET" 2>/dev/null || true
|
|
||||||
echo "Synced ${SECRET} from ${user_token}"
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f "$SECRET" ]; then
|
|
||||||
install -o "$user" -g "$(id -gn "$user" 2>/dev/null || echo "$user")" -m 0600 "$SECRET" "$user_token" 2>/dev/null \
|
|
||||||
|| cp "$SECRET" "$user_token"
|
|
||||||
chown "$user":"$(id -gn "$user" 2>/dev/null || echo "$user")" "$user_token" 2>/dev/null || true
|
|
||||||
chmod 0600 "$user_token" 2>/dev/null || true
|
|
||||||
echo "Installed ${user_token} from ${SECRET}"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
if [ -n "${SUDO_USER:-}" ] && [ "$SUDO_USER" != "root" ]; then
|
|
||||||
home="$(getent passwd "$SUDO_USER" | cut -d: -f6)"
|
|
||||||
sync_for_user "$SUDO_USER" "$home"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "${USER:-}" ] && [ "$USER" != "root" ] && [ "$USER" != "${SUDO_USER:-}" ]; then
|
|
||||||
home="$(getent passwd "$USER" | cut -d: -f6)"
|
|
||||||
sync_for_user "$USER" "$home"
|
|
||||||
fi
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
@@ -1,405 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c)2019 ZeroTier, Inc.
|
|
||||||
*
|
|
||||||
* Use of this software is governed by the Business Source License included
|
|
||||||
* in the LICENSE.TXT file in the project's root directory.
|
|
||||||
*/
|
|
||||||
/****/
|
|
||||||
|
|
||||||
#include "CommsController.hpp"
|
|
||||||
|
|
||||||
#include "../node/Mutex.hpp"
|
|
||||||
#include "../node/Utils.hpp"
|
|
||||||
#include "../osdep/OSUtils.hpp"
|
|
||||||
|
|
||||||
#include <map>
|
|
||||||
#include <set>
|
|
||||||
#include <sstream>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace ZeroTier {
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
struct IceEntry
|
|
||||||
{
|
|
||||||
std::string fromPeerId;
|
|
||||||
std::string candidate;
|
|
||||||
std::string sdpMid;
|
|
||||||
int sdpMLineIndex;
|
|
||||||
int64_t at;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct CallState
|
|
||||||
{
|
|
||||||
std::string callId;
|
|
||||||
std::string callType;
|
|
||||||
std::string networkId;
|
|
||||||
std::string callerId;
|
|
||||||
std::string calleeId;
|
|
||||||
std::string callerName;
|
|
||||||
std::string state;
|
|
||||||
std::string offerSdp;
|
|
||||||
std::string answerSdp;
|
|
||||||
int64_t createdAt;
|
|
||||||
};
|
|
||||||
|
|
||||||
Mutex g_commsMutex;
|
|
||||||
std::map<std::string, CallState> g_calls;
|
|
||||||
std::map<std::string, std::vector<IceEntry> > g_iceByCall;
|
|
||||||
std::map<std::string, std::set<std::string> > g_callsByPeer;
|
|
||||||
|
|
||||||
std::string lowerId(const std::string &id)
|
|
||||||
{
|
|
||||||
std::string out = id;
|
|
||||||
for (std::string::iterator i(out.begin()); i != out.end(); ++i) {
|
|
||||||
if (*i >= 'A' && *i <= 'F') {
|
|
||||||
*i = (char)(*i - 'A' + 'a');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string newCallId()
|
|
||||||
{
|
|
||||||
uint8_t randBytes[8];
|
|
||||||
Utils::getSecureRandom(randBytes, sizeof(randBytes));
|
|
||||||
char randHex[17];
|
|
||||||
Utils::hex(randBytes, (unsigned int)sizeof(randBytes), randHex);
|
|
||||||
return std::string("call_") + std::to_string(OSUtils::now()) + "_" + randHex;
|
|
||||||
}
|
|
||||||
|
|
||||||
void trackCallForPeer(const std::string &peerId, const std::string &callId)
|
|
||||||
{
|
|
||||||
if (peerId.empty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
g_callsByPeer[peerId].insert(callId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void untrackCall(const CallState &call)
|
|
||||||
{
|
|
||||||
const std::string caller = lowerId(call.callerId);
|
|
||||||
const std::string callee = lowerId(call.calleeId);
|
|
||||||
if (!caller.empty()) {
|
|
||||||
g_callsByPeer[caller].erase(call.callId);
|
|
||||||
if (g_callsByPeer[caller].empty()) {
|
|
||||||
g_callsByPeer.erase(caller);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!callee.empty()) {
|
|
||||||
g_callsByPeer[callee].erase(call.callId);
|
|
||||||
if (g_callsByPeer[callee].empty()) {
|
|
||||||
g_callsByPeer.erase(callee);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void purgeCall(const std::string &callId)
|
|
||||||
{
|
|
||||||
std::map<std::string, CallState>::iterator it = g_calls.find(callId);
|
|
||||||
if (it != g_calls.end()) {
|
|
||||||
untrackCall(it->second);
|
|
||||||
g_calls.erase(it);
|
|
||||||
}
|
|
||||||
g_iceByCall.erase(callId);
|
|
||||||
}
|
|
||||||
|
|
||||||
nlohmann::json callToIncomingJson(const CallState &call)
|
|
||||||
{
|
|
||||||
nlohmann::json out = nlohmann::json::object();
|
|
||||||
out["callId"] = call.callId;
|
|
||||||
out["callType"] = call.callType;
|
|
||||||
out["networkId"] = call.networkId;
|
|
||||||
out["callerId"] = call.callerId;
|
|
||||||
out["callerName"] = call.callerName;
|
|
||||||
out["sdp"] = call.offerSdp;
|
|
||||||
out["state"] = call.state;
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
nlohmann::json webrtcConfigFromLocal(const nlohmann::json &localConfig)
|
|
||||||
{
|
|
||||||
nlohmann::json out = nlohmann::json::object();
|
|
||||||
out["icePreferOverlay"] = true;
|
|
||||||
out["iceFallbackTimeoutMs"] = 8000;
|
|
||||||
out["webhubPort"] = 9994;
|
|
||||||
out["iceServers"] = nlohmann::json::array();
|
|
||||||
out["iceServers"].push_back(nlohmann::json::object({ { "urls", "stun:stun.l.google.com:19302" } }));
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (localConfig.contains("settings") && localConfig["settings"].is_object()) {
|
|
||||||
const nlohmann::json &settings = localConfig["settings"];
|
|
||||||
if (settings.contains("icePreferOverlay") && settings["icePreferOverlay"].is_boolean()) {
|
|
||||||
out["icePreferOverlay"] = settings["icePreferOverlay"].get<bool>();
|
|
||||||
}
|
|
||||||
if (settings.contains("iceFallbackTimeoutMs") && settings["iceFallbackTimeoutMs"].is_number_integer()) {
|
|
||||||
out["iceFallbackTimeoutMs"] = settings["iceFallbackTimeoutMs"].get<int64_t>();
|
|
||||||
}
|
|
||||||
if (settings.contains("webhubPort") && settings["webhubPort"].is_number_integer()) {
|
|
||||||
out["webhubPort"] = settings["webhubPort"].get<int64_t>();
|
|
||||||
}
|
|
||||||
if (settings.contains("iceServers") && settings["iceServers"].is_array()) {
|
|
||||||
out["iceServers"] = settings["iceServers"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch ( ... ) {
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
void CommsController::configureRoutes(
|
|
||||||
httplib::Server &s,
|
|
||||||
httplib::Server &sv6,
|
|
||||||
const std::function<nlohmann::json()> &localConfigGetter,
|
|
||||||
const std::function<void(const httplib::Request&, httplib::Response&, std::string)> &setContent)
|
|
||||||
{
|
|
||||||
auto webrtcConfigGet = [localConfigGetter, setContent](const httplib::Request &req, httplib::Response &res) {
|
|
||||||
nlohmann::json localConfig = nlohmann::json::object();
|
|
||||||
try {
|
|
||||||
localConfig = localConfigGetter();
|
|
||||||
} catch ( ... ) {
|
|
||||||
}
|
|
||||||
setContent(req, res, webrtcConfigFromLocal(localConfig).dump());
|
|
||||||
};
|
|
||||||
s.Get("/app/api/webrtc-config", webrtcConfigGet);
|
|
||||||
sv6.Get("/app/api/webrtc-config", webrtcConfigGet);
|
|
||||||
|
|
||||||
auto callCreate = [setContent](const httplib::Request &req, httplib::Response &res) {
|
|
||||||
try {
|
|
||||||
nlohmann::json body = OSUtils::jsonParse(req.body);
|
|
||||||
if (!body.is_object()) {
|
|
||||||
res.status = 400;
|
|
||||||
setContent(req, res, "{\"error\":\"body must be a JSON object\"}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const std::string targetId = lowerId(body.contains("targetId") && body["targetId"].is_string()
|
|
||||||
? body["targetId"].get<std::string>() : "");
|
|
||||||
const std::string sdp = body.contains("sdp") && body["sdp"].is_string()
|
|
||||||
? body["sdp"].get<std::string>() : "";
|
|
||||||
if (targetId.empty() || sdp.empty()) {
|
|
||||||
res.status = 400;
|
|
||||||
setContent(req, res, "{\"error\":\"targetId and sdp are required\"}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
std::string callType = "voice";
|
|
||||||
if (body.contains("callType") && body["callType"].is_string()) {
|
|
||||||
callType = body["callType"].get<std::string>();
|
|
||||||
} else if (body.contains("type") && body["type"].is_string()) {
|
|
||||||
const std::string t = body["type"].get<std::string>();
|
|
||||||
if (t == "voice" || t == "video") {
|
|
||||||
callType = t;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const std::string networkId = body.contains("networkId") && body["networkId"].is_string()
|
|
||||||
? lowerId(body["networkId"].get<std::string>()) : "";
|
|
||||||
const std::string callerId = body.contains("callerId") && body["callerId"].is_string()
|
|
||||||
? lowerId(body["callerId"].get<std::string>()) : "local";
|
|
||||||
const std::string callerName = body.contains("callerName") && body["callerName"].is_string()
|
|
||||||
? body["callerName"].get<std::string>() : callerId;
|
|
||||||
|
|
||||||
CallState call;
|
|
||||||
call.callId = newCallId();
|
|
||||||
call.callType = callType;
|
|
||||||
call.networkId = networkId;
|
|
||||||
call.callerId = callerId;
|
|
||||||
call.calleeId = targetId;
|
|
||||||
call.callerName = callerName;
|
|
||||||
call.state = "ringing";
|
|
||||||
call.offerSdp = sdp;
|
|
||||||
call.createdAt = OSUtils::now();
|
|
||||||
|
|
||||||
Mutex::Lock _l(g_commsMutex);
|
|
||||||
g_calls[call.callId] = call;
|
|
||||||
trackCallForPeer(call.callerId, call.callId);
|
|
||||||
trackCallForPeer(call.calleeId, call.callId);
|
|
||||||
|
|
||||||
nlohmann::json out = nlohmann::json::object();
|
|
||||||
out["callId"] = call.callId;
|
|
||||||
out["state"] = call.state;
|
|
||||||
setContent(req, res, out.dump());
|
|
||||||
} catch ( ... ) {
|
|
||||||
res.status = 400;
|
|
||||||
setContent(req, res, "{\"error\":\"invalid JSON\"}");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
s.Post("/controller/comms/call", callCreate);
|
|
||||||
sv6.Post("/controller/comms/call", callCreate);
|
|
||||||
|
|
||||||
auto callIncoming = [setContent](const httplib::Request &req, httplib::Response &res) {
|
|
||||||
const std::string peerId = lowerId(req.has_param("peerId") ? req.get_param_value("peerId") : "");
|
|
||||||
if (peerId.empty()) {
|
|
||||||
res.status = 400;
|
|
||||||
setContent(req, res, "{\"error\":\"peerId query parameter is required\"}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
nlohmann::json calls = nlohmann::json::array();
|
|
||||||
Mutex::Lock _l(g_commsMutex);
|
|
||||||
std::map<std::string, std::set<std::string> >::const_iterator it = g_callsByPeer.find(peerId);
|
|
||||||
if (it != g_callsByPeer.end()) {
|
|
||||||
for (std::set<std::string>::const_iterator c = it->second.begin(); c != it->second.end(); ++c) {
|
|
||||||
std::map<std::string, CallState>::const_iterator callIt = g_calls.find(*c);
|
|
||||||
if (callIt == g_calls.end()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (lowerId(callIt->second.calleeId) != peerId || callIt->second.state != "ringing") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
calls.push_back(callToIncomingJson(callIt->second));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
nlohmann::json out = nlohmann::json::object();
|
|
||||||
out["calls"] = calls;
|
|
||||||
setContent(req, res, out.dump());
|
|
||||||
};
|
|
||||||
s.Get("/controller/comms/call/incoming", callIncoming);
|
|
||||||
sv6.Get("/controller/comms/call/incoming", callIncoming);
|
|
||||||
|
|
||||||
auto callAnswer = [setContent](const httplib::Request &req, httplib::Response &res) {
|
|
||||||
const std::string callId = req.matches[1];
|
|
||||||
try {
|
|
||||||
nlohmann::json body = OSUtils::jsonParse(req.body);
|
|
||||||
const std::string sdp = body.contains("sdp") && body["sdp"].is_string()
|
|
||||||
? body["sdp"].get<std::string>() : "";
|
|
||||||
if (sdp.empty()) {
|
|
||||||
res.status = 400;
|
|
||||||
setContent(req, res, "{\"error\":\"sdp is required\"}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Mutex::Lock _l(g_commsMutex);
|
|
||||||
std::map<std::string, CallState>::iterator it = g_calls.find(callId);
|
|
||||||
if (it == g_calls.end()) {
|
|
||||||
res.status = 404;
|
|
||||||
setContent(req, res, "{\"error\":\"call not found\"}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
it->second.answerSdp = sdp;
|
|
||||||
it->second.state = "accepted";
|
|
||||||
nlohmann::json out = nlohmann::json::object();
|
|
||||||
out["callId"] = callId;
|
|
||||||
out["state"] = "accepted";
|
|
||||||
out["sdp"] = sdp;
|
|
||||||
setContent(req, res, out.dump());
|
|
||||||
} catch ( ... ) {
|
|
||||||
res.status = 400;
|
|
||||||
setContent(req, res, "{\"error\":\"invalid JSON\"}");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
s.Post("/controller/comms/call/([^/]+)/answer", callAnswer);
|
|
||||||
sv6.Post("/controller/comms/call/([^/]+)/answer", callAnswer);
|
|
||||||
|
|
||||||
auto callReject = [setContent](const httplib::Request &req, httplib::Response &res) {
|
|
||||||
const std::string callId = req.matches[1];
|
|
||||||
Mutex::Lock _l(g_commsMutex);
|
|
||||||
purgeCall(callId);
|
|
||||||
nlohmann::json out = nlohmann::json::object();
|
|
||||||
out["callId"] = callId;
|
|
||||||
out["state"] = "rejected";
|
|
||||||
setContent(req, res, out.dump());
|
|
||||||
};
|
|
||||||
s.Post("/controller/comms/call/([^/]+)/reject", callReject);
|
|
||||||
sv6.Post("/controller/comms/call/([^/]+)/reject", callReject);
|
|
||||||
|
|
||||||
auto callEnd = [setContent](const httplib::Request &req, httplib::Response &res) {
|
|
||||||
const std::string callId = req.matches[1];
|
|
||||||
Mutex::Lock _l(g_commsMutex);
|
|
||||||
purgeCall(callId);
|
|
||||||
nlohmann::json out = nlohmann::json::object();
|
|
||||||
out["callId"] = callId;
|
|
||||||
out["state"] = "ended";
|
|
||||||
setContent(req, res, out.dump());
|
|
||||||
};
|
|
||||||
s.Post("/controller/comms/call/([^/]+)/end", callEnd);
|
|
||||||
sv6.Post("/controller/comms/call/([^/]+)/end", callEnd);
|
|
||||||
|
|
||||||
auto callIcePost = [setContent](const httplib::Request &req, httplib::Response &res) {
|
|
||||||
const std::string callId = req.matches[1];
|
|
||||||
try {
|
|
||||||
nlohmann::json body = OSUtils::jsonParse(req.body);
|
|
||||||
IceEntry entry;
|
|
||||||
entry.fromPeerId = body.contains("fromPeerId") && body["fromPeerId"].is_string()
|
|
||||||
? lowerId(body["fromPeerId"].get<std::string>()) : "";
|
|
||||||
entry.candidate = body.contains("candidate") && body["candidate"].is_string()
|
|
||||||
? body["candidate"].get<std::string>() : "";
|
|
||||||
entry.sdpMid = body.contains("sdpMid") && body["sdpMid"].is_string()
|
|
||||||
? body["sdpMid"].get<std::string>() : "";
|
|
||||||
entry.sdpMLineIndex = body.contains("sdpMLineIndex") && body["sdpMLineIndex"].is_number_integer()
|
|
||||||
? (int)body["sdpMLineIndex"].get<int64_t>() : 0;
|
|
||||||
entry.at = OSUtils::now();
|
|
||||||
if (entry.candidate.empty()) {
|
|
||||||
res.status = 400;
|
|
||||||
setContent(req, res, "{\"error\":\"candidate is required\"}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Mutex::Lock _l(g_commsMutex);
|
|
||||||
g_iceByCall[callId].push_back(entry);
|
|
||||||
nlohmann::json out = nlohmann::json::object();
|
|
||||||
out["ok"] = true;
|
|
||||||
setContent(req, res, out.dump());
|
|
||||||
} catch ( ... ) {
|
|
||||||
res.status = 400;
|
|
||||||
setContent(req, res, "{\"error\":\"invalid JSON\"}");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
s.Post("/controller/comms/call/([^/]+)/ice", callIcePost);
|
|
||||||
sv6.Post("/controller/comms/call/([^/]+)/ice", callIcePost);
|
|
||||||
|
|
||||||
auto callIceGet = [setContent](const httplib::Request &req, httplib::Response &res) {
|
|
||||||
const std::string callId = req.matches[1];
|
|
||||||
int64_t since = 0;
|
|
||||||
if (req.has_param("since")) {
|
|
||||||
try {
|
|
||||||
since = std::stoll(req.get_param_value("since"));
|
|
||||||
} catch ( ... ) {
|
|
||||||
since = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const std::string peerId = lowerId(req.has_param("peerId") ? req.get_param_value("peerId") : "");
|
|
||||||
nlohmann::json candidates = nlohmann::json::array();
|
|
||||||
Mutex::Lock _l(g_commsMutex);
|
|
||||||
std::map<std::string, std::vector<IceEntry> >::const_iterator it = g_iceByCall.find(callId);
|
|
||||||
if (it != g_iceByCall.end()) {
|
|
||||||
for (std::vector<IceEntry>::const_iterator e = it->second.begin(); e != it->second.end(); ++e) {
|
|
||||||
if (e->at <= since) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!peerId.empty() && e->fromPeerId == peerId) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
nlohmann::json item = nlohmann::json::object();
|
|
||||||
item["candidate"] = e->candidate;
|
|
||||||
item["sdpMid"] = e->sdpMid;
|
|
||||||
item["sdpMLineIndex"] = e->sdpMLineIndex;
|
|
||||||
item["at"] = e->at;
|
|
||||||
candidates.push_back(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
nlohmann::json out = nlohmann::json::object();
|
|
||||||
out["candidates"] = candidates;
|
|
||||||
setContent(req, res, out.dump());
|
|
||||||
};
|
|
||||||
s.Get("/controller/comms/call/([^/]+)/ice", callIceGet);
|
|
||||||
sv6.Get("/controller/comms/call/([^/]+)/ice", callIceGet);
|
|
||||||
|
|
||||||
auto callGet = [setContent](const httplib::Request &req, httplib::Response &res) {
|
|
||||||
const std::string callId = req.matches[1];
|
|
||||||
Mutex::Lock _l(g_commsMutex);
|
|
||||||
std::map<std::string, CallState>::const_iterator it = g_calls.find(callId);
|
|
||||||
if (it == g_calls.end()) {
|
|
||||||
res.status = 404;
|
|
||||||
setContent(req, res, "{\"error\":\"call not found\"}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
nlohmann::json out = callToIncomingJson(it->second);
|
|
||||||
if (it->second.state == "accepted" && !it->second.answerSdp.empty()) {
|
|
||||||
out["answerSdp"] = it->second.answerSdp;
|
|
||||||
}
|
|
||||||
setContent(req, res, out.dump());
|
|
||||||
};
|
|
||||||
s.Get("/controller/comms/call/([^/]+)", callGet);
|
|
||||||
sv6.Get("/controller/comms/call/([^/]+)", callGet);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace ZeroTier
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c)2019 ZeroTier, Inc.
|
|
||||||
*
|
|
||||||
* Use of this software is governed by the Business Source License included
|
|
||||||
* in the LICENSE.TXT file in the project's root directory.
|
|
||||||
*/
|
|
||||||
/****/
|
|
||||||
|
|
||||||
#ifndef ZT_COMMSCONTROLLER_HPP
|
|
||||||
#define ZT_COMMSCONTROLLER_HPP
|
|
||||||
|
|
||||||
#include <functional>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
#include <cpp-httplib/httplib.h>
|
|
||||||
#include <nlohmann/json.hpp>
|
|
||||||
|
|
||||||
namespace ZeroTier {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ephemeral in-memory WebRTC signaling fallback and webrtc-config API.
|
|
||||||
*/
|
|
||||||
class CommsController
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
void configureRoutes(
|
|
||||||
httplib::Server &s,
|
|
||||||
httplib::Server &sv6,
|
|
||||||
const std::function<nlohmann::json()> &localConfigGetter,
|
|
||||||
const std::function<void(const httplib::Request&, httplib::Response&, std::string)> &setContent);
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace ZeroTier
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1120,12 +1120,6 @@ void EmbeddedNetworkController::configureHTTPControlPlane(
|
|||||||
};
|
};
|
||||||
s.Get(memberListPath2, memberListGet2);
|
s.Get(memberListPath2, memberListGet2);
|
||||||
sv6.Get(memberListPath2, memberListGet2);
|
sv6.Get(memberListPath2, memberListGet2);
|
||||||
std::string appMemberListPath = "/app/api/network/([0-9a-fA-F]{16})/members";
|
|
||||||
s.Get(appMemberListPath, memberListGet2);
|
|
||||||
sv6.Get(appMemberListPath, memberListGet2);
|
|
||||||
std::string backoneMemberListPath = "/api/backone/network/([0-9a-fA-F]{16})/members";
|
|
||||||
s.Get(backoneMemberListPath, memberListGet2);
|
|
||||||
sv6.Get(backoneMemberListPath, memberListGet2);
|
|
||||||
|
|
||||||
auto memberGet = [&, setContent](const httplib::Request &req, httplib::Response &res) {
|
auto memberGet = [&, setContent](const httplib::Request &req, httplib::Response &res) {
|
||||||
auto networkID = req.matches[1];
|
auto networkID = req.matches[1];
|
||||||
@@ -1460,7 +1454,7 @@ void EmbeddedNetworkController::_request(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If we do not yet know this member's identity, learn it.
|
// If we do not yet know this member's identity, learn it.
|
||||||
char idtmp[1024];
|
char idtmp[ZT_IDENTITY_STRING_BUFFER_LENGTH];
|
||||||
member["identity"] = identity.toString(false,idtmp);
|
member["identity"] = identity.toString(false,idtmp);
|
||||||
}
|
}
|
||||||
#ifdef CENTRAL_CONTROLLER_REQUEST_BENCHMARK
|
#ifdef CENTRAL_CONTROLLER_REQUEST_BENCHMARK
|
||||||
|
|||||||
+13
-2
@@ -380,12 +380,23 @@ bool LFDB::save(nlohmann::json &record,bool notifyListeners)
|
|||||||
|
|
||||||
void LFDB::eraseNetwork(const uint64_t networkId)
|
void LFDB::eraseNetwork(const uint64_t networkId)
|
||||||
{
|
{
|
||||||
// TODO
|
nlohmann::json network, nullJson;
|
||||||
|
get(networkId, network);
|
||||||
|
_networkChanged(network, nullJson, true);
|
||||||
|
std::lock_guard<std::mutex> l(_state_l);
|
||||||
|
_state.erase(networkId);
|
||||||
}
|
}
|
||||||
|
|
||||||
void LFDB::eraseMember(const uint64_t networkId,const uint64_t memberId)
|
void LFDB::eraseMember(const uint64_t networkId,const uint64_t memberId)
|
||||||
{
|
{
|
||||||
// TODO
|
nlohmann::json network, member, nullJson;
|
||||||
|
get(networkId, network, memberId, member);
|
||||||
|
_memberChanged(member, nullJson, true);
|
||||||
|
std::lock_guard<std::mutex> l(_state_l);
|
||||||
|
auto nw = _state.find(networkId);
|
||||||
|
if (nw != _state.end()) {
|
||||||
|
nw->second.members.erase(memberId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void LFDB::nodeIsOnline(const uint64_t networkId,const uint64_t memberId,const InetAddress &physicalAddress)
|
void LFDB::nodeIsOnline(const uint64_t networkId,const uint64_t memberId,const InetAddress &physicalAddress)
|
||||||
|
|||||||
@@ -199,8 +199,13 @@ PostgreSQL::PostgreSQL(const Identity &myId, const char *path, int listenPort, R
|
|||||||
}
|
}
|
||||||
const char *redisMemberStatus = getenv("ZT_REDIS_MEMBER_STATUS");
|
const char *redisMemberStatus = getenv("ZT_REDIS_MEMBER_STATUS");
|
||||||
if (redisMemberStatus && (strcmp(redisMemberStatus, "true") == 0)) {
|
if (redisMemberStatus && (strcmp(redisMemberStatus, "true") == 0)) {
|
||||||
_redisMemberStatus = true;
|
if (_rc != NULL) {
|
||||||
fprintf(stderr, "Using redis for member status\n");
|
_redisMemberStatus = true;
|
||||||
|
fprintf(stderr, "Using redis for member status\n");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
fprintf(stderr, "ZT_REDIS_MEMBER_STATUS=true ignored: no settings.redis config\n");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto c = _pool->borrow();
|
auto c = _pool->borrow();
|
||||||
@@ -222,7 +227,7 @@ PostgreSQL::PostgreSQL(const Identity &myId, const char *path, int listenPort, R
|
|||||||
opts.host = _rc->hostname;
|
opts.host = _rc->hostname;
|
||||||
opts.port = _rc->port;
|
opts.port = _rc->port;
|
||||||
opts.password = _rc->password;
|
opts.password = _rc->password;
|
||||||
opts.db = 0;
|
opts.db = _rc->clusterMode ? 0 : _rc->db;
|
||||||
opts.keep_alive = true;
|
opts.keep_alive = true;
|
||||||
opts.connect_timeout = std::chrono::seconds(3);
|
opts.connect_timeout = std::chrono::seconds(3);
|
||||||
poolOpts.size = 25;
|
poolOpts.size = 25;
|
||||||
@@ -1063,7 +1068,7 @@ void PostgreSQL::initializeMembers()
|
|||||||
|
|
||||||
void PostgreSQL::heartbeat()
|
void PostgreSQL::heartbeat()
|
||||||
{
|
{
|
||||||
char publicId[1024];
|
char publicId[ZT_IDENTITY_STRING_BUFFER_LENGTH];
|
||||||
char hostnameTmp[1024];
|
char hostnameTmp[1024];
|
||||||
_myId.toString(false,publicId);
|
_myId.toString(false,publicId);
|
||||||
if (gethostname(hostnameTmp, sizeof(hostnameTmp))!= 0) {
|
if (gethostname(hostnameTmp, sizeof(hostnameTmp))!= 0) {
|
||||||
|
|||||||
@@ -5,10 +5,11 @@
|
|||||||
|
|
||||||
namespace ZeroTier {
|
namespace ZeroTier {
|
||||||
struct RedisConfig {
|
struct RedisConfig {
|
||||||
std::string hostname;
|
std::string hostname;
|
||||||
int port;
|
int port;
|
||||||
std::string password;
|
std::string password;
|
||||||
bool clusterMode;
|
bool clusterMode;
|
||||||
|
int db;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
-1825
File diff suppressed because it is too large.
Load diff
Vendored
-1462
File diff suppressed because it is too large.
Load diff
Vendored
-2
@@ -1,2 +0,0 @@
|
|||||||
8131853dbc8c3be5171fa96353de7a884a79d3f1d6b3fbf48f99037f5f95fe27 ext/libpqxx-7.7.3/config/config.guess
|
|
||||||
1ee0b1581032c46aac6ebd1de74b130f35082fb45a12e9ead42ad5c775f9b64f ext/libpqxx-7.7.3/config/config.sub
|
|
||||||
-257
@@ -1,257 +0,0 @@
|
|||||||
backone (1.17.1) stable; urgency=medium
|
|
||||||
|
|
||||||
* Fix /api/backone routes and reinstallable Web UI packages
|
|
||||||
|
|
||||||
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 12:02:07 +0700
|
|
||||||
|
|
||||||
backone (1.17.0) stable; urgency=medium
|
|
||||||
|
|
||||||
* Tray app, auto web server, peer contact UI
|
|
||||||
|
|
||||||
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 09:55:07 +0700
|
|
||||||
|
|
||||||
backone (1.16.0) stable; urgency=medium
|
|
||||||
|
|
||||||
* Release notes
|
|
||||||
|
|
||||||
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 08:59:49 +0700
|
|
||||||
|
|
||||||
backone (1.15.0) stable; urgency=medium
|
|
||||||
|
|
||||||
* Separated addon architecture: voice, video, chat, updater
|
|
||||||
|
|
||||||
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 08:37:16 +0700
|
|
||||||
zerotier-one (1.14.1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 11 Sep 2024 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.14.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 19 Mar 2024 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.12.2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 12 Sep 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.12.1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 25 Aug 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.12.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 17 Aug 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.6) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 21 Mar 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.5) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 10 Mar 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.4) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 06 Mar 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.3) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Sat, 21 Jan 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 13 Oct 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 27 Jun 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 03 Jun 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.10) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 10 May 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.9) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 25 Apr 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.8) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 11 Apr 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.7) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 21 Mar 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.6) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 07 Mar 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.5) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 17 Dec 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.4) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 23 Nov 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.3) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 15 Nov 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 08 Nov 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 20 Oct 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 15 Sep 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.6) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Backport endpoint mitigation against address collision attack.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 21 Sep 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.5) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Fix path filtering bug that could cause "software laser" effect.
|
|
||||||
* Fix printf overflow in CLI (not exploitable or security related)
|
|
||||||
* Fix Windows device enumeration issue.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 13 Apr 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.4) unstable; urgency=medium
|
|
||||||
|
|
||||||
* REALLY fix a problem causing nodes to go into a "coma" with some network configurations.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 15 Feb 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.3-1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Fix a problem causing nodes to go into a "coma" with some network configurations.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 02 Feb 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.2-2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* This is a minor update to the 1.6.2 package to address issues with
|
|
||||||
running on ARMv6 CPUs like the Raspberry Pi Zero and original v1 Pi.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 31 Nov 2020 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 30 Nov 2020 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 24 Nov 2020 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 19 Nov 2020 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.5.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Version 1.5.0 is actually 1.6.0-beta1
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 05 Aug 2020 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.4.6) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Update default root server list
|
|
||||||
* Fix build flags on "armhf" (32-bit ARM) platforms for better
|
|
||||||
compatibility with Pi Zero and other devices.
|
|
||||||
* Fix license text in one.cpp.
|
|
||||||
* Add a clarification to LICENSE.txt.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 30 Aug 2019 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.4.4) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See https://github.com/zerotier/ZeroTierOne for release notes.
|
|
||||||
* License changed to BSL 1.1
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 23 Aug 2019 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.4.2-2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See https://github.com/zerotier/ZeroTierOne for release notes.
|
|
||||||
* This is a new build that fixes a binary build issue with containers and SELinux
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 04 Aug 2019 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.4.2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See https://github.com/zerotier/ZeroTierOne for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 04 Aug 2019 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.4.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See https://github.com/zerotier/ZeroTierOne for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 29 Jul 2019 01:00:00 -0700
|
|
||||||
|
|
||||||
# Older entries have been removed from this changelog.
|
|
||||||
# To read the complete changelog use `apt changelog backone-addon-chat`.
|
|
||||||
Whitespace-only changes.
-257
@@ -1,257 +0,0 @@
|
|||||||
backone (1.17.1) stable; urgency=medium
|
|
||||||
|
|
||||||
* Fix /api/backone routes and reinstallable Web UI packages
|
|
||||||
|
|
||||||
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 12:02:07 +0700
|
|
||||||
|
|
||||||
backone (1.17.0) stable; urgency=medium
|
|
||||||
|
|
||||||
* Tray app, auto web server, peer contact UI
|
|
||||||
|
|
||||||
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 09:55:07 +0700
|
|
||||||
|
|
||||||
backone (1.16.0) stable; urgency=medium
|
|
||||||
|
|
||||||
* Release notes
|
|
||||||
|
|
||||||
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 08:59:49 +0700
|
|
||||||
|
|
||||||
backone (1.15.0) stable; urgency=medium
|
|
||||||
|
|
||||||
* Separated addon architecture: voice, video, chat, updater
|
|
||||||
|
|
||||||
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 08:37:16 +0700
|
|
||||||
zerotier-one (1.14.1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 11 Sep 2024 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.14.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 19 Mar 2024 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.12.2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 12 Sep 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.12.1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 25 Aug 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.12.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 17 Aug 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.6) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 21 Mar 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.5) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 10 Mar 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.4) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 06 Mar 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.3) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Sat, 21 Jan 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 13 Oct 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 27 Jun 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 03 Jun 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.10) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 10 May 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.9) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 25 Apr 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.8) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 11 Apr 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.7) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 21 Mar 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.6) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 07 Mar 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.5) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 17 Dec 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.4) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 23 Nov 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.3) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 15 Nov 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 08 Nov 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 20 Oct 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 15 Sep 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.6) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Backport endpoint mitigation against address collision attack.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 21 Sep 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.5) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Fix path filtering bug that could cause "software laser" effect.
|
|
||||||
* Fix printf overflow in CLI (not exploitable or security related)
|
|
||||||
* Fix Windows device enumeration issue.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 13 Apr 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.4) unstable; urgency=medium
|
|
||||||
|
|
||||||
* REALLY fix a problem causing nodes to go into a "coma" with some network configurations.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 15 Feb 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.3-1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Fix a problem causing nodes to go into a "coma" with some network configurations.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 02 Feb 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.2-2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* This is a minor update to the 1.6.2 package to address issues with
|
|
||||||
running on ARMv6 CPUs like the Raspberry Pi Zero and original v1 Pi.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 31 Nov 2020 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 30 Nov 2020 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 24 Nov 2020 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 19 Nov 2020 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.5.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Version 1.5.0 is actually 1.6.0-beta1
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 05 Aug 2020 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.4.6) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Update default root server list
|
|
||||||
* Fix build flags on "armhf" (32-bit ARM) platforms for better
|
|
||||||
compatibility with Pi Zero and other devices.
|
|
||||||
* Fix license text in one.cpp.
|
|
||||||
* Add a clarification to LICENSE.txt.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 30 Aug 2019 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.4.4) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See https://github.com/zerotier/ZeroTierOne for release notes.
|
|
||||||
* License changed to BSL 1.1
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 23 Aug 2019 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.4.2-2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See https://github.com/zerotier/ZeroTierOne for release notes.
|
|
||||||
* This is a new build that fixes a binary build issue with containers and SELinux
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 04 Aug 2019 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.4.2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See https://github.com/zerotier/ZeroTierOne for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 04 Aug 2019 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.4.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See https://github.com/zerotier/ZeroTierOne for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 29 Jul 2019 01:00:00 -0700
|
|
||||||
|
|
||||||
# Older entries have been removed from this changelog.
|
|
||||||
# To read the complete changelog use `apt changelog backone-addon-comms-core`.
|
|
||||||
Whitespace-only changes.
-257
@@ -1,257 +0,0 @@
|
|||||||
backone (1.17.1) stable; urgency=medium
|
|
||||||
|
|
||||||
* Fix /api/backone routes and reinstallable Web UI packages
|
|
||||||
|
|
||||||
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 12:02:07 +0700
|
|
||||||
|
|
||||||
backone (1.17.0) stable; urgency=medium
|
|
||||||
|
|
||||||
* Tray app, auto web server, peer contact UI
|
|
||||||
|
|
||||||
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 09:55:07 +0700
|
|
||||||
|
|
||||||
backone (1.16.0) stable; urgency=medium
|
|
||||||
|
|
||||||
* Release notes
|
|
||||||
|
|
||||||
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 08:59:49 +0700
|
|
||||||
|
|
||||||
backone (1.15.0) stable; urgency=medium
|
|
||||||
|
|
||||||
* Separated addon architecture: voice, video, chat, updater
|
|
||||||
|
|
||||||
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 08:37:16 +0700
|
|
||||||
zerotier-one (1.14.1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 11 Sep 2024 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.14.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 19 Mar 2024 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.12.2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 12 Sep 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.12.1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 25 Aug 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.12.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 17 Aug 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.6) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 21 Mar 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.5) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 10 Mar 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.4) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 06 Mar 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.3) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Sat, 21 Jan 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 13 Oct 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 27 Jun 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 03 Jun 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.10) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 10 May 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.9) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 25 Apr 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.8) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 11 Apr 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.7) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 21 Mar 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.6) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 07 Mar 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.5) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 17 Dec 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.4) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 23 Nov 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.3) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 15 Nov 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 08 Nov 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 20 Oct 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 15 Sep 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.6) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Backport endpoint mitigation against address collision attack.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 21 Sep 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.5) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Fix path filtering bug that could cause "software laser" effect.
|
|
||||||
* Fix printf overflow in CLI (not exploitable or security related)
|
|
||||||
* Fix Windows device enumeration issue.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 13 Apr 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.4) unstable; urgency=medium
|
|
||||||
|
|
||||||
* REALLY fix a problem causing nodes to go into a "coma" with some network configurations.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 15 Feb 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.3-1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Fix a problem causing nodes to go into a "coma" with some network configurations.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 02 Feb 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.2-2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* This is a minor update to the 1.6.2 package to address issues with
|
|
||||||
running on ARMv6 CPUs like the Raspberry Pi Zero and original v1 Pi.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 31 Nov 2020 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 30 Nov 2020 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 24 Nov 2020 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 19 Nov 2020 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.5.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Version 1.5.0 is actually 1.6.0-beta1
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 05 Aug 2020 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.4.6) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Update default root server list
|
|
||||||
* Fix build flags on "armhf" (32-bit ARM) platforms for better
|
|
||||||
compatibility with Pi Zero and other devices.
|
|
||||||
* Fix license text in one.cpp.
|
|
||||||
* Add a clarification to LICENSE.txt.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 30 Aug 2019 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.4.4) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See https://github.com/zerotier/ZeroTierOne for release notes.
|
|
||||||
* License changed to BSL 1.1
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 23 Aug 2019 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.4.2-2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See https://github.com/zerotier/ZeroTierOne for release notes.
|
|
||||||
* This is a new build that fixes a binary build issue with containers and SELinux
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 04 Aug 2019 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.4.2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See https://github.com/zerotier/ZeroTierOne for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 04 Aug 2019 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.4.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See https://github.com/zerotier/ZeroTierOne for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 29 Jul 2019 01:00:00 -0700
|
|
||||||
|
|
||||||
# Older entries have been removed from this changelog.
|
|
||||||
# To read the complete changelog use `apt changelog backone-addon-updater`.
|
|
||||||
Whitespace-only changes.
-257
@@ -1,257 +0,0 @@
|
|||||||
backone (1.17.1) stable; urgency=medium
|
|
||||||
|
|
||||||
* Fix /api/backone routes and reinstallable Web UI packages
|
|
||||||
|
|
||||||
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 12:02:07 +0700
|
|
||||||
|
|
||||||
backone (1.17.0) stable; urgency=medium
|
|
||||||
|
|
||||||
* Tray app, auto web server, peer contact UI
|
|
||||||
|
|
||||||
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 09:55:07 +0700
|
|
||||||
|
|
||||||
backone (1.16.0) stable; urgency=medium
|
|
||||||
|
|
||||||
* Release notes
|
|
||||||
|
|
||||||
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 08:59:49 +0700
|
|
||||||
|
|
||||||
backone (1.15.0) stable; urgency=medium
|
|
||||||
|
|
||||||
* Separated addon architecture: voice, video, chat, updater
|
|
||||||
|
|
||||||
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 08:37:16 +0700
|
|
||||||
zerotier-one (1.14.1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 11 Sep 2024 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.14.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 19 Mar 2024 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.12.2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 12 Sep 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.12.1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 25 Aug 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.12.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 17 Aug 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.6) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 21 Mar 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.5) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 10 Mar 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.4) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 06 Mar 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.3) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Sat, 21 Jan 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 13 Oct 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 27 Jun 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 03 Jun 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.10) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 10 May 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.9) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 25 Apr 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.8) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 11 Apr 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.7) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 21 Mar 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.6) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 07 Mar 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.5) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 17 Dec 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.4) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 23 Nov 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.3) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 15 Nov 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 08 Nov 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 20 Oct 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 15 Sep 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.6) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Backport endpoint mitigation against address collision attack.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 21 Sep 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.5) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Fix path filtering bug that could cause "software laser" effect.
|
|
||||||
* Fix printf overflow in CLI (not exploitable or security related)
|
|
||||||
* Fix Windows device enumeration issue.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 13 Apr 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.4) unstable; urgency=medium
|
|
||||||
|
|
||||||
* REALLY fix a problem causing nodes to go into a "coma" with some network configurations.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 15 Feb 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.3-1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Fix a problem causing nodes to go into a "coma" with some network configurations.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 02 Feb 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.2-2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* This is a minor update to the 1.6.2 package to address issues with
|
|
||||||
running on ARMv6 CPUs like the Raspberry Pi Zero and original v1 Pi.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 31 Nov 2020 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 30 Nov 2020 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 24 Nov 2020 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 19 Nov 2020 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.5.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Version 1.5.0 is actually 1.6.0-beta1
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 05 Aug 2020 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.4.6) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Update default root server list
|
|
||||||
* Fix build flags on "armhf" (32-bit ARM) platforms for better
|
|
||||||
compatibility with Pi Zero and other devices.
|
|
||||||
* Fix license text in one.cpp.
|
|
||||||
* Add a clarification to LICENSE.txt.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 30 Aug 2019 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.4.4) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See https://github.com/zerotier/ZeroTierOne for release notes.
|
|
||||||
* License changed to BSL 1.1
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 23 Aug 2019 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.4.2-2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See https://github.com/zerotier/ZeroTierOne for release notes.
|
|
||||||
* This is a new build that fixes a binary build issue with containers and SELinux
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 04 Aug 2019 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.4.2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See https://github.com/zerotier/ZeroTierOne for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 04 Aug 2019 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.4.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See https://github.com/zerotier/ZeroTierOne for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 29 Jul 2019 01:00:00 -0700
|
|
||||||
|
|
||||||
# Older entries have been removed from this changelog.
|
|
||||||
# To read the complete changelog use `apt changelog backone-addon-video`.
|
|
||||||
Whitespace-only changes.
-257
@@ -1,257 +0,0 @@
|
|||||||
backone (1.17.1) stable; urgency=medium
|
|
||||||
|
|
||||||
* Fix /api/backone routes and reinstallable Web UI packages
|
|
||||||
|
|
||||||
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 12:02:07 +0700
|
|
||||||
|
|
||||||
backone (1.17.0) stable; urgency=medium
|
|
||||||
|
|
||||||
* Tray app, auto web server, peer contact UI
|
|
||||||
|
|
||||||
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 09:55:07 +0700
|
|
||||||
|
|
||||||
backone (1.16.0) stable; urgency=medium
|
|
||||||
|
|
||||||
* Release notes
|
|
||||||
|
|
||||||
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 08:59:49 +0700
|
|
||||||
|
|
||||||
backone (1.15.0) stable; urgency=medium
|
|
||||||
|
|
||||||
* Separated addon architecture: voice, video, chat, updater
|
|
||||||
|
|
||||||
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 08:37:16 +0700
|
|
||||||
zerotier-one (1.14.1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 11 Sep 2024 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.14.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 19 Mar 2024 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.12.2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 12 Sep 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.12.1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 25 Aug 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.12.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 17 Aug 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.6) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 21 Mar 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.5) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 10 Mar 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.4) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 06 Mar 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.3) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Sat, 21 Jan 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 13 Oct 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 27 Jun 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 03 Jun 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.10) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 10 May 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.9) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 25 Apr 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.8) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 11 Apr 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.7) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 21 Mar 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.6) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 07 Mar 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.5) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 17 Dec 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.4) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 23 Nov 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.3) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 15 Nov 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 08 Nov 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 20 Oct 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 15 Sep 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.6) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Backport endpoint mitigation against address collision attack.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 21 Sep 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.5) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Fix path filtering bug that could cause "software laser" effect.
|
|
||||||
* Fix printf overflow in CLI (not exploitable or security related)
|
|
||||||
* Fix Windows device enumeration issue.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 13 Apr 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.4) unstable; urgency=medium
|
|
||||||
|
|
||||||
* REALLY fix a problem causing nodes to go into a "coma" with some network configurations.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 15 Feb 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.3-1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Fix a problem causing nodes to go into a "coma" with some network configurations.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 02 Feb 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.2-2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* This is a minor update to the 1.6.2 package to address issues with
|
|
||||||
running on ARMv6 CPUs like the Raspberry Pi Zero and original v1 Pi.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 31 Nov 2020 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 30 Nov 2020 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 24 Nov 2020 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 19 Nov 2020 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.5.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Version 1.5.0 is actually 1.6.0-beta1
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 05 Aug 2020 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.4.6) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Update default root server list
|
|
||||||
* Fix build flags on "armhf" (32-bit ARM) platforms for better
|
|
||||||
compatibility with Pi Zero and other devices.
|
|
||||||
* Fix license text in one.cpp.
|
|
||||||
* Add a clarification to LICENSE.txt.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 30 Aug 2019 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.4.4) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See https://github.com/zerotier/ZeroTierOne for release notes.
|
|
||||||
* License changed to BSL 1.1
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 23 Aug 2019 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.4.2-2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See https://github.com/zerotier/ZeroTierOne for release notes.
|
|
||||||
* This is a new build that fixes a binary build issue with containers and SELinux
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 04 Aug 2019 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.4.2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See https://github.com/zerotier/ZeroTierOne for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 04 Aug 2019 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.4.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See https://github.com/zerotier/ZeroTierOne for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 29 Jul 2019 01:00:00 -0700
|
|
||||||
|
|
||||||
# Older entries have been removed from this changelog.
|
|
||||||
# To read the complete changelog use `apt changelog backone-addon-voice`.
|
|
||||||
Whitespace-only changes.
-257
@@ -1,257 +0,0 @@
|
|||||||
backone (1.17.1) stable; urgency=medium
|
|
||||||
|
|
||||||
* Fix /api/backone routes and reinstallable Web UI packages
|
|
||||||
|
|
||||||
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 12:02:07 +0700
|
|
||||||
|
|
||||||
backone (1.17.0) stable; urgency=medium
|
|
||||||
|
|
||||||
* Tray app, auto web server, peer contact UI
|
|
||||||
|
|
||||||
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 09:55:07 +0700
|
|
||||||
|
|
||||||
backone (1.16.0) stable; urgency=medium
|
|
||||||
|
|
||||||
* Release notes
|
|
||||||
|
|
||||||
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 08:59:49 +0700
|
|
||||||
|
|
||||||
backone (1.15.0) stable; urgency=medium
|
|
||||||
|
|
||||||
* Separated addon architecture: voice, video, chat, updater
|
|
||||||
|
|
||||||
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 08:37:16 +0700
|
|
||||||
zerotier-one (1.14.1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 11 Sep 2024 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.14.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 19 Mar 2024 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.12.2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 12 Sep 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.12.1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 25 Aug 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.12.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 17 Aug 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.6) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 21 Mar 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.5) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 10 Mar 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.4) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 06 Mar 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.3) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Sat, 21 Jan 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 13 Oct 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 27 Jun 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 03 Jun 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.10) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 10 May 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.9) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 25 Apr 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.8) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 11 Apr 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.7) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 21 Mar 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.6) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 07 Mar 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.5) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 17 Dec 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.4) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 23 Nov 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.3) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 15 Nov 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 08 Nov 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 20 Oct 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 15 Sep 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.6) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Backport endpoint mitigation against address collision attack.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 21 Sep 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.5) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Fix path filtering bug that could cause "software laser" effect.
|
|
||||||
* Fix printf overflow in CLI (not exploitable or security related)
|
|
||||||
* Fix Windows device enumeration issue.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 13 Apr 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.4) unstable; urgency=medium
|
|
||||||
|
|
||||||
* REALLY fix a problem causing nodes to go into a "coma" with some network configurations.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 15 Feb 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.3-1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Fix a problem causing nodes to go into a "coma" with some network configurations.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 02 Feb 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.2-2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* This is a minor update to the 1.6.2 package to address issues with
|
|
||||||
running on ARMv6 CPUs like the Raspberry Pi Zero and original v1 Pi.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 31 Nov 2020 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 30 Nov 2020 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 24 Nov 2020 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 19 Nov 2020 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.5.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Version 1.5.0 is actually 1.6.0-beta1
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 05 Aug 2020 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.4.6) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Update default root server list
|
|
||||||
* Fix build flags on "armhf" (32-bit ARM) platforms for better
|
|
||||||
compatibility with Pi Zero and other devices.
|
|
||||||
* Fix license text in one.cpp.
|
|
||||||
* Add a clarification to LICENSE.txt.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 30 Aug 2019 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.4.4) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See https://github.com/zerotier/ZeroTierOne for release notes.
|
|
||||||
* License changed to BSL 1.1
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 23 Aug 2019 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.4.2-2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See https://github.com/zerotier/ZeroTierOne for release notes.
|
|
||||||
* This is a new build that fixes a binary build issue with containers and SELinux
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 04 Aug 2019 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.4.2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See https://github.com/zerotier/ZeroTierOne for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 04 Aug 2019 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.4.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See https://github.com/zerotier/ZeroTierOne for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 29 Jul 2019 01:00:00 -0700
|
|
||||||
|
|
||||||
# Older entries have been removed from this changelog.
|
|
||||||
# To read the complete changelog use `apt changelog backone-app-shell`.
|
|
||||||
Whitespace-only changes.
-257
@@ -1,257 +0,0 @@
|
|||||||
backone (1.17.1) stable; urgency=medium
|
|
||||||
|
|
||||||
* Fix /api/backone routes and reinstallable Web UI packages
|
|
||||||
|
|
||||||
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 12:02:07 +0700
|
|
||||||
|
|
||||||
backone (1.17.0) stable; urgency=medium
|
|
||||||
|
|
||||||
* Tray app, auto web server, peer contact UI
|
|
||||||
|
|
||||||
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 09:55:07 +0700
|
|
||||||
|
|
||||||
backone (1.16.0) stable; urgency=medium
|
|
||||||
|
|
||||||
* Release notes
|
|
||||||
|
|
||||||
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 08:59:49 +0700
|
|
||||||
|
|
||||||
backone (1.15.0) stable; urgency=medium
|
|
||||||
|
|
||||||
* Separated addon architecture: voice, video, chat, updater
|
|
||||||
|
|
||||||
-- Dedy Sutanto <dsutanto@backone.cloud> Tue, 30 Jun 2026 08:37:16 +0700
|
|
||||||
zerotier-one (1.14.1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 11 Sep 2024 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.14.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 19 Mar 2024 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.12.2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 12 Sep 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.12.1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 25 Aug 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.12.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 17 Aug 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.6) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 21 Mar 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.5) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 10 Mar 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.4) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 06 Mar 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.3) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Sat, 21 Jan 2023 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 13 Oct 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 27 Jun 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.10.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 03 Jun 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.10) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 10 May 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.9) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 25 Apr 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.8) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 11 Apr 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.7) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 21 Mar 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.6) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 07 Mar 2022 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.5) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 17 Dec 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.4) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 23 Nov 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.3) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 15 Nov 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 08 Nov 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 20 Oct 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.8.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Wed, 15 Sep 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.6) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Backport endpoint mitigation against address collision attack.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 21 Sep 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.5) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Fix path filtering bug that could cause "software laser" effect.
|
|
||||||
* Fix printf overflow in CLI (not exploitable or security related)
|
|
||||||
* Fix Windows device enumeration issue.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 13 Apr 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.4) unstable; urgency=medium
|
|
||||||
|
|
||||||
* REALLY fix a problem causing nodes to go into a "coma" with some network configurations.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 15 Feb 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.3-1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Fix a problem causing nodes to go into a "coma" with some network configurations.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 02 Feb 2021 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.2-2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* This is a minor update to the 1.6.2 package to address issues with
|
|
||||||
running on ARMv6 CPUs like the Raspberry Pi Zero and original v1 Pi.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 31 Nov 2020 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 30 Nov 2020 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Tue, 24 Nov 2020 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.6.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 19 Nov 2020 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.5.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Version 1.5.0 is actually 1.6.0-beta1
|
|
||||||
* See RELEASE-NOTES.md for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Mon, 05 Aug 2020 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.4.6) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Update default root server list
|
|
||||||
* Fix build flags on "armhf" (32-bit ARM) platforms for better
|
|
||||||
compatibility with Pi Zero and other devices.
|
|
||||||
* Fix license text in one.cpp.
|
|
||||||
* Add a clarification to LICENSE.txt.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 30 Aug 2019 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.4.4) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See https://github.com/zerotier/ZeroTierOne for release notes.
|
|
||||||
* License changed to BSL 1.1
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Fri, 23 Aug 2019 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.4.2-2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See https://github.com/zerotier/ZeroTierOne for release notes.
|
|
||||||
* This is a new build that fixes a binary build issue with containers and SELinux
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 04 Aug 2019 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.4.2) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See https://github.com/zerotier/ZeroTierOne for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 04 Aug 2019 01:00:00 -0700
|
|
||||||
|
|
||||||
zerotier-one (1.4.0) unstable; urgency=medium
|
|
||||||
|
|
||||||
* See https://github.com/zerotier/ZeroTierOne for release notes.
|
|
||||||
|
|
||||||
-- Adam Ierymenko <adam.ierymenko@zerotier.com> Thu, 29 Jul 2019 01:00:00 -0700
|
|
||||||
|
|
||||||
# Older entries have been removed from this changelog.
|
|
||||||
# To read the complete changelog use `apt changelog backone-app-tray`.
|
|
||||||
Whitespace-only changes.
Vendored
-12
@@ -1,12 +0,0 @@
|
|||||||
#!/bin/sh -e
|
|
||||||
case "$1" in
|
|
||||||
configure)
|
|
||||||
if [ -x /usr/lib/backone/addons/updater/backone-updater ]; then
|
|
||||||
/usr/lib/backone/addons/updater/backone-updater rebuild-manifest || true
|
|
||||||
elif [ -x /usr/lib/backone/addons/updater/rebuild-addon-manifest.sh ]; then
|
|
||||||
BACKONE_HOME=/var/lib/backone /usr/lib/backone/addons/updater/rebuild-addon-manifest.sh || true
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
#DEBHELPER#
|
|
||||||
exit 0
|
|
||||||
Vendored
-2
@@ -1,2 +0,0 @@
|
|||||||
misc:Depends=
|
|
||||||
misc:Pre-Depends=
|
|
||||||
-12
@@ -1,12 +0,0 @@
|
|||||||
Package: backone-addon-chat
|
|
||||||
Source: backone
|
|
||||||
Version: 1.17.1
|
|
||||||
Architecture: all
|
|
||||||
Maintainer: Dedy Sutanto <dsutanto@backone.cloud>
|
|
||||||
Installed-Size: 28
|
|
||||||
Depends: backone, backone-addon-comms-core, backone-app-shell
|
|
||||||
Section: net
|
|
||||||
Priority: optional
|
|
||||||
Homepage: https://backone.cloud
|
|
||||||
Description: BackOne chat P2P addon
|
|
||||||
Text chat with image and document attachments over BackOne.
|
|
||||||
-3
@@ -1,3 +0,0 @@
|
|||||||
cb73fba9ab4a23ff5f4dc1e3fa7d6520 usr/lib/backone/addons/chat/addon.json
|
|
||||||
6020963c701ef2f1c4c4c8a78f2d6845 usr/share/doc/backone-addon-chat/changelog.gz
|
|
||||||
463936e9518d72d5ce8f85b746e0b8b5 var/lib/backone/app/addons/chat/index.js
|
|
||||||
-12
@@ -1,12 +0,0 @@
|
|||||||
#!/bin/sh -e
|
|
||||||
case "$1" in
|
|
||||||
configure)
|
|
||||||
if [ -x /usr/lib/backone/addons/updater/backone-updater ]; then
|
|
||||||
/usr/lib/backone/addons/updater/backone-updater rebuild-manifest || true
|
|
||||||
elif [ -x /usr/lib/backone/addons/updater/rebuild-addon-manifest.sh ]; then
|
|
||||||
BACKONE_HOME=/var/lib/backone /usr/lib/backone/addons/updater/rebuild-addon-manifest.sh || true
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
Loaded 100 of 6918 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user